Add support for group snapshots

This patch adds support for group snapshots.

Server side API patch was merged:
    https://review.openstack.org/#/c/361369/

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>

Depends-on: I2e628968afcf058113e1f1aeb851570c7f0f3a08
Partial-Implements: blueprint generic-volume-group
Change-Id: I5c311fe5a6aeadd1d4fca60493f4295dc368944c
This commit is contained in:
xing-yang 2016-05-21 08:09:00 -04:00
parent 6c5a764c77
commit f7928c4058
9 changed files with 549 additions and 1 deletions

@ -32,7 +32,7 @@ if not LOG.handlers:
# key is a deprecated version and value is an alternative version.
DEPRECATED_VERSIONS = {"1": "2"}
MAX_VERSION = "3.13"
MAX_VERSION = "3.14"
_SUBSTITUTIONS = {}

@ -37,6 +37,25 @@ def _stub_group(detailed=True, **kwargs):
return group
def _stub_group_snapshot(detailed=True, **kwargs):
group_snapshot = {
"name": None,
"id": "5678",
}
if detailed:
details = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": None,
"id": "5678",
"status": "available",
"group_id": "1234",
}
group_snapshot.update(details)
group_snapshot.update(kwargs)
return group_snapshot
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, api_version=None, *args, **kwargs):
@ -302,3 +321,46 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, {})
def post_groups_action(self, body, **kw):
group = _stub_group(id='1234', group_type='my_group_type',
volume_types=['type1', 'type2'])
resp = 202
assert len(list(body)) == 1
action = list(body)[0]
if action == 'create-from-src':
assert ('group_snapshot_id' in body[action] or
'source_group_id' in body[action])
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, {'group': group})
#
# group_snapshots
#
def get_group_snapshots_detail(self, **kw):
return (200, {}, {"group_snapshots": [
_stub_group_snapshot(id='1234'),
_stub_group_snapshot(id='4567')]})
def get_group_snapshots(self, **kw):
return (200, {}, {"group_snapshots": [
_stub_group_snapshot(detailed=False, id='1234'),
_stub_group_snapshot(detailed=False, id='4567')]})
def get_group_snapshots_1234(self, **kw):
return (200, {}, {'group_snapshot': _stub_group_snapshot(id='1234')})
def get_group_snapshots_5678(self, **kw):
return (200, {}, {'group_snapshot': _stub_group_snapshot(id='5678')})
def post_group_snapshots(self, **kw):
group_snap = _stub_group_snapshot()
return (202, {}, {'group_snapshot': group_snap})
def put_group_snapshots_1234(self, **kw):
return (200, {}, {'group_snapshot': {}})
def delete_group_snapshots_1234(self, **kw):
return (202, {}, {})

@ -0,0 +1,102 @@
# 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.
import ddt
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v3 import fakes
cs = fakes.FakeClient()
@ddt.ddt
class GroupSnapshotsTest(utils.TestCase):
def test_delete_group_snapshot(self):
s1 = cs.group_snapshots.list()[0]
snap = s1.delete()
self._assert_request_id(snap)
cs.assert_called('DELETE', '/group_snapshots/1234')
snap = cs.group_snapshots.delete('1234')
cs.assert_called('DELETE', '/group_snapshots/1234')
self._assert_request_id(snap)
snap = cs.group_snapshots.delete(s1)
cs.assert_called('DELETE', '/group_snapshots/1234')
self._assert_request_id(snap)
def test_create_group_snapshot(self):
snap = cs.group_snapshots.create('group_snap')
cs.assert_called('POST', '/group_snapshots')
self._assert_request_id(snap)
def test_create_group_snapshot_with_group_id(self):
snap = cs.group_snapshots.create('1234')
expected = {'group_snapshot': {'status': 'creating',
'description': None,
'user_id': None,
'name': None,
'group_id': '1234',
'project_id': None}}
cs.assert_called('POST', '/group_snapshots', body=expected)
self._assert_request_id(snap)
def test_update_group_snapshot(self):
s1 = cs.group_snapshots.list()[0]
expected = {'group_snapshot': {'name': 'grp_snap2'}}
snap = s1.update(name='grp_snap2')
cs.assert_called('PUT', '/group_snapshots/1234', body=expected)
self._assert_request_id(snap)
snap = cs.group_snapshots.update('1234', name='grp_snap2')
cs.assert_called('PUT', '/group_snapshots/1234', body=expected)
self._assert_request_id(snap)
snap = cs.group_snapshots.update(s1, name='grp_snap2')
cs.assert_called('PUT', '/group_snapshots/1234', body=expected)
self._assert_request_id(snap)
def test_update_group_snapshot_no_props(self):
ret = cs.group_snapshots.update('1234')
self.assertIsNone(ret)
def test_list_group_snapshot(self):
lst = cs.group_snapshots.list()
cs.assert_called('GET', '/group_snapshots/detail')
self._assert_request_id(lst)
@ddt.data(
{'detailed': True, 'url': '/group_snapshots/detail'},
{'detailed': False, 'url': '/group_snapshots'}
)
@ddt.unpack
def test_list_group_snapshot_detailed(self, detailed, url):
lst = cs.group_snapshots.list(detailed=detailed)
cs.assert_called('GET', url)
self._assert_request_id(lst)
@ddt.data(
{'foo': 'bar'},
{'foo': 'bar', '123': None}
)
def test_list_group_snapshot_with_search_opts(self, opts):
lst = cs.group_snapshots.list(search_opts=opts)
cs.assert_called('GET', '/group_snapshots/detail?foo=bar')
self._assert_request_id(lst)
def test_get_group_snapshot(self):
group_snapshot_id = '1234'
snap = cs.group_snapshots.get(group_snapshot_id)
cs.assert_called('GET', '/group_snapshots/%s' % group_snapshot_id)
self._assert_request_id(snap)

