Merge "Add group snapshots - APIs"

This commit is contained in:
Jenkins 2016-08-31 16:47:09 +00:00 committed by Gerrit Code Review
commit bf8bd9f358
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.12 - Add volumes summary API.
* 3.13 - Add generic volume groups API.
* 3.14 - Add group snapshot and create group from src APIs.
"""
@ -69,7 +70,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v1 or /v2 enpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.13"
_MAX_API_VERSION = "3.14"
_LEGACY_API_VERSION1 = "1.0"
_LEGACY_API_VERSION2 = "2.0"

View File

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

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__)
GROUP_API_VERSION = '3.13'
GROUP_CREATE_FROM_SRC_API_VERSION = '3.14'
class GroupsController(wsgi.Controller):
@ -163,6 +164,63 @@ class GroupsController(wsgi.Controller):
retval = self._view_builder.summary(req, new_group)
return retval
@wsgi.Controller.api_version(GROUP_CREATE_FROM_SRC_API_VERSION)
@wsgi.action("create-from-src")
@wsgi.response(202)
def create_from_src(self, req, body):
"""Create a new group from a source.
The source can be a group snapshot or a group. Note that
this does not require group_type and volume_types as the
"create" API above.
"""
LOG.debug('Creating new group %s.', body)
self.assert_valid_body(body, 'create-from-src')
context = req.environ['cinder.context']
group = body['create-from-src']
self.validate_name_and_description(group)
name = group.get('name', None)
description = group.get('description', None)
group_snapshot_id = group.get('group_snapshot_id', None)
source_group_id = group.get('source_group_id', None)
if not group_snapshot_id and not source_group_id:
msg = (_("Either 'group_snapshot_id' or 'source_group_id' must be "
"provided to create group %(name)s from source.")
% {'name': name})
raise exc.HTTPBadRequest(explanation=msg)
if group_snapshot_id and source_group_id:
msg = _("Cannot provide both 'group_snapshot_id' and "
"'source_group_id' to create group %(name)s from "
"source.") % {'name': name}
raise exc.HTTPBadRequest(explanation=msg)
if group_snapshot_id:
LOG.info(_LI("Creating group %(name)s from group_snapshot "
"%(snap)s."),
{'name': name, 'snap': group_snapshot_id},
context=context)
elif source_group_id:
LOG.info(_LI("Creating group %(name)s from "
"source group %(source_group_id)s."),
{'name': name, 'source_group_id': source_group_id},
context=context)
try:
new_group = self.group_api.create_from_src(
context, name, description, group_snapshot_id, source_group_id)
except exception.InvalidGroup as error:
raise exc.HTTPBadRequest(explanation=error.msg)
except (exception.GroupNotFound, exception.GroupSnapshotNotFound):
# Not found exception will be handled at the wsgi level
raise
except exception.CinderException as error:
raise exc.HTTPBadRequest(explanation=error.msg)
retval = self._view_builder.summary(req, new_group)
return retval
@wsgi.Controller.api_version(GROUP_API_VERSION)
def update(self, req, id, body):
"""Update the group.

View File

