Add group type and group specs

This patch adds support for group types and group specs.
This is the first patch to implement the blueprint
generic-volume-group.

The client side patch is here:
https://review.openstack.org/#/c/320157/

Current microversion is 3.11. The following CLI's are supported.
cinder --os-volume-api-version 3.11 group-type-create my_test_group
cinder --os-volume-api-version 3.11 group-type-list
cinder --os-volume-api-version 3.11 group-type-show my_test_group
cinder --os-volume-api-version 3.11 group-type-key my_test_group
    set test_key=test_val
cinder --os-volume-api-version 3.11 group-specs-list
cinder --os-volume-api-version 3.11 group-type-key my_test_group
    unset test_key
cinder --os-volume-api-version 3.11 group-type-update <group type uuid>
    --name "new_group" --description "my group type"
cinder --os-volume-api-version 3.11 group-type-delete new_group

APIImpact
DocImpact
Change-Id: I38b938782e0c3b2df624f975bd07e0b81684c888
Partial-Implements: blueprint generic-volume-group
This commit is contained in:
xing-yang 2016-05-14 18:09:18 -04:00
parent 7a8804aa20
commit 8cf9786e00
25 changed files with 2345 additions and 2 deletions

View File

@ -58,6 +58,7 @@ REST_API_VERSION_HISTORY = """
* 3.8 - Adds resources from volume_manage and snapshot_manage extensions.
* 3.9 - Add backup update interface.
* 3.10 - Add group_id filter to list/detail volumes in _get_volumes.
* 3.11 - Add group types and group specs API.
"""
@ -66,7 +67,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v1 or /v2 enpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.10"
_MAX_API_VERSION = "3.11"
_LEGACY_API_VERSION1 = "1.0"
_LEGACY_API_VERSION2 = "2.0"

View File

@ -170,3 +170,7 @@ user documentation.
----
Added the filter parameters ``group_id`` to
list/detail volumes requests.
3.11
----
Added group types and group specs API.

View File

@ -0,0 +1,149 @@
# Copyright (c) 2016 EMC Corporation
#
# 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.
"""The group types specs controller"""
import webob
from cinder.api import common
from cinder.api.openstack import wsgi
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import rpc
from cinder.volume import group_types
class GroupTypeSpecsController(wsgi.Controller):
"""The group type specs API controller for the OpenStack API."""
def _get_group_specs(self, context, group_type_id):
group_specs = db.group_type_specs_get(context, group_type_id)
specs_dict = {}
for key, value in group_specs.items():
specs_dict[key] = value
return dict(group_specs=specs_dict)
def _check_type(self, context, group_type_id):
try:
group_types.get_group_type(context, group_type_id)
except exception.GroupTypeNotFound as ex:
raise webob.exc.HTTPNotFound(explanation=ex.msg)
@wsgi.Controller.api_version('3.11')
def index(self, req, group_type_id):
"""Returns the list of group specs for a given group type."""
context = req.environ['cinder.context']
self._check_type(context, group_type_id)
return self._get_group_specs(context, group_type_id)
def _validate_group_specs(self, specs):
"""Validating key and value of group specs."""
for key, value in specs.items():
if key is not None:
self.validate_string_length(key, 'Key "%s"' % key,
min_length=1, max_length=255)
if value is not None:
self.validate_string_length(value, 'Value for key "%s"' % key,
min_length=0, max_length=255)
@wsgi.Controller.api_version('3.11')
@wsgi.response(202)
def create(self, req, group_type_id, body=None):
context = req.environ['cinder.context']
self.assert_valid_body(body, 'group_specs')
self._check_type(context, group_type_id)
specs = body['group_specs']
self._check_key_names(specs.keys())
self._validate_group_specs(specs)
db.group_type_specs_update_or_create(context,
group_type_id,
specs)
notifier_info = dict(type_id=group_type_id, specs=specs)
notifier = rpc.get_notifier('groupTypeSpecs')
notifier.info(context, 'group_type_specs.create',
notifier_info)
return body
@wsgi.Controller.api_version('3.11')
def update(self, req, group_type_id, id, body=None):
context = req.environ['cinder.context']
if not body:
expl = _('Request body empty')
raise webob.exc.HTTPBadRequest(explanation=expl)
self._check_type(context, group_type_id)
if id not in body:
expl = _('Request body and URI mismatch')
raise webob.exc.HTTPBadRequest(explanation=expl)
if len(body) > 1:
expl = _('Request body contains too many items')
raise webob.exc.HTTPBadRequest(explanation=expl)
self._check_key_names(body.keys())
self._validate_group_specs(body)
db.group_type_specs_update_or_create(context,
group_type_id,
body)
notifier_info = dict(type_id=group_type_id, id=id)
notifier = rpc.get_notifier('groupTypeSpecs')
notifier.info(context,
'group_type_specs.update',
notifier_info)
return body
@wsgi.Controller.api_version('3.11')
def show(self, req, group_type_id, id):
"""Return a single extra spec item."""
context = req.environ['cinder.context']
self._check_type(context, group_type_id)
specs = self._get_group_specs(context, group_type_id)
if id in specs['group_specs']:
return {id: specs['group_specs'][id]}
else:
msg = _("Group Type %(type_id)s has no extra spec with key "
"%(id)s.") % ({'type_id': group_type_id, 'id': id})
raise webob.exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version('3.11')
def delete(self, req, group_type_id, id):
"""Deletes an existing group spec."""
context = req.environ['cinder.context']
self._check_type(context, group_type_id)
try:
db.group_type_specs_delete(context, group_type_id, id)
except exception.GroupTypeExtraSpecsNotFound as error:
raise webob.exc.HTTPNotFound(explanation=error.msg)
notifier_info = dict(type_id=group_type_id, id=id)
notifier = rpc.get_notifier('groupTypeSpecs')
notifier.info(context,
'group_type_specs.delete',
notifier_info)
return webob.Response(status_int=202)
def _check_key_names(self, keys):
if not common.validate_key_names(keys):
expl = _('Key names can only contain alphanumeric characters, '
'underscores, periods, colons and hyphens.')
raise webob.exc.HTTPBadRequest(explanation=expl)
def create_resource():
return wsgi.Resource(GroupTypeSpecsController())

View File