@ -113,3 +113,37 @@ class GroupsTest(utils.TestCase):
grp = cs.groups.get(group_id)
cs.assert_called('GET', '/groups/%s' % group_id)
self._assert_request_id(grp)
def test_create_group_from_src_snap(self):
grp = cs.groups.create_from_src('5678', None, name='group')
expected = {
'create-from-src': {
'status': 'creating',
'description': None,
'user_id': None,
'name': 'group',
'group_snapshot_id': '5678',
'project_id': None,
'source_group_id': None
}
}
cs.assert_called('POST', '/groups/action',
body=expected)
self._assert_request_id(grp)
def test_create_group_from_src_group_(self):
grp = cs.groups.create_from_src(None, '5678', name='group')
expected = {
'create-from-src': {
'status': 'creating',
'description': None,
'user_id': None,
'name': 'group',
'source_group_id': '5678',
'project_id': None,
'group_snapshot_id': None
}
}
cs.assert_called('POST', '/groups/action',
body=expected)
self._assert_request_id(grp)

@ -261,3 +261,51 @@ class ShellTest(utils.TestCase):
self.assertRaises(exceptions.ClientException,
self.run_command,
'--os-volume-api-version 3.13 group-update 1234')
def test_group_snapshot_list(self):
self.run_command('--os-volume-api-version 3.14 group-snapshot-list')
self.assert_called_anytime('GET', '/group_snapshots/detail')
def test_group_snapshot_show(self):
self.run_command('--os-volume-api-version 3.14 '
'group-snapshot-show 1234')
self.assert_called('GET', '/group_snapshots/1234')
def test_group_snapshot_delete(self):
cmd = '--os-volume-api-version 3.14 group-snapshot-delete 1234'
self.run_command(cmd)
self.assert_called('DELETE', '/group_snapshots/1234')
def test_group_snapshot_create(self):
expected = {'group_snapshot': {'name': 'test-1',
'description': 'test-1-desc',
'user_id': None,
'project_id': None,
'group_id': '1234',
'status': 'creating'}}
self.run_command('--os-volume-api-version 3.14 '
'group-snapshot-create --name test-1 '
'--description test-1-desc 1234')
self.assert_called_anytime('POST', '/group_snapshots', body=expected)
@ddt.data(
{'grp_snap_id': '1234', 'src_grp_id': None,
'src': '--group-snapshot 1234'},
{'grp_snap_id': None, 'src_grp_id': '1234',
'src': '--source-group 1234'},
)
@ddt.unpack
def test_group_create_from_src(self, grp_snap_id, src_grp_id, src):
expected = {'create-from-src': {'name': 'test-1',
'description': 'test-1-desc',
'user_id': None,
'project_id': None,
'status': 'creating',
'group_snapshot_id': grp_snap_id,
'source_group_id': src_grp_id}}
cmd = ('--os-volume-api-version 3.14 '
'group-create-from-src --name test-1 '
'--description test-1-desc ')
cmd += src
self.run_command(cmd)
self.assert_called_anytime('POST', '/groups/action', body=expected)