@ -23,17 +23,18 @@ from cinder.api import extensions
import cinder.api.openstack
from cinder.api.v2 import limits
from cinder.api.v2 import snapshot_metadata
from cinder.api.v2 import snapshots
from cinder.api.v2 import types
from cinder.api.v2 import volume_metadata
from cinder.api.v3 import backups
from cinder.api.v3 import clusters
from cinder.api.v3 import consistencygroups
from cinder.api.v3 import group_snapshots
from cinder.api.v3 import group_specs
from cinder.api.v3 import group_types
from cinder.api.v3 import groups
from cinder.api.v3 import messages
from cinder.api.v3 import snapshot_manage
from cinder.api.v3 import snapshots
from cinder.api.v3 import volume_manage
from cinder.api.v3 import volumes
from cinder.api import versions
@ -93,6 +94,17 @@ class APIRouter(cinder.api.openstack.APIRouter):
controller=self.resources["groups"],
action="action",
conditions={"action": ["POST"]})
mapper.connect("groups/action",
"/{project_id}/groups/action",
controller=self.resources["groups"],
action="action",
conditions={"action": ["POST"]})
self.resources['group_snapshots'] = (group_snapshots.create_resource())
mapper.resource("group_snapshot", "group_snapshots",
controller=self.resources['group_snapshots'],
collection={'detail': 'GET'},
member={'action': 'POST'})
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
mapper.resource("snapshot", "snapshots",

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):
"""Detailed view of a single group."""
return {
group_ref = {
'group': {
'id': group.id,
'status': group.status,
@ -57,6 +57,15 @@ class ViewBuilder(common.ViewBuilder):
}
}
req_version = request.api_version_request
# Add group_snapshot_id and source_group_id if min version is greater
# than or equal to 3.14.
if req_version.matches("3.14", None):
group_ref['group']['group_snapshot_id'] = group.group_snapshot_id
group_ref['group']['source_group_id'] = group.source_group_id
return group_ref
def _list_view(self, func, request, groups):
"""Provide a view for a list of groups."""
groups_list = [

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

@ -424,9 +424,9 @@ def snapshot_get_all_for_cgsnapshot(context, project_id):
return IMPL.snapshot_get_all_for_cgsnapshot(context, project_id)
def snapshot_get_all_for_group_snapshot(context, project_id):
def snapshot_get_all_for_group_snapshot(context, group_snapshot_id):
"""Get all snapshots belonging to a group snapshot."""
return IMPL.snapshot_get_all_for_group_snapshot(context, project_id)
return IMPL.snapshot_get_all_for_group_snapshot(context, group_snapshot_id)
def snapshot_get_all_for_volume(context, volume_id):

View File

@ -26,9 +26,10 @@ from oslo_utils import excutils
from oslo_utils import timeutils
from cinder.common import constants
from cinder import db
from cinder.db import base
from cinder import exception
from cinder.i18n import _, _LE, _LW
from cinder.i18n import _, _LE, _LI, _LW
from cinder import objects
from cinder.objects import base as objects_base
from cinder.objects import fields as c_fields
@ -180,6 +181,212 @@ class API(base.Base):
return group
def create_from_src(self, context, name, description=None,
group_snapshot_id=None, source_group_id=None):
check_policy(context, 'create')
kwargs = {
'user_id': context.user_id,
'project_id': context.project_id,
'status': c_fields.GroupStatus.CREATING,
'name': name,
'description': description,
'group_snapshot_id': group_snapshot_id,
'source_group_id': source_group_id,
}
group = None
try:
group = objects.Group(context=context, **kwargs)
group.create(group_snapshot_id=group_snapshot_id,
source_group_id=source_group_id)
except exception.GroupNotFound:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Source Group %(source_group)s not found when "
"creating group %(group)s from "
"source."),
{'group': name, 'source_group': source_group_id})
except exception.GroupSnapshotNotFound:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Group snapshot %(group_snap)s not found when "
"creating group %(group)s from source."),
{'group': name, 'group_snap': group_snapshot_id})
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error occurred when creating group"
" %(group)s from group_snapshot %(grp_snap)s."),
{'group': name, 'grp_snap': group_snapshot_id})
# Update quota for groups
self.update_quota(context, group, 1)
if not group.host:
msg = _("No host to create group %s.") % group.id
LOG.error(msg)
raise exception.InvalidGroup(reason=msg)
if group_snapshot_id:
self._create_group_from_group_snapshot(context, group,
group_snapshot_id)
elif source_group_id:
self._create_group_from_source_group(context, group,
source_group_id)
return group
def _create_group_from_group_snapshot(self, context, group,
group_snapshot_id):
try:
group_snapshot = objects.GroupSnapshot.get_by_id(
context, group_snapshot_id)
snapshots = objects.SnapshotList.get_all_for_group_snapshot(
context, group_snapshot.id)
if not snapshots:
msg = _("Group snapshot is empty. No group will be created.")
raise exception.InvalidGroup(reason=msg)
for snapshot in snapshots:
kwargs = {}
kwargs['availability_zone'] = group.availability_zone
kwargs['group_snapshot'] = group_snapshot
kwargs['group'] = group
kwargs['snapshot'] = snapshot
volume_type_id = snapshot.volume_type_id
if volume_type_id:
kwargs['volume_type'] = volume_types.get_volume_type(
context, volume_type_id)
# Create group volume_type mapping entries
try:
db.group_volume_type_mapping_create(context, group.id,
volume_type_id)
except exception.GroupVolumeTypeMappingExists:
# Only need to create one group volume_type mapping
# entry for the same combination, skipping.
LOG.info(_LI("A mapping entry already exists for group"
" %(grp)s and volume type %(vol_type)s. "
"Do not need to create again."),
{'grp': group.id,
'vol_type': volume_type_id})
pass
# Since group snapshot is passed in, the following call will
# create a db entry for the volume, but will not call the
# volume manager to create a real volume in the backend yet.
# If error happens, taskflow will handle rollback of quota
# and removal of volume entry in the db.
try:
self.volume_api.create(context,
snapshot.volume_size,
None,
None,
**kwargs)
except exception.CinderException:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error occurred when creating volume "
"entry from snapshot in the process of "
"creating group %(group)s "
"from group snapshot %(group_snap)s."),
{'group': group.id,
'group_snap': group_snapshot.id})
except Exception:
with excutils.save_and_reraise_exception():
try:
group.destroy()
finally:
LOG.error(_LE("Error occurred when creating group "
"%(group)s from group snapshot "
"%(group_snap)s."),
{'group': group.id,
'group_snap': group_snapshot.id})
volumes = objects.VolumeList.get_all_by_generic_group(context,
group.id)
for vol in volumes:
# Update the host field for the volume.
vol.host = group.host
vol.save()
self.volume_rpcapi.create_group_from_src(
context, group, group_snapshot)
def _create_group_from_source_group(self, context, group,
source_group_id):
try:
source_group = objects.Group.get_by_id(context,
source_group_id)
source_vols = objects.VolumeList.get_all_by_generic_group(
context, source_group.id)
if not source_vols:
msg = _("Source Group is empty. No group "
"will be created.")
raise exception.InvalidGroup(reason=msg)
for source_vol in source_vols:
kwargs = {}
kwargs['availability_zone'] = group.availability_zone
kwargs['source_group'] = source_group
kwargs['group'] = group
kwargs['source_volume'] = source_vol
volume_type_id = source_vol.volume_type_id
if volume_type_id:
kwargs['volume_type'] = volume_types.get_volume_type(
context, volume_type_id)
# Create group volume_type mapping entries
try:
db.group_volume_type_mapping_create(context, group.id,
volume_type_id)
except exception.GroupVolumeTypeMappingExists:
# Only need to create one group volume_type mapping
# entry for the same combination, skipping.
LOG.info(_LI("A mapping entry already exists for group"
" %(grp)s and volume type %(vol_type)s. "
"Do not need to create again."),
{'grp': group.id,
'vol_type': volume_type_id})
pass
# Since source_group is passed in, the following call will
# create a db entry for the volume, but will not call the
# volume manager to create a real volume in the backend yet.
# If error happens, taskflow will handle rollback of quota
# and removal of volume entry in the db.
try:
self.volume_api.create(context,
source_vol.size,
None,
None,
**kwargs)
except exception.CinderException:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error occurred when creating cloned "
"volume in the process of creating "
"group %(group)s from "
"source group %(source_group)s."),
{'group': group.id,
'source_group': source_group.id})
except Exception:
with excutils.save_and_reraise_exception():
try:
group.destroy()
finally:
LOG.error(_LE("Error occurred when creating "
"group %(group)s from source group "
"%(source_group)s."),
{'group': group.id,
'source_group': source_group.id})
volumes = objects.VolumeList.get_all_by_generic_group(context,
group.id)
for vol in volumes:
# Update the host field for the volume.
vol.host = group.host
vol.save()
self.volume_rpcapi.create_group_from_src(context, group,
None, source_group)
def _cast_create_group(self, context, group,
group_spec,
request_spec_list,
@ -542,3 +749,90 @@ class API(base.Base):
limit=limit, offset=offset, sort_keys=sort_keys,
sort_dirs=sort_dirs)
return groups
def create_group_snapshot(self, context, group, name, description):
options = {'group_id': group.id,
'user_id': context.user_id,
'project_id': context.project_id,
'status': "creating",
'name': name,
'description': description}
group_snapshot = None
group_snapshot_id = None
try:
group_snapshot = objects.GroupSnapshot(context, **options)
group_snapshot.create()
group_snapshot_id = group_snapshot.id
snap_name = group_snapshot.name
snap_desc = group_snapshot.description
with group.obj_as_admin():
self.volume_api.create_snapshots_in_db(
context, group.volumes, snap_name, snap_desc,
None, group_snapshot_id)
except Exception:
with excutils.save_and_reraise_exception():
try:
# If the group_snapshot has been created
if group_snapshot.obj_attr_is_set('id'):
group_snapshot.destroy()
finally:
LOG.error(_LE("Error occurred when creating group_snapshot"
" %s."), group_snapshot_id)
self.volume_rpcapi.create_group_snapshot(context, group_snapshot)
return group_snapshot
def delete_group_snapshot(self, context, group_snapshot, force=False):
check_policy(context, 'delete_group_snapshot')
values = {'status': 'deleting'}
expected = {'status': ('available', 'error')}
filters = [~db.group_creating_from_src(
group_snapshot_id=group_snapshot.id)]
res = group_snapshot.conditional_update(values, expected, filters)
if not res:
msg = _('GroupSnapshot status must be available or error, and no '
'Group can be currently using it as source for its '
'creation.')
raise exception.InvalidGroupSnapshot(reason=msg)
snapshots = objects.SnapshotList.get_all_for_group_snapshot(
context, group_snapshot.id)
# TODO(xyang): Add a new db API to update all snapshots statuses
# in one db API call.
for snap in snapshots:
snap.status = c_fields.SnapshotStatus.DELETING
snap.save()
self.volume_rpcapi.delete_group_snapshot(context.elevated(),
group_snapshot)
def update_group_snapshot(self, context, group_snapshot, fields):
check_policy(context, 'update_group_snapshot')
group_snapshot.update(fields)
group_snapshot.save()
def get_group_snapshot(self, context, group_snapshot_id):
check_policy(context, 'get_group_snapshot')
group_snapshots = objects.GroupSnapshot.get_by_id(context,
group_snapshot_id)
return group_snapshots
def get_all_group_snapshots(self, context, search_opts=None):
check_policy(context, 'get_all_group_snapshots')
search_opts = search_opts or {}
if context.is_admin and 'all_tenants' in search_opts:
# Need to remove all_tenants to pass the filtering below.
del search_opts['all_tenants']
group_snapshots = objects.GroupSnapshotList.get_all(context,
search_opts)
else:
group_snapshots = objects.GroupSnapshotList.get_all_by_project(
context.elevated(), context.project_id, search_opts)
return group_snapshots