@ -0,0 +1,258 @@
# Copyright (c) 2016 EMC Corporation
#
# 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.
"""The group type & group type specs controller."""
from oslo_utils import strutils
import six
import webob
from webob import exc
from cinder.api import common
from cinder.api.openstack import wsgi
from cinder.api.v3.views import group_types as views_types
from cinder import exception
from cinder.i18n import _
from cinder import rpc
from cinder import utils
from cinder.volume import group_types
class GroupTypesController(wsgi.Controller):
"""The group types API controller for the OpenStack API."""
_view_builder_class = views_types.ViewBuilder
def _notify_group_type_error(self, context, method, err,
group_type=None, id=None, name=None):
payload = dict(
group_types=group_type, name=name, id=id, error_message=err)
rpc.get_notifier('groupType').error(context, method, payload)
def _notify_group_type_info(self, context, method, group_type):
payload = dict(group_types=group_type)
rpc.get_notifier('groupType').info(context, method, payload)
@wsgi.Controller.api_version('3.11')
@wsgi.response(202)
def create(self, req, body):
"""Creates a new group type."""
context = req.environ['cinder.context']
self.assert_valid_body(body, 'group_type')
grp_type = body['group_type']
name = grp_type.get('name', None)
description = grp_type.get('description')
specs = grp_type.get('group_specs', {})
is_public = grp_type.get('is_public', True)
if name is None or len(name.strip()) == 0:
msg = _("Group type name can not be empty.")
raise webob.exc.HTTPBadRequest(explanation=msg)
utils.check_string_length(name, 'Type name',
min_length=1, max_length=255)
if description is not None:
utils.check_string_length(description, 'Type description',
min_length=0, max_length=255)
try:
group_types.create(context,
name,
specs,
is_public,
description=description)
grp_type = group_types.get_group_type_by_name(context, name)
req.cache_resource(grp_type, name='group_types')
self._notify_group_type_info(
context, 'group_type.create', grp_type)
except exception.GroupTypeExists as err:
self._notify_group_type_error(
context, 'group_type.create', err, group_type=grp_type)
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
except exception.GroupTypeNotFoundByName as err:
self._notify_group_type_error(
context, 'group_type.create', err, name=name)
raise webob.exc.HTTPNotFound(explanation=err.msg)
return self._view_builder.show(req, grp_type)
@wsgi.Controller.api_version('3.11')
def update(self, req, id, body):
# Update description for a given group type.
context = req.environ['cinder.context']
self.assert_valid_body(body, 'group_type')
grp_type = body['group_type']
description = grp_type.get('description')
name = grp_type.get('name')
is_public = grp_type.get('is_public')
# Name and description can not be both None.
# If name specified, name can not be empty.
if name and len(name.strip()) == 0:
msg = _("Group type name can not be empty.")
raise webob.exc.HTTPBadRequest(explanation=msg)
if name is None and description is None and is_public is None:
msg = _("Specify group type name, description or "
"a combination thereof.")
raise webob.exc.HTTPBadRequest(explanation=msg)
if is_public is not None and not utils.is_valid_boolstr(is_public):
msg = _("Invalid value '%s' for is_public. Accepted values: "
"True or False.") % is_public
raise webob.exc.HTTPBadRequest(explanation=msg)
if name:
utils.check_string_length(name, 'Type name',
min_length=1, max_length=255)
if description is not None:
utils.check_string_length(description, 'Type description',
min_length=0, max_length=255)
try:
group_types.update(context, id, name, description,
is_public=is_public)
# Get the updated
grp_type = group_types.get_group_type(context, id)
req.cache_resource(grp_type, name='group_types')
self._notify_group_type_info(
context, 'group_type.update', grp_type)
except exception.GroupTypeNotFound as err:
self._notify_group_type_error(
context, 'group_type.update', err, id=id)
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
except exception.GroupTypeExists as err:
self._notify_group_type_error(
context, 'group_type.update', err, group_type=grp_type)
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
except exception.GroupTypeUpdateFailed as err:
self._notify_group_type_error(
context, 'group_type.update', err, group_type=grp_type)
raise webob.exc.HTTPInternalServerError(
explanation=six.text_type(err))
return self._view_builder.show(req, grp_type)
@wsgi.Controller.api_version('3.11')
def delete(self, req, id):
"""Deletes an existing group type."""
context = req.environ['cinder.context']
try:
grp_type = group_types.get_group_type(context, id)
group_types.destroy(context, grp_type['id'])
self._notify_group_type_info(
context, 'group_type.delete', grp_type)
except exception.GroupTypeInUse as err:
self._notify_group_type_error(
context, 'group_type.delete', err, group_type=grp_type)
msg = _('Target group type is still in use.')
raise webob.exc.HTTPBadRequest(explanation=msg)
except exception.GroupTypeNotFound as err:
self._notify_group_type_error(
context, 'group_type.delete', err, id=id)
raise webob.exc.HTTPNotFound(explanation=err.msg)
return webob.Response(status_int=202)
@wsgi.Controller.api_version('3.11')
def index(self, req):
"""Returns the list of group types."""
limited_types = self._get_group_types(req)
req.cache_resource(limited_types, name='group_types')
return self._view_builder.index(req, limited_types)
@wsgi.Controller.api_version('3.11')
def show(self, req, id):
"""Return a single group type item."""
context = req.environ['cinder.context']
# get default group type
if id is not None and id == 'default':
grp_type = group_types.get_default_group_type()
if not grp_type:
msg = _("Default group type can not be found.")
raise exc.HTTPNotFound(explanation=msg)
req.cache_resource(grp_type, name='group_types')
else:
try:
grp_type = group_types.get_group_type(context, id)
req.cache_resource(grp_type, name='group_types')
except exception.GroupTypeNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
return self._view_builder.show(req, grp_type)
def _parse_is_public(self, is_public):
"""Parse is_public into something usable.
* True: List public group types only
* False: List private group types only
* None: List both public and private group types
"""
if is_public is None:
# preserve default value of showing only public types
return True
elif utils.is_none_string(is_public):
return None
else:
try:
return strutils.bool_from_string(is_public, strict=True)
except ValueError:
msg = _('Invalid is_public filter [%s]') % is_public
raise exc.HTTPBadRequest(explanation=msg)
def _get_group_types(self, req):
"""Helper function that returns a list of type dicts."""
params = req.params.copy()
marker, limit, offset = common.get_pagination_params(params)
sort_keys, sort_dirs = common.get_sort_params(params)
filters = {}
context = req.environ['cinder.context']
if context.is_admin:
# Only admin has query access to all group types
filters['is_public'] = self._parse_is_public(
req.params.get('is_public', None))
else:
filters['is_public'] = True
utils.remove_invalid_filter_options(context,
filters,
self._get_grp_type_filter_options()
)
limited_types = group_types.get_all_group_types(context,
filters=filters,
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
offset=offset,
list_result=True)
return limited_types
def _get_grp_type_filter_options(self):
"""Return group type search options allowed by non-admin."""
return ['is_public']
def create_resource():
return wsgi.Resource(GroupTypesController())

View File

