manila/manila/share/share_types.py

422 lines
14 KiB
Python

# Copyright (c) 2014 OpenStack Foundation.
# Copyright (c) 2015 Tom Barron. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Built-in share type properties."""
import re
from oslo_config import cfg
from oslo_db import exception as db_exception
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import uuidutils
import six
from manila.common import constants
from manila import context
from manila import db
from manila import exception
from manila.i18n import _
CONF = cfg.CONF
LOG = log.getLogger(__name__)
def create(context, name, extra_specs=None, is_public=True,
projects=None, description=None):
"""Creates share types."""
extra_specs = extra_specs or {}
projects = projects or []
try:
get_valid_required_extra_specs(extra_specs)
get_valid_optional_extra_specs(extra_specs)
except exception.InvalidExtraSpec as e:
raise exception.InvalidShareType(reason=six.text_type(e))
extra_specs = sanitize_extra_specs(extra_specs)
try:
type_ref = db.share_type_create(context,
dict(name=name,
description=description,
extra_specs=extra_specs,
is_public=is_public),
projects=projects)
except db_exception.DBError:
LOG.exception('DB error.')
raise exception.ShareTypeCreateFailed(name=name,
extra_specs=extra_specs)
return type_ref
def sanitize_extra_specs(extra_specs):
"""Post process extra specs here if necessary"""
az_spec = constants.ExtraSpecs.AVAILABILITY_ZONES
if az_spec in extra_specs:
extra_specs[az_spec] = sanitize_csv(extra_specs[az_spec])
return extra_specs
def update(context, id, name, description, is_public=None):
"""Update share type by id."""
values = {}
if name:
values.update({'name': name})
if description == "":
values.update({'description': None})
elif description:
values.update({'description': description})
if is_public is not None:
values.update({'is_public': is_public})
try:
db.share_type_update(context, id, values)
except db_exception.DBError:
LOG.exception('DB error.')
raise exception.ShareTypeUpdateFailed(id=id)
def destroy(context, id):
"""Marks share types as deleted."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidShareType(reason=msg)
else:
db.share_type_destroy(context, id)
def get_all_types(context, inactive=0, search_opts=None):
"""Get all non-deleted share_types.
"""
search_opts = search_opts or {}
filters = {}
if 'is_public' in search_opts:
filters['is_public'] = search_opts.pop('is_public')
share_types = db.share_type_get_all(context, inactive, filters=filters)
for type_name, type_args in share_types.items():
required_extra_specs = {}
try:
required_extra_specs = get_valid_required_extra_specs(
type_args['extra_specs'])
except exception.InvalidExtraSpec:
LOG.exception('Share type %(share_type)s has invalid required'
' extra specs.', {'share_type': type_name})
type_args['required_extra_specs'] = required_extra_specs
search_vars = {}
availability_zones = search_opts.get('extra_specs', {}).pop(
'availability_zones', None)
extra_specs = search_opts.pop('extra_specs', {})
if extra_specs:
search_vars['extra_specs'] = extra_specs
if availability_zones:
search_vars['availability_zones'] = availability_zones.split(',')
if search_opts:
# No other search options are currently supported
return {}
elif not search_vars:
return share_types
LOG.debug("Searching by: %s", search_vars)
def _check_extra_specs_match(share_type, searchdict):
for k, v in searchdict.items():
if (k not in share_type['extra_specs'].keys()
or share_type['extra_specs'][k] != v):
return False
return True
def _check_availability_zones_match(share_type, availability_zones):
type_azs = share_type['extra_specs'].get('availability_zones')
if type_azs:
type_azs = type_azs.split(',')
return set(availability_zones).issubset(set(type_azs))
return True
# search_option to filter_name mapping.
filter_mapping = {
'extra_specs': _check_extra_specs_match,
'availability_zones': _check_availability_zones_match,
}
result = {}
for type_name, type_args in share_types.items():
# go over all filters in the list (*AND* operation)
type_matches = True
for opt, value in search_vars.items():
try:
filter_func = filter_mapping[opt]
except KeyError:
# no such filter - ignore it, go to next filter
continue
else:
if not filter_func(type_args, value):
type_matches = False
break
if type_matches:
result[type_name] = type_args
return result
def get_share_type(ctxt, id, expected_fields=None):
"""Retrieves single share type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidShareType(reason=msg)
if ctxt is None:
ctxt = context.get_admin_context()
return db.share_type_get(ctxt, id, expected_fields=expected_fields)
def get_share_type_by_name(context, name):
"""Retrieves single share type by name."""
if name is None:
msg = _("name cannot be None")
raise exception.InvalidShareType(reason=msg)
return db.share_type_get_by_name(context, name)
def get_share_type_by_name_or_id(context, share_type=None):
if not share_type:
share_type_ref = get_default_share_type(context)
if not share_type_ref:
msg = _("Default share type not found.")
raise exception.ShareTypeNotFound(message=msg)
return share_type_ref
if uuidutils.is_uuid_like(share_type):
return get_share_type(context, share_type)
else:
return get_share_type_by_name(context, share_type)
def get_default_share_type(ctxt=None):
"""Get the default share type."""
name = CONF.default_share_type
if name is None:
return {}
if ctxt is None:
ctxt = context.get_admin_context()
share_type = {}
try:
share_type = get_share_type_by_name(ctxt, name)
required_extra_specs = get_valid_required_extra_specs(
share_type['extra_specs'])
share_type['required_extra_specs'] = required_extra_specs
return share_type
except exception.ShareTypeNotFoundByName as e:
# Couldn't find share type with the name in default_share_type
# flag, record this issue and move on
# TODO(zhiteng) consider add notification to warn admin
LOG.exception('Default share type is not found, '
'please check default_share_type config: %s', e)
except exception.InvalidExtraSpec as ex:
LOG.exception('Default share type has invalid required extra'
' specs: %s', ex)
def get_share_type_extra_specs(share_type_id, key=False):
share_type = get_share_type(context.get_admin_context(),
share_type_id)
extra_specs = share_type['extra_specs']
if key:
if extra_specs.get(key):
return extra_specs.get(key)
else:
return False
else:
return extra_specs
def get_required_extra_specs():
return constants.ExtraSpecs.REQUIRED
def get_optional_extra_specs():
return constants.ExtraSpecs.OPTIONAL
def get_tenant_visible_extra_specs():
return constants.ExtraSpecs.TENANT_VISIBLE
def get_boolean_extra_specs():
return constants.ExtraSpecs.BOOLEAN
def is_valid_required_extra_spec(key, value):
"""Validates required extra_spec value.
:param key: extra_spec name
:param value: extra_spec value
:return: None if provided extra_spec is not required
True/False if extra_spec is required and valid or not.
"""
if key not in get_required_extra_specs():
return
if key == constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS:
return strutils.bool_from_string(value, default=None) is not None
return False
def get_valid_required_extra_specs(extra_specs):
"""Validates and returns required extra specs from dict.
Raises InvalidExtraSpec if extra specs are not valid, or if any required
extra specs are missing.
"""
extra_specs = extra_specs or {}
missed_extra_specs = set(get_required_extra_specs()) - set(extra_specs)
if missed_extra_specs:
specs = ",".join(missed_extra_specs)
msg = _("Required extra specs '%s' not specified.") % specs
raise exception.InvalidExtraSpec(reason=msg)
required_extra_specs = {}
for k in get_required_extra_specs():
value = extra_specs.get(k, '')
if not is_valid_required_extra_spec(k, value):
msg = _("Value of required extra_spec %s is not valid") % k
raise exception.InvalidExtraSpec(reason=msg)
required_extra_specs[k] = value
return required_extra_specs
def is_valid_csv(extra_spec_value):
if not isinstance(extra_spec_value, six.string_types):
extra_spec_value = six.text_type(extra_spec_value)
values = extra_spec_value.split(',')
return all([v.strip() for v in values])
def sanitize_csv(csv_string):
return ','.join(value.strip() for value in csv_string.split(',')
if (csv_string and value))
def is_valid_optional_extra_spec(key, value):
"""Validates optional but standardized extra_spec value.
:param key: extra_spec name
:param value: extra_spec value
:return: None if provided extra_spec is not required
True/False if extra_spec is required and valid or not.
"""
if key not in get_optional_extra_specs():
return
if key == constants.ExtraSpecs.SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.REPLICATION_TYPE_SPEC:
return value in constants.ExtraSpecs.REPLICATION_TYPES
elif key == constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.AVAILABILITY_ZONES:
return is_valid_csv(value)
return False
def get_valid_optional_extra_specs(extra_specs):
"""Validates and returns optional/standard extra specs from dict.
Raises InvalidExtraSpec if extra specs are not valid.
"""
extra_specs = extra_specs or {}
present_optional_extra_spec_keys = set(extra_specs).intersection(
set(get_optional_extra_specs()))
optional_extra_specs = {}
for key in present_optional_extra_spec_keys:
value = extra_specs.get(key, '')
if not is_valid_optional_extra_spec(key, value):
msg = _("Value of optional extra_spec %s is not valid.") % key
raise exception.InvalidExtraSpec(reason=msg)
optional_extra_specs[key] = value
return optional_extra_specs
def add_share_type_access(context, share_type_id, project_id):
"""Add access to share type for project_id."""
if share_type_id is None:
msg = _("share_type_id cannot be None")
raise exception.InvalidShareType(reason=msg)
return db.share_type_access_add(context, share_type_id, project_id)
def remove_share_type_access(context, share_type_id, project_id):
"""Remove access to share type for project_id."""
if share_type_id is None:
msg = _("share_type_id cannot be None")
raise exception.InvalidShareType(reason=msg)
return db.share_type_access_remove(context, share_type_id, project_id)
def get_extra_specs_from_share(share):
type_id = share.get('share_type_id', None)
return get_share_type_extra_specs(type_id)
def parse_boolean_extra_spec(extra_spec_key, extra_spec_value):
"""Parse extra spec values of the form '<is> True' or '<is> False'
This method returns the boolean value of an extra spec value. If
the value does not conform to the standard boolean pattern, it raises
an InvalidExtraSpec exception.
"""
if not isinstance(extra_spec_value, six.string_types):
extra_spec_value = six.text_type(extra_spec_value)
match = re.match(r'^<is>\s*(?P<value>True|False)$',
extra_spec_value.strip(),
re.IGNORECASE)
if match:
extra_spec_value = match.group('value')
try:
return strutils.bool_from_string(extra_spec_value, strict=True)
except ValueError:
msg = (_('Invalid boolean extra spec %(key)s : %(value)s') %
{'key': extra_spec_key, 'value': extra_spec_value})
raise exception.InvalidExtraSpec(reason=msg)