View File

@ -15,9 +15,17 @@ import iso8601
from cinder.message import defined_messages
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder import utils
FAKE_UUID = fake.OBJECT_ID
DEFAULT_VOL_NAME = "displayname"
DEFAULT_VOL_DESCRIPTION = "displaydesc"
DEFAULT_VOL_SIZE = 1
DEFAULT_VOL_TYPE = "vol_type_name"
DEFAULT_VOL_STATUS = "fakestatus"
DEFAULT_VOL_ID = fake.VOLUME_ID
DEFAULT_AZ = "fakeaz"
def stub_message(id, **kwargs):
@ -40,3 +48,68 @@ def stub_message(id, **kwargs):
def stub_message_get(self, context, message_id):
return stub_message(message_id)
def stub_volume(id, **kwargs):
volume = {
'id': id,
'user_id': fake.USER_ID,
'project_id': fake.PROJECT_ID,
'host': 'fakehost',
'size': DEFAULT_VOL_SIZE,
'availability_zone': DEFAULT_AZ,
'status': DEFAULT_VOL_STATUS,
'migration_status': None,
'attach_status': 'attached',
'name': 'vol name',
'display_name': DEFAULT_VOL_NAME,
'display_description': DEFAULT_VOL_DESCRIPTION,
'updated_at': datetime.datetime(1900, 1, 1, 1, 1, 1,
tzinfo=iso8601.iso8601.Utc()),
'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1,
tzinfo=iso8601.iso8601.Utc()),
'snapshot_id': None,
'source_volid': None,
'volume_type_id': '3e196c20-3c06-11e2-81c1-0800200c9a66',
'encryption_key_id': None,
'volume_admin_metadata': [{'key': 'attached_mode', 'value': 'rw'},
{'key': 'readonly', 'value': 'False'}],
'bootable': False,
'launched_at': datetime.datetime(1900, 1, 1, 1, 1, 1,
tzinfo=iso8601.iso8601.Utc()),
'volume_type': fake_volume.fake_db_volume_type(name=DEFAULT_VOL_TYPE),
'replication_status': 'disabled',
'replication_extended_status': None,
'replication_driver_data': None,
'volume_attachment': [],
'multiattach': False,
'group_id': fake.GROUP_ID,
}
volume.update(kwargs)
if kwargs.get('volume_glance_metadata', None):
volume['bootable'] = True
if kwargs.get('attach_status') == 'detached':
del volume['volume_admin_metadata'][0]
return volume
def stub_volume_create(self, context, size, name, description, snapshot=None,
group_id=None, **param):
vol = stub_volume(DEFAULT_VOL_ID)
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
source_volume = param.get('source_volume') or {}
vol['source_volid'] = source_volume.get('id')
vol['bootable'] = False
vol['volume_attachment'] = []
vol['multiattach'] = utils.get_bool_param('multiattach', param)
try:
vol['snapshot_id'] = snapshot['id']
except (KeyError, TypeError):
vol['snapshot_id'] = None
vol['availability_zone'] = param.get('availability_zone', 'fakeaz')
if group_id:
vol['group_id'] = group_id
return vol

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 import test
from cinder.tests.unit.api import fakes
from cinder.tests.unit.api.v3 import stubs
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import utils
from cinder.volume import api as volume_api
GROUP_MICRO_VERSION = '3.13'
GROUP_FROM_SRC_MICRO_VERSION = '3.14'
@ddt.ddt
@ -804,3 +807,70 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
req, self.group1.id, body)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_create_group_from_src_snap(self, mock_validate):
self.mock_object(volume_api.API, "create", stubs.stub_volume_create)
group = utils.create_group(self.ctxt,
group_type_id=fake.GROUP_TYPE_ID)
volume = utils.create_volume(
self.ctxt,
group_id=group.id)
group_snapshot = utils.create_group_snapshot(
self.ctxt, group_id=group.id)
snapshot = utils.create_snapshot(
self.ctxt,
volume.id,
group_snapshot_id=group_snapshot.id,
status=fields.SnapshotStatus.AVAILABLE)
test_grp_name = 'test grp'
body = {"create-from-src": {"name": test_grp_name,
"description": "Group 1",
"group_snapshot_id": group_snapshot.id}}
req = fakes.HTTPRequest.blank('/v3/%s/groups/action' %
fake.PROJECT_ID,
version=GROUP_FROM_SRC_MICRO_VERSION)
res_dict = self.controller.create_from_src(req, body)
self.assertIn('id', res_dict['group'])
self.assertEqual(test_grp_name, res_dict['group']['name'])
self.assertTrue(mock_validate.called)
grp_ref = objects.Group.get_by_id(
self.ctxt.elevated(), res_dict['group']['id'])
grp_ref.destroy()
snapshot.destroy()
volume.destroy()
group.destroy()
group_snapshot.destroy()
def test_create_group_from_src_grp(self):
self.mock_object(volume_api.API, "create", stubs.stub_volume_create)
source_grp = utils.create_group(self.ctxt,
group_type_id=fake.GROUP_TYPE_ID)
volume = utils.create_volume(
self.ctxt,
group_id=source_grp.id)
test_grp_name = 'test cg'
body = {"create-from-src": {"name": test_grp_name,
"description": "Consistency Group 1",
"source_group_id": source_grp.id}}
req = fakes.HTTPRequest.blank('/v3/%s/groups/action' %
fake.PROJECT_ID,
version=GROUP_FROM_SRC_MICRO_VERSION)
res_dict = self.controller.create_from_src(req, body)
self.assertIn('id', res_dict['group'])
self.assertEqual(test_grp_name, res_dict['group']['name'])
grp = objects.Group.get_by_id(
self.ctxt, res_dict['group']['id'])
grp.destroy()
volume.destroy()
source_grp.destroy()

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.assertTrue(mock_validate.called)
@ddt.data('3.13', '3.12')
@ddt.data('3.14', '3.13')
@mock.patch.object(group_api.API, 'get')
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
autospec=True)