@ -29,6 +29,8 @@ from cinder.api.v2 import volume_metadata
from cinder.api.v3 import backups
from cinder.api.v3 import clusters
from cinder.api.v3 import consistencygroups
from cinder.api.v3 import group_specs
from cinder.api.v3 import group_types
from cinder.api.v3 import messages
from cinder.api.v3 import snapshot_manage
from cinder.api.v3 import volume_manage
@ -69,6 +71,17 @@ class APIRouter(cinder.api.openstack.APIRouter):
controller=self.resources['types'],
member={'action': 'POST'})
self.resources['group_types'] = group_types.create_resource()
mapper.resource("group_type", "group_types",
controller=self.resources['group_types'],
member={'action': 'POST'})
self.resources['group_specs'] = group_specs.create_resource()
mapper.resource("group_spec", "group_specs",
controller=self.resources['group_specs'],
parent_resource=dict(member_name='group_type',
collection_name='group_types'))
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
mapper.resource("snapshot", "snapshots",
controller=self.resources['snapshots'],

View File

@ -0,0 +1,43 @@
# Copyright 2016 EMC Corporation
# 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.
from cinder.api import common
class ViewBuilder(common.ViewBuilder):
def show(self, request, group_type, brief=False):
"""Trim away extraneous group type attributes."""
context = request.environ['cinder.context']
trimmed = dict(id=group_type.get('id'),
name=group_type.get('name'),
description=group_type.get('description'),
is_public=group_type.get('is_public'))
if common.validate_policy(
context,
'group:access_group_types_specs'):
trimmed['group_specs'] = group_type.get('group_specs')
return trimmed if brief else dict(group_type=trimmed)
def index(self, request, group_types):
"""Index over trimmed group types."""
group_types_list = [self.show(request, group_type, True)
for group_type in group_types]
group_type_links = self._get_collection_links(request, group_types,
'group_types')
group_types_dict = dict(group_types=group_types_list)
if group_type_links:
group_types_dict['group_type_links'] = group_type_links
return group_types_dict

View File

@ -142,6 +142,8 @@ global_opts = [
'storage_availability_zone, instead of failing.'),
cfg.StrOpt('default_volume_type',
help='Default volume type to use'),
cfg.StrOpt('default_group_type',
help='Default group type to use'),
cfg.StrOpt('volume_usage_audit_period',
default='month',
help='Time period for which to generate volume usages. '

View File

@ -622,6 +622,94 @@ def volume_type_access_remove(context, type_id, project_id):
####################
def group_type_create(context, values, projects=None):
"""Create a new group type."""
return IMPL.group_type_create(context, values, projects)
def group_type_update(context, group_type_id, values):
return IMPL.group_type_update(context, group_type_id, values)
def group_type_get_all(context, inactive=False, filters=None, marker=None,
limit=None, sort_keys=None, sort_dirs=None,
offset=None, list_result=False):
"""Get all group types.
:param context: context to query under
:param inactive: Include inactive group types to the result set
:param filters: Filters for the query in the form of key/value.
:param marker: the last item of the previous page, used to determine the
next page of results to return
:param limit: maximum number of items to return
:param sort_keys: list of attributes by which results should be sorted,
paired with corresponding item in sort_dirs
:param sort_dirs: list of directions in which results should be sorted,
paired with corresponding item in sort_keys
:param list_result: For compatibility, if list_result = True, return a list
instead of dict.
:is_public: Filter group types based on visibility:
* **True**: List public group types only
* **False**: List private group types only
* **None**: List both public and private group types
:returns: list/dict of matching group types
"""
return IMPL.group_type_get_all(context, inactive, filters, marker=marker,
limit=limit, sort_keys=sort_keys,
sort_dirs=sort_dirs, offset=offset,
list_result=list_result)
def group_type_get(context, id, inactive=False, expected_fields=None):
"""Get group type by id.
:param context: context to query under
:param id: Group type id to get.
:param inactive: Consider inactive group types when searching
:param expected_fields: Return those additional fields.
Supported fields are: projects.
:returns: group type
"""
return IMPL.group_type_get(context, id, inactive, expected_fields)
def group_type_get_by_name(context, name):
"""Get group type by name."""
return IMPL.group_type_get_by_name(context, name)
def group_types_get_by_name_or_id(context, group_type_list):
"""Get group types by name or id."""
return IMPL.group_types_get_by_name_or_id(context, group_type_list)
def group_type_destroy(context, id):
"""Delete a group type."""
return IMPL.group_type_destroy(context, id)
def group_type_access_get_all(context, type_id):
"""Get all group type access of a group type."""
return IMPL.group_type_access_get_all(context, type_id)
def group_type_access_add(context, type_id, project_id):
"""Add group type access for project."""
return IMPL.group_type_access_add(context, type_id, project_id)
def group_type_access_remove(context, type_id, project_id):
"""Remove group type access for project."""
return IMPL.group_type_access_remove(context, type_id, project_id)
####################
def volume_type_extra_specs_get(context, volume_type_id):
"""Get all extra specs for a volume type."""
return IMPL.volume_type_extra_specs_get(context, volume_type_id)
@ -648,6 +736,32 @@ def volume_type_extra_specs_update_or_create(context,
###################
def group_type_specs_get(context, group_type_id):
"""Get all group specs for a group type."""
return IMPL.group_type_specs_get(context, group_type_id)
def group_type_specs_delete(context, group_type_id, key):
"""Delete the given group specs item."""
return IMPL.group_type_specs_delete(context, group_type_id, key)
def group_type_specs_update_or_create(context,
group_type_id,
group_specs):
"""Create or update group type specs.
This adds or modifies the key/value pairs specified in the group specs dict
argument.
"""
return IMPL.group_type_specs_update_or_create(context,
group_type_id,
group_specs)
###################
def volume_type_encryption_get(context, volume_type_id, session=None):
return IMPL.volume_type_encryption_get(context, volume_type_id, session)

View File

@ -709,6 +709,37 @@ def _dict_with_extra_specs_if_authorized(context, inst_type_query):
###################
def _dict_with_group_specs_if_authorized(context, inst_type_query):
"""Convert group type query result to dict with spec and rate_limit.
Takes a group type query returned by sqlalchemy and returns it
as a dictionary, converting the extra_specs entry from a list
of dicts. NOTE the contents of extra-specs are admin readable
only. If the context passed in for this request is not admin
then we will return an empty extra-specs dict rather than
providing the admin only details.
Example response with admin context:
'group_specs' : [{'key': 'k1', 'value': 'v1', ...}, ...]
to a single dict:
'group_specs' : {'k1': 'v1'}
"""
inst_type_dict = dict(inst_type_query)
if not is_admin_context(context):
del(inst_type_dict['group_specs'])
else:
group_specs = {x['key']: x['value']
for x in inst_type_query['group_specs']}
inst_type_dict['group_specs'] = group_specs
return inst_type_dict
###################
@require_context
def _quota_get(context, project_id, resource, session=None):
result = model_query(context, models.Quota, session=session,
@ -2819,6 +2850,48 @@ def volume_type_create(context, values, projects=None):
return volume_type_ref
@handle_db_data_error
@require_admin_context
def group_type_create(context, values, projects=None):
"""Create a new group type.
In order to pass in group specs, the values dict should contain a
'group_specs' key/value pair:
{'group_specs' : {'k1': 'v1', 'k2': 'v2', ...}}
"""
if not values.get('id'):
values['id'] = six.text_type(uuid.uuid4())
projects = projects or []
session = get_session()
with session.begin():
try:
_group_type_get_by_name(context, values['name'], session)
raise exception.GroupTypeExists(id=values['name'])
except exception.GroupTypeNotFoundByName:
pass
try:
_group_type_get(context, values['id'], session)
raise exception.GroupTypeExists(id=values['id'])
except exception.GroupTypeNotFound:
pass
try:
values['group_specs'] = _metadata_refs(values.get('group_specs'),
models.GroupTypeSpecs)
group_type_ref = models.GroupTypes()
group_type_ref.update(values)
session.add(group_type_ref)
except Exception as e:
raise db_exc.DBError(e)
for project in set(projects):
access_ref = models.GroupTypeProjects()
access_ref.update({"group_type_id": group_type_ref.id,
"project_id": project})
access_ref.save(session=session)
return group_type_ref
def _volume_type_get_query(context, session=None, read_deleted='no',
expected_fields=None):
expected_fields = expected_fields or []
@ -2842,6 +2915,29 @@ def _volume_type_get_query(context, session=None, read_deleted='no',
return query
def _group_type_get_query(context, session=None, read_deleted='no',
expected_fields=None):
expected_fields = expected_fields or []
query = model_query(context,
models.GroupTypes,
session=session,
read_deleted=read_deleted).\
options(joinedload('group_specs'))
if 'projects' in expected_fields:
query = query.options(joinedload('projects'))
if not context.is_admin:
the_filter = [models.VolumeTypes.is_public == true()]
projects_attr = getattr(models.VolumeTypes, 'projects')
the_filter.extend([
projects_attr.any(project_id=context.project_id)
])
query = query.filter(or_(*the_filter))
return query
def _process_volume_types_filters(query, filters):
context = filters.pop('context', None)
if 'is_public' in filters and filters['is_public'] is not None:
@ -2877,6 +2973,41 @@ def _process_volume_types_filters(query, filters):
return query
def _process_group_types_filters(query, filters):
context = filters.pop('context', None)
if 'is_public' in filters and filters['is_public'] is not None:
the_filter = [models.GroupTypes.is_public == filters['is_public']]
if filters['is_public'] and context.project_id is not None:
projects_attr = getattr(models.GroupTypes, 'projects')
the_filter.extend([
projects_attr.any(project_id=context.project_id, deleted=0)
])
if len(the_filter) > 1:
query = query.filter(or_(*the_filter))
else:
query = query.filter(the_filter[0])
if 'is_public' in filters:
del filters['is_public']
if filters:
# Ensure that filters' keys exist on the model
if not is_valid_model_filters(models.GroupTypes, filters):
return
if filters.get('group_specs') is not None:
the_filter = []
searchdict = filters.get('group_specs')
group_specs = getattr(models.GroupTypes, 'group_specs')
for k, v in searchdict.items():
the_filter.extend([group_specs.any(key=k, value=v,
deleted=False)])
if len(the_filter) > 1:
query = query.filter(and_(*the_filter))
else:
query = query.filter(the_filter[0])
del filters['group_specs']
query = query.filter_by(**filters)
return query
@handle_db_data_error
@require_admin_context
def volume_type_update(context, volume_type_id, values):
@ -2921,6 +3052,50 @@ def volume_type_update(context, volume_type_id, values):
return volume_type_ref
@handle_db_data_error
@require_admin_context
def group_type_update(context, group_type_id, values):
session = get_session()
with session.begin():
# Check it exists
group_type_ref = _group_type_ref_get(context,
group_type_id,
session)
if not group_type_ref:
raise exception.GroupTypeNotFound(type_id=group_type_id)
# No description change
if values['description'] is None:
del values['description']
# No is_public change
if values['is_public'] is None:
del values['is_public']
# No name change
if values['name'] is None:
del values['name']
else:
# Group type name is unique. If change to a name that belongs to
# a different group_type , it should be prevented.
check_grp_type = None
try:
check_grp_type = \
_group_type_get_by_name(context,
values['name'],
session=session)
except exception.GroupTypeNotFoundByName:
pass
else:
if check_grp_type.get('id') != group_type_id:
raise exception.GroupTypeExists(id=values['name'])
group_type_ref.update(values)
group_type_ref.save(session=session)
return group_type_ref
@require_context
def volume_type_get_all(context, inactive=False, filters=None, marker=None,
limit=None, sort_keys=None, sort_dirs=None,
@ -2973,6 +3148,58 @@ def volume_type_get_all(context, inactive=False, filters=None, marker=None,
return result
@require_context
def group_type_get_all(context, inactive=False, filters=None, marker=None,
limit=None, sort_keys=None, sort_dirs=None,
offset=None, list_result=False):
"""Returns a dict describing all group_types with name as key.
If no sort parameters are specified then the returned group types are
sorted first by the 'created_at' key and then by the 'id' key in descending
order.
:param context: context to query under
:param marker: the last item of the previous page, used to determine the
next page of results to return
:param limit: maximum number of items to return
:param sort_keys: list of attributes by which results should be sorted,
paired with corresponding item in sort_dirs
:param sort_dirs: list of directions in which results should be sorted,
paired with corresponding item in sort_keys
:param filters: dictionary of filters; values that are in lists, tuples,
or sets cause an 'IN' operation, while exact matching
is used for other values, see _process_volume_type_filters
function for more information
:param list_result: For compatibility, if list_result = True, return a list
instead of dict.
:returns: list/dict of matching group types
"""
session = get_session()
with session.begin():
# Add context for _process_group_types_filters
filters = filters or {}
filters['context'] = context
# Generate the query
query = _generate_paginate_query(context, session, marker, limit,
sort_keys, sort_dirs, filters, offset,
models.GroupTypes)
# No group types would match, return empty dict or list
if query is None:
if list_result:
return []
return {}
rows = query.all()
if list_result:
result = [_dict_with_group_specs_if_authorized(context, row)
for row in rows]
return result
result = {row['name']: _dict_with_group_specs_if_authorized(context,
row)
for row in rows}
return result
def _volume_type_get_id_from_volume_type_query(context, id, session=None):
return model_query(
context, models.VolumeTypes.id, read_deleted="no",
@ -2980,6 +3207,13 @@ def _volume_type_get_id_from_volume_type_query(context, id, session=None):
filter_by(id=id)
def _group_type_get_id_from_group_type_query(context, id, session=None):
return model_query(
context, models.GroupTypes.id, read_deleted="no",
session=session, base_model=models.GroupTypes).\
filter_by(id=id)
def _volume_type_get_id_from_volume_type(context, id, session=None):
result = _volume_type_get_id_from_volume_type_query(
context, id, session=session).first()
@ -2988,6 +3222,14 @@ def _volume_type_get_id_from_volume_type(context, id, session=None):
return result[0]
def _group_type_get_id_from_group_type(context, id, session=None):
result = _group_type_get_id_from_group_type_query(
context, id, session=session).first()
if not result:
raise exception.GroupTypeNotFound(group_type_id=id)
return result[0]
def _volume_type_get_db_object(context, id, session=None, inactive=False,
expected_fields=None):
read_deleted = "yes" if inactive else "no"
@ -2998,6 +3240,16 @@ def _volume_type_get_db_object(context, id, session=None, inactive=False,
return result
def _group_type_get_db_object(context, id, session=None, inactive=False,
expected_fields=None):
read_deleted = "yes" if inactive else "no"
result = _group_type_get_query(
context, session, read_deleted, expected_fields).\
filter_by(id=id).\
first()
return result
@require_context
def _volume_type_get(context, id, session=None, inactive=False,
expected_fields=None):
@ -3015,6 +3267,23 @@ def _volume_type_get(context, id, session=None, inactive=False,
return vtype
@require_context
def _group_type_get(context, id, session=None, inactive=False,
expected_fields=None):
expected_fields = expected_fields or []
result = _group_type_get_db_object(context, id, session, inactive,
expected_fields)
if not result:
raise exception.GroupTypeNotFound(group_type_id=id)
gtype = _dict_with_group_specs_if_authorized(context, result)
if 'projects' in expected_fields:
gtype['projects'] = [p['project_id'] for p in result['projects']]
return gtype
@require_context
def volume_type_get(context, id, inactive=False, expected_fields=None):
"""Return a dict describing specific volume_type."""
@ -3025,12 +3294,28 @@ def volume_type_get(context, id, inactive=False, expected_fields=None):
expected_fields=expected_fields)
@require_context
def group_type_get(context, id, inactive=False, expected_fields=None):
"""Return a dict describing specific group_type."""
return _group_type_get(context, id,
session=None,
inactive=inactive,
expected_fields=expected_fields)
def _volume_type_get_full(context, id):
"""Return dict for a specific volume_type with extra_specs and projects."""
return _volume_type_get(context, id, session=None, inactive=False,
expected_fields=('extra_specs', 'projects'))
def _group_type_get_full(context, id):
"""Return dict for a specific group_type with group_specs and projects."""
return _group_type_get(context, id, session=None, inactive=False,
expected_fields=('group_specs', 'projects'))
@require_context
def _volume_type_ref_get(context, id, session=None, inactive=False):
read_deleted = "yes" if inactive else "no"
@ -3048,6 +3333,23 @@ def _volume_type_ref_get(context, id, session=None, inactive=False):
return result
@require_context
def _group_type_ref_get(context, id, session=None, inactive=False):
read_deleted = "yes" if inactive else "no"
result = model_query(context,
models.GroupTypes,
session=session,
read_deleted=read_deleted).\
options(joinedload('group_specs')).\
filter_by(id=id).\
first()
if not result:
raise exception.GroupTypeNotFound(group_type_id=id)
return result
@require_context
def _volume_type_get_by_name(context, name, session=None):
result = model_query(context, models.VolumeTypes, session=session).\
@ -3061,6 +3363,19 @@ def _volume_type_get_by_name(context, name, session=None):
return _dict_with_extra_specs_if_authorized(context, result)
@require_context
def _group_type_get_by_name(context, name, session=None):
result = model_query(context, models.GroupTypes, session=session).\
options(joinedload('group_specs')).\
filter_by(name=name).\
first()
if not result:
raise exception.GroupTypeNotFoundByName(group_type_name=name)
return _dict_with_group_specs_if_authorized(context, result)
@require_context
def volume_type_get_by_name(context, name):
"""Return a dict describing specific volume_type."""
@ -3068,6 +3383,13 @@ def volume_type_get_by_name(context, name):
return _volume_type_get_by_name(context, name)
@require_context
def group_type_get_by_name(context, name):
"""Return a dict describing specific group_type."""
return _group_type_get_by_name(context, name)
@require_context
def volume_types_get_by_name_or_id(context, volume_type_list):
"""Return a dict describing specific volume_type."""
@ -3081,6 +3403,19 @@ def volume_types_get_by_name_or_id(context, volume_type_list):
return req_volume_types
@require_context
def group_types_get_by_name_or_id(context, group_type_list):
"""Return a dict describing specific group_type."""
req_group_types = []
for grp_t in group_type_list:
if not uuidutils.is_uuid_like(grp_t):
grp_type = _group_type_get_by_name(context, grp_t)
else:
grp_type = _group_type_get(context, grp_t)
req_group_types.append(grp_type)
return req_group_types
@require_admin_context
def volume_type_qos_associations_get(context, qos_specs_id, inactive=False):
read_deleted = "yes" if inactive else "no"
@ -3205,6 +3540,31 @@ def volume_type_destroy(context, id):
return updated_values
@require_admin_context
@_retry_on_deadlock
def group_type_destroy(context, id):
session = get_session()
with session.begin():
_group_type_get(context, id, session)
# TODO(xyang): Uncomment the following after groups table is added.
# results = model_query(context, models.Group, session=session). \
# filter_by(group_type_id=id).all()
# if results:
# LOG.error(_LE('GroupType %s deletion failed, '
# 'GroupType in use.'), id)
# raise exception.GroupTypeInUse(group_type_id=id)
model_query(context, models.GroupTypes, session=session).\
filter_by(id=id).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
model_query(context, models.GroupTypeSpecs, session=session).\
filter_by(group_type_id=id).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
@require_context
def volume_get_active_by_window(context,
begin,
@ -3235,6 +3595,11 @@ def _volume_type_access_query(context, session=None):
read_deleted="int_no")
def _group_type_access_query(context, session=None):
return model_query(context, models.GroupTypeProjects, session=session,
read_deleted="int_no")
@require_admin_context
def volume_type_access_get_all(context, type_id):
volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
@ -3242,6 +3607,13 @@ def volume_type_access_get_all(context, type_id):
filter_by(volume_type_id=volume_type_id).all()
@require_admin_context
def group_type_access_get_all(context, type_id):
group_type_id = _group_type_get_id_from_group_type(context, type_id)
return _group_type_access_query(context).\
filter_by(group_type_id=group_type_id).all()
@require_admin_context
def volume_type_access_add(context, type_id, project_id):
"""Add given tenant to the volume type access list."""
@ -3261,6 +3633,25 @@ def volume_type_access_add(context, type_id, project_id):
return access_ref
@require_admin_context
def group_type_access_add(context, type_id, project_id):
"""Add given tenant to the group type access list."""
group_type_id = _group_type_get_id_from_group_type(context, type_id)
access_ref = models.GroupTypeProjects()
access_ref.update({"group_type_id": group_type_id,
"project_id": project_id})
session = get_session()
with session.begin():
try:
access_ref.save(session=session)
except db_exc.DBDuplicateEntry:
raise exception.GroupTypeAccessExists(group_type_id=type_id,
project_id=project_id)
return access_ref
@require_admin_context
def volume_type_access_remove(context, type_id, project_id):
"""Remove given tenant from the volume type access list."""
@ -3275,6 +3666,20 @@ def volume_type_access_remove(context, type_id, project_id):
volume_type_id=type_id, project_id=project_id)
@require_admin_context
def group_type_access_remove(context, type_id, project_id):
"""Remove given tenant from the group type access list."""
group_type_id = _group_type_get_id_from_group_type(context, type_id)
count = (_group_type_access_query(context).
filter_by(group_type_id=group_type_id).
filter_by(project_id=project_id).
soft_delete(synchronize_session=False))
if count == 0:
raise exception.GroupTypeAccessNotFound(
group_type_id=type_id, project_id=project_id)
####################
@ -3349,6 +3754,77 @@ def volume_type_extra_specs_update_or_create(context, volume_type_id,
####################
def _group_type_specs_query(context, group_type_id, session=None):
return model_query(context, models.GroupTypeSpecs, session=session,
read_deleted="no").\
filter_by(group_type_id=group_type_id)
@require_context
def group_type_specs_get(context, group_type_id):
rows = _group_type_specs_query(context, group_type_id).\
all()
result = {}
for row in rows:
result[row['key']] = row['value']
return result
@require_context
def group_type_specs_delete(context, group_type_id, key):
session = get_session()
with session.begin():
_group_type_specs_get_item(context, group_type_id, key,
session)
_group_type_specs_query(context, group_type_id, session).\
filter_by(key=key).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
@require_context
def _group_type_specs_get_item(context, group_type_id, key,
session=None):
result = _group_type_specs_query(
context, group_type_id, session=session).\
filter_by(key=key).\
first()
if not result:
raise exception.GroupTypeSpecsNotFound(
group_specs_key=key,
group_type_id=group_type_id)
return result
@handle_db_data_error
@require_context
def group_type_specs_update_or_create(context, group_type_id,
specs):
session = get_session()
with session.begin():
spec_ref = None
for key, value in specs.items():
try:
spec_ref = _group_type_specs_get_item(
context, group_type_id, key, session)
except exception.GroupTypeSpecsNotFound:
spec_ref = models.GroupTypeSpecs()
spec_ref.update({"key": key, "value": value,
"group_type_id": group_type_id,
"deleted": False})
spec_ref.save(session=session)
return specs
####################
@require_admin_context
def qos_specs_create(context, values):
"""Create a new QoS specs.
@ -4921,7 +5397,9 @@ PAGINATION_HELPERS = {
_process_consistencygroups_filters,
_consistencygroup_get),
models.Message: (_messages_get_query, _process_messages_filters,
_message_get)
_message_get),
models.GroupTypes: (_group_type_get_query, _process_group_types_filters,
_group_type_get_db_object),
}
@ -5109,6 +5587,7 @@ def get_model_for_versioned_object(versioned_object):
'BackupImport': models.Backup,
'VolumeType': models.VolumeTypes,
'CGSnapshot': models.Cgsnapshot,
'GroupType': models.GroupTypes,
}
if isinstance(versioned_object, six.string_types):
@ -5127,6 +5606,7 @@ def _get_get_method(model):
models.ConsistencyGroup: consistencygroup_get,
models.VolumeTypes: _volume_type_get_full,
models.QualityOfServiceSpecs: qos_specs_get,
models.GroupTypes: _group_type_get_full,
}
if model in GET_EXCEPTIONS:

View File

@ -0,0 +1,75 @@
# 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.
from sqlalchemy import Boolean, Column, DateTime, Integer
from sqlalchemy import ForeignKey, MetaData, String, Table, UniqueConstraint
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
# New table
group_types = Table(
'group_types',
meta,
Column('id', String(36), primary_key=True, nullable=False),
Column('name', String(255), nullable=False),
Column('description', String(255)),
Column('created_at', DateTime(timezone=False)),
Column('updated_at', DateTime(timezone=False)),
Column('deleted_at', DateTime(timezone=False)),
Column('deleted', Boolean),
Column('is_public', Boolean),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
group_types.create()
# New table
group_type_specs = Table(
'group_type_specs',
meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('key', String(255)),
Column('value', String(255)),
Column('group_type_id', String(36),
ForeignKey('group_types.id'),
nullable=False),
Column('created_at', DateTime(timezone=False)),
Column('updated_at', DateTime(timezone=False)),
Column('deleted_at', DateTime(timezone=False)),
Column('deleted', Boolean),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
group_type_specs.create()
# New table
group_type_projects = Table(
'group_type_projects', meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('group_type_id', String(36),
ForeignKey('group_types.id')),
Column('project_id', String(length=255)),
Column('deleted', Boolean(create_constraint=True, name=None)),
UniqueConstraint('group_type_id', 'project_id', 'deleted'),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
group_type_projects.create()

View File

@ -323,6 +323,22 @@ class VolumeTypes(BASE, CinderBase):
'VolumeTypes.deleted == False)')
class GroupTypes(BASE, CinderBase):
"""Represent possible group_types of groups offered."""
__tablename__ = "group_types"
id = Column(String(36), primary_key=True)
name = Column(String(255))
description = Column(String(255))
is_public = Column(Boolean, default=True)
# TODO(xyang): Uncomment the following after groups table is added.
# groups = relationship(Group,
# backref=backref('group_type', uselist=False),
# foreign_keys=id,
# primaryjoin='and_('
# 'Group.group_type_id == GroupTypes.id, '
# 'GroupTypes.deleted == False)')
class VolumeTypeProjects(BASE, CinderBase):
"""Represent projects associated volume_types."""
__tablename__ = "volume_type_projects"
@ -345,6 +361,28 @@ class VolumeTypeProjects(BASE, CinderBase):
'VolumeTypeProjects.deleted == 0)')
class GroupTypeProjects(BASE, CinderBase):
"""Represent projects associated group_types."""
__tablename__ = "group_type_projects"
__table_args__ = (schema.UniqueConstraint(
"group_type_id", "project_id", "deleted",
name="uniq_group_type_projects0group_type_id0project_id0deleted"),
)
id = Column(Integer, primary_key=True)
group_type_id = Column(Integer, ForeignKey('group_types.id'),
nullable=False)
project_id = Column(String(255))
deleted = Column(Integer, default=0)
group_type = relationship(
GroupTypes,
backref="projects",
foreign_keys=group_type_id,
primaryjoin='and_('
'GroupTypeProjects.group_type_id == GroupTypes.id,'
'GroupTypeProjects.deleted == 0)')
class VolumeTypeExtraSpecs(BASE, CinderBase):
"""Represents additional specs as key/value pairs for a volume_type."""
__tablename__ = 'volume_type_extra_specs'
@ -364,6 +402,25 @@ class VolumeTypeExtraSpecs(BASE, CinderBase):
)
class GroupTypeSpecs(BASE, CinderBase):
"""Represents additional specs as key/value pairs for a group_type."""
__tablename__ = 'group_type_specs'
id = Column(Integer, primary_key=True)
key = Column(String(255))
value = Column(String(255))
group_type_id = Column(String(36),
ForeignKey('group_types.id'),
nullable=False)
group_type = relationship(
GroupTypes,
backref="group_specs",
foreign_keys=group_type_id,
primaryjoin='and_('
'GroupTypeSpecs.group_type_id == GroupTypes.id,'
'GroupTypeSpecs.deleted == False)'
)
class QualityOfServiceSpecs(BASE, CinderBase):
"""Represents QoS specs as key/value pairs.

View File

@ -204,6 +204,10 @@ class InvalidVolumeType(Invalid):
message = _("Invalid volume type: %(reason)s")
class InvalidGroupType(Invalid):
message = _("Invalid group type: %(reason)s")
class InvalidVolume(Invalid):
message = _("Invalid volume: %(reason)s")
@ -358,6 +362,30 @@ class VolumeTypeInUse(CinderException):
"volumes present with the type.")
class GroupTypeNotFound(NotFound):
message = _("Group type %(group_type_id)s could not be found.")
class GroupTypeNotFoundByName(GroupTypeNotFound):
message = _("Group type with name %(group_type_name)s "
"could not be found.")
class GroupTypeAccessNotFound(NotFound):
message = _("Group type access not found for %(group_type_id)s / "
"%(project_id)s combination.")
class GroupTypeSpecsNotFound(NotFound):
message = _("Group Type %(group_type_id)s has no specs with "
"key %(group_specs_key)s.")
class GroupTypeInUse(CinderException):
message = _("Group Type %(group_type_id)s deletion is not allowed with "
"groups present with the type.")
class SnapshotNotFound(NotFound):
message = _("Snapshot %(snapshot_id)s could not be found.")
@ -503,6 +531,23 @@ class VolumeTypeEncryptionNotFound(NotFound):
message = _("Volume type encryption for type %(type_id)s does not exist.")
class GroupTypeExists(Duplicate):
message = _("Group Type %(id)s already exists.")
class GroupTypeAccessExists(Duplicate):
message = _("Group type access for %(group_type_id)s / "
"%(project_id)s combination already exists.")
class GroupTypeEncryptionExists(Invalid):
message = _("Group type encryption for type %(type_id)s already exists.")
class GroupTypeEncryptionNotFound(NotFound):
message = _("Group type encryption for type %(type_id)s does not exist.")
class MalformedRequestBody(CinderException):
message = _("Malformed message body: %(reason)s")
@ -595,6 +640,15 @@ class VolumeTypeUpdateFailed(CinderException):
message = _("Cannot update volume_type %(id)s")
class GroupTypeCreateFailed(CinderException):
message = _("Cannot create group_type with "
"name %(name)s and specs %(group_specs)s")
class GroupTypeUpdateFailed(CinderException):
message = _("Cannot update group_type %(id)s")
class UnknownCmd(VolumeDriverException):
message = _("Unknown or unsupported command %(cmd)s")

View File

@ -35,3 +35,4 @@ def register_all():
__import__('cinder.objects.volume')
__import__('cinder.objects.volume_attachment')
__import__('cinder.objects.volume_type')
__import__('cinder.objects.group_type')

View File

@ -111,6 +111,7 @@ OBJ_VERSIONS.add('1.7', {'Cluster': '1.0', 'ClusterList': '1.0',
'Service': '1.4', 'Volume': '1.4',
'ConsistencyGroup': '1.3'})
OBJ_VERSIONS.add('1.8', {'RequestSpec': '1.0', 'VolumeProperties': '1.0'})
OBJ_VERSIONS.add('1.9', {'GroupType': '1.0', 'GroupTypeList': '1.0'})
class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -0,0 +1,121 @@
# Copyright 2016 EMC Corporation
#
# 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.
from oslo_versionedobjects import fields
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder.objects import base
from cinder.volume import group_types
OPTIONAL_FIELDS = ['group_specs', 'projects']
@base.CinderObjectRegistry.register
class GroupType(base.CinderPersistentObject, base.CinderObject,
base.CinderObjectDictCompat, base.CinderComparableObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.UUIDField(),
'name': fields.StringField(nullable=True),
'description': fields.StringField(nullable=True),
'is_public': fields.BooleanField(default=True, nullable=True),
'projects': fields.ListOfStringsField(nullable=True),
'group_specs': fields.DictOfNullableStringsField(nullable=True),
}
@classmethod
def _get_expected_attrs(cls, context):
return 'group_specs', 'projects'
@staticmethod
def _from_db_object(context, type, db_type, expected_attrs=None):
if expected_attrs is None:
expected_attrs = []
for name, field in type.fields.items():
if name in OPTIONAL_FIELDS:
continue
value = db_type[name]
if isinstance(field, fields.IntegerField):
value = value or 0
type[name] = value
# Get data from db_type object that was queried by joined query
# from DB
if 'group_specs' in expected_attrs:
type.group_specs = {}
specs = db_type.get('group_specs')
if specs and isinstance(specs, list):
type.group_specs = {item['key']: item['value']
for item in specs}
elif specs and isinstance(specs, dict):
type.group_specs = specs
if 'projects' in expected_attrs:
type.projects = db_type.get('projects', [])
type._context = context
type.obj_reset_changes()
return type
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason=_('already created'))
db_group_type = group_types.create(self._context, self.name,
self.group_specs,
self.is_public, self.projects,
self.description)
self._from_db_object(self._context, self, db_group_type)
def save(self):
updates = self.cinder_obj_get_changes()
if updates:
group_types.update(self._context, self.id, self.name,
self.description)
self.obj_reset_changes()
def destroy(self):
with self.obj_as_admin():
group_types.destroy(self._context, self.id)
@base.CinderObjectRegistry.register
class GroupTypeList(base.ObjectListBase, base.CinderObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'objects': fields.ListOfObjectsField('GroupType'),
}
child_versions = {
'1.0': '1.0',
}
@classmethod
def get_all(cls, context, inactive=0, filters=None, marker=None,
limit=None, sort_keys=None, sort_dirs=None, offset=None):
types = group_types.get_all_group_types(context, inactive, filters,
marker=marker, limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
offset=offset)
expected_attrs = GroupType._get_expected_attrs(context)
return base.obj_make_list(context, cls(context),
objects.GroupType, types.values(),
expected_attrs=expected_attrs)

View File

@ -0,0 +1,535 @@
# Copyright 2016 EMC Corporation
# 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.
import uuid
import mock
from oslo_utils import timeutils
import six
import webob
import cinder.api.common as common
from cinder.api.v3 import group_types as v3_group_types
from cinder.api.v3.views import group_types as views_types
from cinder import context
from cinder import exception
from cinder import test
from cinder.tests.unit.api import fakes
from cinder.tests.unit import fake_constants as fake
from cinder.volume import group_types
GROUP_TYPE_MICRO_VERSION = '3.11'
def stub_group_type(id):
specs = {
"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4": "value4",
"key5": "value5"
}
return dict(
id=id,
name='group_type_%s' % six.text_type(id),
description='group_type_desc_%s' % six.text_type(id),
group_specs=specs,
)
def return_group_types_get_all_types(context, filters=None, marker=None,
limit=None, sort_keys=None,
sort_dirs=None, offset=None,
list_result=False):
result = dict(group_type_1=stub_group_type(1),
group_type_2=stub_group_type(2),
group_type_3=stub_group_type(3)
)
if list_result:
return list(result.values())
return result
def return_empty_group_types_get_all_types(context, filters=None, marker=None,
limit=None, sort_keys=None,
sort_dirs=None, offset=None,
list_result=False):
if list_result:
return []
return {}
def return_group_types_get_group_type(context, id):
if id == fake.WILL_NOT_BE_FOUND_ID:
raise exception.GroupTypeNotFound(group_type_id=id)
return stub_group_type(id)
def return_group_types_get_default():
return stub_group_type(1)
def return_group_types_get_default_not_found():
return {}
class GroupTypesApiTest(test.TestCase):
def _create_group_type(self, group_type_name, group_specs=None,
is_public=True, projects=None):
return group_types.create(self.ctxt, group_type_name, group_specs,
is_public, projects).get('id')
def setUp(self):
super(GroupTypesApiTest, self).setUp()
self.controller = v3_group_types.GroupTypesController()
self.ctxt = context.RequestContext(user_id=fake.USER_ID,
project_id=fake.PROJECT_ID,
is_admin=True)
self.type_id1 = self._create_group_type('group_type1',
{'key1': 'value1'})
self.type_id2 = self._create_group_type('group_type2',
{'key2': 'value2'})
self.type_id3 = self._create_group_type('group_type3',
{'key3': 'value3'}, False,
[fake.PROJECT_ID])
def test_group_types_index(self):
self.stubs.Set(group_types, 'get_all_group_types',
return_group_types_get_all_types)
req = fakes.HTTPRequest.blank('/v3/%s/group_types' % fake.PROJECT_ID,
use_admin_context=True,
version=GROUP_TYPE_MICRO_VERSION)
res_dict = self.controller.index(req)
self.assertEqual(3, len(res_dict['group_types']))
expected_names = ['group_type_1', 'group_type_2', 'group_type_3']
actual_names = map(lambda e: e['name'], res_dict['group_types'])
self.assertEqual(set(expected_names), set(actual_names))
for entry in res_dict['group_types']:
self.assertEqual('value1', entry['group_specs']['key1'])
def test_group_types_index_no_data(self):
self.stubs.Set(group_types, 'get_all_group_types',
return_empty_group_types_get_all_types)
req = fakes.HTTPRequest.blank('/v3/%s/group_types' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
res_dict = self.controller.index(req)
self.assertEqual(0, len(res_dict['group_types']))
def test_group_types_index_with_limit(self):
req = fakes.HTTPRequest.blank('/v3/%s/group_types?limit=1' %
fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(1, len(res['group_types']))
self.assertEqual(self.type_id3, res['group_types'][0]['id'])
expect_next_link = ('http://localhost/v3/%s/group_types?limit=1'
'&marker=%s' %
(fake.PROJECT_ID, res['group_types'][0]['id']))
self.assertEqual(expect_next_link, res['group_type_links'][0]['href'])
def test_group_types_index_with_offset(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?offset=1' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(2, len(res['group_types']))
def test_group_types_index_with_offset_out_of_range(self):
url = '/v3/%s/group_types?offset=424366766556787' % fake.PROJECT_ID
req = fakes.HTTPRequest.blank(url, version=GROUP_TYPE_MICRO_VERSION)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
def test_group_types_index_with_limit_and_offset(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?limit=2&offset=1' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(2, len(res['group_types']))
self.assertEqual(self.type_id2, res['group_types'][0]['id'])
self.assertEqual(self.type_id1, res['group_types'][1]['id'])
def test_group_types_index_with_limit_and_marker(self):
req = fakes.HTTPRequest.blank('/v3/%s/group_types?limit=1'
'&marker=%s' %
(fake.PROJECT_ID,
self.type_id2),
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(1, len(res['group_types']))
self.assertEqual(self.type_id1, res['group_types'][0]['id'])
def test_group_types_index_with_valid_filter(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?is_public=True' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(3, len(res['group_types']))
self.assertEqual(self.type_id3, res['group_types'][0]['id'])
self.assertEqual(self.type_id2, res['group_types'][1]['id'])
self.assertEqual(self.type_id1, res['group_types'][2]['id'])
def test_group_types_index_with_invalid_filter(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?id=%s' % (fake.PROJECT_ID, self.type_id1),
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
self.assertEqual(3, len(res['group_types']))
def test_group_types_index_with_sort_keys(self):
req = fakes.HTTPRequest.blank('/v3/%s/group_types?sort=id' %
fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
expect_result = [self.type_id1, self.type_id2, self.type_id3]
expect_result.sort(reverse=True)
self.assertEqual(3, len(res['group_types']))
self.assertEqual(expect_result[0], res['group_types'][0]['id'])
self.assertEqual(expect_result[1], res['group_types'][1]['id'])
self.assertEqual(expect_result[2], res['group_types'][2]['id'])
def test_group_types_index_with_sort_and_limit(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?sort=id&limit=2' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
expect_result = [self.type_id1, self.type_id2, self.type_id3]
expect_result.sort(reverse=True)
self.assertEqual(2, len(res['group_types']))
self.assertEqual(expect_result[0], res['group_types'][0]['id'])
self.assertEqual(expect_result[1], res['group_types'][1]['id'])
def test_group_types_index_with_sort_keys_and_sort_dirs(self):
req = fakes.HTTPRequest.blank(
'/v3/%s/group_types?sort=id:asc' % fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.environ['cinder.context'] = self.ctxt
res = self.controller.index(req)
expect_result = [self.type_id1, self.type_id2, self.type_id3]
expect_result.sort()
self.assertEqual(3, len(res['group_types']))
self.assertEqual(expect_result[0], res['group_types'][0]['id'])
self.assertEqual(expect_result[1], res['group_types'][1]['id'])
self.assertEqual(expect_result[2], res['group_types'][2]['id'])
def test_group_types_show(self):
self.stubs.Set(group_types, 'get_group_type',
return_group_types_get_group_type)
type_id = six.text_type(uuid.uuid4())
req = fakes.HTTPRequest.blank('/v3/%s/group_types/' % fake.PROJECT_ID
+ type_id,
version=GROUP_TYPE_MICRO_VERSION)
res_dict = self.controller.show(req, type_id)
self.assertEqual(1, len(res_dict))
self.assertEqual(type_id, res_dict['group_type']['id'])
type_name = 'group_type_' + type_id
self.assertEqual(type_name, res_dict['group_type']['name'])
def test_group_types_show_pre_microversion(self):
self.stubs.Set(group_types, 'get_group_type',
return_group_types_get_group_type)
type_id = six.text_type(uuid.uuid4())
req = fakes.HTTPRequest.blank('/v3/%s/group_types/' % fake.PROJECT_ID
+ type_id,
version='3.5')
self.assertRaises(exception.VersionNotFoundForAPIMethod,
self.controller.show, req, type_id)
def test_group_types_show_not_found(self):
self.stubs.Set(group_types, 'get_group_type',
return_group_types_get_group_type)
req = fakes.HTTPRequest.blank('/v3/%s/group_types/%s' %
(fake.PROJECT_ID,
fake.WILL_NOT_BE_FOUND_ID),
version=GROUP_TYPE_MICRO_VERSION)
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, fake.WILL_NOT_BE_FOUND_ID)
def test_get_default(self):
self.stubs.Set(group_types, 'get_default_group_type',
return_group_types_get_default)
req = fakes.HTTPRequest.blank('/v3/%s/group_types/default' %
fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.method = 'GET'
res_dict = self.controller.show(req, 'default')
self.assertEqual(1, len(res_dict))
self.assertEqual('group_type_1', res_dict['group_type']['name'])
self.assertEqual('group_type_desc_1',
res_dict['group_type']['description'])
def test_get_default_not_found(self):
self.stubs.Set(group_types, 'get_default_group_type',
return_group_types_get_default_not_found)
req = fakes.HTTPRequest.blank('/v3/%s/group_types/default' %
fake.PROJECT_ID,
version=GROUP_TYPE_MICRO_VERSION)
req.method = 'GET'
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show, req, 'default')
def test_view_builder_show(self):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v3",
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.show(request, raw_group_type)
self.assertIn('group_type', output)
expected_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
id=42,
)
self.assertDictMatch(expected_group_type, output['group_type'])
def test_view_builder_show_admin(self):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v3", use_admin_context=True,
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.show(request, raw_group_type)
self.assertIn('group_type', output)
expected_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
group_specs={},
id=42,
)
self.assertDictMatch(expected_group_type, output['group_type'])
def __test_view_builder_show_qos_specs_id_policy(self):
with mock.patch.object(common,
'validate_policy',
side_effect=[False, True]):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v3",
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.show(request, raw_group_type)
self.assertIn('group_type', output)
expected_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
id=42,
)
self.assertDictMatch(expected_group_type, output['group_type'])
def test_view_builder_show_group_specs_policy(self):
with mock.patch.object(common,
'validate_policy',
side_effect=[True, False]):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v3",
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.show(request, raw_group_type)
self.assertIn('group_type', output)
expected_group_type = dict(
name='new_type',
description='new_type_desc',
group_specs={},
is_public=True,
id=42,
)
self.assertDictMatch(expected_group_type, output['group_type'])
def test_view_builder_show_pass_all_policy(self):
with mock.patch.object(common,
'validate_policy',
side_effect=[True, True]):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v3",
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.show(request, raw_group_type)
self.assertIn('group_type', output)
expected_group_type = dict(
name='new_type',
description='new_type_desc',
group_specs={},
is_public=True,
id=42,
)
self.assertDictMatch(expected_group_type, output['group_type'])
def test_view_builder_list(self):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_types = []
for i in range(0, 10):
raw_group_types.append(
dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42 + i
)
)
request = fakes.HTTPRequest.blank("/v3",
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.index(request, raw_group_types)
self.assertIn('group_types', output)
for i in range(0, 10):
expected_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
id=42 + i
)
self.assertDictMatch(expected_group_type,
output['group_types'][i])
def test_view_builder_list_admin(self):
view_builder = views_types.ViewBuilder()
now = timeutils.utcnow().isoformat()
raw_group_types = []
for i in range(0, 10):
raw_group_types.append(
dict(
name='new_type',
description='new_type_desc',
is_public=True,
deleted=False,
created_at=now,
updated_at=now,
group_specs={},
deleted_at=None,
id=42 + i
)
)
request = fakes.HTTPRequest.blank("/v3", use_admin_context=True,
version=GROUP_TYPE_MICRO_VERSION)
output = view_builder.index(request, raw_group_types)
self.assertIn('group_types', output)
for i in range(0, 10):
expected_group_type = dict(
name='new_type',
description='new_type_desc',
is_public=True,
group_specs={},
id=42 + i
)
self.assertDictMatch(expected_group_type,
output['group_types'][i])

View File

@ -71,3 +71,4 @@ VOLUME_TYPE3_ID = 'a3d55d15-eeb1-4816-ada9-bf82decc09b3'
VOLUME_TYPE4_ID = '69943076-754d-4da8-8718-0b0117e9cab1'
VOLUME_TYPE5_ID = '1c450d81-8aab-459e-b338-a6569139b835'
WILL_NOT_BE_FOUND_ID = 'ce816f65-c5aa-46d6-bd62-5272752d584a'
GROUP_TYPE_ID = '29514915-5208-46ab-9ece-1cc4688ad0c1'

View File

@ -0,0 +1,49 @@
# Copyright 2016 EMC Corporation
#
# 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.
from oslo_versionedobjects import fields
from cinder import objects
from cinder.tests.unit import fake_constants as fake
def fake_db_group_type(**updates):
db_group_type = {
'id': fake.GROUP_TYPE_ID,
'name': 'type-1',
'description': 'A fake group type',
'is_public': True,
'projects': [],
'group_specs': {},
}
for name, field in objects.GroupType.fields.items():
if name in db_group_type:
continue
if field.nullable:
db_group_type[name] = None
elif field.default != fields.UnspecifiedDefault:
db_group_type[name] = field.default
else:
raise Exception('fake_db_group_type needs help with %s.' % name)
if updates:
db_group_type.update(updates)
return db_group_type
def fake_group_type_obj(context, **updates):
return objects.GroupType._from_db_object(
context, objects.GroupType(), fake_db_group_type(**updates))

View File

@ -0,0 +1,127 @@
# Copyright 2016 EMC Corporation
#
# 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.
import mock
import six
from cinder import objects
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_group
from cinder.tests.unit import objects as test_objects
class TestGroupType(test_objects.BaseObjectsTestCase):
@mock.patch('cinder.db.sqlalchemy.api._group_type_get_full')
def test_get_by_id(self, group_type_get):
db_group_type = fake_group.fake_db_group_type()
group_type_get.return_value = db_group_type
group_type = objects.GroupType.get_by_id(self.context,
fake.GROUP_TYPE_ID)
self._compare(self, db_group_type, group_type)
@mock.patch('cinder.volume.group_types.create')
def test_create(self, group_type_create):
db_group_type = fake_group.fake_db_group_type()
group_type_create.return_value = db_group_type
group_type = objects.GroupType(context=self.context)
group_type.name = db_group_type['name']
group_type.group_specs = db_group_type['group_specs']
group_type.is_public = db_group_type['is_public']
group_type.projects = db_group_type['projects']
group_type.description = db_group_type['description']
group_type.create()
group_type_create.assert_called_once_with(
self.context, db_group_type['name'],
db_group_type['group_specs'], db_group_type['is_public'],
db_group_type['projects'], db_group_type['description'])
@mock.patch('cinder.volume.group_types.update')
def test_save(self, group_type_update):
db_group_type = fake_group.fake_db_group_type()
group_type = objects.GroupType._from_db_object(self.context,
objects.GroupType(),
db_group_type)
group_type.description = 'foobar'
group_type.save()
group_type_update.assert_called_once_with(self.context,
group_type.id,
group_type.name,
group_type.description)
@mock.patch('cinder.volume.group_types.destroy')
def test_destroy(self, group_type_destroy):
db_group_type = fake_group.fake_db_group_type()
group_type = objects.GroupType._from_db_object(self.context,
objects.GroupType(),
db_group_type)
group_type.destroy()
self.assertTrue(group_type_destroy.called)
admin_context = group_type_destroy.call_args[0][0]
self.assertTrue(admin_context.is_admin)
@mock.patch('cinder.db.sqlalchemy.api._group_type_get_full')
def test_refresh(self, group_type_get):
db_type1 = fake_group.fake_db_group_type()
db_type2 = db_type1.copy()
db_type2['description'] = 'foobar'
# updated description
group_type_get.side_effect = [db_type1, db_type2]
group_type = objects.GroupType.get_by_id(self.context,
fake.GROUP_TYPE_ID)
self._compare(self, db_type1, group_type)
# description was updated, so a group type refresh should have a new
# value for that field
group_type.refresh()
self._compare(self, db_type2, group_type)
if six.PY3:
call_bool = mock.call.__bool__()
else:
call_bool = mock.call.__nonzero__()
group_type_get.assert_has_calls([mock.call(self.context,
fake.GROUP_TYPE_ID),
call_bool,
mock.call(self.context,
fake.GROUP_TYPE_ID)])
class TestGroupTypeList(test_objects.BaseObjectsTestCase):
@mock.patch('cinder.volume.group_types.get_all_group_types')
def test_get_all(self, get_all_types):
db_group_type = fake_group.fake_db_group_type()
get_all_types.return_value = {db_group_type['name']: db_group_type}
group_types = objects.GroupTypeList.get_all(self.context)
self.assertEqual(1, len(group_types))
TestGroupType._compare(self, db_group_type, group_types[0])
@mock.patch('cinder.volume.group_types.get_all_group_types')
def test_get_all_with_pagination(self, get_all_types):
db_group_type = fake_group.fake_db_group_type()
get_all_types.return_value = {db_group_type['name']: db_group_type}
group_types = objects.GroupTypeList.get_all(self.context,
filters={'is_public':
True},
marker=None,
limit=1,
sort_keys='id',
sort_dirs='desc',
offset=None)
self.assertEqual(1, len(group_types))
TestGroupType._compare(self, db_group_type, group_types[0])

View File

@ -46,6 +46,8 @@ object_data = {
'VolumeProperties': '1.0-42f00cf1f6c657377a3e2a7efbed0bca',
'VolumeType': '1.2-02ecb0baac87528d041f4ddd95b95579',
'VolumeTypeList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'GroupType': '1.0-d4a7b272199d0b0d6fc3ceed58539d30',
'GroupTypeList': '1.0-1b54e51ad0fc1f3a8878f5010e7e16dc',
}

View File

@ -113,6 +113,11 @@
"consistencygroup:get_cgsnapshot": "",
"consistencygroup:get_all_cgsnapshots": "",
"group:group_types_manage": "rule:admin_api",
"group:group_types_specs": "rule:admin_api",
"group:access_group_types_specs": "rule:admin_api",
"group:group_type_access": "rule:admin_or_owner",
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner",

View File

@ -878,6 +878,69 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
self.assertIsInstance(columns.status.type, self.VARCHAR_TYPE)
self.assertIsInstance(columns.service_id.type, self.INTEGER_TYPE)
def _check_077(self, engine, data):
"""Test adding group types and specs tables."""
self.assertTrue(engine.dialect.has_table(engine.connect(),
"group_types"))
group_types = db_utils.get_table(engine, 'group_types')
self.assertIsInstance(group_types.c.id.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_types.c.name.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_types.c.description.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_types.c.created_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_types.c.updated_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_types.c.deleted_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_types.c.deleted.type,
self.BOOL_TYPE)
self.assertIsInstance(group_types.c.is_public.type,
self.BOOL_TYPE)
self.assertTrue(engine.dialect.has_table(engine.connect(),
"group_type_specs"))
group_specs = db_utils.get_table(engine, 'group_type_specs')
self.assertIsInstance(group_specs.c.id.type,
self.INTEGER_TYPE)
self.assertIsInstance(group_specs.c.key.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_specs.c.value.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_specs.c.group_type_id.type,
self.VARCHAR_TYPE)
self.assertIsInstance(group_specs.c.created_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_specs.c.updated_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_specs.c.deleted_at.type,
self.TIME_TYPE)
self.assertIsInstance(group_specs.c.deleted.type,
self.BOOL_TYPE)
self.assertTrue(engine.dialect.has_table(engine.connect(),
"group_type_projects"))
type_projects = db_utils.get_table(engine, 'group_type_projects')
self.assertIsInstance(type_projects.c.id.type,
self.INTEGER_TYPE)
self.assertIsInstance(type_projects.c.created_at.type,
self.TIME_TYPE)
self.assertIsInstance(type_projects.c.updated_at.type,
self.TIME_TYPE)
self.assertIsInstance(type_projects.c.deleted_at.type,
self.TIME_TYPE)
self.assertIsInstance(type_projects.c.deleted.type,
self.BOOL_TYPE)
self.assertIsInstance(type_projects.c.group_type_id.type,
self.VARCHAR_TYPE)
self.assertIsInstance(type_projects.c.project_id.type,
self.VARCHAR_TYPE)
def test_walk_versions(self):
self.walk_versions(False, False)

View File

@ -0,0 +1,180 @@
# Copyright (c) 2016 EMC Corporation
#
# 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 group type properties."""
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from cinder import context
from cinder import db
from cinder import exception
from cinder.i18n import _, _LE
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def create(context,
name,
group_specs=None,
is_public=True,
projects=None,
description=None):
"""Creates group types."""
group_specs = group_specs or {}
projects = projects or []
elevated = context if context.is_admin else context.elevated()
try:
type_ref = db.group_type_create(elevated,
dict(name=name,
group_specs=group_specs,
is_public=is_public,
description=description),
projects=projects)
except db_exc.DBError:
LOG.exception(_LE('DB error:'))
raise exception.GroupTypeCreateFailed(name=name,
group_specs=group_specs)
return type_ref
def update(context, id, name, description, is_public=None):
"""Update group type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidGroupType(reason=msg)
elevated = context if context.is_admin else context.elevated()
try:
type_updated = db.group_type_update(elevated,
id,
dict(name=name,
description=description,
is_public=is_public))
except db_exc.DBError:
LOG.exception(_LE('DB error:'))
raise exception.GroupTypeUpdateFailed(id=id)
return type_updated
def destroy(context, id):
"""Marks group types as deleted."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidGroupType(reason=msg)
else:
elevated = context if context.is_admin else context.elevated()
db.group_type_destroy(elevated, id)
def get_all_group_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 group_types.
Pass true as argument if you want deleted group types returned also.
"""
grp_types = db.group_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 grp_types
def get_group_type(ctxt, id, expected_fields=None):
"""Retrieves single group type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidGroupType(reason=msg)
if ctxt is None:
ctxt = context.get_admin_context()
return db.group_type_get(ctxt, id, expected_fields=expected_fields)
def get_group_type_by_name(context, name):
"""Retrieves single group type by name."""
if name is None:
msg = _("name cannot be None")
raise exception.InvalidGroupType(reason=msg)
return db.group_type_get_by_name(context, name)
def get_default_group_type():
"""Get the default group type."""
name = CONF.default_group_type
grp_type = {}
if name is not None:
ctxt = context.get_admin_context()
try:
grp_type = get_group_type_by_name(ctxt, name)
except exception.GroupTypeNotFoundByName:
# Couldn't find group type with the name in default_group_type
# flag, record this issue and move on
LOG.exception(_LE('Default group type is not found. '
'Please check default_group_type config.'))
return grp_type
def get_group_type_specs(group_type_id, key=False):
group_type = get_group_type(context.get_admin_context(),
group_type_id)
group_specs = group_type['group_specs']
if key:
if group_specs.get(key):
return group_specs.get(key)
else:
return False
else:
return group_specs
def is_public_group_type(context, group_type_id):
"""Return is_public boolean value of group type"""
group_type = db.group_type_get(context, group_type_id)
return group_type['is_public']
def add_group_type_access(context, group_type_id, project_id):
"""Add access to group type for project_id."""
if group_type_id is None:
msg = _("group_type_id cannot be None")
raise exception.InvalidGroupType(reason=msg)
elevated = context if context.is_admin else context.elevated()
if is_public_group_type(elevated, group_type_id):
msg = _("Type access modification is not applicable to public group "
"type.")
raise exception.InvalidGroupType(reason=msg)
return db.group_type_access_add(elevated, group_type_id, project_id)
def remove_group_type_access(context, group_type_id, project_id):
"""Remove access to group type for project_id."""
if group_type_id is None:
msg = _("group_type_id cannot be None")
raise exception.InvalidGroupType(reason=msg)
elevated = context if context.is_admin else context.elevated()
if is_public_group_type(elevated, group_type_id):
msg = _("Type access modification is not applicable to public group "
"type.")
raise exception.InvalidGroupType(reason=msg)
return db.group_type_access_remove(elevated, group_type_id, project_id)

View File

@ -109,6 +109,11 @@
"consistencygroup:get_cgsnapshot": "group:nobody",
"consistencygroup:get_all_cgsnapshots": "group:nobody",
"group:group_types_manage": "rule:admin_api",
"group:group_types_specs": "rule:admin_api",
"group:access_group_types_specs": "rule:admin_api",
"group:group_type_access": "rule:admin_or_owner",
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner",
"message:get": "rule:admin_or_owner",

View File

@ -0,0 +1,3 @@
---
features:
- Added group type and group specs APIs.