@ -23,6 +23,7 @@ from cinderclient.v3 import clusters
from cinderclient.v3 import consistencygroups
from cinderclient.v3 import capabilities
from cinderclient.v3 import groups
from cinderclient.v3 import group_snapshots
from cinderclient.v3 import group_types
from cinderclient.v3 import limits
from cinderclient.v3 import pools
@ -89,6 +90,7 @@ class Client(object):
ConsistencygroupManager(self)
self.groups = groups.GroupManager(self)
self.cgsnapshots = cgsnapshots.CgsnapshotManager(self)
self.group_snapshots = group_snapshots.GroupSnapshotManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
self.pools = pools.PoolManager(self)

@ -0,0 +1,129 @@
# 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.
"""group snapshot interface (v3)."""
import six
from six.moves.urllib.parse import urlencode
from cinderclient import base
from cinderclient.openstack.common.apiclient import base as common_base
class GroupSnapshot(base.Resource):
"""A group snapshot is a snapshot of a group."""
def __repr__(self):
return "<group_snapshot: %s>" % self.id
def delete(self):
"""Delete this group snapshot."""
return self.manager.delete(self)
def update(self, **kwargs):
"""Update the name or description for this group snapshot."""
return self.manager.update(self, **kwargs)
class GroupSnapshotManager(base.ManagerWithFind):
"""Manage :class:`GroupSnapshot` resources."""
resource_class = GroupSnapshot
def create(self, group_id, name=None, description=None,
user_id=None,
project_id=None):
"""Creates a group snapshot.
:param group_id: Name or uuid of a group
:param name: Name of the group snapshot
:param description: Description of the group snapshot
:param user_id: User id derived from context
:param project_id: Project id derived from context
:rtype: :class:`GroupSnapshot`
"""
body = {
'group_snapshot': {
'group_id': group_id,
'name': name,
'description': description,
'user_id': user_id,
'project_id': project_id,
'status': "creating",
}
}
return self._create('/group_snapshots', body, 'group_snapshot')
def get(self, group_snapshot_id):
"""Get a group snapshot.
:param group_snapshot_id: The ID of the group snapshot to get.
:rtype: :class:`GroupSnapshot`
"""
return self._get("/group_snapshots/%s" % group_snapshot_id,
"group_snapshot")
def list(self, detailed=True, search_opts=None):
"""Lists all group snapshots.
:param detailed: list detailed info or not
:param search_opts: search options
:rtype: list of :class:`GroupSnapshot`
"""
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/group_snapshots%s%s" % (detail, query_string),
"group_snapshots")
def delete(self, group_snapshot):
"""Delete a group_snapshot.
:param group_snapshot: The :class:`GroupSnapshot` to delete.
"""
return self._delete("/group_snapshots/%s" % base.getid(group_snapshot))
def update(self, group_snapshot, **kwargs):
"""Update the name or description for a group_snapshot.
:param group_snapshot: The :class:`GroupSnapshot` to update.
"""
if not kwargs:
return
body = {"group_snapshot": kwargs}
return self._update("/group_snapshots/%s" % base.getid(group_snapshot),
body)
def _action(self, action, group_snapshot, info=None, **kwargs):
"""Perform a group_snapshot action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/group_snapshots/%s/action' % base.getid(group_snapshot)
resp, body = self.api.client.post(url, body=body)
return common_base.TupleWithMeta((resp, body), resp)

@ -66,6 +66,33 @@ class GroupManager(base.ManagerWithFind):
return self._create('/groups', body, 'group')
def create_from_src(self, group_snapshot_id, source_group_id,
name=None, description=None, user_id=None,
project_id=None):
"""Creates a group from a group snapshot or a source group.
:param group_snapshot_id: UUID of a GroupSnapshot
:param source_group_id: UUID of a source Group
:param name: Name of the Group
:param description: Description of the Group
:param user_id: User id derived from context
:param project_id: Project id derived from context
:rtype: A dictionary containing Group metadata
"""
body = {'create-from-src': {'name': name,
'description': description,
'group_snapshot_id': group_snapshot_id,
'source_group_id': source_group_id,
'user_id': user_id,
'project_id': project_id,
'status': "creating", }}
self.run_hooks('modify_body_for_action', body,
'create-from-src')
resp, body = self.api.client.post(
"/groups/action", body=body)
return common_base.DictWithMeta(body['group'], resp)
def get(self, group_id):
"""Get a group.