View File

@ -21,6 +21,7 @@ import ddt
import mock
from cinder import context
from cinder import db
import cinder.group
from cinder import objects
from cinder.objects import fields
@ -174,3 +175,243 @@ class GroupAPITestCase(test.TestCase):
mock_rpc_update_group.assert_called_once_with(self.ctxt, ret_group,
add_volumes = vol1.id,
remove_volumes = vol2.id)
@mock.patch('cinder.objects.GroupSnapshot.get_by_id')
@mock.patch('cinder.group.api.check_policy')
def test_get_group_snapshot(self, mock_policy, mock_group_snap):
fake_group_snap = 'fake_group_snap'
mock_group_snap.return_value = fake_group_snap
grp_snap = self.group_api.get_group_snapshot(
self.ctxt, fake.GROUP_SNAPSHOT_ID)
self.assertEqual(fake_group_snap, grp_snap)
@ddt.data(True, False)
@mock.patch('cinder.objects.GroupSnapshotList.get_all')
@mock.patch('cinder.objects.GroupSnapshotList.get_all_by_project')
@mock.patch('cinder.group.api.check_policy')
def test_get_all_group_snapshots(self, is_admin, mock_policy,
mock_get_all_by_project,
mock_get_all):
fake_group_snaps = ['fake_group_snap1', 'fake_group_snap2']
fake_group_snaps_by_project = ['fake_group_snap1']
mock_get_all.return_value = fake_group_snaps
mock_get_all_by_project.return_value = fake_group_snaps_by_project
if is_admin:
grp_snaps = self.group_api.get_all_group_snapshots(
self.ctxt, search_opts={'all_tenants': True})
self.assertEqual(fake_group_snaps, grp_snaps)
else:
grp_snaps = self.group_api.get_all_group_snapshots(
self.user_ctxt)
self.assertEqual(fake_group_snaps_by_project, grp_snaps)
@mock.patch('cinder.objects.GroupSnapshot')
@mock.patch('cinder.group.api.check_policy')
def test_update_group_snapshot(self, mock_policy, mock_group_snap):
grp_snap_update = {"name": "new_name",
"description": "This is a new description"}
self.group_api.update_group_snapshot(self.ctxt, mock_group_snap,
grp_snap_update)
mock_group_snap.update.assert_called_once_with(grp_snap_update)
mock_group_snap.save.assert_called_once_with()
@mock.patch('cinder.volume.rpcapi.VolumeAPI.delete_group_snapshot')
@mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_snapshot')
@mock.patch('cinder.volume.api.API.create_snapshots_in_db')
@mock.patch('cinder.objects.Group')
@mock.patch('cinder.objects.GroupSnapshot')
@mock.patch('cinder.objects.SnapshotList.get_all_for_group_snapshot')
@mock.patch('cinder.group.api.check_policy')
def test_create_delete_group_snapshot(self, mock_policy,
mock_snap_get_all,
mock_group_snap, mock_group,
mock_create_in_db,
mock_create_api, mock_delete_api):
name = "fake_name"
description = "fake description"
mock_group.id = fake.GROUP_ID
mock_group.volumes = []
ret_group_snap = self.group_api.create_group_snapshot(
self.ctxt, mock_group, name, description)
mock_snap_get_all.return_value = []
options = {'group_id': fake.GROUP_ID,
'user_id': self.ctxt.user_id,
'project_id': self.ctxt.project_id,
'status': "creating",
'name': name,
'description': description}
mock_group_snap.assert_called_once_with(self.ctxt, **options)
ret_group_snap.create.assert_called_once_with()
mock_create_in_db.assert_called_once_with(self.ctxt, [],
ret_group_snap.name,
ret_group_snap.description,
None,
ret_group_snap.id)
mock_create_api.assert_called_once_with(self.ctxt, ret_group_snap)
self.group_api.delete_group_snapshot(self.ctxt, ret_group_snap)
mock_delete_api.assert_called_once_with(mock.ANY, ret_group_snap)
@mock.patch('cinder.volume.volume_types.get_volume_type')
@mock.patch('cinder.db.group_volume_type_mapping_create')
@mock.patch('cinder.volume.api.API.create')
@mock.patch('cinder.objects.GroupSnapshot.get_by_id')
@mock.patch('cinder.objects.SnapshotList.get_all_for_group_snapshot')
@mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_from_src')
@mock.patch('cinder.objects.VolumeList.get_all_by_generic_group')
def test_create_group_from_snap(self, mock_volume_get_all,
mock_rpc_create_group_from_src,
mock_snap_get_all, mock_group_snap_get,
mock_volume_api_create,
mock_mapping_create,
mock_get_volume_type):
vol_type = utils.create_volume_type(self.ctxt,
name = 'fake_volume_type')
mock_get_volume_type.return_value = vol_type
grp_snap = utils.create_group_snapshot(
self.ctxt, fake.GROUP_ID,
group_type_id = fake.GROUP_TYPE_ID,
status = fields.GroupStatus.CREATING)
mock_group_snap_get.return_value = grp_snap
vol1 = utils.create_volume(
self.ctxt,
availability_zone = 'nova',
volume_type_id = vol_type['id'],
group_id = fake.GROUP_ID)
snap = utils.create_snapshot(self.ctxt, vol1.id,
volume_type_id = vol_type['id'],
status = fields.GroupStatus.CREATING)
mock_snap_get_all.return_value = [snap]
name = "test_group"
description = "this is a test group"
grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID,
volume_type_ids = [vol_type['id']],
availability_zone = 'nova',
name = name, description = description,
group_snapshot_id = grp_snap.id,
status = fields.GroupStatus.CREATING)
vol2 = utils.create_volume(
self.ctxt,
availability_zone = grp.availability_zone,
volume_type_id = vol_type['id'],
group_id = grp.id,
snapshot_id = snap.id)
mock_volume_get_all.return_value = [vol2]
self.group_api._create_group_from_group_snapshot(self.ctxt, grp,
grp_snap.id)
mock_volume_api_create.assert_called_once_with(
self.ctxt, 1, None, None,
availability_zone = grp.availability_zone,
group_snapshot = grp_snap,
group = grp,
snapshot = snap,
volume_type = vol_type)
mock_rpc_create_group_from_src.assert_called_once_with(
self.ctxt, grp, grp_snap)
vol2.destroy()
grp.destroy()
snap.destroy()
vol1.destroy()
grp_snap.destroy()
db.volume_type_destroy(self.ctxt, vol_type['id'])
@mock.patch('cinder.volume.volume_types.get_volume_type')
@mock.patch('cinder.db.group_volume_type_mapping_create')
@mock.patch('cinder.volume.api.API.create')
@mock.patch('cinder.objects.Group.get_by_id')
@mock.patch('cinder.volume.rpcapi.VolumeAPI.create_group_from_src')
@mock.patch('cinder.objects.VolumeList.get_all_by_generic_group')
@mock.patch('cinder.group.api.check_policy')
def test_create_group_from_group(self, mock_policy, mock_volume_get_all,
mock_rpc_create_group_from_src,
mock_group_get,
mock_volume_api_create,
mock_mapping_create,
mock_get_volume_type):
vol_type = utils.create_volume_type(self.ctxt,
name = 'fake_volume_type')
mock_get_volume_type.return_value = vol_type
grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID,
volume_type_ids = [vol_type['id']],
availability_zone = 'nova',
status = fields.GroupStatus.CREATING)
mock_group_get.return_value = grp
vol = utils.create_volume(
self.ctxt,
availability_zone = grp.availability_zone,
volume_type_id = fake.VOLUME_TYPE_ID,
group_id = grp.id)
mock_volume_get_all.return_value = [vol]
grp2 = utils.create_group(self.ctxt,
group_type_id = fake.GROUP_TYPE_ID,
volume_type_ids = [vol_type['id']],
availability_zone = 'nova',
source_group_id = grp.id,
status = fields.GroupStatus.CREATING)
vol2 = utils.create_volume(
self.ctxt,
availability_zone = grp.availability_zone,
volume_type_id = vol_type['id'],
group_id = grp2.id,
source_volid = vol.id)
self.group_api._create_group_from_source_group(self.ctxt, grp2,
grp.id)
mock_volume_api_create.assert_called_once_with(
self.ctxt, 1, None, None,
availability_zone = grp.availability_zone,
source_group = grp,
group = grp2,
source_volume = vol,
volume_type = vol_type)
mock_rpc_create_group_from_src.assert_called_once_with(
self.ctxt, grp2, None, grp)
vol2.destroy()
grp2.destroy()
vol.destroy()
grp.destroy()
db.volume_type_destroy(self.ctxt, vol_type['id'])
@mock.patch('cinder.group.api.API._create_group_from_group_snapshot')
@mock.patch('cinder.group.api.API._create_group_from_source_group')
@mock.patch('cinder.group.api.API.update_quota')
@mock.patch('cinder.objects.Group')
@mock.patch('cinder.group.api.check_policy')
def test_create_from_src(self, mock_policy, mock_group, mock_update_quota,
mock_create_from_group, mock_create_from_snap):
name = "test_group"
description = "this is a test group"
grp = utils.create_group(self.ctxt, group_type_id = fake.GROUP_TYPE_ID,
volume_type_ids = [fake.VOLUME_TYPE_ID],
availability_zone = 'nova',
name = name, description = description,
status = fields.GroupStatus.CREATING,
group_snapshot_id = fake.GROUP_SNAPSHOT_ID,
source_group_id = fake.GROUP_ID)
mock_group.return_value = grp
ret_group = self.group_api.create_from_src(
self.ctxt, name, description,
group_snapshot_id = fake.GROUP_SNAPSHOT_ID,
source_group_id = None)
self.assertEqual(grp.obj_to_primitive(), ret_group.obj_to_primitive())
mock_create_from_snap.assert_called_once_with(
self.ctxt, grp, fake.GROUP_SNAPSHOT_ID)

