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:
parent
4a67bc8218
commit
708b9be9c0
@ -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"
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
146
cinder/api/v3/group_snapshots.py
Normal file
146
cinder/api/v3/group_snapshots.py
Normal 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())
|
@ -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.
|
||||||
|
@ -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",
|
||||||
|
30
cinder/api/v3/snapshots.py
Normal file
30
cinder/api/v3/snapshots.py
Normal 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))
|
64
cinder/api/v3/views/group_snapshots.py
Normal file
64
cinder/api/v3/views/group_snapshots.py
Normal 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
|
@ -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 = [
|
||||||
|
33
cinder/api/v3/views/snapshots.py
Normal file
33
cinder/api/v3/views/snapshots.py
Normal 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
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
420
cinder/tests/unit/api/v3/test_group_snapshots.py
Normal file
420
cinder/tests/unit/api/v3/test_group_snapshots.py
Normal 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()
|
@ -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()
|
||||||
|
79
cinder/tests/unit/api/v3/test_snapshots.py
Normal file
79
cinder/tests/unit/api/v3/test_snapshots.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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')
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
4
releasenotes/notes/group-snapshots-36264409bbb8850c.yaml
Normal file
4
releasenotes/notes/group-snapshots-36264409bbb8850c.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added create/delete APIs for group snapshots and
|
||||||
|
an API to create group from source.
|
Loading…
Reference in New Issue
Block a user