Add group snapshots - APIs

This is the fifth patch that implements the generic-volume-group
bluerpint. It adds APIs for group snapshots and create group
from source.

This patch depends on the fourth patch which implements group
snapshots support in the volume manager:
    https://review.openstack.org/#/c/361376/

Client side patch is here:
    https://review.openstack.org/#/c/329770/

Current microversion is 3.14. The following CLI's are supported:
cinder --os-volume-api-version 3.14 group-create-from-src
    --name my_group --group-snapshot <group snapshot uuid>
cinder --os-volume-api-version 3.14 group-create-from-src
    --name my_group --source-group <source group uuid>
cinder --os-volume-api-version 3.14 group-snapshot-create
    --name <name> <group uuid>
cinder --os-volume-api-version 3.14 group-snapshot-list
cinder --os-volume-api-version 3.14 group-snapshot-show
    <group snapshot uuid>
cinder --os-volume-api-version 3.14 group-snapshot-delete
    <group snapshot uuid>

APIImpact
DocImpact
Partial-Implements: blueprint generic-volume-group

Change-Id: I2e628968afcf058113e1f1aeb851570c7f0f3a08
This commit is contained in:
xing-yang 2016-07-17 10:56:36 -04:00
parent 4a67bc8218
commit 708b9be9c0
24 changed files with 1714 additions and 34 deletions

View File