View File

@ -125,6 +125,12 @@
"group:get": "",
"group:get_all": "",
"group:create_group_snapshot": "",
"group:delete_group_snapshot": "",
"group:update_group_snapshot": "",
"group:get_group_snapshot": "",
"group:get_all_group_snapshots": "",
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner",

View File

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

View File

@ -242,6 +242,53 @@ def create_cgsnapshot(ctxt,
return objects.CGSnapshot.get_by_id(ctxt, cgsnap.id)
def create_group_snapshot(ctxt,
group_id,
group_type_id=None,
name='test_group_snapshot',
description='this is a test group snapshot',
status='creating',
recursive_create_if_needed=True,
return_vo=True,
**kwargs):
"""Create a group snapshot object in the DB."""
values = {
'user_id': ctxt.user_id or fake.USER_ID,
'project_id': ctxt.project_id or fake.PROJECT_ID,
'status': status,
'name': name,
'description': description,
'group_id': group_id}
values.update(kwargs)
if recursive_create_if_needed and group_id:
create_grp = False
try:
objects.Group.get_by_id(ctxt,
group_id)
create_vol = not db.volume_get_all_by_generic_group(
ctxt, group_id)
except exception.GroupNotFound:
create_grp = True
create_vol = True
if create_grp:
create_group(ctxt, id=group_id, group_type_id=group_type_id)
if create_vol:
create_volume(ctxt, group_id=group_id)
if not return_vo:
return db.group_snapshot_create(ctxt, values)
else:
group_snapshot = objects.GroupSnapshot(ctxt)
new_id = values.pop('id', None)
group_snapshot.update(values)
group_snapshot.create()
if new_id and new_id != group_snapshot.id:
db.group_snapshot_update(ctxt, group_snapshot.id, {'id': new_id})
group_snapshot = objects.GroupSnapshot.get_by_id(ctxt, new_id)
return group_snapshot
def create_backup(ctxt,
volume_id=fake.VOLUME_ID,
display_name='test_backup',

View File

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

View File

@ -99,14 +99,16 @@ class VolumeAPI(rpc.RPCAPI):
2.0 - Remove 1.x compatibility
2.1 - Add get_manageable_volumes() and get_manageable_snapshots().
2.2 - Adds support for sending objects over RPC in manage_existing().
2.2 - Adds support for sending objects over RPC in manage_existing().
2.3 - Adds support for sending objects over RPC in
initialize_connection().
2.4 - Sends request_spec as object in create_volume().
2.4 - Sends request_spec as object in create_volume().
2.5 - Adds create_group, delete_group, and update_group
2.6 - Adds create_group_snapshot, delete_group_snapshot, and
create_group_from_src().
"""
RPC_API_VERSION = '2.5'
RPC_API_VERSION = '2.6'
TOPIC = constants.VOLUME_TOPIC
BINARY = 'cinder-volume'
@ -359,3 +361,21 @@ class VolumeAPI(rpc.RPCAPI):
group=group,
add_volumes=add_volumes,
remove_volumes=remove_volumes)
def create_group_from_src(self, ctxt, group, group_snapshot=None,
source_group=None):
cctxt = self._get_cctxt(group.host, '2.6')
cctxt.cast(ctxt, 'create_group_from_src',
group=group,
group_snapshot=group_snapshot,
source_group=source_group)
def create_group_snapshot(self, ctxt, group_snapshot):
cctxt = self._get_cctxt(group_snapshot.group.host, '2.6')
cctxt.cast(ctxt, 'create_group_snapshot',
group_snapshot=group_snapshot)
def delete_group_snapshot(self, ctxt, group_snapshot):
cctxt = self._get_cctxt(group_snapshot.group.host, '2.6')
cctxt.cast(ctxt, 'delete_group_snapshot',
group_snapshot=group_snapshot)

View File

@ -121,6 +121,12 @@
"group:get": "rule:admin_or_owner",
"group:get_all": "rule:admin_or_owner",
"group:create_group_snapshot": "",
"group:delete_group_snapshot": "rule:admin_or_owner",
"group:update_group_snapshot": "rule:admin_or_owner",
"group:get_group_snapshot": "rule:admin_or_owner",
"group:get_all_group_snapshots": "rule:admin_or_owner",
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
"message:delete": "rule:admin_or_owner",
"message:get": "rule:admin_or_owner",

View File

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