Merge "Add group type and group specs"
This commit is contained in:
commit
0db18b8351
@ -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"
|
||||
|
||||
|
@ -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.
|
||||
|
149
cinder/api/v3/group_specs.py
Normal file
149
cinder/api/v3/group_specs.py
Normal 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())
|
258
cinder/api/v3/group_types.py
Normal file
258
cinder/api/v3/group_types.py
Normal 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())
|
@ -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'],
|
||||
|
43
cinder/api/v3/views/group_types.py
Normal file
43
cinder/api/v3/views/group_types.py
Normal 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
|
@ -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. '
|
||||
|
114
cinder/db/api.py
114
cinder/db/api.py
@ -621,6 +621,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)
|
||||
@ -647,6 +735,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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
@ -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.
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
121
cinder/objects/group_type.py
Normal file
121
cinder/objects/group_type.py
Normal 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)
|
535
cinder/tests/unit/api/v3/test_group_types.py
Normal file
535
cinder/tests/unit/api/v3/test_group_types.py
Normal 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])
|
@ -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'
|
||||
|
49
cinder/tests/unit/fake_group.py
Normal file
49
cinder/tests/unit/fake_group.py
Normal 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))
|
127
cinder/tests/unit/objects/test_group_type.py
Normal file
127
cinder/tests/unit/objects/test_group_type.py
Normal 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])
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
||||
|
180
cinder/volume/group_types.py
Normal file
180
cinder/volume/group_types.py
Normal 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)
|
@ -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",
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added group type and group specs APIs.
|
Loading…
Reference in New Issue
Block a user