cinder/cinder/volume/volume_types.py

476 lines
18 KiB
Python

# Copyright (c) 2011 Zadara Storage Inc.
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright (c) 2010 Citrix Systems, Inc.
# Copyright 2011 Ken Pepple
#
# 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 volume type properties."""
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import uuidutils
from cinder import context
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import quota
from cinder import rpc
from cinder import utils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
QUOTAS = quota.QUOTAS
ENCRYPTION_IGNORED_FIELDS = ['volume_type_id', 'created_at', 'updated_at',
'deleted_at', 'encryption_id']
DEFAULT_VOLUME_TYPE = "__DEFAULT__"
MIN_SIZE_KEY = "provisioning:min_vol_size"
MAX_SIZE_KEY = "provisioning:max_vol_size"
def create(context,
name,
extra_specs=None,
is_public=True,
projects=None,
description=None):
"""Creates volume types."""
extra_specs = extra_specs or {}
projects = projects or []
elevated = context if context.is_admin else context.elevated()
try:
type_ref = db.volume_type_create(elevated,
dict(name=name,
extra_specs=extra_specs,
is_public=is_public,
description=description),
projects=projects)
except db_exc.DBError:
LOG.exception('DB error:')
raise exception.VolumeTypeCreateFailed(name=name,
extra_specs=extra_specs)
return type_ref
def update(context, id, name, description, is_public=None):
"""Update volume type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
elevated = context if context.is_admin else context.elevated()
old_volume_type = get_volume_type(elevated, id)
try:
db.volume_type_update(elevated, id,
dict(name=name, description=description,
is_public=is_public))
# Rename resource in quota if volume type name is changed.
if name:
old_type_name = old_volume_type.get('name')
if old_type_name != name:
old_description = old_volume_type.get('description')
old_public = old_volume_type.get('is_public')
try:
QUOTAS.update_quota_resource(elevated,
old_type_name,
name)
# Rollback the updated information to the original
except db_exc.DBError:
db.volume_type_update(elevated, id,
dict(name=old_type_name,
description=old_description,
is_public=old_public))
raise
except db_exc.DBError:
LOG.exception('DB error:')
raise exception.VolumeTypeUpdateFailed(id=id)
def destroy(context, id):
"""Marks volume types as deleted.
There must exist at least one volume type (i.e. the default type) in
the deployment.
This method achieves that by ensuring:
1) the default_volume_type is set and is a valid one
2) the type requested to delete isn't the default type
:raises VolumeTypeDefaultDeletionError: when the type requested to
delete is the default type
"""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
projects_with_default_type = db.get_all_projects_with_default_type(
context.elevated(), id)
if len(projects_with_default_type) > 0:
# don't allow delete if the type requested is a project default
project_list = [p.project_id for p in projects_with_default_type]
LOG.exception('Default type with %(volume_type_id)s is associated '
'with projects %(projects)s',
{'volume_type_id': id,
'projects': project_list})
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
# Default type *must* be set in order to delete any volume type.
# If the default isn't set, the following call will raise
# VolumeTypeDefaultMisconfiguredError exception which will error out the
# delete operation.
default_type = get_default_volume_type()
# don't allow delete if the type requested is the conf default type
if id == default_type.get('id'):
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
elevated = context if context.is_admin else context.elevated()
return db.volume_type_destroy(elevated, id)
def get_all_types(context, inactive=0, filters=None, marker=None,
limit=None, sort_keys=None, sort_dirs=None,
offset=None, list_result=False):
"""Get all non-deleted volume_types.
Pass true as argument if you want deleted volume types returned also.
"""
vol_types = db.volume_type_get_all(context, inactive, filters=filters,
marker=marker, limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs, offset=offset,
list_result=list_result)
return vol_types
def get_all_types_by_group(context, group_id):
"""Get all volume_types in a group."""
vol_types = db.volume_type_get_all_by_group(context, group_id)
return vol_types
def get_volume_type(ctxt, id, expected_fields=None):
"""Retrieves single volume type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
if ctxt is None:
ctxt = context.get_admin_context()
return db.volume_type_get(ctxt, id, expected_fields=expected_fields)
def get_by_name_or_id(context, identity):
"""Retrieves volume type by id or name"""
if uuidutils.is_uuid_like(identity):
# both name and id can be in uuid format
try:
return get_volume_type(context, identity)
except exception.VolumeTypeNotFound:
try:
# A user can create a type with the name in a UUID format,
# so here we check for the uuid-like name.
return get_volume_type_by_name(context, identity)
except exception.VolumeTypeNotFoundByName:
raise exception.VolumeTypeNotFound(volume_type_id=identity)
return get_volume_type_by_name(context, identity)
def get_volume_type_by_name(context, name):
"""Retrieves single volume type by name."""
if name is None:
msg = _("name cannot be None")
raise exception.InvalidVolumeType(reason=msg)
return db.volume_type_get_by_name(context, name)
def get_default_volume_type(contxt=None):
"""Get the default volume type.
:raises VolumeTypeDefaultMisconfiguredError: when the configured default
is not found
"""
if contxt:
project_default = db.project_default_volume_type_get(
contxt, contxt.project_id)
if project_default:
return get_volume_type(contxt, project_default.volume_type_id)
name = CONF.default_volume_type
ctxt = context.get_admin_context()
vol_type = {}
try:
vol_type = get_volume_type_by_name(ctxt, name)
except (exception.VolumeTypeNotFoundByName, exception.InvalidVolumeType):
# Couldn't find volume type with the name in default_volume_type
# flag, record this issue and raise exception
LOG.exception('Default volume type is not found. '
'Please check default_volume_type config:')
raise exception.VolumeTypeDefaultMisconfiguredError(
volume_type_name=name)
return vol_type
def get_volume_type_extra_specs(volume_type_id, key=False):
volume_type = get_volume_type(context.get_admin_context(),
volume_type_id)
extra_specs = volume_type['extra_specs']
if key:
if extra_specs.get(key):
return extra_specs.get(key)
else:
return False
else:
return extra_specs
def is_public_volume_type(context, volume_type_id):
"""Return is_public boolean value of volume type"""
volume_type = db.volume_type_get(context, volume_type_id)
return volume_type['is_public']
@utils.if_notifications_enabled
def notify_about_volume_type_access_usage(context,
volume_type_id,
project_id,
event_suffix,
host=None):
"""Notify about successful usage type-access-(add/remove) command.
:param context: security context
:param volume_type_id: volume type uuid
:param project_id: tenant uuid
:param event_suffix: name of called operation access-(add/remove)
:param host: hostname
"""
notifier_info = {'volume_type_id': volume_type_id,
'project_id': project_id}
if not host:
host = CONF.host
notifier = rpc.get_notifier("volume_type_project", host)
notifier.info(context,
'volume_type_project.%s' % event_suffix,
notifier_info)
def add_volume_type_access(context, volume_type_id, project_id):
"""Add access to volume type for project_id."""
if volume_type_id is None:
msg = _("volume_type_id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
elevated = context if context.is_admin else context.elevated()
if is_public_volume_type(elevated, volume_type_id):
msg = _("Type access modification is not applicable to public volume "
"type.")
raise exception.InvalidVolumeType(reason=msg)
db.volume_type_access_add(elevated, volume_type_id, project_id)
notify_about_volume_type_access_usage(context,
volume_type_id,
project_id,
'access.add')
def remove_volume_type_access(context, volume_type_id, project_id):
"""Remove access to volume type for project_id."""
if volume_type_id is None:
msg = _("volume_type_id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
elevated = context if context.is_admin else context.elevated()
if is_public_volume_type(elevated, volume_type_id):
msg = _("Type access modification is not applicable to public volume "
"type.")
raise exception.InvalidVolumeType(reason=msg)
db.volume_type_access_remove(elevated, volume_type_id, project_id)
notify_about_volume_type_access_usage(context,
volume_type_id,
project_id,
'access.remove')
def is_encrypted(context, volume_type_id):
return get_volume_type_encryption(context, volume_type_id) is not None
def get_volume_type_encryption(context, volume_type_id):
if volume_type_id is None:
return None
encryption = db.volume_type_encryption_get(context, volume_type_id)
return encryption
def get_volume_type_qos_specs(volume_type_id):
"""Get all qos specs for given volume type."""
ctxt = context.get_admin_context()
res = db.volume_type_qos_specs_get(ctxt,
volume_type_id)
return res
def volume_types_diff(context, vol_type_id1, vol_type_id2):
"""Returns a 'diff' of two volume types and whether they are equal.
Returns a tuple of (diff, equal), where 'equal' is a boolean indicating
whether there is any difference, and 'diff' is a dictionary with the
following format:
.. code-block:: default
{
'extra_specs': {'key1': (value_in_1st_vol_type,
value_in_2nd_vol_type),
'key2': (value_in_1st_vol_type,
value_in_2nd_vol_type),
{...}}
'qos_specs': {'key1': (value_in_1st_vol_type,
value_in_2nd_vol_type),
'key2': (value_in_1st_vol_type,
value_in_2nd_vol_type),
{...}}
'encryption': {'cipher': (value_in_1st_vol_type,
value_in_2nd_vol_type),
{'key_size': (value_in_1st_vol_type,
value_in_2nd_vol_type),
{...}}
}
"""
def _fix_qos_specs(qos_specs):
if qos_specs:
qos_specs.pop('id', None)
qos_specs.pop('name', None)
qos_specs.update(qos_specs.pop('specs', {}))
def _fix_encryption_specs(encryption):
if encryption:
encryption = dict(encryption)
for param in ENCRYPTION_IGNORED_FIELDS:
encryption.pop(param, None)
return encryption
def _dict_diff(dict1, dict2):
res = {}
equal = True
if dict1 is None:
dict1 = {}
if dict2 is None:
dict2 = {}
for k, v in dict1.items():
res[k] = (v, dict2.get(k))
if k not in dict2 or res[k][0] != res[k][1]:
equal = False
for k, v in dict2.items():
res[k] = (dict1.get(k), v)
if k not in dict1 or res[k][0] != res[k][1]:
equal = False
return (res, equal)
all_equal = True
diff = {}
vol_type_data = []
for vol_type_id in (vol_type_id1, vol_type_id2):
if vol_type_id is None:
specs = {'extra_specs': None,
'qos_specs': None,
'encryption': None}
else:
specs = {}
vol_type = get_volume_type(context, vol_type_id)
specs['extra_specs'] = vol_type.get('extra_specs')
qos_specs = get_volume_type_qos_specs(vol_type_id)
specs['qos_specs'] = qos_specs.get('qos_specs')
_fix_qos_specs(specs['qos_specs'])
specs['encryption'] = get_volume_type_encryption(context,
vol_type_id)
specs['encryption'] = _fix_encryption_specs(specs['encryption'])
vol_type_data.append(specs)
diff['extra_specs'], equal = _dict_diff(vol_type_data[0]['extra_specs'],
vol_type_data[1]['extra_specs'])
if not equal:
all_equal = False
diff['qos_specs'], equal = _dict_diff(vol_type_data[0]['qos_specs'],
vol_type_data[1]['qos_specs'])
if not equal:
all_equal = False
diff['encryption'], equal = _dict_diff(vol_type_data[0]['encryption'],
vol_type_data[1]['encryption'])
if not equal:
all_equal = False
return (diff, all_equal)
def volume_types_encryption_changed(context, vol_type_id1, vol_type_id2):
"""Return whether encryptions of two volume types are same."""
def _get_encryption(enc):
enc = dict(enc)
for param in ENCRYPTION_IGNORED_FIELDS:
enc.pop(param, None)
return enc
enc1 = get_volume_type_encryption(context, vol_type_id1)
enc2 = get_volume_type_encryption(context, vol_type_id2)
enc1_filtered = _get_encryption(enc1) if enc1 else None
enc2_filtered = _get_encryption(enc2) if enc2 else None
return enc1_filtered != enc2_filtered
def provision_filter_on_size(context, volume_type, size):
"""This function filters volume provisioning requests on size limits.
If a volume type has provisioning size min/max set, this filter
will ensure that the volume size requested is within the size
limits specified in the volume type.
"""
if not volume_type:
volume_type = get_default_volume_type()
if volume_type:
size_int = int(size)
extra_specs = volume_type.get('extra_specs', {})
min_size = extra_specs.get(MIN_SIZE_KEY)
if min_size and size_int < int(min_size):
msg = _("Specified volume size of '%(req_size)d' is less "
"than the minimum required size of '%(min_size)s' "
"for volume type '%(vol_type)s'.") % {
'req_size': size_int, 'min_size': min_size,
'vol_type': volume_type['name']
}
raise exception.InvalidInput(reason=msg)
max_size = extra_specs.get(MAX_SIZE_KEY)
if max_size and size_int > int(max_size):
msg = _("Specified volume size of '%(req_size)d' is "
"greater than the maximum allowable size of "
"'%(max_size)s' for volume type '%(vol_type)s'."
) % {
'req_size': size_int, 'max_size': max_size,
'vol_type': volume_type['name']}
raise exception.InvalidInput(reason=msg)