From 44ebdd22526e9a4ae0646d9f9ae2b391e70bed57 Mon Sep 17 00:00:00 2001 From: xing-yang Date: Sun, 13 Nov 2016 20:58:15 -0500 Subject: [PATCH] CG API changes for migrating CGs CG APIs work as follows: * Create CG - Create only in groups table * Modify CG - Modify in CG table if CG in CG table, otherwise modify in groups table. * Delete CG - Delete from CG or groups table depending on where it is * List CG - Check both CG and groups tables * List CG snapshots - Check both CG and groups tables * Show CG - Check both tables * Show CG snapshot - Check both tables * Create CG snapshot - Create either in CG or groups table depending on the CG. * Create CG from source - Create in either CG or groups table depending on the source. * Create volume - Add volume either to CG or group Additional notes: * default_cgsnapshot_type is reserved for migrating CGs. * Group APIs will only write/read in/from the groups table. * Group APIs won't work on groups with default_cgsnapshot_type. * Groups with default_cgsnapshot_type can only be operated by CG APIs. * After CG tables are removed, we'll allow default_cgsnapshot_type to be used by group APIs. Partial-Implements: blueprint generic-volume-group Change-Id: Idd88a5c9587023a56231de42ce59d672e9600770 --- cinder/api/contrib/cgsnapshots.py | 96 +++++++++--- cinder/api/contrib/consistencygroups.py | 120 ++++++++++++--- cinder/api/v2/views/volumes.py | 14 ++ cinder/api/v2/volumes.py | 14 +- cinder/api/v3/group_snapshots.py | 29 +++- cinder/api/v3/groups.py | 40 ++++- cinder/api/v3/views/group_snapshots.py | 6 +- cinder/api/v3/volumes.py | 12 +- cinder/api/views/cgsnapshots.py | 12 +- cinder/api/views/consistencygroups.py | 13 +- cinder/db/sqlalchemy/api.py | 9 +- cinder/group/api.py | 15 ++ cinder/objects/base.py | 1 + cinder/objects/cgsnapshot.py | 14 +- cinder/objects/consistencygroup.py | 20 ++- .../unit/api/contrib/test_cgsnapshots.py | 4 +- .../api/contrib/test_consistencygroups.py | 12 +- cinder/tests/unit/api/v2/test_volumes.py | 2 +- .../tests/unit/api/v3/test_group_snapshots.py | 9 +- cinder/tests/unit/api/v3/test_groups.py | 18 ++- cinder/tests/unit/group/test_groups_api.py | 63 ++++++-- cinder/tests/unit/objects/test_objects.py | 4 +- cinder/tests/unit/test_volume_rpcapi.py | 4 +- cinder/tests/unit/utils.py | 3 +- cinder/volume/group_types.py | 5 + cinder/volume/manager.py | 141 +++++++++++++++--- ...-groups-with-cp-apis-e5835c6673191805.yaml | 29 ++++ 27 files changed, 594 insertions(+), 115 deletions(-) create mode 100644 releasenotes/notes/operate-migrated-groups-with-cp-apis-e5835c6673191805.yaml diff --git a/cinder/api/contrib/cgsnapshots.py b/cinder/api/contrib/cgsnapshots.py index 082d996e0..db6d603ab 100644 --- a/cinder/api/contrib/cgsnapshots.py +++ b/cinder/api/contrib/cgsnapshots.py @@ -24,9 +24,14 @@ from cinder.api import common from cinder.api import extensions from cinder.api.openstack import wsgi from cinder.api.views import cgsnapshots as cgsnapshot_views -from cinder import consistencygroup as consistencygroupAPI +from cinder import consistencygroup as consistencygroup_api from cinder import exception +from cinder import group as group_api from cinder.i18n import _, _LI +from cinder.objects import cgsnapshot as cgsnap_obj +from cinder.objects import consistencygroup as cg_obj +from cinder.objects import group as grp_obj +from cinder.objects import group_snapshot as grpsnap_obj LOG = logging.getLogger(__name__) @@ -37,7 +42,8 @@ class CgsnapshotsController(wsgi.Controller): _view_builder_class = cgsnapshot_views.ViewBuilder def __init__(self): - self.cgsnapshot_api = consistencygroupAPI.API() + self.cgsnapshot_api = consistencygroup_api.API() + self.group_snapshot_api = group_api.API() super(CgsnapshotsController, self).__init__() def show(self, req, id): @@ -46,9 +52,7 @@ class CgsnapshotsController(wsgi.Controller): context = req.environ['cinder.context'] # Not found exception will be handled at the wsgi level - cgsnapshot = self.cgsnapshot_api.get_cgsnapshot( - context, - cgsnapshot_id=id) + cgsnapshot = self._get_cgsnapshot(context, id) return self._view_builder.detail(req, cgsnapshot) @@ -60,14 +64,21 @@ class CgsnapshotsController(wsgi.Controller): LOG.info(_LI('Delete cgsnapshot with id: %s'), id) try: - cgsnapshot = self.cgsnapshot_api.get_cgsnapshot( - context, - cgsnapshot_id=id) - self.cgsnapshot_api.delete_cgsnapshot(context, cgsnapshot) - except exception.CgSnapshotNotFound: + cgsnapshot = self._get_cgsnapshot(context, id) + if isinstance(cgsnapshot, cgsnap_obj.CGSnapshot): + self.cgsnapshot_api.delete_cgsnapshot(context, cgsnapshot) + elif isinstance(cgsnapshot, grpsnap_obj.GroupSnapshot): + self.group_snapshot_api.delete_group_snapshot( + context, cgsnapshot) + else: + msg = _("Group snapshot '%s' not found.") % id + raise exc.HTTPNotFound(explanation=msg) + except (exception.CgSnapshotNotFound, + exception.GroupSnapshotNotFound): # Not found exception will be handled at the wsgi level raise - except exception.InvalidCgSnapshot as e: + except (exception.InvalidCgSnapshot, + exception.InvalidGroupSnapshot) as e: raise exc.HTTPBadRequest(explanation=six.text_type(e)) except Exception: msg = _("Failed cgsnapshot") @@ -83,16 +94,54 @@ class CgsnapshotsController(wsgi.Controller): """Returns a detailed list of cgsnapshots.""" return self._get_cgsnapshots(req, is_detail=True) + def _get_cg(self, context, id): + # Not found exception will be handled at the wsgi level + try: + consistencygroup = self.cgsnapshot_api.get( + context, + group_id=id) + except exception.ConsistencyGroupNotFound: + consistencygroup = self.group_snapshot_api.get( + context, group_id=id) + + return consistencygroup + + def _get_cgsnapshot(self, context, id): + # Not found exception will be handled at the wsgi level + try: + cgsnapshot = self.cgsnapshot_api.get_cgsnapshot( + context, + cgsnapshot_id=id) + except exception.CgSnapshotNotFound: + cgsnapshot = self.group_snapshot_api.get_group_snapshot( + context, + group_snapshot_id=id) + + return cgsnapshot + def _get_cgsnapshots(self, req, is_detail): """Returns a list of cgsnapshots, transformed through view builder.""" context = req.environ['cinder.context'] cgsnapshots = self.cgsnapshot_api.get_all_cgsnapshots(context) - limited_list = common.limited(cgsnapshots, req) + cgsnap_limited_list = common.limited(cgsnapshots, req) + grp_snapshots = self.group_snapshot_api.get_all_group_snapshots( + context) + grpsnap_limited_list = common.limited(grp_snapshots, req) if is_detail: - cgsnapshots = self._view_builder.detail_list(req, limited_list) + cgsnapshots = self._view_builder.detail_list( + req, cgsnap_limited_list) + grp_snapshots = self._view_builder.detail_list( + req, grpsnap_limited_list) else: - cgsnapshots = self._view_builder.summary_list(req, limited_list) + cgsnapshots = self._view_builder.summary_list( + req, cgsnap_limited_list) + grp_snapshots = self._view_builder.summary_list( + req, grpsnap_limited_list) + + cgsnapshots['cgsnapshots'] = (cgsnapshots['cgsnapshots'] + + grp_snapshots['cgsnapshots']) + return cgsnapshots @wsgi.response(202) @@ -112,7 +161,7 @@ class CgsnapshotsController(wsgi.Controller): raise exc.HTTPBadRequest(explanation=msg) # Not found exception will be handled at the wsgi level - group = self.cgsnapshot_api.get(context, group_id) + group = self._get_cg(context, group_id) name = cgsnapshot.get('name', None) description = cgsnapshot.get('description', None) @@ -122,10 +171,21 @@ class CgsnapshotsController(wsgi.Controller): context=context) try: - new_cgsnapshot = self.cgsnapshot_api.create_cgsnapshot( - context, group, name, description) + if isinstance(group, cg_obj.ConsistencyGroup): + new_cgsnapshot = self.cgsnapshot_api.create_cgsnapshot( + context, group, name, description) + elif isinstance(group, grp_obj.Group): + new_cgsnapshot = self.group_snapshot_api.create_group_snapshot( + context, group, name, description) + else: + msg = _("Group %s not found.") % group.id + raise exc.HTTPNotFound(explanation=msg) # Not found exception will be handled at the wsgi level - except exception.InvalidCgSnapshot as error: + except (exception.InvalidCgSnapshot, + exception.InvalidConsistencyGroup, + exception.InvalidGroup, + exception.InvalidGroupSnapshot, + exception.InvalidVolume) as error: raise exc.HTTPBadRequest(explanation=error.msg) retval = self._view_builder.summary(req, new_cgsnapshot) diff --git a/cinder/api/contrib/consistencygroups.py b/cinder/api/contrib/consistencygroups.py index ec0bc2b53..cecace324 100644 --- a/cinder/api/contrib/consistencygroups.py +++ b/cinder/api/contrib/consistencygroups.py @@ -24,9 +24,15 @@ from cinder.api import common from cinder.api import extensions from cinder.api.openstack import wsgi from cinder.api.views import consistencygroups as consistencygroup_views -from cinder import consistencygroup as consistencygroupAPI +from cinder import consistencygroup as consistencygroup_api from cinder import exception +from cinder import group as group_api from cinder.i18n import _, _LI +from cinder.objects import cgsnapshot as cgsnap_obj +from cinder.objects import consistencygroup as cg_obj +from cinder.objects import group as grp_obj +from cinder.objects import group_snapshot as grpsnap_obj +from cinder.volume import group_types LOG = logging.getLogger(__name__) @@ -37,7 +43,8 @@ class ConsistencyGroupsController(wsgi.Controller): _view_builder_class = consistencygroup_views.ViewBuilder def __init__(self): - self.consistencygroup_api = consistencygroupAPI.API() + self.consistencygroup_api = consistencygroup_api.API() + self.group_api = group_api.API() super(ConsistencyGroupsController, self).__init__() def show(self, req, id): @@ -46,9 +53,7 @@ class ConsistencyGroupsController(wsgi.Controller): context = req.environ['cinder.context'] # Not found exception will be handled at the wsgi level - consistencygroup = self.consistencygroup_api.get( - context, - group_id=id) + consistencygroup = self._get(context, id) return self._view_builder.detail(req, consistencygroup) @@ -74,8 +79,14 @@ class ConsistencyGroupsController(wsgi.Controller): LOG.info(_LI('Delete consistency group with id: %s'), id) try: - group = self.consistencygroup_api.get(context, id) - self.consistencygroup_api.delete(context, group, force) + group = self._get(context, id) + if isinstance(group, cg_obj.ConsistencyGroup): + self.consistencygroup_api.delete(context, group, force) + elif isinstance(group, grp_obj.Group): + self.group_api.delete(context, group, force) + else: + msg = _("Group '%s' not found.") % id + raise exc.HTTPNotFound(explanation=msg) # Not found exception will be handled at the wsgi level except exception.InvalidConsistencyGroup as error: raise exc.HTTPBadRequest(explanation=error.msg) @@ -90,6 +101,30 @@ class ConsistencyGroupsController(wsgi.Controller): """Returns a detailed list of consistency groups.""" return self._get_consistencygroups(req, is_detail=True) + def _get(self, context, id): + # Not found exception will be handled at the wsgi level + try: + consistencygroup = self.consistencygroup_api.get( + context, + group_id=id) + except exception.ConsistencyGroupNotFound: + consistencygroup = self.group_api.get(context, group_id=id) + + return consistencygroup + + def _get_cgsnapshot(self, context, id): + # Not found exception will be handled at the wsgi level + try: + cgsnapshot = self.consistencygroup_api.get_cgsnapshot( + context, + cgsnapshot_id=id) + except exception.CgSnapshotNotFound: + cgsnapshot = self.group_api.get_group_snapshot( + context, + group_snapshot_id=id) + + return cgsnapshot + def _get_consistencygroups(self, req, is_detail): """Returns a list of consistency groups through view builder.""" context = req.environ['cinder.context'] @@ -101,12 +136,22 @@ class ConsistencyGroupsController(wsgi.Controller): context, filters=filters, marker=marker, limit=limit, offset=offset, sort_keys=sort_keys, sort_dirs=sort_dirs) + groups = self.group_api.get_all( + context, filters=filters, marker=marker, limit=limit, + offset=offset, sort_keys=sort_keys, sort_dirs=sort_dirs) + if is_detail: consistencygroups = self._view_builder.detail_list( req, consistencygroups) + groups = self._view_builder.detail_list(req, groups) else: consistencygroups = self._view_builder.summary_list( req, consistencygroups) + groups = self._view_builder.summary_list(req, groups) + + consistencygroups['consistencygroups'] = ( + consistencygroups['consistencygroups'] + + groups['consistencygroups']) return consistencygroups @wsgi.response(202) @@ -125,20 +170,30 @@ class ConsistencyGroupsController(wsgi.Controller): msg = _("volume_types must be provided to create " "consistency group %(name)s.") % {'name': name} raise exc.HTTPBadRequest(explanation=msg) + volume_types = volume_types.rstrip(',').split(',') availability_zone = consistencygroup.get('availability_zone', None) + group_type = group_types.get_default_cgsnapshot_type() + if not group_type: + msg = (_('Group type %s not found. Rerun migration script to ' + 'create the default cgsnapshot type.') % + group_types.DEFAULT_CGSNAPSHOT_TYPE) + raise exc.HTTPBadRequest(explanation=msg) LOG.info(_LI("Creating consistency group %(name)s."), {'name': name}) try: - new_consistencygroup = self.consistencygroup_api.create( - context, name, description, volume_types, + new_consistencygroup = self.group_api.create( + context, name, description, group_type['id'], volume_types, availability_zone=availability_zone) - # Not found exception will be handled at the wsgi level - except exception.InvalidConsistencyGroup as error: - raise exc.HTTPBadRequest(explanation=error.msg) - except exception.InvalidVolumeType as error: + except (exception.InvalidConsistencyGroup, + exception.InvalidGroup, + exception.InvalidVolumeType, + exception.ObjectActionError) as error: raise exc.HTTPBadRequest(explanation=error.msg) + except exception.NotFound: + # Not found exception will be handled at the wsgi level + raise retval = self._view_builder.summary(req, new_consistencygroup) return retval @@ -183,8 +238,25 @@ class ConsistencyGroupsController(wsgi.Controller): {'name': name, 'source_cgid': source_cgid}) try: - new_consistencygroup = self.consistencygroup_api.create_from_src( - context, name, description, cgsnapshot_id, source_cgid) + src_grp = None + src_snap = None + if source_cgid: + src_grp = self._get(context, source_cgid) + if cgsnapshot_id: + src_snap = self._get_cgsnapshot(context, cgsnapshot_id) + if (isinstance(src_grp, cg_obj.ConsistencyGroup) or + isinstance(src_snap, cgsnap_obj.CGSnapshot)): + new_group = self.consistencygroup_api.create_from_src( + context, name, description, cgsnapshot_id, source_cgid) + elif (isinstance(src_grp, grp_obj.Group) or + isinstance(src_snap, grpsnap_obj.GroupSnapshot)): + new_group = self.group_api.create_from_src( + context, name, description, cgsnapshot_id, source_cgid) + else: + msg = (_("Source CGSnapshot %(cgsnap)s or source CG %(cg)s " + "not found.") % {'cgsnap': cgsnapshot_id, + 'cg': source_cgid}) + raise exc.HTTPNotFound(explanation=msg) except exception.InvalidConsistencyGroup as error: raise exc.HTTPBadRequest(explanation=error.msg) except exception.NotFound: @@ -193,7 +265,7 @@ class ConsistencyGroupsController(wsgi.Controller): except exception.CinderException as error: raise exc.HTTPBadRequest(explanation=error.msg) - retval = self._view_builder.summary(req, new_consistencygroup) + retval = self._view_builder.summary(req, new_group) return retval def _check_update_parameters(self, name, description, add_volumes, @@ -215,11 +287,17 @@ class ConsistencyGroupsController(wsgi.Controller): 'add_volumes': add_volumes, 'remove_volumes': remove_volumes}) - # Handle relevant exceptions at wsgi level - group = self.consistencygroup_api.get(context, id) - self.consistencygroup_api.update(context, group, name, description, - add_volumes, remove_volumes, - allow_empty) + group = self._get(context, id) + if isinstance(group, cg_obj.ConsistencyGroup): + self.consistencygroup_api.update(context, group, name, description, + add_volumes, remove_volumes, + allow_empty) + elif isinstance(group, grp_obj.Group): + self.group_api.update(context, group, name, description, + add_volumes, remove_volumes) + else: + msg = _("Group '%s' not found.") % id + raise exc.HTTPNotFound(explanation=msg) def update(self, req, id, body): """Update the consistency group. diff --git a/cinder/api/v2/views/volumes.py b/cinder/api/v2/views/volumes.py index dd74bc811..8e9139be0 100644 --- a/cinder/api/v2/views/volumes.py +++ b/cinder/api/v2/views/volumes.py @@ -16,7 +16,9 @@ import six from cinder.api import common +from cinder import group as group_api from cinder.objects import fields +from cinder.volume import group_types class ViewBuilder(common.ViewBuilder): @@ -92,6 +94,18 @@ class ViewBuilder(common.ViewBuilder): if request.environ['cinder.context'].is_admin: volume_ref['volume']['migration_status'] = ( volume.get('migration_status')) + + # NOTE(xyang): Display group_id as consistencygroup_id in detailed + # view of the volume if group is converted from cg. + group_id = volume.get('group_id') + if group_id is not None: + # Not found exception will be handled at the wsgi level + ctxt = request.environ['cinder.context'] + grp = group_api.API().get(ctxt, group_id) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if grp.group_type_id == cgsnap_type['id']: + volume_ref['volume']['consistencygroup_id'] = group_id + return volume_ref def _is_volume_encrypted(self, volume): diff --git a/cinder/api/v2/volumes.py b/cinder/api/v2/volumes.py index ecdb862bd..0f91bf18e 100644 --- a/cinder/api/v2/volumes.py +++ b/cinder/api/v2/volumes.py @@ -27,6 +27,7 @@ from cinder.api.openstack import wsgi from cinder.api.v2.views import volumes as volume_views from cinder import consistencygroup as consistencygroupAPI from cinder import exception +from cinder import group as group_api from cinder.i18n import _, _LI from cinder.image import glance from cinder import utils @@ -47,6 +48,7 @@ class VolumeController(wsgi.Controller): def __init__(self, ext_mgr): self.volume_api = cinder_volume.API() self.consistencygroup_api = consistencygroupAPI.API() + self.group_api = group_api.API() self.ext_mgr = ext_mgr super(VolumeController, self).__init__() @@ -236,10 +238,14 @@ class VolumeController(wsgi.Controller): consistencygroup_id = volume.get('consistencygroup_id') if consistencygroup_id is not None: - # Not found exception will be handled at the wsgi level - kwargs['consistencygroup'] = \ - self.consistencygroup_api.get(context, - consistencygroup_id) + try: + kwargs['consistencygroup'] = ( + self.consistencygroup_api.get(context, + consistencygroup_id)) + except exception.ConsistencyGroupNotFound: + # Not found exception will be handled at the wsgi level + kwargs['group'] = self.group_api.get( + context, consistencygroup_id) else: kwargs['consistencygroup'] = None diff --git a/cinder/api/v3/group_snapshots.py b/cinder/api/v3/group_snapshots.py index 0e43c6854..09f38d87d 100644 --- a/cinder/api/v3/group_snapshots.py +++ b/cinder/api/v3/group_snapshots.py @@ -27,6 +27,7 @@ from cinder import exception from cinder import group as group_api from cinder.i18n import _, _LI from cinder import rpc +from cinder.volume import group_types LOG = logging.getLogger(__name__) @@ -42,6 +43,14 @@ class GroupSnapshotsController(wsgi.Controller): self.group_snapshot_api = group_api.API() super(GroupSnapshotsController, self).__init__() + def _check_default_cgsnapshot_type(self, group_type_id): + if group_types.is_default_cgsnapshot_type(group_type_id): + msg = (_("Group_type %(group_type)s is reserved for migrating " + "CGs to groups. Migrated group snapshots can only be " + "operated by CG snapshot APIs.") + % {'group_type': group_type_id}) + raise exc.HTTPBadRequest(explanation=msg) + @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) def show(self, req, id): """Return data about the given group_snapshot.""" @@ -52,6 +61,8 @@ class GroupSnapshotsController(wsgi.Controller): context, group_snapshot_id=id) + self._check_default_cgsnapshot_type(group_snapshot.group_type_id) + return self._view_builder.detail(req, group_snapshot) @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) @@ -66,6 +77,7 @@ class GroupSnapshotsController(wsgi.Controller): group_snapshot = self.group_snapshot_api.get_group_snapshot( context, group_snapshot_id=id) + self._check_default_cgsnapshot_type(group_snapshot.group_type_id) self.group_snapshot_api.delete_group_snapshot(context, group_snapshot) except exception.InvalidGroupSnapshot as e: @@ -102,7 +114,20 @@ class GroupSnapshotsController(wsgi.Controller): else: group_snapshots = self._view_builder.summary_list(req, limited_list) - return group_snapshots + + new_group_snapshots = [] + for grp_snap in group_snapshots['group_snapshots']: + try: + # Only show group snapshots not migrated from CG snapshots + self._check_default_cgsnapshot_type(grp_snap['group_type_id']) + if not is_detail: + grp_snap.pop('group_type_id', None) + new_group_snapshots.append(grp_snap) + except exc.HTTPBadRequest: + # Skip migrated group snapshot + pass + + return {'group_snapshots': new_group_snapshots} @wsgi.Controller.api_version(GROUP_SNAPSHOT_API_VERSION) @wsgi.response(202) @@ -122,7 +147,7 @@ class GroupSnapshotsController(wsgi.Controller): raise exc.HTTPBadRequest(explanation=msg) group = self.group_snapshot_api.get(context, group_id) - + self._check_default_cgsnapshot_type(group.group_type_id) name = group_snapshot.get('name', None) description = group_snapshot.get('description', None) diff --git a/cinder/api/v3/groups.py b/cinder/api/v3/groups.py index 95f944543..3336222cd 100644 --- a/cinder/api/v3/groups.py +++ b/cinder/api/v3/groups.py @@ -16,6 +16,7 @@ from oslo_log import log as logging from oslo_utils import strutils +from oslo_utils import uuidutils import webob from webob import exc @@ -25,6 +26,7 @@ from cinder.api.v3.views import groups as views_groups from cinder import exception from cinder import group as group_api from cinder.i18n import _, _LI +from cinder.volume import group_types LOG = logging.getLogger(__name__) @@ -41,6 +43,13 @@ class GroupsController(wsgi.Controller): self.group_api = group_api.API() super(GroupsController, self).__init__() + def _check_default_cgsnapshot_type(self, group_type_id): + if group_types.is_default_cgsnapshot_type(group_type_id): + msg = _("Group_type %(group_type)s is reserved for migrating " + "CGs to groups. Migrated group can only be operated by " + "CG APIs.") % {'group_type': group_type_id} + raise exc.HTTPBadRequest(explanation=msg) + @wsgi.Controller.api_version(GROUP_API_VERSION) def show(self, req, id): """Return data about the given group.""" @@ -52,6 +61,8 @@ class GroupsController(wsgi.Controller): context, group_id=id) + self._check_default_cgsnapshot_type(group.group_type_id) + return self._view_builder.detail(req, group) @wsgi.Controller.api_version(GROUP_API_VERSION) @@ -85,6 +96,7 @@ class GroupsController(wsgi.Controller): try: group = self.group_api.get(context, id) + self._check_default_cgsnapshot_type(group.group_type_id) self.group_api.delete(context, group, del_vol) except exception.GroupNotFound: # Not found exception will be handled at the wsgi level @@ -115,12 +127,22 @@ class GroupsController(wsgi.Controller): context, filters=filters, marker=marker, limit=limit, offset=offset, sort_keys=sort_keys, sort_dirs=sort_dirs) + new_groups = [] + for grp in groups: + try: + # Only show groups not migrated from CGs + self._check_default_cgsnapshot_type(grp.group_type_id) + new_groups.append(grp) + except exc.HTTPBadRequest: + # Skip migrated group + pass + if is_detail: groups = self._view_builder.detail_list( - req, groups) + req, new_groups) else: groups = self._view_builder.summary_list( - req, groups) + req, new_groups) return groups @wsgi.Controller.api_version(GROUP_API_VERSION) @@ -140,6 +162,11 @@ class GroupsController(wsgi.Controller): msg = _("group_type must be provided to create " "group %(name)s.") % {'name': name} raise exc.HTTPBadRequest(explanation=msg) + if not uuidutils.is_uuid_like(group_type): + req_group_type = group_types.get_group_type_by_name(context, + group_type) + group_type = req_group_type.id + self._check_default_cgsnapshot_type(group_type) volume_types = group.get('volume_types') if not volume_types: msg = _("volume_types must be provided to create " @@ -196,16 +223,24 @@ class GroupsController(wsgi.Controller): "source.") % {'name': name} raise exc.HTTPBadRequest(explanation=msg) + group_type_id = None if group_snapshot_id: LOG.info(_LI("Creating group %(name)s from group_snapshot " "%(snap)s."), {'name': name, 'snap': group_snapshot_id}, context=context) + grp_snap = self.group_api.get_group_snapshot(context, + group_snapshot_id) + group_type_id = grp_snap.group_type_id 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) + source_group = self.group_api.get(context, source_group_id) + group_type_id = source_group.group_type_id + + self._check_default_cgsnapshot_type(group_type_id) try: new_group = self.group_api.create_from_src( @@ -274,6 +309,7 @@ class GroupsController(wsgi.Controller): try: group = self.group_api.get(context, id) + self._check_default_cgsnapshot_type(group.group_type_id) self.group_api.update( context, group, name, description, add_volumes, remove_volumes) diff --git a/cinder/api/v3/views/group_snapshots.py b/cinder/api/v3/views/group_snapshots.py index b3411fd05..ac30724ff 100644 --- a/cinder/api/v3/views/group_snapshots.py +++ b/cinder/api/v3/views/group_snapshots.py @@ -38,7 +38,10 @@ class ViewBuilder(common.ViewBuilder): return { 'group_snapshot': { 'id': group_snapshot.id, - 'name': group_snapshot.name + 'name': group_snapshot.name, + # NOTE(xyang): group_type_id is added for migrating CGs + # to generic volume groups + 'group_type_id': group_snapshot.group_type_id, } } @@ -48,6 +51,7 @@ class ViewBuilder(common.ViewBuilder): 'group_snapshot': { 'id': group_snapshot.id, 'group_id': group_snapshot.group_id, + 'group_type_id': group_snapshot.group_type_id, 'status': group_snapshot.status, 'created_at': group_snapshot.created_at, 'name': group_snapshot.name, diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index b0a446a26..94b78e12e 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -194,10 +194,14 @@ class VolumeController(volumes_v2.VolumeController): consistencygroup_id = volume.get('consistencygroup_id') if consistencygroup_id is not None: - # Not found exception will be handled at the wsgi level - kwargs['consistencygroup'] = ( - self.consistencygroup_api.get(context, - consistencygroup_id)) + try: + kwargs['consistencygroup'] = ( + self.consistencygroup_api.get(context, + consistencygroup_id)) + except exception.ConsistencyGroupNotFound: + # Not found exception will be handled at the wsgi level + kwargs['group'] = self.group_api.get( + context, consistencygroup_id) else: kwargs['consistencygroup'] = None diff --git a/cinder/api/views/cgsnapshots.py b/cinder/api/views/cgsnapshots.py index 37175995b..26396e2e8 100644 --- a/cinder/api/views/cgsnapshots.py +++ b/cinder/api/views/cgsnapshots.py @@ -44,10 +44,20 @@ class ViewBuilder(common.ViewBuilder): def detail(self, request, cgsnapshot): """Detailed view of a single cgsnapshot.""" + try: + group_id = cgsnapshot.consistencygroup_id + except AttributeError: + try: + group_id = cgsnapshot.group_id + except AttributeError: + group_id = None + else: + group_id = None + return { 'cgsnapshot': { 'id': cgsnapshot.id, - 'consistencygroup_id': cgsnapshot.consistencygroup_id, + 'consistencygroup_id': group_id, 'status': cgsnapshot.status, 'created_at': cgsnapshot.created_at, 'name': cgsnapshot.name, diff --git a/cinder/api/views/consistencygroups.py b/cinder/api/views/consistencygroups.py index 8fec2801e..0644cabb5 100644 --- a/cinder/api/views/consistencygroups.py +++ b/cinder/api/views/consistencygroups.py @@ -44,11 +44,16 @@ class ViewBuilder(common.ViewBuilder): def detail(self, request, consistencygroup): """Detailed view of a single consistency group.""" - if consistencygroup.volume_type_id: - volume_types = consistencygroup.volume_type_id.split(",") + try: + volume_types = (consistencygroup.volume_type_id.split(",") + if consistencygroup.volume_type_id else []) volume_types = [type_id for type_id in volume_types if type_id] - else: - volume_types = [] + except AttributeError: + try: + volume_types = [v_type.id for v_type in + consistencygroup.volume_types] + except AttributeError: + volume_types = [] return { 'consistencygroup': { diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index 240c3d548..1798d7205 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -5502,6 +5502,8 @@ def group_create(context, values, group_snapshot_id=None, values.pop('group_type_id', None) values.pop('availability_zone', None) values.pop('host', None) + # NOTE(xyang): Save volume_type_ids to update later. + volume_type_ids = values.pop('volume_type_ids', []) sel = session.query(group_model.group_type_id, group_model.availability_zone, @@ -5521,6 +5523,12 @@ def group_create(context, values, group_snapshot_id=None, group_id=source_group_id) raise exception.GroupSnapshotNotFound( group_snapshot_id=group_snapshot_id) + + for item in volume_type_ids: + mapping = models.GroupVolumeTypeMapping() + mapping['volume_type_id'] = item + mapping['group_id'] = values['id'] + session.add(mapping) else: mappings = [] for item in values.get('volume_type_ids') or []: @@ -5528,7 +5536,6 @@ def group_create(context, values, group_snapshot_id=None, mapping['volume_type_id'] = item mapping['group_id'] = values['id'] mappings.append(mapping) - values['volume_types'] = mappings group = group_model() diff --git a/cinder/group/api.py b/cinder/group/api.py index 487ac5fd0..231784ac4 100644 --- a/cinder/group/api.py +++ b/cinder/group/api.py @@ -190,6 +190,19 @@ class API(base.Base): group_snapshot_id=None, source_group_id=None): check_policy(context, 'create') + # Populate group_type_id and volume_type_ids + group_type_id = None + volume_type_ids = [] + if group_snapshot_id: + grp_snap = self.get_group_snapshot(context, group_snapshot_id) + group_type_id = grp_snap.group_type_id + grp_snap_src_grp = self.get(context, grp_snap.group_id) + volume_type_ids = [vt.id for vt in grp_snap_src_grp.volume_types] + elif source_group_id: + source_group = self.get(context, source_group_id) + group_type_id = source_group.group_type_id + volume_type_ids = [vt.id for vt in source_group.volume_types] + kwargs = { 'user_id': context.user_id, 'project_id': context.project_id, @@ -198,6 +211,8 @@ class API(base.Base): 'description': description, 'group_snapshot_id': group_snapshot_id, 'source_group_id': source_group_id, + 'group_type_id': group_type_id, + 'volume_type_ids': volume_type_ids, } group = None diff --git a/cinder/objects/base.py b/cinder/objects/base.py index 4be268188..478b4434a 100644 --- a/cinder/objects/base.py +++ b/cinder/objects/base.py @@ -123,6 +123,7 @@ OBJ_VERSIONS.add('1.15', {'Volume': '1.6', 'Snapshot': '1.2'}) OBJ_VERSIONS.add('1.16', {'BackupDeviceInfo': '1.0'}) OBJ_VERSIONS.add('1.17', {'VolumeAttachment': '1.1'}) OBJ_VERSIONS.add('1.18', {'Snapshot': '1.3'}) +OBJ_VERSIONS.add('1.19', {'ConsistencyGroup': '1.4', 'CGSnapshot': '1.1'}) class CinderObjectRegistry(base.VersionedObjectRegistry): diff --git a/cinder/objects/cgsnapshot.py b/cinder/objects/cgsnapshot.py index 2e12fec2b..7a8dc74a5 100644 --- a/cinder/objects/cgsnapshot.py +++ b/cinder/objects/cgsnapshot.py @@ -23,7 +23,9 @@ from oslo_versionedobjects import fields @base.CinderObjectRegistry.register class CGSnapshot(base.CinderPersistentObject, base.CinderObject, base.CinderObjectDictCompat): - VERSION = '1.0' + # Version 1.0: Initial version + # Version 1.1: Added from_group_snapshot + VERSION = '1.1' OPTIONAL_FIELDS = ['consistencygroup', 'snapshots'] @@ -85,6 +87,16 @@ class CGSnapshot(base.CinderPersistentObject, base.CinderObject, db_cgsnapshots = db.cgsnapshot_create(self._context, updates) self._from_db_object(self._context, self, db_cgsnapshots) + def from_group_snapshot(self, group_snapshot): + """Convert a generic volume group object to a cg object.""" + self.id = group_snapshot.id + self.consistencygroup_id = group_snapshot.group_id + self.user_id = group_snapshot.user_id + self.project_id = group_snapshot.project_id + self.name = group_snapshot.name + self.description = group_snapshot.description + self.status = group_snapshot.status + def obj_load_attr(self, attrname): if attrname not in self.OPTIONAL_FIELDS: raise exception.ObjectActionError( diff --git a/cinder/objects/consistencygroup.py b/cinder/objects/consistencygroup.py index 46fb5e461..05979cd65 100644 --- a/cinder/objects/consistencygroup.py +++ b/cinder/objects/consistencygroup.py @@ -30,7 +30,8 @@ class ConsistencyGroup(base.CinderPersistentObject, base.CinderObject, # Version 1.1: Added cgsnapshots and volumes relationships # Version 1.2: Changed 'status' field to use ConsistencyGroupStatusField # Version 1.3: Added cluster fields - VERSION = '1.3' + # Version 1.4: Added from_group + VERSION = '1.4' OPTIONAL_FIELDS = ('cgsnapshots', 'volumes', 'cluster') @@ -136,6 +137,23 @@ class ConsistencyGroup(base.CinderPersistentObject, base.CinderObject, cg_id) self._from_db_object(self._context, self, db_consistencygroups) + def from_group(self, group): + """Convert a generic volume group object to a cg object.""" + self.id = group.id + self.user_id = group.user_id + self.project_id = group.project_id + self.cluster_name = group.cluster_name + self.host = group.host + self.availability_zone = group.availability_zone + self.name = group.name + self.description = group.description + self.volume_type_id = "" + for v_type in group.volume_types: + self.volume_type_id += v_type.id + "," + self.status = group.status + self.cgsnapshot_id = group.group_snapshot_id + self.source_cgid = group.source_group_id + def obj_load_attr(self, attrname): if attrname not in self.OPTIONAL_FIELDS: raise exception.ObjectActionError( diff --git a/cinder/tests/unit/api/contrib/test_cgsnapshots.py b/cinder/tests/unit/api/contrib/test_cgsnapshots.py index 9f30855e8..68c106d54 100644 --- a/cinder/tests/unit/api/contrib/test_cgsnapshots.py +++ b/cinder/tests/unit/api/contrib/test_cgsnapshots.py @@ -85,7 +85,7 @@ class CgsnapshotsAPITestCase(test.TestCase): self.assertEqual(404, res.status_int) self.assertEqual(404, res_dict['itemNotFound']['code']) - self.assertEqual('CgSnapshot %s could not be found.' % + self.assertEqual('GroupSnapshot %s could not be found.' % fake.WILL_NOT_BE_FOUND_ID, res_dict['itemNotFound']['message']) @@ -410,7 +410,7 @@ class CgsnapshotsAPITestCase(test.TestCase): self.assertEqual(404, res.status_int) self.assertEqual(404, res_dict['itemNotFound']['code']) - self.assertEqual('CgSnapshot %s could not be found.' % + self.assertEqual('GroupSnapshot %s could not be found.' % fake.WILL_NOT_BE_FOUND_ID, res_dict['itemNotFound']['message']) diff --git a/cinder/tests/unit/api/contrib/test_consistencygroups.py b/cinder/tests/unit/api/contrib/test_consistencygroups.py index e49de58be..6ae77038b 100644 --- a/cinder/tests/unit/api/contrib/test_consistencygroups.py +++ b/cinder/tests/unit/api/contrib/test_consistencygroups.py @@ -110,7 +110,7 @@ class ConsistencyGroupsAPITestCase(test.TestCase): self.assertEqual(404, res.status_int) self.assertEqual(404, res_dict['itemNotFound']['code']) - self.assertEqual('ConsistencyGroup %s could not be found.' % + self.assertEqual('Group %s could not be found.' % fake.WILL_NOT_BE_FOUND_ID, res_dict['itemNotFound']['message']) @@ -411,8 +411,12 @@ class ConsistencyGroupsAPITestCase(test.TestCase): self.assertTrue(mock_validate.called) group_id = res_dict['consistencygroup']['id'] - cg = objects.ConsistencyGroup.get_by_id(self.ctxt, - group_id) + try: + cg = objects.ConsistencyGroup.get_by_id(self.ctxt, + group_id) + except exception.ConsistencyGroupNotFound: + cg = objects.Group.get_by_id(self.ctxt, + group_id) cg.destroy() def test_create_consistencygroup_with_no_body(self): @@ -524,7 +528,7 @@ class ConsistencyGroupsAPITestCase(test.TestCase): self.assertEqual(404, res.status_int) self.assertEqual(404, res_dict['itemNotFound']['code']) - self.assertEqual('ConsistencyGroup %s could not be found.' % + self.assertEqual('Group %s could not be found.' % fake.WILL_NOT_BE_FOUND_ID, res_dict['itemNotFound']['message']) diff --git a/cinder/tests/unit/api/v2/test_volumes.py b/cinder/tests/unit/api/v2/test_volumes.py index 6985e7d59..740da80ca 100644 --- a/cinder/tests/unit/api/v2/test_volumes.py +++ b/cinder/tests/unit/api/v2/test_volumes.py @@ -384,7 +384,7 @@ class VolumeApiTest(test.TestCase): body = {"volume": vol} req = fakes.HTTPRequest.blank('/v2/volumes') # Raise 404 when consistency group is not found. - self.assertRaises(exception.ConsistencyGroupNotFound, + self.assertRaises(exception.GroupNotFound, self.controller.create, req, body) context = req.environ['cinder.context'] diff --git a/cinder/tests/unit/api/v3/test_group_snapshots.py b/cinder/tests/unit/api/v3/test_group_snapshots.py index 0e24dda0e..b29a6f3ba 100644 --- a/cinder/tests/unit/api/v3/test_group_snapshots.py +++ b/cinder/tests/unit/api/v3/test_group_snapshots.py @@ -98,11 +98,14 @@ class GroupSnapshotsAPITestCase(test.TestCase): group_id=group.id, volume_type_id=fake.VOLUME_TYPE_ID)['id'] group_snapshot1 = utils.create_group_snapshot( - self.context, group_id=group.id) + self.context, group_id=group.id, + group_type_id=group.group_type_id) group_snapshot2 = utils.create_group_snapshot( - self.context, group_id=group.id) + self.context, group_id=group.id, + group_type_id=group.group_type_id) group_snapshot3 = utils.create_group_snapshot( - self.context, group_id=group.id) + self.context, group_id=group.id, + group_type_id=group.group_type_id) req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots' % fake.PROJECT_ID, diff --git a/cinder/tests/unit/api/v3/test_groups.py b/cinder/tests/unit/api/v3/test_groups.py index b491193ee..e736acc74 100644 --- a/cinder/tests/unit/api/v3/test_groups.py +++ b/cinder/tests/unit/api/v3/test_groups.py @@ -830,17 +830,21 @@ class GroupsAPITestCase(test.TestCase): self.mock_object(volume_api.API, "create", v3_fakes.fake_volume_create) group = utils.create_group(self.ctxt, - group_type_id=fake.GROUP_TYPE_ID) + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID]) volume = utils.create_volume( self.ctxt, - group_id=group.id) + group_id=group.id, + volume_type_id=fake.VOLUME_TYPE_ID) group_snapshot = utils.create_group_snapshot( - self.ctxt, group_id=group.id) + self.ctxt, group_id=group.id, + group_type_id=group.group_type_id) snapshot = utils.create_snapshot( self.ctxt, volume.id, group_snapshot_id=group_snapshot.id, - status=fields.SnapshotStatus.AVAILABLE) + status=fields.SnapshotStatus.AVAILABLE, + volume_type_id=volume.volume_type_id) test_grp_name = 'test grp' body = {"create-from-src": {"name": test_grp_name, @@ -868,10 +872,12 @@ class GroupsAPITestCase(test.TestCase): self.mock_object(volume_api.API, "create", v3_fakes.fake_volume_create) source_grp = utils.create_group(self.ctxt, - group_type_id=fake.GROUP_TYPE_ID) + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID]) volume = utils.create_volume( self.ctxt, - group_id=source_grp.id) + group_id=source_grp.id, + volume_type_id=fake.VOLUME_TYPE_ID) test_grp_name = 'test cg' body = {"create-from-src": {"name": test_grp_name, diff --git a/cinder/tests/unit/group/test_groups_api.py b/cinder/tests/unit/group/test_groups_api.py index 3cc82aa3b..dcd0d7d43 100644 --- a/cinder/tests/unit/group/test_groups_api.py +++ b/cinder/tests/unit/group/test_groups_api.py @@ -493,28 +493,63 @@ class GroupAPITestCase(test.TestCase): @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.objects.GroupSnapshot.get_by_id') + @mock.patch('cinder.objects.SnapshotList.get_all_for_group_snapshot') @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): + def test_create_from_src(self, mock_policy, mock_snap_get_all, + mock_group_snap_get, 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 + status = fields.GroupStatus.AVAILABLE,) - 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) + vol1 = utils.create_volume( + self.ctxt, + availability_zone = 'nova', + volume_type_id = fake.VOLUME_TYPE_ID, + group_id = grp.id) + + snap = utils.create_snapshot(self.ctxt, vol1.id, + volume_type_id = fake.VOLUME_TYPE_ID, + status = fields.SnapshotStatus.AVAILABLE) + mock_snap_get_all.return_value = [snap] + + grp_snap = utils.create_group_snapshot( + self.ctxt, grp.id, + group_type_id = fake.GROUP_TYPE_ID, + status = fields.GroupStatus.AVAILABLE) + mock_group_snap_get.return_value = grp_snap + + grp2 = 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 = grp_snap.id) + + with mock.patch('cinder.objects.Group') as mock_group: + mock_group.return_value = grp2 + with mock.patch('cinder.objects.group.Group.create'): + ret_group = self.group_api.create_from_src( + self.ctxt, name, description, + group_snapshot_id = grp_snap.id, + source_group_id = None) + self.assertEqual(grp2.obj_to_primitive(), + ret_group.obj_to_primitive()) + mock_create_from_snap.assert_called_once_with( + self.ctxt, grp2, grp_snap.id) + + snap.destroy() + grp_snap.destroy() + vol1.destroy() + grp.destroy() + grp2.destroy() @mock.patch('oslo_utils.timeutils.utcnow') @mock.patch('cinder.objects.GroupSnapshot') diff --git a/cinder/tests/unit/objects/test_objects.py b/cinder/tests/unit/objects/test_objects.py index e3b478bf8..5bec70ae4 100644 --- a/cinder/tests/unit/objects/test_objects.py +++ b/cinder/tests/unit/objects/test_objects.py @@ -30,9 +30,9 @@ object_data = { 'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28', 'Cluster': '1.0-6f06e867c073e9d31722c53b0a9329b8', 'ClusterList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', - 'CGSnapshot': '1.0-3212ac2b4c2811b7134fb9ba2c49ff74', + 'CGSnapshot': '1.1-3212ac2b4c2811b7134fb9ba2c49ff74', 'CGSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', - 'ConsistencyGroup': '1.3-7bf01a79b82516639fc03cd3ab6d9c01', + 'ConsistencyGroup': '1.4-7bf01a79b82516639fc03cd3ab6d9c01', 'ConsistencyGroupList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'QualityOfServiceSpecs': '1.0-0b212e0a86ee99092229874e03207fe8', 'QualityOfServiceSpecsList': '1.0-1b54e51ad0fc1f3a8878f5010e7e16dc', diff --git a/cinder/tests/unit/test_volume_rpcapi.py b/cinder/tests/unit/test_volume_rpcapi.py index ead962945..443466e7e 100644 --- a/cinder/tests/unit/test_volume_rpcapi.py +++ b/cinder/tests/unit/test_volume_rpcapi.py @@ -91,13 +91,13 @@ class VolumeRpcAPITestCase(test.TestCase): generic_group = tests_utils.create_group( self.context, availability_zone=CONF.storage_availability_zone, - group_type_id='group_type1', + group_type_id=fake.GROUP_TYPE_ID, host='fakehost@fakedrv#fakepool') group_snapshot = tests_utils.create_group_snapshot( self.context, group_id=generic_group.id, - group_type_id='group_type1') + group_type_id=fake.GROUP_TYPE_ID) cg = objects.ConsistencyGroup.get_by_id(self.context, cg.id) cg2 = objects.ConsistencyGroup.get_by_id(self.context, cg2.id) diff --git a/cinder/tests/unit/utils.py b/cinder/tests/unit/utils.py index 564598f3f..7fbc66674 100644 --- a/cinder/tests/unit/utils.py +++ b/cinder/tests/unit/utils.py @@ -260,7 +260,8 @@ def create_group_snapshot(ctxt, 'status': status, 'name': name, 'description': description, - 'group_id': group_id} + 'group_id': group_id, + 'group_type_id': group_type_id} values.update(kwargs) if recursive_create_if_needed and group_id: diff --git a/cinder/volume/group_types.py b/cinder/volume/group_types.py index 7975fe64b..b898a1ef2 100644 --- a/cinder/volume/group_types.py +++ b/cinder/volume/group_types.py @@ -157,6 +157,11 @@ def get_default_cgsnapshot_type(): return grp_type +def is_default_cgsnapshot_type(group_type_id): + cgsnap_type = get_default_cgsnapshot_type() + return group_type_id == cgsnap_type['id'] + + def get_group_type_specs(group_type_id, key=False): group_type = get_group_type(context.get_admin_context(), group_type_id) diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 6247a7bd8..5f1e2dea4 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -68,6 +68,8 @@ from cinder.message import api as message_api from cinder.message import defined_messages from cinder.message import resource_types from cinder import objects +from cinder.objects import cgsnapshot +from cinder.objects import consistencygroup from cinder.objects import fields from cinder import quota from cinder import utils @@ -76,6 +78,7 @@ from cinder.volume import configuration as config from cinder.volume.flows.manager import create_volume from cinder.volume.flows.manager import manage_existing from cinder.volume.flows.manager import manage_existing_snapshot +from cinder.volume import group_types from cinder.volume import rpcapi as volume_rpcapi from cinder.volume import utils as vol_utils from cinder.volume import volume_types @@ -2481,8 +2484,14 @@ class VolumeManager(manager.CleanableManager, model_update = self.driver.create_group(context, group) except NotImplementedError: - model_update = self._create_group_generic(context, - group) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group.group_type_id != cgsnap_type['id']: + model_update = self._create_group_generic(context, + group) + else: + cg, __ = self._convert_group_to_cg(group, []) + model_update = self.driver.create_consistencygroup( + context, cg) else: model_update = self.driver.create_consistencygroup(context, group) @@ -2752,10 +2761,30 @@ class VolumeManager(manager.CleanableManager, context, group, volumes, group_snapshot, sorted_snapshots, source_group, sorted_source_vols)) except NotImplementedError: - model_update, volumes_model_update = ( - self._create_group_from_src_generic( - context, group, volumes, group_snapshot, - sorted_snapshots, source_group, sorted_source_vols)) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group.group_type_id != cgsnap_type['id']: + model_update, volumes_model_update = ( + self._create_group_from_src_generic( + context, group, volumes, group_snapshot, + sorted_snapshots, source_group, + sorted_source_vols)) + else: + cg, volumes = self._convert_group_to_cg( + group, volumes) + cgsnapshot, sorted_snapshots = ( + self._convert_group_snapshot_to_cgsnapshot( + group_snapshot, sorted_snapshots)) + source_cg, sorted_source_vols = ( + self._convert_group_to_cg(source_group, + sorted_source_vols)) + model_update, volumes_model_update = ( + self.driver.create_consistencygroup_from_src( + context, cg, volumes, cgsnapshot, + sorted_snapshots, source_cg, sorted_source_vols)) + self._remove_cgsnapshot_id_from_snapshots(sorted_snapshots) + self._remove_consistencygroup_id_from_volumes(volumes) + self._remove_consistencygroup_id_from_volumes( + sorted_source_vols) if volumes_model_update: for update in volumes_model_update: @@ -3100,8 +3129,17 @@ class VolumeManager(manager.CleanableManager, model_update, volumes_model_update = ( self.driver.delete_group(context, group, volumes)) except NotImplementedError: - model_update, volumes_model_update = ( - self._delete_group_generic(context, group, volumes)) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group.group_type_id != cgsnap_type['id']: + model_update, volumes_model_update = ( + self._delete_group_generic(context, group, volumes)) + else: + cg, volumes = self._convert_group_to_cg( + group, volumes) + model_update, volumes_model_update = ( + self.driver.delete_consistencygroup(context, cg, + volumes)) + self._remove_consistencygroup_id_from_volumes(volumes) if volumes_model_update: for update in volumes_model_update: @@ -3190,6 +3228,38 @@ class VolumeManager(manager.CleanableManager, resource={'type': 'group', 'id': group.id}) + def _convert_group_to_cg(self, group, volumes): + if not group: + return None, None + cg = consistencygroup.ConsistencyGroup() + cg.from_group(group) + for vol in volumes: + vol.consistencygroup_id = vol.group_id + + return group, volumes + + def _remove_consistencygroup_id_from_volumes(self, volumes): + if not volumes: + return + for vol in volumes: + vol.consistencygroup_id = None + + def _convert_group_snapshot_to_cgsnapshot(self, group_snapshot, snapshots): + if not group_snapshot: + return None, None + cgsnap = cgsnapshot.CGSnapshot() + cgsnap.from_group_snapshot(group_snapshot) + for snap in snapshots: + snap.cgsnapshot_id = snap.group_snapshot_id + + return cgsnap, snapshots + + def _remove_cgsnapshot_id_from_snapshots(self, snapshots): + if not snapshots: + return + for snap in snapshots: + snap.cgsnapshot_id = None + def _create_group_generic(self, context, group): """Creates a group.""" # A group entry is already created in db. Just returns a status here. @@ -3437,11 +3507,23 @@ class VolumeManager(manager.CleanableManager, add_volumes=add_volumes_ref, remove_volumes=remove_volumes_ref)) except NotImplementedError: - model_update, add_volumes_update, remove_volumes_update = ( - self._update_group_generic( - context, group, - add_volumes=add_volumes_ref, - remove_volumes=remove_volumes_ref)) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group.group_type_id != cgsnap_type['id']: + model_update, add_volumes_update, remove_volumes_update = ( + self._update_group_generic( + context, group, + add_volumes=add_volumes_ref, + remove_volumes=remove_volumes_ref)) + else: + cg, remove_volumes_ref = self._convert_group_to_cg( + group, remove_volumes_ref) + model_update, add_volumes_update, remove_volumes_update = ( + self.driver.update_consistencygroup( + context, group, + add_volumes=add_volumes_ref, + remove_volumes=remove_volumes_ref)) + self._remove_consistencygroup_id_from_volumes( + remove_volumes_ref) if add_volumes_update: self.db.volumes_update(context, add_volumes_update) @@ -3643,10 +3725,19 @@ class VolumeManager(manager.CleanableManager, self.driver.create_group_snapshot(context, group_snapshot, snapshots)) except NotImplementedError: - model_update, snapshots_model_update = ( - self._create_group_snapshot_generic( - context, group_snapshot, snapshots)) - + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group_snapshot.group_type_id != cgsnap_type['id']: + model_update, snapshots_model_update = ( + self._create_group_snapshot_generic( + context, group_snapshot, snapshots)) + else: + cgsnapshot, snapshots = ( + self._convert_group_snapshot_to_cgsnapshot( + group_snapshot, snapshots)) + model_update, snapshots_model_update = ( + self.driver.create_cgsnapshot(context, cgsnapshot, + snapshots)) + self._remove_cgsnapshot_id_from_snapshots(snapshots) if snapshots_model_update: for snap_model in snapshots_model_update: # Update db for snapshot. @@ -3898,9 +3989,19 @@ class VolumeManager(manager.CleanableManager, self.driver.delete_group_snapshot(context, group_snapshot, snapshots)) except NotImplementedError: - model_update, snapshots_model_update = ( - self._delete_group_snapshot_generic( - context, group_snapshot, snapshots)) + cgsnap_type = group_types.get_default_cgsnapshot_type() + if group_snapshot.group_type_id != cgsnap_type['id']: + model_update, snapshots_model_update = ( + self._delete_group_snapshot_generic( + context, group_snapshot, snapshots)) + else: + cgsnapshot, snapshots = ( + self._convert_group_snapshot_to_cgsnapshot( + group_snapshot, snapshots)) + model_update, snapshots_model_update = ( + self.driver.delete_cgsnapshot(context, cgsnapshot, + snapshots)) + self._remove_cgsnapshot_id_from_snapshots(snapshots) if snapshots_model_update: for snap_model in snapshots_model_update: diff --git a/releasenotes/notes/operate-migrated-groups-with-cp-apis-e5835c6673191805.yaml b/releasenotes/notes/operate-migrated-groups-with-cp-apis-e5835c6673191805.yaml new file mode 100644 index 000000000..241007d7a --- /dev/null +++ b/releasenotes/notes/operate-migrated-groups-with-cp-apis-e5835c6673191805.yaml @@ -0,0 +1,29 @@ +--- +upgrade: + - | + After running the migration script to migrate CGs to + generic volume groups, CG and group APIs work as follows. + + * Create CG only creates in the groups table. + * Modify CG modifies in the CG table if the CG is in the + CG table, otherwise it modifies in the groups table. + * Delete CG deletes from the CG or the groups table + depending on where the CG is. + * List CG checks both CG and groups tables. + * List CG Snapshots checks both the CG and the groups + tables. + * Show CG checks both tables. + * Show CG Snapshot checks both tables. + * Create CG Snapshot creates either in the CG or the groups + table depending on where the CG is. + * Create CG from Source creates in either the CG or the + groups table depending on the source. + * Create Volume adds the volume either to the CG or the + group. + * default_cgsnapshot_type is reserved for migrating CGs. + * Group APIs will only write/read in/from the groups table. + * Group APIs will not work on groups with default_cgsnapshot_type. + * Groups with default_cgsnapshot_type can only be operated by + CG APIs. + * After CG tables are removed, we will allow default_cgsnapshot_type + to be used by group APIs.