422 lines
14 KiB
Python
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)
|