diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 94c856876c9..bc656fee2cd 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -61,6 +61,7 @@ REST_API_VERSION_HISTORY = """ * 3.11 - Add group types and group specs API. * 3.12 - Add volumes summary API. * 3.13 - Add generic volume groups API. + * 3.14 - Add group snapshot and create group from src APIs. """ @@ -69,7 +70,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.13" +_MAX_API_VERSION = "3.14" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 75bae9f1ff8..28d3487351d 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -182,3 +182,7 @@ user documentation. 3.13 ---- Added create/delete/update/list/show APIs for generic volume groups. + +3.14 +---- + Added group snapshots and create group from src APIs. diff --git a/cinder/api/v3/group_snapshots.py b/cinder/api/v3/group_snapshots.py new file mode 100644 index 00000000000..0da9dd3ecfc --- /dev/null +++ b/cinder/api/v3/group_snapshots.py @@ -0,0 +1,146 @@ +# Copyright (C) 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. + +"""The group_snapshots api.""" + +from oslo_log import log as logging +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_snapshots as group_snapshot_views +from cinder import exception +from cinder import group as group_api +from cinder.i18n import _, _LI + +LOG = logging.getLogger(__name__) + +GROUP_SNAPSHOT_API_VERSION = '3.14' + + +class GroupSnapshotsController(wsgi.Controller): + """The group_snapshots API controller for the OpenStack API.""" + + _view_builder_class = group_snapshot_views.ViewBuilder + + def __init__(self): + self.group_snapshot_api = group_api.API() + super(GroupSnapshotsController, self).__init__() + + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) + def show(self, req, id): + """Return data about the given group_snapshot.""" + LOG.debug('show called for member %s', id) + context = req.environ['cinder.context'] + + group_snapshot = self.group_snapshot_api.get_group_snapshot( + context, + group_snapshot_id=id) + + return self._view_builder.detail(req, group_snapshot) + + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) + def delete(self, req, id): + """Delete a group_snapshot.""" + LOG.debug('delete called for member %s', id) + context = req.environ['cinder.context'] + + LOG.info(_LI('Delete group_snapshot with id: %s'), id, context=context) + + try: + group_snapshot = self.group_snapshot_api.get_group_snapshot( + context, + group_snapshot_id=id) + self.group_snapshot_api.delete_group_snapshot(context, + group_snapshot) + except exception.InvalidGroupSnapshot as e: + raise exc.HTTPBadRequest(explanation=six.text_type(e)) + except exception.GroupSnapshotNotFound: + # Not found exception will be handled at the wsgi level + raise + except Exception: + msg = _("Error occurred when deleting group snapshot %s.") % id + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) + + return webob.Response(status_int=202) + + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) + def index(self, req): + """Returns a summary list of group_snapshots.""" + return self._get_group_snapshots(req, is_detail=False) + + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) + def detail(self, req): + """Returns a detailed list of group_snapshots.""" + return self._get_group_snapshots(req, is_detail=True) + + def _get_group_snapshots(self, req, is_detail): + """Returns a list of group_snapshots through view builder.""" + context = req.environ['cinder.context'] + group_snapshots = self.group_snapshot_api.get_all_group_snapshots( + context) + limited_list = common.limited(group_snapshots, req) + + if is_detail: + group_snapshots = self._view_builder.detail_list(req, limited_list) + else: + group_snapshots = self._view_builder.summary_list(req, + limited_list) + return group_snapshots + + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) + @wsgi.response(202) + def create(self, req, body): + """Create a new group_snapshot.""" + LOG.debug('Creating new group_snapshot %s', body) + self.assert_valid_body(body, 'group_snapshot') + + context = req.environ['cinder.context'] + group_snapshot = body['group_snapshot'] + self.validate_name_and_description(group_snapshot) + + try: + group_id = group_snapshot['group_id'] + except KeyError: + msg = _("'group_id' must be specified") + raise exc.HTTPBadRequest(explanation=msg) + + group = self.group_snapshot_api.get(context, group_id) + + name = group_snapshot.get('name', None) + description = group_snapshot.get('description', None) + + LOG.info(_LI("Creating group_snapshot %(name)s."), + {'name': name}, + context=context) + + try: + new_group_snapshot = self.group_snapshot_api.create_group_snapshot( + context, group, name, description) + except (exception.InvalidGroup, + exception.InvalidGroupSnapshot, + exception.InvalidVolume) as error: + raise exc.HTTPBadRequest(explanation=error.msg) + + retval = self._view_builder.summary(req, new_group_snapshot) + + return retval + + +def create_resource(): + return wsgi.Resource(GroupSnapshotsController()) diff --git a/cinder/api/v3/groups.py b/cinder/api/v3/groups.py index 4d7a5c5b5be..95f944543d6 100644 --- a/cinder/api/v3/groups.py +++ b/cinder/api/v3/groups.py @@ -29,6 +29,7 @@ from cinder.i18n import _, _LI LOG = logging.getLogger(__name__) GROUP_API_VERSION = '3.13' +GROUP_CREATE_FROM_SRC_API_VERSION = '3.14' class GroupsController(wsgi.Controller): @@ -163,6 +164,63 @@ class GroupsController(wsgi.Controller): retval = self._view_builder.summary(req, new_group) return retval + @wsgi.Controller.api_version(GROUP_CREATE_FROM_SRC_API_VERSION) + @wsgi.action("create-from-src") + @wsgi.response(202) + def create_from_src(self, req, body): + """Create a new group from a source. + + The source can be a group snapshot or a group. Note that + this does not require group_type and volume_types as the + "create" API above. + """ + LOG.debug('Creating new group %s.', body) + self.assert_valid_body(body, 'create-from-src') + + context = req.environ['cinder.context'] + group = body['create-from-src'] + self.validate_name_and_description(group) + name = group.get('name', None) + description = group.get('description', None) + group_snapshot_id = group.get('group_snapshot_id', None) + source_group_id = group.get('source_group_id', None) + if not group_snapshot_id and not source_group_id: + msg = (_("Either 'group_snapshot_id' or 'source_group_id' must be " + "provided to create group %(name)s from source.") + % {'name': name}) + raise exc.HTTPBadRequest(explanation=msg) + + if group_snapshot_id and source_group_id: + msg = _("Cannot provide both 'group_snapshot_id' and " + "'source_group_id' to create group %(name)s from " + "source.") % {'name': name} + raise exc.HTTPBadRequest(explanation=msg) + + if group_snapshot_id: + LOG.info(_LI("Creating group %(name)s from group_snapshot " + "%(snap)s."), + {'name': name, 'snap': group_snapshot_id}, + context=context) + elif source_group_id: + LOG.info(_LI("Creating group %(name)s from " + "source group %(source_group_id)s."), + {'name': name, 'source_group_id': source_group_id}, + context=context) + + try: + new_group = self.group_api.create_from_src( + context, name, description, group_snapshot_id, source_group_id) + except exception.InvalidGroup as error: + raise exc.HTTPBadRequest(explanation=error.msg) + except (exception.GroupNotFound, exception.GroupSnapshotNotFound): + # Not found exception will be handled at the wsgi level + raise + except exception.CinderException as error: + raise exc.HTTPBadRequest(explanation=error.msg) + + retval = self._view_builder.summary(req, new_group) + return retval + @wsgi.Controller.api_version(GROUP_API_VERSION) def update(self, req, id, body): """Update the group. diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 75ad4fbb42d..d771b8a3295 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -23,17 +23,18 @@ from cinder.api import extensions import cinder.api.openstack from cinder.api.v2 import limits from cinder.api.v2 import snapshot_metadata -from cinder.api.v2 import snapshots from cinder.api.v2 import types 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_snapshots from cinder.api.v3 import group_specs from cinder.api.v3 import group_types from cinder.api.v3 import groups from cinder.api.v3 import messages from cinder.api.v3 import snapshot_manage +from cinder.api.v3 import snapshots from cinder.api.v3 import volume_manage from cinder.api.v3 import volumes from cinder.api import versions @@ -93,6 +94,17 @@ class APIRouter(cinder.api.openstack.APIRouter): controller=self.resources["groups"], action="action", conditions={"action": ["POST"]}) + mapper.connect("groups/action", + "/{project_id}/groups/action", + controller=self.resources["groups"], + action="action", + conditions={"action": ["POST"]}) + + self.resources['group_snapshots'] = (group_snapshots.create_resource()) + mapper.resource("group_snapshot", "group_snapshots", + controller=self.resources['group_snapshots'], + collection={'detail': 'GET'}, + member={'action': 'POST'}) self.resources['snapshots'] = snapshots.create_resource(ext_mgr) mapper.resource("snapshot", "snapshots", diff --git a/cinder/api/v3/snapshots.py b/cinder/api/v3/snapshots.py new file mode 100644 index 00000000000..a46fcac413b --- /dev/null +++ b/cinder/api/v3/snapshots.py @@ -0,0 +1,30 @@ +# 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. + +"""The volumes snapshots V3 api.""" + +from cinder.api.openstack import wsgi +from cinder.api.v2 import snapshots as snapshots_v2 +from cinder.api.v3.views import snapshots as snapshot_views + + +class SnapshotsController(snapshots_v2.SnapshotsController): + """The Snapshots API controller for the OpenStack API.""" + + _view_builder_class = snapshot_views.ViewBuilder + + +def create_resource(ext_mgr): + return wsgi.Resource(SnapshotsController(ext_mgr)) diff --git a/cinder/api/v3/views/group_snapshots.py b/cinder/api/v3/views/group_snapshots.py new file mode 100644 index 00000000000..b3411fd05ba --- /dev/null +++ b/cinder/api/v3/views/group_snapshots.py @@ -0,0 +1,64 @@ +# Copyright (C) 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): + """Model group_snapshot API responses as a python dictionary.""" + + _collection_name = "group_snapshots" + + def __init__(self): + """Initialize view builder.""" + super(ViewBuilder, self).__init__() + + def summary_list(self, request, group_snapshots): + """Show a list of group_snapshots without many details.""" + return self._list_view(self.summary, request, group_snapshots) + + def detail_list(self, request, group_snapshots): + """Detailed view of a list of group_snapshots .""" + return self._list_view(self.detail, request, group_snapshots) + + def summary(self, request, group_snapshot): + """Generic, non-detailed view of a group_snapshot.""" + return { + 'group_snapshot': { + 'id': group_snapshot.id, + 'name': group_snapshot.name + } + } + + def detail(self, request, group_snapshot): + """Detailed view of a single group_snapshot.""" + return { + 'group_snapshot': { + 'id': group_snapshot.id, + 'group_id': group_snapshot.group_id, + 'status': group_snapshot.status, + 'created_at': group_snapshot.created_at, + 'name': group_snapshot.name, + 'description': group_snapshot.description + } + } + + def _list_view(self, func, request, group_snapshots): + """Provide a view for a list of group_snapshots.""" + group_snapshots_list = [func(request, group_snapshot)['group_snapshot'] + for group_snapshot in group_snapshots] + group_snapshots_dict = dict(group_snapshots=group_snapshots_list) + + return group_snapshots_dict diff --git a/cinder/api/v3/views/groups.py b/cinder/api/v3/views/groups.py index 661d232f968..1d012804b02 100644 --- a/cinder/api/v3/views/groups.py +++ b/cinder/api/v3/views/groups.py @@ -44,7 +44,7 @@ class ViewBuilder(common.ViewBuilder): def detail(self, request, group): """Detailed view of a single group.""" - return { + group_ref = { 'group': { 'id': group.id, 'status': group.status, @@ -57,6 +57,15 @@ class ViewBuilder(common.ViewBuilder): } } + req_version = request.api_version_request + # Add group_snapshot_id and source_group_id if min version is greater + # than or equal to 3.14. + if req_version.matches("3.14", None): + group_ref['group']['group_snapshot_id'] = group.group_snapshot_id + group_ref['group']['source_group_id'] = group.source_group_id + + return group_ref + def _list_view(self, func, request, groups): """Provide a view for a list of groups.""" groups_list = [ diff --git a/cinder/api/v3/views/snapshots.py b/cinder/api/v3/views/snapshots.py new file mode 100644 index 00000000000..1f3e3b79fdd --- /dev/null +++ b/cinder/api/v3/views/snapshots.py @@ -0,0 +1,33 @@ +# 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.views import snapshots as views_v2 + + +class ViewBuilder(views_v2.ViewBuilder): + """Model a snapshots API V3 response as a python dictionary.""" + + def detail(self, request, snapshot): + """Detailed view of a single snapshot.""" + snapshot_ref = super(ViewBuilder, self).detail(request, snapshot) + + req_version = request.api_version_request + # Add group_snapshot_id if min version is greater than or equal + # to 3.14. + if req_version.matches("3.14", None): + snapshot_ref['snapshot']['group_snapshot_id'] = ( + snapshot.get('group_snapshot_id')) + + return snapshot_ref diff --git a/cinder/db/api.py b/cinder/db/api.py index fb2c7e809fc..263eda82ee9 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -428,9 +428,9 @@ def snapshot_get_all_for_cgsnapshot(context, project_id): return IMPL.snapshot_get_all_for_cgsnapshot(context, project_id) -def snapshot_get_all_for_group_snapshot(context, project_id): +def snapshot_get_all_for_group_snapshot(context, group_snapshot_id): """Get all snapshots belonging to a group snapshot.""" - return IMPL.snapshot_get_all_for_group_snapshot(context, project_id) + return IMPL.snapshot_get_all_for_group_snapshot(context, group_snapshot_id) def snapshot_get_all_for_volume(context, volume_id): diff --git a/cinder/group/api.py b/cinder/group/api.py index 924f512c31a..eac10495a6d 100644 --- a/cinder/group/api.py +++ b/cinder/group/api.py @@ -26,9 +26,10 @@ from oslo_utils import excutils from oslo_utils import timeutils from cinder.common import constants +from cinder import db from cinder.db import base from cinder import exception -from cinder.i18n import _, _LE, _LW +from cinder.i18n import _, _LE, _LI, _LW from cinder import objects from cinder.objects import base as objects_base from cinder.objects import fields as c_fields @@ -180,6 +181,212 @@ class API(base.Base): return group + def create_from_src(self, context, name, description=None, + group_snapshot_id=None, source_group_id=None): + check_policy(context, 'create') + + kwargs = { + 'user_id': context.user_id, + 'project_id': context.project_id, + 'status': c_fields.GroupStatus.CREATING, + 'name': name, + 'description': description, + 'group_snapshot_id': group_snapshot_id, + 'source_group_id': source_group_id, + } + + group = None + try: + group = objects.Group(context=context, **kwargs) + group.create(group_snapshot_id=group_snapshot_id, + source_group_id=source_group_id) + except exception.GroupNotFound: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Source Group %(source_group)s not found when " + "creating group %(group)s from " + "source."), + {'group': name, 'source_group': source_group_id}) + except exception.GroupSnapshotNotFound: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Group snapshot %(group_snap)s not found when " + "creating group %(group)s from source."), + {'group': name, 'group_snap': group_snapshot_id}) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error occurred when creating group" + " %(group)s from group_snapshot %(grp_snap)s."), + {'group': name, 'grp_snap': group_snapshot_id}) + + # Update quota for groups + self.update_quota(context, group, 1) + + if not group.host: + msg = _("No host to create group %s.") % group.id + LOG.error(msg) + raise exception.InvalidGroup(reason=msg) + + if group_snapshot_id: + self._create_group_from_group_snapshot(context, group, + group_snapshot_id) + elif source_group_id: + self._create_group_from_source_group(context, group, + source_group_id) + + return group + + def _create_group_from_group_snapshot(self, context, group, + group_snapshot_id): + try: + group_snapshot = objects.GroupSnapshot.get_by_id( + context, group_snapshot_id) + snapshots = objects.SnapshotList.get_all_for_group_snapshot( + context, group_snapshot.id) + + if not snapshots: + msg = _("Group snapshot is empty. No group will be created.") + raise exception.InvalidGroup(reason=msg) + + for snapshot in snapshots: + kwargs = {} + kwargs['availability_zone'] = group.availability_zone + kwargs['group_snapshot'] = group_snapshot + kwargs['group'] = group + kwargs['snapshot'] = snapshot + volume_type_id = snapshot.volume_type_id + if volume_type_id: + kwargs['volume_type'] = volume_types.get_volume_type( + context, volume_type_id) + # Create group volume_type mapping entries + try: + db.group_volume_type_mapping_create(context, group.id, + volume_type_id) + except exception.GroupVolumeTypeMappingExists: + # Only need to create one group volume_type mapping + # entry for the same combination, skipping. + LOG.info(_LI("A mapping entry already exists for group" + " %(grp)s and volume type %(vol_type)s. " + "Do not need to create again."), + {'grp': group.id, + 'vol_type': volume_type_id}) + pass + + # Since group snapshot is passed in, the following call will + # create a db entry for the volume, but will not call the + # volume manager to create a real volume in the backend yet. + # If error happens, taskflow will handle rollback of quota + # and removal of volume entry in the db. + try: + self.volume_api.create(context, + snapshot.volume_size, + None, + None, + **kwargs) + except exception.CinderException: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error occurred when creating volume " + "entry from snapshot in the process of " + "creating group %(group)s " + "from group snapshot %(group_snap)s."), + {'group': group.id, + 'group_snap': group_snapshot.id}) + except Exception: + with excutils.save_and_reraise_exception(): + try: + group.destroy() + finally: + LOG.error(_LE("Error occurred when creating group " + "%(group)s from group snapshot " + "%(group_snap)s."), + {'group': group.id, + 'group_snap': group_snapshot.id}) + + volumes = objects.VolumeList.get_all_by_generic_group(context, + group.id) + for vol in volumes: + # Update the host field for the volume. + vol.host = group.host + vol.save() + + self.volume_rpcapi.create_group_from_src( + context, group, group_snapshot) + + def _create_group_from_source_group(self, context, group, + source_group_id): + try: + source_group = objects.Group.get_by_id(context, + source_group_id) + source_vols = objects.VolumeList.get_all_by_generic_group( + context, source_group.id) + + if not source_vols: + msg = _("Source Group is empty. No group " + "will be created.") + raise exception.InvalidGroup(reason=msg) + + for source_vol in source_vols: + kwargs = {} + kwargs['availability_zone'] = group.availability_zone + kwargs['source_group'] = source_group + kwargs['group'] = group + kwargs['source_volume'] = source_vol + volume_type_id = source_vol.volume_type_id + if volume_type_id: + kwargs['volume_type'] = volume_types.get_volume_type( + context, volume_type_id) + # Create group volume_type mapping entries + try: + db.group_volume_type_mapping_create(context, group.id, + volume_type_id) + except exception.GroupVolumeTypeMappingExists: + # Only need to create one group volume_type mapping + # entry for the same combination, skipping. + LOG.info(_LI("A mapping entry already exists for group" + " %(grp)s and volume type %(vol_type)s. " + "Do not need to create again."), + {'grp': group.id, + 'vol_type': volume_type_id}) + pass + + # Since source_group is passed in, the following call will + # create a db entry for the volume, but will not call the + # volume manager to create a real volume in the backend yet. + # If error happens, taskflow will handle rollback of quota + # and removal of volume entry in the db. + try: + self.volume_api.create(context, + source_vol.size, + None, + None, + **kwargs) + except exception.CinderException: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error occurred when creating cloned " + "volume in the process of creating " + "group %(group)s from " + "source group %(source_group)s."), + {'group': group.id, + 'source_group': source_group.id}) + except Exception: + with excutils.save_and_reraise_exception(): + try: + group.destroy() + finally: + LOG.error(_LE("Error occurred when creating " + "group %(group)s from source group " + "%(source_group)s."), + {'group': group.id, + 'source_group': source_group.id}) + + volumes = objects.VolumeList.get_all_by_generic_group(context, + group.id) + for vol in volumes: + # Update the host field for the volume. + vol.host = group.host + vol.save() + + self.volume_rpcapi.create_group_from_src(context, group, + None, source_group) + def _cast_create_group(self, context, group, group_spec, request_spec_list, @@ -542,3 +749,90 @@ class API(base.Base): limit=limit, offset=offset, sort_keys=sort_keys, sort_dirs=sort_dirs) return groups + + def create_group_snapshot(self, context, group, name, description): + options = {'group_id': group.id, + 'user_id': context.user_id, + 'project_id': context.project_id, + 'status': "creating", + 'name': name, + 'description': description} + + group_snapshot = None + group_snapshot_id = None + try: + group_snapshot = objects.GroupSnapshot(context, **options) + group_snapshot.create() + group_snapshot_id = group_snapshot.id + + snap_name = group_snapshot.name + snap_desc = group_snapshot.description + with group.obj_as_admin(): + self.volume_api.create_snapshots_in_db( + context, group.volumes, snap_name, snap_desc, + None, group_snapshot_id) + + except Exception: + with excutils.save_and_reraise_exception(): + try: + # If the group_snapshot has been created + if group_snapshot.obj_attr_is_set('id'): + group_snapshot.destroy() + finally: + LOG.error(_LE("Error occurred when creating group_snapshot" + " %s."), group_snapshot_id) + + self.volume_rpcapi.create_group_snapshot(context, group_snapshot) + + return group_snapshot + + def delete_group_snapshot(self, context, group_snapshot, force=False): + check_policy(context, 'delete_group_snapshot') + values = {'status': 'deleting'} + expected = {'status': ('available', 'error')} + filters = [~db.group_creating_from_src( + group_snapshot_id=group_snapshot.id)] + res = group_snapshot.conditional_update(values, expected, filters) + + if not res: + msg = _('GroupSnapshot status must be available or error, and no ' + 'Group can be currently using it as source for its ' + 'creation.') + raise exception.InvalidGroupSnapshot(reason=msg) + + snapshots = objects.SnapshotList.get_all_for_group_snapshot( + context, group_snapshot.id) + + # TODO(xyang): Add a new db API to update all snapshots statuses + # in one db API call. + for snap in snapshots: + snap.status = c_fields.SnapshotStatus.DELETING + snap.save() + + self.volume_rpcapi.delete_group_snapshot(context.elevated(), + group_snapshot) + + def update_group_snapshot(self, context, group_snapshot, fields): + check_policy(context, 'update_group_snapshot') + group_snapshot.update(fields) + group_snapshot.save() + + def get_group_snapshot(self, context, group_snapshot_id): + check_policy(context, 'get_group_snapshot') + group_snapshots = objects.GroupSnapshot.get_by_id(context, + group_snapshot_id) + return group_snapshots + + def get_all_group_snapshots(self, context, search_opts=None): + check_policy(context, 'get_all_group_snapshots') + search_opts = search_opts or {} + + if context.is_admin and 'all_tenants' in search_opts: + # Need to remove all_tenants to pass the filtering below. + del search_opts['all_tenants'] + group_snapshots = objects.GroupSnapshotList.get_all(context, + search_opts) + else: + group_snapshots = objects.GroupSnapshotList.get_all_by_project( + context.elevated(), context.project_id, search_opts) + return group_snapshots diff --git a/cinder/tests/unit/api/v3/stubs.py b/cinder/tests/unit/api/v3/stubs.py index f7e10318b2f..aef67a39af8 100644 --- a/cinder/tests/unit/api/v3/stubs.py +++ b/cinder/tests/unit/api/v3/stubs.py @@ -15,9 +15,17 @@ import iso8601 from cinder.message import defined_messages from cinder.tests.unit import fake_constants as fake - +from cinder.tests.unit import fake_volume +from cinder import utils FAKE_UUID = fake.OBJECT_ID +DEFAULT_VOL_NAME = "displayname" +DEFAULT_VOL_DESCRIPTION = "displaydesc" +DEFAULT_VOL_SIZE = 1 +DEFAULT_VOL_TYPE = "vol_type_name" +DEFAULT_VOL_STATUS = "fakestatus" +DEFAULT_VOL_ID = fake.VOLUME_ID +DEFAULT_AZ = "fakeaz" def stub_message(id, **kwargs): @@ -40,3 +48,68 @@ def stub_message(id, **kwargs): def stub_message_get(self, context, message_id): return stub_message(message_id) + + +def stub_volume(id, **kwargs): + volume = { + 'id': id, + 'user_id': fake.USER_ID, + 'project_id': fake.PROJECT_ID, + 'host': 'fakehost', + 'size': DEFAULT_VOL_SIZE, + 'availability_zone': DEFAULT_AZ, + 'status': DEFAULT_VOL_STATUS, + 'migration_status': None, + 'attach_status': 'attached', + 'name': 'vol name', + 'display_name': DEFAULT_VOL_NAME, + 'display_description': DEFAULT_VOL_DESCRIPTION, + 'updated_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + 'snapshot_id': None, + 'source_volid': None, + 'volume_type_id': '3e196c20-3c06-11e2-81c1-0800200c9a66', + 'encryption_key_id': None, + 'volume_admin_metadata': [{'key': 'attached_mode', 'value': 'rw'}, + {'key': 'readonly', 'value': 'False'}], + 'bootable': False, + 'launched_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + 'volume_type': fake_volume.fake_db_volume_type(name=DEFAULT_VOL_TYPE), + 'replication_status': 'disabled', + 'replication_extended_status': None, + 'replication_driver_data': None, + 'volume_attachment': [], + 'multiattach': False, + 'group_id': fake.GROUP_ID, + } + + volume.update(kwargs) + if kwargs.get('volume_glance_metadata', None): + volume['bootable'] = True + if kwargs.get('attach_status') == 'detached': + del volume['volume_admin_metadata'][0] + return volume + + +def stub_volume_create(self, context, size, name, description, snapshot=None, + group_id=None, **param): + vol = stub_volume(DEFAULT_VOL_ID) + vol['size'] = size + vol['display_name'] = name + vol['display_description'] = description + source_volume = param.get('source_volume') or {} + vol['source_volid'] = source_volume.get('id') + vol['bootable'] = False + vol['volume_attachment'] = [] + vol['multiattach'] = utils.get_bool_param('multiattach', param) + try: + vol['snapshot_id'] = snapshot['id'] + except (KeyError, TypeError): + vol['snapshot_id'] = None + vol['availability_zone'] = param.get('availability_zone', 'fakeaz') + if group_id: + vol['group_id'] = group_id + return vol diff --git a/cinder/tests/unit/api/v3/test_group_snapshots.py b/cinder/tests/unit/api/v3/test_group_snapshots.py new file mode 100644 index 00000000000..93f520b1b45 --- /dev/null +++ b/cinder/tests/unit/api/v3/test_group_snapshots.py @@ -0,0 +1,420 @@ +# Copyright (C) 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. + +""" +Tests for group_snapshot code. +""" + +import mock +import webob + +from cinder.api.v3 import group_snapshots as v3_group_snapshots +from cinder import context +from cinder import db +from cinder import exception +from cinder.group import api as group_api +from cinder import objects +from cinder import test +from cinder.tests.unit.api import fakes +from cinder.tests.unit import fake_constants as fake +from cinder.tests.unit import utils +import cinder.volume + +GROUP_MICRO_VERSION = '3.14' + + +class GroupSnapshotsAPITestCase(test.TestCase): + """Test Case for group_snapshots API.""" + + def setUp(self): + super(GroupSnapshotsAPITestCase, self).setUp() + self.controller = v3_group_snapshots.GroupSnapshotsController() + self.volume_api = cinder.volume.API() + self.context = context.get_admin_context() + self.context.project_id = fake.PROJECT_ID + self.context.user_id = fake.USER_ID + self.user_ctxt = context.RequestContext( + fake.USER_ID, fake.PROJECT_ID, auth_token=True) + + def test_show_group_snapshot(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot = utils.create_group_snapshot( + self.context, group_id=group.id) + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, group_snapshot.id), + version=GROUP_MICRO_VERSION) + res_dict = self.controller.show(req, group_snapshot.id) + + self.assertEqual(1, len(res_dict)) + self.assertEqual('this is a test group snapshot', + res_dict['group_snapshot']['description']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshot']['name']) + self.assertEqual('creating', res_dict['group_snapshot']['status']) + + group_snapshot.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() + + def test_show_group_snapshot_with_group_snapshot_NotFound(self): + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, + fake.WILL_NOT_BE_FOUND_ID), + version=GROUP_MICRO_VERSION) + self.assertRaises(exception.GroupSnapshotNotFound, + self.controller.show, + req, fake.WILL_NOT_BE_FOUND_ID) + + def test_list_group_snapshots_json(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot1 = utils.create_group_snapshot( + self.context, group_id=group.id) + group_snapshot2 = utils.create_group_snapshot( + self.context, group_id=group.id) + group_snapshot3 = utils.create_group_snapshot( + self.context, group_id=group.id) + + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + res_dict = self.controller.index(req) + + self.assertEqual(1, len(res_dict)) + self.assertEqual(group_snapshot1.id, + res_dict['group_snapshots'][0]['id']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][0]['name']) + self.assertEqual(group_snapshot2.id, + res_dict['group_snapshots'][1]['id']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][1]['name']) + self.assertEqual(group_snapshot3.id, + res_dict['group_snapshots'][2]['id']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][2]['name']) + + group_snapshot3.destroy() + group_snapshot2.destroy() + group_snapshot1.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() + + def test_list_group_snapshots_detail_json(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot1 = utils.create_group_snapshot( + self.context, group_id=group.id) + group_snapshot2 = utils.create_group_snapshot( + self.context, group_id=group.id) + group_snapshot3 = utils.create_group_snapshot( + self.context, group_id=group.id) + + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/detail' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + res_dict = self.controller.detail(req) + + self.assertEqual(1, len(res_dict)) + self.assertEqual(3, len(res_dict['group_snapshots'])) + self.assertEqual('this is a test group snapshot', + res_dict['group_snapshots'][0]['description']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][0]['name']) + self.assertEqual(group_snapshot1.id, + res_dict['group_snapshots'][0]['id']) + self.assertEqual('creating', + res_dict['group_snapshots'][0]['status']) + + self.assertEqual('this is a test group snapshot', + res_dict['group_snapshots'][1]['description']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][1]['name']) + self.assertEqual(group_snapshot2.id, + res_dict['group_snapshots'][1]['id']) + self.assertEqual('creating', + res_dict['group_snapshots'][1]['status']) + + self.assertEqual('this is a test group snapshot', + res_dict['group_snapshots'][2]['description']) + self.assertEqual('test_group_snapshot', + res_dict['group_snapshots'][2]['name']) + self.assertEqual(group_snapshot3.id, + res_dict['group_snapshots'][2]['id']) + self.assertEqual('creating', + res_dict['group_snapshots'][2]['status']) + + group_snapshot3.destroy() + group_snapshot2.destroy() + group_snapshot1.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() + + @mock.patch( + 'cinder.api.openstack.wsgi.Controller.validate_name_and_description') + @mock.patch('cinder.db.volume_type_get') + @mock.patch('cinder.quota.VolumeTypeQuotaEngine.reserve') + def test_create_group_snapshot_json(self, mock_quota, mock_vol_type, + mock_validate): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + body = {"group_snapshot": {"name": "group_snapshot1", + "description": + "Group Snapshot 1", + "group_id": group.id}} + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + res_dict = self.controller.create(req, body) + + self.assertEqual(1, len(res_dict)) + self.assertIn('id', res_dict['group_snapshot']) + self.assertTrue(mock_validate.called) + + group.destroy() + group_snapshot = objects.GroupSnapshot.get_by_id( + context.get_admin_context(), res_dict['group_snapshot']['id']) + db.volume_destroy(context.get_admin_context(), + volume_id) + group_snapshot.destroy() + + @mock.patch( + 'cinder.api.openstack.wsgi.Controller.validate_name_and_description') + @mock.patch('cinder.db.volume_type_get') + def test_create_group_snapshot_when_volume_in_error_status( + self, mock_vol_type, mock_validate): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + status='error', + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + body = {"group_snapshot": {"name": "group_snapshot1", + "description": + "Group Snapshot 1", + "group_id": group.id}} + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body) + self.assertTrue(mock_validate.called) + + group.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + + def test_create_group_snapshot_with_no_body(self): + # omit body from the request + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, None) + + @mock.patch.object(group_api.API, 'create_group_snapshot', + side_effect=exception.InvalidGroupSnapshot( + reason='Invalid group snapshot')) + def test_create_with_invalid_group_snapshot(self, mock_create_group_snap): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + status='error', + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + body = {"group_snapshot": {"name": "group_snapshot1", + "description": + "Group Snapshot 1", + "group_id": group.id}} + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body) + + group.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + + @mock.patch.object(group_api.API, 'create_group_snapshot', + side_effect=exception.GroupSnapshotNotFound( + group_snapshot_id='invalid_id')) + def test_create_with_group_snapshot_not_found(self, mock_create_grp_snap): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + status='error', + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + body = {"group_snapshot": {"name": "group_snapshot1", + "description": + "Group Snapshot 1", + "group_id": group.id}} + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + self.assertRaises(exception.GroupSnapshotNotFound, + self.controller.create, + req, body) + + group.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + + def test_create_group_snapshot_from_empty_group(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + body = {"group_snapshot": {"name": "group_snapshot1", + "description": + "Group Snapshot 1", + "group_id": group.id}} + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % + fake.PROJECT_ID, + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body) + + group.destroy() + + def test_delete_group_snapshot_available(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot = utils.create_group_snapshot( + self.context, + group_id=group.id, + status='available') + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, group_snapshot.id), + version=GROUP_MICRO_VERSION) + res_dict = self.controller.delete(req, group_snapshot.id) + + group_snapshot = objects.GroupSnapshot.get_by_id(self.context, + group_snapshot.id) + self.assertEqual(202, res_dict.status_int) + self.assertEqual('deleting', group_snapshot.status) + + group_snapshot.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() + + def test_delete_group_snapshot_available_used_as_source(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot = utils.create_group_snapshot( + self.context, + group_id=group.id, + status='available') + + group2 = utils.create_group( + self.context, status='creating', + group_snapshot_id=group_snapshot.id, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, group_snapshot.id), + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, group_snapshot.id) + + group_snapshot.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() + group2.destroy() + + def test_delete_group_snapshot_with_group_snapshot_NotFound(self): + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, + fake.WILL_NOT_BE_FOUND_ID), + version=GROUP_MICRO_VERSION) + self.assertRaises(exception.GroupSnapshotNotFound, + self.controller.delete, + req, fake.WILL_NOT_BE_FOUND_ID) + + def test_delete_group_snapshot_with_Invalid_group_snapshot(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID],) + volume_id = utils.create_volume( + self.context, + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID)['id'] + group_snapshot = utils.create_group_snapshot( + self.context, + group_id=group.id, + status='invalid') + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s' % + (fake.PROJECT_ID, group_snapshot.id), + version=GROUP_MICRO_VERSION) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, group_snapshot.id) + + group_snapshot.destroy() + db.volume_destroy(context.get_admin_context(), + volume_id) + group.destroy() diff --git a/cinder/tests/unit/api/v3/test_groups.py b/cinder/tests/unit/api/v3/test_groups.py index 2f5b73e8c86..8619fcbe7b8 100644 --- a/cinder/tests/unit/api/v3/test_groups.py +++ b/cinder/tests/unit/api/v3/test_groups.py @@ -30,10 +30,13 @@ from cinder import objects from cinder.objects import fields from cinder import test from cinder.tests.unit.api import fakes +from cinder.tests.unit.api.v3 import stubs from cinder.tests.unit import fake_constants as fake from cinder.tests.unit import utils +from cinder.volume import api as volume_api GROUP_MICRO_VERSION = '3.13' +GROUP_FROM_SRC_MICRO_VERSION = '3.14' @ddt.ddt @@ -804,3 +807,70 @@ class GroupsAPITestCase(test.TestCase): self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, req, self.group1.id, body) + + @mock.patch( + 'cinder.api.openstack.wsgi.Controller.validate_name_and_description') + def test_create_group_from_src_snap(self, mock_validate): + self.mock_object(volume_api.API, "create", stubs.stub_volume_create) + + group = utils.create_group(self.ctxt, + group_type_id=fake.GROUP_TYPE_ID) + volume = utils.create_volume( + self.ctxt, + group_id=group.id) + group_snapshot = utils.create_group_snapshot( + self.ctxt, group_id=group.id) + snapshot = utils.create_snapshot( + self.ctxt, + volume.id, + group_snapshot_id=group_snapshot.id, + status=fields.SnapshotStatus.AVAILABLE) + + test_grp_name = 'test grp' + body = {"create-from-src": {"name": test_grp_name, + "description": "Group 1", + "group_snapshot_id": group_snapshot.id}} + req = fakes.HTTPRequest.blank('/v3/%s/groups/action' % + fake.PROJECT_ID, + version=GROUP_FROM_SRC_MICRO_VERSION) + res_dict = self.controller.create_from_src(req, body) + + self.assertIn('id', res_dict['group']) + self.assertEqual(test_grp_name, res_dict['group']['name']) + self.assertTrue(mock_validate.called) + + grp_ref = objects.Group.get_by_id( + self.ctxt.elevated(), res_dict['group']['id']) + + grp_ref.destroy() + snapshot.destroy() + volume.destroy() + group.destroy() + group_snapshot.destroy() + + def test_create_group_from_src_grp(self): + self.mock_object(volume_api.API, "create", stubs.stub_volume_create) + + source_grp = utils.create_group(self.ctxt, + group_type_id=fake.GROUP_TYPE_ID) + volume = utils.create_volume( + self.ctxt, + group_id=source_grp.id) + + test_grp_name = 'test cg' + body = {"create-from-src": {"name": test_grp_name, + "description": "Consistency Group 1", + "source_group_id": source_grp.id}} + req = fakes.HTTPRequest.blank('/v3/%s/groups/action' % + fake.PROJECT_ID, + version=GROUP_FROM_SRC_MICRO_VERSION) + res_dict = self.controller.create_from_src(req, body) + + self.assertIn('id', res_dict['group']) + self.assertEqual(test_grp_name, res_dict['group']['name']) + + grp = objects.Group.get_by_id( + self.ctxt, res_dict['group']['id']) + grp.destroy() + volume.destroy() + source_grp.destroy() diff --git a/cinder/tests/unit/api/v3/test_snapshots.py b/cinder/tests/unit/api/v3/test_snapshots.py new file mode 100644 index 00000000000..3e035ce765a --- /dev/null +++ b/cinder/tests/unit/api/v3/test_snapshots.py @@ -0,0 +1,79 @@ +# 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 ddt + +import mock + +from cinder.api.openstack import api_version_request as api_version +from cinder.api.v3 import snapshots +from cinder import context +from cinder import exception +from cinder.objects import fields +from cinder import test +from cinder.tests.unit.api import fakes +from cinder.tests.unit import fake_constants as fake +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume + +UUID = '00000000-0000-0000-0000-000000000001' +INVALID_UUID = '00000000-0000-0000-0000-000000000002' + + +@ddt.ddt +class SnapshotApiTest(test.TestCase): + def setUp(self): + super(SnapshotApiTest, self).setUp() + self.controller = snapshots.SnapshotsController() + self.ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + + @ddt.data('3.14', '3.13') + @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) + @mock.patch('cinder.objects.Volume.get_by_id') + @mock.patch('cinder.objects.Snapshot.get_by_id') + def test_snapshot_show(self, max_ver, snapshot_get_by_id, volume_get_by_id, + snapshot_metadata_get): + snapshot = { + 'id': UUID, + 'volume_id': fake.VOLUME_ID, + 'status': fields.SnapshotStatus.AVAILABLE, + 'volume_size': 100, + 'display_name': 'Default name', + 'display_description': 'Default description', + 'expected_attrs': ['metadata'], + 'group_snapshot_id': None, + } + ctx = context.RequestContext(fake.PROJECT_ID, fake.USER_ID, True) + snapshot_obj = fake_snapshot.fake_snapshot_obj(ctx, **snapshot) + fake_volume_obj = fake_volume.fake_volume_obj(ctx) + snapshot_get_by_id.return_value = snapshot_obj + volume_get_by_id.return_value = fake_volume_obj + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) + req.api_version_request = api_version.APIVersionRequest(max_ver) + resp_dict = self.controller.show(req, UUID) + + self.assertIn('snapshot', resp_dict) + self.assertEqual(UUID, resp_dict['snapshot']['id']) + self.assertIn('updated_at', resp_dict['snapshot']) + if max_ver == '3.14': + self.assertIn('group_snapshot_id', resp_dict['snapshot']) + elif max_ver == '3.13': + self.assertNotIn('group_snapshot_id', resp_dict['snapshot']) + + def test_snapshot_show_invalid_id(self): + snapshot_id = INVALID_UUID + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id) + self.assertRaises(exception.SnapshotNotFound, + self.controller.show, req, snapshot_id) diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index 2ebf225808b..484a3b94ada 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -317,7 +317,7 @@ class VolumeApiTest(test.TestCase): self.assertEqual(ex, res_dict) self.assertTrue(mock_validate.called) - @ddt.data('3.13', '3.12') + @ddt.data('3.14', '3.13') @mock.patch.object(group_api.API, 'get') @mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full', autospec=True) diff --git a/cinder/tests/unit/group/test_groups_api.py b/cinder/tests/unit/group/test_groups_api.py index a5be4ff86e3..bc06817add7 100644 --- a/cinder/tests/unit/group/test_groups_api.py +++ b/cinder/tests/unit/group/test_groups_api.py @@ -21,6 +21,7 @@ import ddt import mock from cinder import context +from cinder import db import cinder.group from cinder import objects from cinder.objects import fields @@ -174,3 +175,243 @@ class GroupAPITestCase(test.TestCase): mock_rpc_update_group.assert_called_once_with(self.ctxt, ret_group, add_volumes = vol1.id, remove_volumes = vol2.id) + + @mock.patch('cinder.objects.GroupSnapshot.get_by_id') + @mock.patch('cinder.group.api.check_policy') + def test_get_group_snapshot(self, mock_policy, mock_group_snap): + fake_group_snap = 'fake_group_snap' + mock_group_snap.return_value = fake_group_snap + grp_snap = self.group_api.get_group_snapshot( + self.ctxt, fake.GROUP_SNAPSHOT_ID) + self.assertEqual(fake_group_snap, grp_snap) + + @ddt.data(True, False) + @mock.patch('cinder.objects.GroupSnapshotList.get_all') + @mock.patch('cinder.objects.GroupSnapshotList.get_all_by_project') + @mock.patch('cinder.group.api.check_policy') + def test_get_all_group_snapshots(self, is_admin, mock_policy, + mock_get_all_by_project, + mock_get_all): + fake_group_snaps = ['fake_group_snap1', 'fake_group_snap2'] + fake_group_snaps_by_project = ['fake_group_snap1'] + mock_get_all.return_value = fake_group_snaps + mock_get_all_by_project.return_value = fake_group_snaps_by_project + + if is_admin: + grp_snaps = self.group_api.get_all_group_snapshots( + self.ctxt, search_opts={'all_tenants': True}) + self.assertEqual(fake_group_snaps, grp_snaps) + else: + grp_snaps = self.group_api.get_all_group_snapshots( + self.user_ctxt) + self.assertEqual(fake_group_snaps_by_project, grp_snaps) + + @mock.patch('cinder.objects.GroupSnapshot') + @mock.patch('cinder.group.api.check_policy') + def test_update_group_snapshot(self, mock_policy, mock_group_snap): + grp_snap_update = {"name": "new_name", + "description": "This is a new description"} + self.group_api.update_group_snapshot(self.ctxt, mock_group_snap, + grp_snap_update) + mock_group_snap.update.assert_called_once_with(grp_snap_update) + mock_group_snap.save.assert_called_once_with() + + @mock.patch('cinder.volume.rpcapi.VolumeAPI.delete_group_snapshot') + @mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_snapshot') + @mock.patch('cinder.volume.api.API.create_snapshots_in_db') + @mock.patch('cinder.objects.Group') + @mock.patch('cinder.objects.GroupSnapshot') + @mock.patch('cinder.objects.SnapshotList.get_all_for_group_snapshot') + @mock.patch('cinder.group.api.check_policy') + def test_create_delete_group_snapshot(self, mock_policy, + mock_snap_get_all, + mock_group_snap, mock_group, + mock_create_in_db, + mock_create_api, mock_delete_api): + name = "fake_name" + description = "fake description" + mock_group.id = fake.GROUP_ID + mock_group.volumes = [] + ret_group_snap = self.group_api.create_group_snapshot( + self.ctxt, mock_group, name, description) + mock_snap_get_all.return_value = [] + + options = {'group_id': fake.GROUP_ID, + 'user_id': self.ctxt.user_id, + 'project_id': self.ctxt.project_id, + 'status': "creating", + 'name': name, + 'description': description} + mock_group_snap.assert_called_once_with(self.ctxt, **options) + ret_group_snap.create.assert_called_once_with() + mock_create_in_db.assert_called_once_with(self.ctxt, [], + ret_group_snap.name, + ret_group_snap.description, + None, + ret_group_snap.id) + mock_create_api.assert_called_once_with(self.ctxt, ret_group_snap) + + self.group_api.delete_group_snapshot(self.ctxt, ret_group_snap) + mock_delete_api.assert_called_once_with(mock.ANY, ret_group_snap) + + @mock.patch('cinder.volume.volume_types.get_volume_type') + @mock.patch('cinder.db.group_volume_type_mapping_create') + @mock.patch('cinder.volume.api.API.create') + @mock.patch('cinder.objects.GroupSnapshot.get_by_id') + @mock.patch('cinder.objects.SnapshotList.get_all_for_group_snapshot') + @mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_from_src') + @mock.patch('cinder.objects.VolumeList.get_all_by_generic_group') + def test_create_group_from_snap(self, mock_volume_get_all, + mock_rpc_create_group_from_src, + mock_snap_get_all, mock_group_snap_get, + mock_volume_api_create, + mock_mapping_create, + mock_get_volume_type): + vol_type = utils.create_volume_type(self.ctxt, + name = 'fake_volume_type') + mock_get_volume_type.return_value = vol_type + + grp_snap = utils.create_group_snapshot( + self.ctxt, fake.GROUP_ID, + group_type_id = fake.GROUP_TYPE_ID, + status = fields.GroupStatus.CREATING) + mock_group_snap_get.return_value = grp_snap + + vol1 = utils.create_volume( + self.ctxt, + availability_zone = 'nova', + volume_type_id = vol_type['id'], + group_id = fake.GROUP_ID) + + snap = utils.create_snapshot(self.ctxt, vol1.id, + volume_type_id = vol_type['id'], + status = fields.GroupStatus.CREATING) + mock_snap_get_all.return_value = [snap] + + name = "test_group" + description = "this is a test group" + grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID, + volume_type_ids = [vol_type['id']], + availability_zone = 'nova', + name = name, description = description, + group_snapshot_id = grp_snap.id, + status = fields.GroupStatus.CREATING) + + vol2 = utils.create_volume( + self.ctxt, + availability_zone = grp.availability_zone, + volume_type_id = vol_type['id'], + group_id = grp.id, + snapshot_id = snap.id) + mock_volume_get_all.return_value = [vol2] + + self.group_api._create_group_from_group_snapshot(self.ctxt, grp, + grp_snap.id) + + mock_volume_api_create.assert_called_once_with( + self.ctxt, 1, None, None, + availability_zone = grp.availability_zone, + group_snapshot = grp_snap, + group = grp, + snapshot = snap, + volume_type = vol_type) + + mock_rpc_create_group_from_src.assert_called_once_with( + self.ctxt, grp, grp_snap) + + vol2.destroy() + grp.destroy() + snap.destroy() + vol1.destroy() + grp_snap.destroy() + db.volume_type_destroy(self.ctxt, vol_type['id']) + + @mock.patch('cinder.volume.volume_types.get_volume_type') + @mock.patch('cinder.db.group_volume_type_mapping_create') + @mock.patch('cinder.volume.api.API.create') + @mock.patch('cinder.objects.Group.get_by_id') + @mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_from_src') + @mock.patch('cinder.objects.VolumeList.get_all_by_generic_group') + @mock.patch('cinder.group.api.check_policy') + def test_create_group_from_group(self, mock_policy, mock_volume_get_all, + mock_rpc_create_group_from_src, + mock_group_get, + mock_volume_api_create, + mock_mapping_create, + mock_get_volume_type): + vol_type = utils.create_volume_type(self.ctxt, + name = 'fake_volume_type') + mock_get_volume_type.return_value = vol_type + + grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID, + volume_type_ids = [vol_type['id']], + availability_zone = 'nova', + status = fields.GroupStatus.CREATING) + mock_group_get.return_value = grp + + vol = utils.create_volume( + self.ctxt, + availability_zone = grp.availability_zone, + volume_type_id = fake.VOLUME_TYPE_ID, + group_id = grp.id) + mock_volume_get_all.return_value = [vol] + + grp2 = utils.create_group(self.ctxt, + group_type_id = fake.GROUP_TYPE_ID, + volume_type_ids = [vol_type['id']], + availability_zone = 'nova', + source_group_id = grp.id, + status = fields.GroupStatus.CREATING) + + vol2 = utils.create_volume( + self.ctxt, + availability_zone = grp.availability_zone, + volume_type_id = vol_type['id'], + group_id = grp2.id, + source_volid = vol.id) + + self.group_api._create_group_from_source_group(self.ctxt, grp2, + grp.id) + + mock_volume_api_create.assert_called_once_with( + self.ctxt, 1, None, None, + availability_zone = grp.availability_zone, + source_group = grp, + group = grp2, + source_volume = vol, + volume_type = vol_type) + + mock_rpc_create_group_from_src.assert_called_once_with( + self.ctxt, grp2, None, grp) + + vol2.destroy() + grp2.destroy() + vol.destroy() + grp.destroy() + db.volume_type_destroy(self.ctxt, vol_type['id']) + + @mock.patch('cinder.group.api.API._create_group_from_group_snapshot') + @mock.patch('cinder.group.api.API._create_group_from_source_group') + @mock.patch('cinder.group.api.API.update_quota') + @mock.patch('cinder.objects.Group') + @mock.patch('cinder.group.api.check_policy') + def test_create_from_src(self, mock_policy, mock_group, mock_update_quota, + mock_create_from_group, mock_create_from_snap): + name = "test_group" + description = "this is a test group" + grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID, + volume_type_ids = [fake.VOLUME_TYPE_ID], + availability_zone = 'nova', + name = name, description = description, + status = fields.GroupStatus.CREATING, + group_snapshot_id = fake.GROUP_SNAPSHOT_ID, + source_group_id = fake.GROUP_ID) + mock_group.return_value = grp + + ret_group = self.group_api.create_from_src( + self.ctxt, name, description, + group_snapshot_id = fake.GROUP_SNAPSHOT_ID, + source_group_id = None) + self.assertEqual(grp.obj_to_primitive(), ret_group.obj_to_primitive()) + mock_create_from_snap.assert_called_once_with( + self.ctxt, grp, fake.GROUP_SNAPSHOT_ID) diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index a5c81fd168e..f5533dd8aa9 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -125,6 +125,12 @@ "group:get": "", "group:get_all": "", + "group:create_group_snapshot": "", + "group:delete_group_snapshot": "", + "group:update_group_snapshot": "", + "group:get_group_snapshot": "", + "group:get_all_group_snapshots": "", + "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", "message:delete": "rule:admin_or_owner", diff --git a/cinder/tests/unit/test_volume_rpcapi.py b/cinder/tests/unit/test_volume_rpcapi.py index 81798038aa6..2202cb70726 100644 --- a/cinder/tests/unit/test_volume_rpcapi.py +++ b/cinder/tests/unit/test_volume_rpcapi.py @@ -73,14 +73,14 @@ class VolumeRpcAPITestCase(test.TestCase): self.context, consistencygroup_id=source_group.id) - group = tests_utils.create_consistencygroup( + cg = tests_utils.create_consistencygroup( self.context, availability_zone=CONF.storage_availability_zone, volume_type='type1,type2', host='fakehost@fakedrv#fakepool', cgsnapshot_id=cgsnapshot.id) - group2 = tests_utils.create_consistencygroup( + cg2 = tests_utils.create_consistencygroup( self.context, availability_zone=CONF.storage_availability_zone, volume_type='type1,type2', @@ -93,20 +93,37 @@ class VolumeRpcAPITestCase(test.TestCase): group_type_id='group_type1', host='fakehost@fakedrv#fakepool') - group = objects.ConsistencyGroup.get_by_id(self.context, group.id) - group2 = objects.ConsistencyGroup.get_by_id(self.context, group2.id) + group_snapshot = tests_utils.create_group_snapshot( + self.context, + group_id=generic_group.id, + group_type_id='group_type1') + + cg = objects.ConsistencyGroup.get_by_id(self.context, cg.id) + cg2 = objects.ConsistencyGroup.get_by_id(self.context, cg2.id) cgsnapshot = objects.CGSnapshot.get_by_id(self.context, cgsnapshot.id) self.fake_volume = jsonutils.to_primitive(volume) self.fake_volume_obj = fake_volume.fake_volume_obj(self.context, **vol) self.fake_volume_metadata = volume["volume_metadata"] self.fake_snapshot = snapshot self.fake_reservations = ["RESERVATION"] - self.fake_cg = group - self.fake_cg2 = group2 + self.fake_cg = cg + self.fake_cg2 = cg2 self.fake_src_cg = jsonutils.to_primitive(source_group) self.fake_cgsnap = cgsnapshot self.fake_backup_obj = fake_backup.fake_backup_obj(self.context) self.fake_group = generic_group + self.fake_group_snapshot = group_snapshot + + self.addCleanup(self._cleanup) + + def _cleanup(self): + self.fake_snapshot.destroy() + self.fake_volume_obj.destroy() + self.fake_group_snapshot.destroy() + self.fake_group.destroy() + self.fake_cgsnap.destroy() + self.fake_cg2.destroy() + self.fake_cg.destroy() def test_serialized_volume_has_id(self): self.assertIn('id', self.fake_volume) @@ -253,11 +270,18 @@ class VolumeRpcAPITestCase(test.TestCase): expected_msg = copy.deepcopy(kwargs) if 'host' in expected_msg: del expected_msg['host'] + if 'group_snapshot' in expected_msg: + group_snapshot = expected_msg['group_snapshot'] + if group_snapshot: + group_snapshot.group + kwargs['group_snapshot'].group if 'host' in kwargs: host = kwargs['host'] elif 'group' in kwargs: host = kwargs['group']['host'] + elif 'group_snapshot' in kwargs: + host = kwargs['group_snapshot'].group.host target['server'] = utils.extract_host(host) target['topic'] = '%s.%s' % (constants.VOLUME_TOPIC, host) @@ -291,6 +315,10 @@ class VolumeRpcAPITestCase(test.TestCase): expected_group = expected_msg[kwarg].obj_to_primitive() group = value.obj_to_primitive() self.assertEqual(expected_group, group) + elif isinstance(value, objects.GroupSnapshot): + expected_grp_snap = expected_msg[kwarg].obj_to_primitive() + grp_snap = value.obj_to_primitive() + self.assertEqual(expected_grp_snap, grp_snap) else: self.assertEqual(expected_msg[kwarg], value) @@ -609,3 +637,20 @@ class VolumeRpcAPITestCase(test.TestCase): self._test_group_api('update_group', rpc_method='cast', group=self.fake_group, add_volumes=['vol1'], remove_volumes=['vol2'], version='2.5') + + def test_create_group_from_src(self): + self._test_group_api('create_group_from_src', rpc_method='cast', + group=self.fake_group, + group_snapshot=self.fake_group_snapshot, + source_group=None, + version='2.6') + + def test_create_group_snapshot(self): + self._test_group_api('create_group_snapshot', rpc_method='cast', + group_snapshot=self.fake_group_snapshot, + version='2.6') + + def test_delete_group_snapshot(self): + self._test_group_api('delete_group_snapshot', rpc_method='cast', + group_snapshot=self.fake_group_snapshot, + version='2.6') diff --git a/cinder/tests/unit/utils.py b/cinder/tests/unit/utils.py index b734a181eab..0309cc94ce7 100644 --- a/cinder/tests/unit/utils.py +++ b/cinder/tests/unit/utils.py @@ -242,6 +242,53 @@ def create_cgsnapshot(ctxt, return objects.CGSnapshot.get_by_id(ctxt, cgsnap.id) +def create_group_snapshot(ctxt, + group_id, + group_type_id=None, + name='test_group_snapshot', + description='this is a test group snapshot', + status='creating', + recursive_create_if_needed=True, + return_vo=True, + **kwargs): + """Create a group snapshot object in the DB.""" + values = { + 'user_id': ctxt.user_id or fake.USER_ID, + 'project_id': ctxt.project_id or fake.PROJECT_ID, + 'status': status, + 'name': name, + 'description': description, + 'group_id': group_id} + values.update(kwargs) + + if recursive_create_if_needed and group_id: + create_grp = False + try: + objects.Group.get_by_id(ctxt, + group_id) + create_vol = not db.volume_get_all_by_generic_group( + ctxt, group_id) + except exception.GroupNotFound: + create_grp = True + create_vol = True + if create_grp: + create_group(ctxt, id=group_id, group_type_id=group_type_id) + if create_vol: + create_volume(ctxt, group_id=group_id) + + if not return_vo: + return db.group_snapshot_create(ctxt, values) + else: + group_snapshot = objects.GroupSnapshot(ctxt) + new_id = values.pop('id', None) + group_snapshot.update(values) + group_snapshot.create() + if new_id and new_id != group_snapshot.id: + db.group_snapshot_update(ctxt, group_snapshot.id, {'id': new_id}) + group_snapshot = objects.GroupSnapshot.get_by_id(ctxt, new_id) + return group_snapshot + + def create_backup(ctxt, volume_id=fake.VOLUME_ID, display_name='test_backup', diff --git a/cinder/volume/api.py b/cinder/volume/api.py index ecc968bb912..3148c404fea 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -213,7 +213,7 @@ class API(base.Base): scheduler_hints=None, source_replica=None, consistencygroup=None, cgsnapshot=None, multiattach=False, source_cg=None, - group=None): + group=None, group_snapshot=None, source_group=None): check_policy(context, 'create') @@ -245,7 +245,7 @@ class API(base.Base): "group).") % volume_type raise exception.InvalidInput(reason=msg) - if group: + if group and (not group_snapshot and not source_group): if not volume_type: msg = _("volume_type must be provided when creating " "a volume in a group.") @@ -320,12 +320,18 @@ class API(base.Base): 'cgsnapshot': cgsnapshot, 'multiattach': multiattach, 'group': group, + 'group_snapshot': group_snapshot, + 'source_group': source_group, } try: - sched_rpcapi = (self.scheduler_rpcapi if (not cgsnapshot and - not source_cg) else None) - volume_rpcapi = (self.volume_rpcapi if (not cgsnapshot and - not source_cg) else None) + sched_rpcapi = (self.scheduler_rpcapi if ( + not cgsnapshot and not source_cg and + not group_snapshot and not source_group) + else None) + volume_rpcapi = (self.volume_rpcapi if ( + not cgsnapshot and not source_cg and + not group_snapshot and not source_group) + else None) flow_engine = create_volume.get_flow(self.db, self.image_service, availability_zones, @@ -417,7 +423,8 @@ class API(base.Base): if cascade: values = {'status': 'deleting'} expected = {'status': ('available', 'error', 'deleting'), - 'cgsnapshot_id': None} + 'cgsnapshot_id': None, + 'group_snapshot_id': None} snapshots = objects.snapshot.SnapshotList.get_all_for_volume( context, volume.id) for s in snapshots: @@ -739,10 +746,12 @@ class API(base.Base): def _create_snapshot(self, context, volume, name, description, force=False, metadata=None, - cgsnapshot_id=None): + cgsnapshot_id=None, + group_snapshot_id=None): snapshot = self.create_snapshot_in_db( context, volume, name, - description, force, metadata, cgsnapshot_id) + description, force, metadata, cgsnapshot_id, + True, group_snapshot_id) self.volume_rpcapi.create_snapshot(context, volume, snapshot) return snapshot @@ -751,7 +760,8 @@ class API(base.Base): volume, name, description, force, metadata, cgsnapshot_id, - commit_quota=True): + commit_quota=True, + group_snapshot_id=None): check_policy(context, 'create_snapshot', volume) if volume['status'] == 'maintenance': @@ -800,6 +810,7 @@ class API(base.Base): kwargs = { 'volume_id': volume['id'], 'cgsnapshot_id': cgsnapshot_id, + 'group_snapshot_id': group_snapshot_id, 'user_id': context.user_id, 'project_id': context.project_id, 'status': fields.SnapshotStatus.CREATING, @@ -830,7 +841,8 @@ class API(base.Base): def create_snapshots_in_db(self, context, volume_list, name, description, - cgsnapshot_id): + cgsnapshot_id, + group_snapshot_id=None): snapshot_list = [] for volume in volume_list: self._create_snapshot_in_db_validate(context, volume, True) @@ -846,7 +858,8 @@ class API(base.Base): options_list = [] for volume in volume_list: options = self._create_snapshot_in_db_options( - context, volume, name, description, cgsnapshot_id) + context, volume, name, description, cgsnapshot_id, + group_snapshot_id) options_list.append(options) try: @@ -919,9 +932,11 @@ class API(base.Base): def _create_snapshot_in_db_options(self, context, volume, name, description, - cgsnapshot_id): + cgsnapshot_id, + group_snapshot_id=None): options = {'volume_id': volume['id'], 'cgsnapshot_id': cgsnapshot_id, + 'group_snapshot_id': group_snapshot_id, 'user_id': context.user_id, 'project_id': context.project_id, 'status': fields.SnapshotStatus.CREATING, @@ -935,9 +950,11 @@ class API(base.Base): def create_snapshot(self, context, volume, name, description, - metadata=None, cgsnapshot_id=None): + metadata=None, cgsnapshot_id=None, + group_snapshot_id=None): result = self._create_snapshot(context, volume, name, description, - False, metadata, cgsnapshot_id) + False, metadata, cgsnapshot_id, + group_snapshot_id) LOG.info(_LI("Snapshot create request issued successfully."), resource=result) return result @@ -955,7 +972,8 @@ class API(base.Base): def delete_snapshot(self, context, snapshot, force=False, unmanage_only=False): # Build required conditions for conditional update - expected = {'cgsnapshot_id': None} + expected = {'cgsnapshot_id': None, + 'group_snapshot_id': None} # If not force deleting we have status conditions if not force: expected['status'] = (fields.SnapshotStatus.AVAILABLE, @@ -966,7 +984,7 @@ class API(base.Base): if not result: status = utils.build_or_str(expected.get('status'), _('status must be %s and')) - msg = (_('Snapshot %s must not be part of a consistency group.') % + msg = (_('Snapshot %s must not be part of a group.') % status) LOG.error(msg) raise exception.InvalidSnapshot(reason=msg) diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py index 52f34be8eb2..ee1d8fcf1c1 100644 --- a/cinder/volume/rpcapi.py +++ b/cinder/volume/rpcapi.py @@ -99,14 +99,16 @@ class VolumeAPI(rpc.RPCAPI): 2.0 - Remove 1.x compatibility 2.1 - Add get_manageable_volumes() and get_manageable_snapshots(). - 2.2 - Adds support for sending objects over RPC in manage_existing(). + 2.2 - Adds support for sending objects over RPC in manage_existing(). 2.3 - Adds support for sending objects over RPC in initialize_connection(). - 2.4 - Sends request_spec as object in create_volume(). + 2.4 - Sends request_spec as object in create_volume(). 2.5 - Adds create_group, delete_group, and update_group + 2.6 - Adds create_group_snapshot, delete_group_snapshot, and + create_group_from_src(). """ - RPC_API_VERSION = '2.5' + RPC_API_VERSION = '2.6' TOPIC = constants.VOLUME_TOPIC BINARY = 'cinder-volume' @@ -359,3 +361,21 @@ class VolumeAPI(rpc.RPCAPI): group=group, add_volumes=add_volumes, remove_volumes=remove_volumes) + + def create_group_from_src(self, ctxt, group, group_snapshot=None, + source_group=None): + cctxt = self._get_cctxt(group.host, '2.6') + cctxt.cast(ctxt, 'create_group_from_src', + group=group, + group_snapshot=group_snapshot, + source_group=source_group) + + def create_group_snapshot(self, ctxt, group_snapshot): + cctxt = self._get_cctxt(group_snapshot.group.host, '2.6') + cctxt.cast(ctxt, 'create_group_snapshot', + group_snapshot=group_snapshot) + + def delete_group_snapshot(self, ctxt, group_snapshot): + cctxt = self._get_cctxt(group_snapshot.group.host, '2.6') + cctxt.cast(ctxt, 'delete_group_snapshot', + group_snapshot=group_snapshot) diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 7d3188b63d0..88183720517 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -121,6 +121,12 @@ "group:get": "rule:admin_or_owner", "group:get_all": "rule:admin_or_owner", + "group:create_group_snapshot": "", + "group:delete_group_snapshot": "rule:admin_or_owner", + "group:update_group_snapshot": "rule:admin_or_owner", + "group:get_group_snapshot": "rule:admin_or_owner", + "group:get_all_group_snapshots": "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", diff --git a/releasenotes/notes/group-snapshots-36264409bbb8850c.yaml b/releasenotes/notes/group-snapshots-36264409bbb8850c.yaml new file mode 100644 index 00000000000..032ad189c35 --- /dev/null +++ b/releasenotes/notes/group-snapshots-36264409bbb8850c.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added create/delete APIs for group snapshots and + an API to create group from source.