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

View File

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

View File

@@ -37,6 +37,25 @@ def _stub_group(detailed=True, **kwargs):
return group 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): class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, api_version=None, *args, **kwargs): def __init__(self, api_version=None, *args, **kwargs):
@@ -302,3 +321,46 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
else: else:
raise AssertionError("Unexpected action: %s" % action) raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, {}) 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, {}, {})

View File

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

View File

@@ -113,3 +113,37 @@ class GroupsTest(utils.TestCase):
grp = cs.groups.get(group_id) grp = cs.groups.get(group_id)
cs.assert_called('GET', '/groups/%s' % group_id) cs.assert_called('GET', '/groups/%s' % group_id)
self._assert_request_id(grp) 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)

View File

@@ -261,3 +261,51 @@ class ShellTest(utils.TestCase):
self.assertRaises(exceptions.ClientException, self.assertRaises(exceptions.ClientException,
self.run_command, self.run_command,
'--os-volume-api-version 3.13 group-update 1234') '--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)

View File

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

View File

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

View File

@@ -66,6 +66,33 @@ class GroupManager(base.ManagerWithFind):
return self._create('/groups', body, 'group') 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): def get(self, group_id):
"""Get a group. """Get a group.

View File

@@ -98,6 +98,11 @@ def _find_cgsnapshot(cs, cgsnapshot):
return utils.find_resource(cs.cgsnapshots, 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): def _find_transfer(cs, transfer):
"""Gets a transfer by name or ID.""" """Gets a transfer by name or ID."""
return utils.find_resource(cs.transfers, transfer) return utils.find_resource(cs.transfers, transfer)
@@ -2773,6 +2778,46 @@ def do_consisgroup_create_from_src(cs, args):
utils.print_dict(info) 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', @utils.arg('consistencygroup',
metavar='<consistencygroup>', nargs='+', metavar='<consistencygroup>', nargs='+',
help='Name or ID of one or more consistency groups ' 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.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', @utils.arg('cgsnapshot',
metavar='<cgsnapshot>', metavar='<cgsnapshot>',
help='Name or ID of cgsnapshot.') help='Name or ID of cgsnapshot.')
@@ -2964,6 +3044,21 @@ def do_cgsnapshot_show(cs, args):
utils.print_dict(info) 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', @utils.arg('consistencygroup',
metavar='<consistencygroup>', metavar='<consistencygroup>',
help='Name or ID of a consistency group.') help='Name or ID of a consistency group.')
@@ -2992,6 +3087,35 @@ def do_cgsnapshot_create(cs, args):
utils.print_dict(info) 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', @utils.arg('cgsnapshot',
metavar='<cgsnapshot>', nargs='+', metavar='<cgsnapshot>', nargs='+',
help='Name or ID of one or more cgsnapshots to be deleted.') help='Name or ID of one or more cgsnapshots to be deleted.')
@@ -3010,6 +3134,26 @@ def do_cgsnapshot_delete(cs, args):
"cgsnapshots.") "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', @utils.arg('--detail',
action='store_true', action='store_true',
help='Show detailed information about pools.') help='Show detailed information about pools.')