@ -61,6 +61,7 @@ REST_API_VERSION_HISTORY = """
* 3.11 - Add group types and group specs API. * 3.11 - Add group types and group specs API.
* 3.12 - Add volumes summary API. * 3.12 - Add volumes summary API.
* 3.13 - Add generic volume groups 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. # minimum version of the API supported.
# Explicitly using /v1 or /v2 enpoints will still work # Explicitly using /v1 or /v2 enpoints will still work
_MIN_API_VERSION = "3.0" _MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.13" _MAX_API_VERSION = "3.14"
_LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION1 = "1.0"
_LEGACY_API_VERSION2 = "2.0" _LEGACY_API_VERSION2 = "2.0"

View File

@ -182,3 +182,7 @@ user documentation.
3.13 3.13
---- ----
Added create/delete/update/list/show APIs for generic volume groups. Added create/delete/update/list/show APIs for generic volume groups.
3.14
----
Added group snapshots and create group from src APIs.

View File

@ -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())

View File

@ -29,6 +29,7 @@ from cinder.i18n import _, _LI
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
GROUP_API_VERSION = '3.13' GROUP_API_VERSION = '3.13'
GROUP_CREATE_FROM_SRC_API_VERSION = '3.14'
class GroupsController(wsgi.Controller): class GroupsController(wsgi.Controller):
@ -163,6 +164,63 @@ class GroupsController(wsgi.Controller):
retval = self._view_builder.summary(req, new_group) retval = self._view_builder.summary(req, new_group)
return retval 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) @wsgi.Controller.api_version(GROUP_API_VERSION)
def update(self, req, id, body): def update(self, req, id, body):
"""Update the group. """Update the group.

View File

@ -23,17 +23,18 @@ from cinder.api import extensions
import cinder.api.openstack import cinder.api.openstack
from cinder.api.v2 import limits from cinder.api.v2 import limits
from cinder.api.v2 import snapshot_metadata from cinder.api.v2 import snapshot_metadata
from cinder.api.v2 import snapshots
from cinder.api.v2 import types from cinder.api.v2 import types
from cinder.api.v2 import volume_metadata from cinder.api.v2 import volume_metadata
from cinder.api.v3 import backups from cinder.api.v3 import backups
from cinder.api.v3 import clusters from cinder.api.v3 import clusters
from cinder.api.v3 import consistencygroups 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_specs
from cinder.api.v3 import group_types from cinder.api.v3 import group_types
from cinder.api.v3 import groups from cinder.api.v3 import groups
from cinder.api.v3 import messages from cinder.api.v3 import messages
from cinder.api.v3 import snapshot_manage 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 volume_manage
from cinder.api.v3 import volumes from cinder.api.v3 import volumes
from cinder.api import versions from cinder.api import versions
@ -93,6 +94,17 @@ class APIRouter(cinder.api.openstack.APIRouter):
controller=self.resources["groups"], controller=self.resources["groups"],
action="action", action="action",
conditions={"action": ["POST"]}) 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) self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
mapper.resource("snapshot", "snapshots", mapper.resource("snapshot", "snapshots",

View File

@ -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))

View File

@ -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

View File

@ -44,7 +44,7 @@ class ViewBuilder(common.ViewBuilder):
def detail(self, request, group): def detail(self, request, group):
"""Detailed view of a single group.""" """Detailed view of a single group."""
return { group_ref = {
'group': { 'group': {
'id': group.id, 'id': group.id,
'status': group.status, '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): def _list_view(self, func, request, groups):
"""Provide a view for a list of groups.""" """Provide a view for a list of groups."""
groups_list = [ groups_list = [

View File

@ -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

View File

@ -428,9 +428,9 @@ def snapshot_get_all_for_cgsnapshot(context, project_id):
return IMPL.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.""" """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): def snapshot_get_all_for_volume(context, volume_id):

View File

@ -26,9 +26,10 @@ from oslo_utils import excutils
from oslo_utils import timeutils from oslo_utils import timeutils
from cinder.common import constants from cinder.common import constants
from cinder import db
from cinder.db import base from cinder.db import base
from cinder import exception from cinder import exception
from cinder.i18n import _, _LE, _LW from cinder.i18n import _, _LE, _LI, _LW
from cinder import objects from cinder import objects
from cinder.objects import base as objects_base from cinder.objects import base as objects_base
from cinder.objects import fields as c_fields from cinder.objects import fields as c_fields
@ -180,6 +181,212 @@ class API(base.Base):
return group 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, def _cast_create_group(self, context, group,
group_spec, group_spec,
request_spec_list, request_spec_list,
@ -542,3 +749,90 @@ class API(base.Base):
limit=limit, offset=offset, sort_keys=sort_keys, limit=limit, offset=offset, sort_keys=sort_keys,
sort_dirs=sort_dirs) sort_dirs=sort_dirs)
return groups 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

View File

@ -15,9 +15,17 @@ import iso8601
from cinder.message import defined_messages from cinder.message import defined_messages
from cinder.tests.unit import fake_constants as fake 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 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): def stub_message(id, **kwargs):
@ -40,3 +48,68 @@ def stub_message(id, **kwargs):
def stub_message_get(self, context, message_id): def stub_message_get(self, context, message_id):
return stub_message(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

View File

@ -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()

View File

@ -30,10 +30,13 @@ from cinder import objects
from cinder.objects import fields from cinder.objects import fields
from cinder import test from cinder import test
from cinder.tests.unit.api import fakes 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 fake_constants as fake
from cinder.tests.unit import utils from cinder.tests.unit import utils
from cinder.volume import api as volume_api
GROUP_MICRO_VERSION = '3.13' GROUP_MICRO_VERSION = '3.13'
GROUP_FROM_SRC_MICRO_VERSION = '3.14'
@ddt.ddt @ddt.ddt
@ -804,3 +807,70 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.controller.update,
req, self.group1.id, body) 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()

View File

@ -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)

View File

@ -317,7 +317,7 @@ class VolumeApiTest(test.TestCase):
self.assertEqual(ex, res_dict) self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called) 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(group_api.API, 'get')
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full', @mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
autospec=True) autospec=True)

View File

@ -21,6 +21,7 @@ import ddt
import mock import mock
from cinder import context from cinder import context
from cinder import db
import cinder.group import cinder.group
from cinder import objects from cinder import objects
from cinder.objects import fields 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, mock_rpc_update_group.assert_called_once_with(self.ctxt, ret_group,
add_volumes = vol1.id, add_volumes = vol1.id,
remove_volumes = vol2.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)

View File

@ -125,6 +125,12 @@
"group:get": "", "group:get": "",
"group:get_all": "", "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", "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner", "message:delete": "rule:admin_or_owner",

View File

@ -73,14 +73,14 @@ class VolumeRpcAPITestCase(test.TestCase):
self.context, self.context,
consistencygroup_id=source_group.id) consistencygroup_id=source_group.id)
group = tests_utils.create_consistencygroup( cg = tests_utils.create_consistencygroup(
self.context, self.context,
availability_zone=CONF.storage_availability_zone, availability_zone=CONF.storage_availability_zone,
volume_type='type1,type2', volume_type='type1,type2',
host='fakehost@fakedrv#fakepool', host='fakehost@fakedrv#fakepool',
cgsnapshot_id=cgsnapshot.id) cgsnapshot_id=cgsnapshot.id)
group2 = tests_utils.create_consistencygroup( cg2 = tests_utils.create_consistencygroup(
self.context, self.context,
availability_zone=CONF.storage_availability_zone, availability_zone=CONF.storage_availability_zone,
volume_type='type1,type2', volume_type='type1,type2',
@ -93,20 +93,37 @@ class VolumeRpcAPITestCase(test.TestCase):
group_type_id='group_type1', group_type_id='group_type1',
host='fakehost@fakedrv#fakepool') host='fakehost@fakedrv#fakepool')
group = objects.ConsistencyGroup.get_by_id(self.context, group.id) group_snapshot = tests_utils.create_group_snapshot(
group2 = objects.ConsistencyGroup.get_by_id(self.context, group2.id) 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) cgsnapshot = objects.CGSnapshot.get_by_id(self.context, cgsnapshot.id)
self.fake_volume = jsonutils.to_primitive(volume) self.fake_volume = jsonutils.to_primitive(volume)
self.fake_volume_obj = fake_volume.fake_volume_obj(self.context, **vol) self.fake_volume_obj = fake_volume.fake_volume_obj(self.context, **vol)
self.fake_volume_metadata = volume["volume_metadata"] self.fake_volume_metadata = volume["volume_metadata"]
self.fake_snapshot = snapshot self.fake_snapshot = snapshot
self.fake_reservations = ["RESERVATION"] self.fake_reservations = ["RESERVATION"]
self.fake_cg = group self.fake_cg = cg
self.fake_cg2 = group2 self.fake_cg2 = cg2
self.fake_src_cg = jsonutils.to_primitive(source_group) self.fake_src_cg = jsonutils.to_primitive(source_group)
self.fake_cgsnap = cgsnapshot self.fake_cgsnap = cgsnapshot
self.fake_backup_obj = fake_backup.fake_backup_obj(self.context) self.fake_backup_obj = fake_backup.fake_backup_obj(self.context)
self.fake_group = generic_group 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): def test_serialized_volume_has_id(self):
self.assertIn('id', self.fake_volume) self.assertIn('id', self.fake_volume)
@ -253,11 +270,18 @@ class VolumeRpcAPITestCase(test.TestCase):
expected_msg = copy.deepcopy(kwargs) expected_msg = copy.deepcopy(kwargs)
if 'host' in expected_msg: if 'host' in expected_msg:
del expected_msg['host'] 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: if 'host' in kwargs:
host = kwargs['host'] host = kwargs['host']
elif 'group' in kwargs: elif 'group' in kwargs:
host = kwargs['group']['host'] host = kwargs['group']['host']
elif 'group_snapshot' in kwargs:
host = kwargs['group_snapshot'].group.host
target['server'] = utils.extract_host(host) target['server'] = utils.extract_host(host)
target['topic'] = '%s.%s' % (constants.VOLUME_TOPIC, 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() expected_group = expected_msg[kwarg].obj_to_primitive()
group = value.obj_to_primitive() group = value.obj_to_primitive()
self.assertEqual(expected_group, group) 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: else:
self.assertEqual(expected_msg[kwarg], value) self.assertEqual(expected_msg[kwarg], value)
@ -609,3 +637,20 @@ class VolumeRpcAPITestCase(test.TestCase):
self._test_group_api('update_group', rpc_method='cast', self._test_group_api('update_group', rpc_method='cast',
group=self.fake_group, add_volumes=['vol1'], group=self.fake_group, add_volumes=['vol1'],
remove_volumes=['vol2'], version='2.5') 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')

View File

@ -242,6 +242,53 @@ def create_cgsnapshot(ctxt,
return objects.CGSnapshot.get_by_id(ctxt, cgsnap.id) 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, def create_backup(ctxt,
volume_id=fake.VOLUME_ID, volume_id=fake.VOLUME_ID,
display_name='test_backup', display_name='test_backup',

View File

@ -213,7 +213,7 @@ class API(base.Base):
scheduler_hints=None, scheduler_hints=None,
source_replica=None, consistencygroup=None, source_replica=None, consistencygroup=None,
cgsnapshot=None, multiattach=False, source_cg=None, cgsnapshot=None, multiattach=False, source_cg=None,
group=None): group=None, group_snapshot=None, source_group=None):
check_policy(context, 'create') check_policy(context, 'create')
@ -245,7 +245,7 @@ class API(base.Base):
"group).") % volume_type "group).") % volume_type
raise exception.InvalidInput(reason=msg) raise exception.InvalidInput(reason=msg)
if group: if group and (not group_snapshot and not source_group):
if not volume_type: if not volume_type:
msg = _("volume_type must be provided when creating " msg = _("volume_type must be provided when creating "
"a volume in a group.") "a volume in a group.")
@ -320,12 +320,18 @@ class API(base.Base):
'cgsnapshot': cgsnapshot, 'cgsnapshot': cgsnapshot,
'multiattach': multiattach, 'multiattach': multiattach,
'group': group, 'group': group,
'group_snapshot': group_snapshot,
'source_group': source_group,
} }
try: try:
sched_rpcapi = (self.scheduler_rpcapi if (not cgsnapshot and sched_rpcapi = (self.scheduler_rpcapi if (
not source_cg) else None) not cgsnapshot and not source_cg and
volume_rpcapi = (self.volume_rpcapi if (not cgsnapshot and not group_snapshot and not source_group)
not source_cg) else None) 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, flow_engine = create_volume.get_flow(self.db,
self.image_service, self.image_service,
availability_zones, availability_zones,
@ -417,7 +423,8 @@ class API(base.Base):
if cascade: if cascade:
values = {'status': 'deleting'} values = {'status': 'deleting'}
expected = {'status': ('available', 'error', 'deleting'), expected = {'status': ('available', 'error', 'deleting'),
'cgsnapshot_id': None} 'cgsnapshot_id': None,
'group_snapshot_id': None}
snapshots = objects.snapshot.SnapshotList.get_all_for_volume( snapshots = objects.snapshot.SnapshotList.get_all_for_volume(
context, volume.id) context, volume.id)
for s in snapshots: for s in snapshots:
@ -739,10 +746,12 @@ class API(base.Base):
def _create_snapshot(self, context, def _create_snapshot(self, context,
volume, name, description, volume, name, description,
force=False, metadata=None, force=False, metadata=None,
cgsnapshot_id=None): cgsnapshot_id=None,
group_snapshot_id=None):
snapshot = self.create_snapshot_in_db( snapshot = self.create_snapshot_in_db(
context, volume, name, 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) self.volume_rpcapi.create_snapshot(context, volume, snapshot)
return snapshot return snapshot
@ -751,7 +760,8 @@ class API(base.Base):
volume, name, description, volume, name, description,
force, metadata, force, metadata,
cgsnapshot_id, cgsnapshot_id,
commit_quota=True): commit_quota=True,
group_snapshot_id=None):
check_policy(context, 'create_snapshot', volume) check_policy(context, 'create_snapshot', volume)
if volume['status'] == 'maintenance': if volume['status'] == 'maintenance':
@ -800,6 +810,7 @@ class API(base.Base):
kwargs = { kwargs = {
'volume_id': volume['id'], 'volume_id': volume['id'],
'cgsnapshot_id': cgsnapshot_id, 'cgsnapshot_id': cgsnapshot_id,
'group_snapshot_id': group_snapshot_id,
'user_id': context.user_id, 'user_id': context.user_id,
'project_id': context.project_id, 'project_id': context.project_id,
'status': fields.SnapshotStatus.CREATING, 'status': fields.SnapshotStatus.CREATING,
@ -830,7 +841,8 @@ class API(base.Base):
def create_snapshots_in_db(self, context, def create_snapshots_in_db(self, context,
volume_list, volume_list,
name, description, name, description,
cgsnapshot_id): cgsnapshot_id,
group_snapshot_id=None):
snapshot_list = [] snapshot_list = []
for volume in volume_list: for volume in volume_list:
self._create_snapshot_in_db_validate(context, volume, True) self._create_snapshot_in_db_validate(context, volume, True)
@ -846,7 +858,8 @@ class API(base.Base):
options_list = [] options_list = []
for volume in volume_list: for volume in volume_list:
options = self._create_snapshot_in_db_options( 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) options_list.append(options)
try: try:
@ -919,9 +932,11 @@ class API(base.Base):
def _create_snapshot_in_db_options(self, context, volume, def _create_snapshot_in_db_options(self, context, volume,
name, description, name, description,
cgsnapshot_id): cgsnapshot_id,
group_snapshot_id=None):
options = {'volume_id': volume['id'], options = {'volume_id': volume['id'],
'cgsnapshot_id': cgsnapshot_id, 'cgsnapshot_id': cgsnapshot_id,
'group_snapshot_id': group_snapshot_id,
'user_id': context.user_id, 'user_id': context.user_id,
'project_id': context.project_id, 'project_id': context.project_id,
'status': fields.SnapshotStatus.CREATING, 'status': fields.SnapshotStatus.CREATING,
@ -935,9 +950,11 @@ class API(base.Base):
def create_snapshot(self, context, def create_snapshot(self, context,
volume, name, description, 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, 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."), LOG.info(_LI("Snapshot create request issued successfully."),
resource=result) resource=result)
return result return result
@ -955,7 +972,8 @@ class API(base.Base):
def delete_snapshot(self, context, snapshot, force=False, def delete_snapshot(self, context, snapshot, force=False,
unmanage_only=False): unmanage_only=False):
# Build required conditions for conditional update # 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 deleting we have status conditions
if not force: if not force:
expected['status'] = (fields.SnapshotStatus.AVAILABLE, expected['status'] = (fields.SnapshotStatus.AVAILABLE,
@ -966,7 +984,7 @@ class API(base.Base):
if not result: if not result:
status = utils.build_or_str(expected.get('status'), status = utils.build_or_str(expected.get('status'),
_('status must be %s and')) _('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) status)
LOG.error(msg) LOG.error(msg)
raise exception.InvalidSnapshot(reason=msg) raise exception.InvalidSnapshot(reason=msg)

View File

@ -99,14 +99,16 @@ class VolumeAPI(rpc.RPCAPI):
2.0 - Remove 1.x compatibility 2.0 - Remove 1.x compatibility
2.1 - Add get_manageable_volumes() and get_manageable_snapshots(). 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 2.3 - Adds support for sending objects over RPC in
initialize_connection(). 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.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 TOPIC = constants.VOLUME_TOPIC
BINARY = 'cinder-volume' BINARY = 'cinder-volume'
@ -359,3 +361,21 @@ class VolumeAPI(rpc.RPCAPI):
group=group, group=group,
add_volumes=add_volumes, add_volumes=add_volumes,
remove_volumes=remove_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)

View File

@ -121,6 +121,12 @@
"group:get": "rule:admin_or_owner", "group:get": "rule:admin_or_owner",
"group:get_all": "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", "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner", "message:delete": "rule:admin_or_owner",
"message:get": "rule:admin_or_owner", "message:get": "rule:admin_or_owner",

View File

@ -0,0 +1,4 @@
---
features:
- Added create/delete APIs for group snapshots and
an API to create group from source.