diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py index 8f7b081..bdd975f 100644 --- a/cinderclient/api_versions.py +++ b/cinderclient/api_versions.py @@ -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 = {} diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py index de06960..814e435 100644 --- a/cinderclient/tests/unit/v3/fakes.py +++ b/cinderclient/tests/unit/v3/fakes.py @@ -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, {}, {}) diff --git a/cinderclient/tests/unit/v3/test_group_snapshots.py b/cinderclient/tests/unit/v3/test_group_snapshots.py new file mode 100644 index 0000000..1642d38 --- /dev/null +++ b/cinderclient/tests/unit/v3/test_group_snapshots.py @@ -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) diff --git a/cinderclient/tests/unit/v3/test_groups.py b/cinderclient/tests/unit/v3/test_groups.py index e22f4ae..1946b55 100644 --- a/cinderclient/tests/unit/v3/test_groups.py +++ b/cinderclient/tests/unit/v3/test_groups.py @@ -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) diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index b19e875..85e4c64 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -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) diff --git a/cinderclient/v3/client.py b/cinderclient/v3/client.py index 20ef9ed..363dad4 100644 --- a/cinderclient/v3/client.py +++ b/cinderclient/v3/client.py @@ -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) diff --git a/cinderclient/v3/group_snapshots.py b/cinderclient/v3/group_snapshots.py new file mode 100644 index 0000000..491447a --- /dev/null +++ b/cinderclient/v3/group_snapshots.py @@ -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 "" % 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) diff --git a/cinderclient/v3/groups.py b/cinderclient/v3/groups.py index 8728911..611851f 100644 --- a/cinderclient/v3/groups.py +++ b/cinderclient/v3/groups.py @@ -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. diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py index c57d142..ec66c33 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -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='', + help='Name or ID of a group snapshot. Default=None.') +@utils.arg('--source-group', + metavar='', + help='Name or ID of a source group. Default=None.') +@utils.arg('--name', + metavar='', + help='Name of a group. Default=None.') +@utils.arg('--description', + metavar='', + 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='', 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='', + default=None, + help='Filters results by a status. Default=None.') +@utils.arg('--group-id', + metavar='', + 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='', 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='', + 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='', 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='', + help='Name or ID of a group.') +@utils.arg('--name', + metavar='', + default=None, + help='Group snapshot name. Default=None.') +@utils.arg('--description', + metavar='', + 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='', 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='', 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.')