@ -98,6 +98,11 @@ def _find_cgsnapshot(cs, cgsnapshot):
return utils.find_resource(cs.cgsnapshots, cgsnapshot)
def _find_group_snapshot(cs, group_snapshot):
"""Gets a group_snapshot by name or ID."""
return utils.find_resource(cs.group_snapshots, group_snapshot)
def _find_transfer(cs, transfer):
"""Gets a transfer by name or ID."""
return utils.find_resource(cs.transfers, transfer)
@ -2773,6 +2778,46 @@ def do_consisgroup_create_from_src(cs, args):
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps('3.14')
@utils.arg('--group-snapshot',
metavar='<group-snapshot>',
help='Name or ID of a group snapshot. Default=None.')
@utils.arg('--source-group',
metavar='<source-group>',
help='Name or ID of a source group. Default=None.')
@utils.arg('--name',
metavar='<name>',
help='Name of a group. Default=None.')
@utils.arg('--description',
metavar='<description>',
help='Description of a group. Default=None.')
def do_group_create_from_src(cs, args):
"""Creates a group from a group snapshot or a source group."""
if not args.group_snapshot and not args.source_group:
msg = ('Cannot create group because neither '
'group snapshot nor source group is provided.')
raise exceptions.ClientException(code=1, message=msg)
if args.group_snapshot and args.source_group:
msg = ('Cannot create group because both '
'group snapshot and source group are provided.')
raise exceptions.ClientException(code=1, message=msg)
group_snapshot = None
if args.group_snapshot:
group_snapshot = _find_group_snapshot(cs, args.group_snapshot)
source_group = None
if args.source_group:
source_group = _find_group(cs, args.source_group)
info = cs.groups.create_from_src(
group_snapshot.id if group_snapshot else None,
source_group.id if source_group else None,
args.name,
args.description)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('consistencygroup',
metavar='<consistencygroup>', nargs='+',
help='Name or ID of one or more consistency groups '
@ -2950,6 +2995,41 @@ def do_cgsnapshot_list(cs, args):
utils.print_list(cgsnapshots, columns)
@utils.service_type('volumev3')
@api_versions.wraps('3.14')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--status',
metavar='<status>',
default=None,
help='Filters results by a status. Default=None.')
@utils.arg('--group-id',
metavar='<group_id>',
default=None,
help='Filters results by a group ID. Default=None.')
def do_group_snapshot_list(cs, args):
"""Lists all group snapshots."""
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'status': args.status,
'group_id': args.group_id,
}
group_snapshots = cs.group_snapshots.list(search_opts=search_opts)
columns = ['ID', 'Status', 'Name']
utils.print_list(group_snapshots, columns)
@utils.arg('cgsnapshot',
metavar='<cgsnapshot>',
help='Name or ID of cgsnapshot.')
@ -2964,6 +3044,21 @@ def do_cgsnapshot_show(cs, args):
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps('3.14')
@utils.arg('group_snapshot',
metavar='<group_snapshot>',
help='Name or ID of group snapshot.')
def do_group_snapshot_show(cs, args):
"""Shows group snapshot details."""
info = dict()
group_snapshot = _find_group_snapshot(cs, args.group_snapshot)
info.update(group_snapshot._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('consistencygroup',
metavar='<consistencygroup>',
help='Name or ID of a consistency group.')
@ -2992,6 +3087,35 @@ def do_cgsnapshot_create(cs, args):
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps('3.14')
@utils.arg('group',
metavar='<group>',
help='Name or ID of a group.')
@utils.arg('--name',
metavar='<name>',
default=None,
help='Group snapshot name. Default=None.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Group snapshot description. Default=None.')
def do_group_snapshot_create(cs, args):
"""Creates a group snapshot."""
group = _find_group(cs, args.group)
group_snapshot = cs.group_snapshots.create(
group.id,
args.name,
args.description)
info = dict()
group_snapshot = cs.group_snapshots.get(group_snapshot.id)
info.update(group_snapshot._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('cgsnapshot',
metavar='<cgsnapshot>', nargs='+',
help='Name or ID of one or more cgsnapshots to be deleted.')
@ -3010,6 +3134,26 @@ def do_cgsnapshot_delete(cs, args):
"cgsnapshots.")
@utils.service_type('volumev3')
@api_versions.wraps('3.14')
@utils.arg('group_snapshot',
metavar='<group_snapshot>', nargs='+',
help='Name or ID of one or more group snapshots to be deleted.')
def do_group_snapshot_delete(cs, args):
"""Removes one or more group snapshots."""
failure_count = 0
for group_snapshot in args.group_snapshot:
try:
_find_group_snapshot(cs, group_snapshot).delete()
except Exception as e:
failure_count += 1
print("Delete for group snapshot %s failed: %s" %
(group_snapshot, e))
if failure_count == len(args.group_snapshot):
raise exceptions.CommandError("Unable to delete any of the specified "
"group snapshots.")
@utils.arg('--detail',
action='store_true',
help='Show detailed information about pools.')