From 37ac58cc62447d463d1977c79f9bba49e02523b2 Mon Sep 17 00:00:00 2001 From: xing-yang Date: Sat, 14 May 2016 17:36:13 -0400 Subject: [PATCH] Add group types and group specs This patch adds support for group types and group specs in the client. Server patch is merged: https://review.openstack.org/#/c/320165/ Current microversion is 3.11. The following CLI's are supported. cinder --os-volume-api-version 3.11 group-type-create my_test_group cinder --os-volume-api-version 3.11 group-type-list cinder --os-volume-api-version 3.11 group-type-show my_test_group cinder --os-volume-api-version 3.11 group-type-key my_test_group set test_key=test_val cinder --os-volume-api-version 3.11 group-specs-list cinder --os-volume-api-version 3.11 group-type-key my_test_group unset test_key cinder --os-volume-api-version 3.11 group-type-update --name "new_group" --description "my group type" cinder --os-volume-api-version 3.11 group-type-delete new_group Change-Id: I161a96aa53208e78146cb115d500fd6b2c42d046 Partial-Implements: blueprint generic-volume-group --- cinderclient/api_versions.py | 2 +- cinderclient/tests/unit/v3/fakes.py | 64 ++++++++ .../tests/unit/v3/test_group_types.py | 99 ++++++++++++ cinderclient/tests/unit/v3/test_shell.py | 38 +++++ cinderclient/v3/client.py | 2 + cinderclient/v3/group_types.py | 148 ++++++++++++++++++ cinderclient/v3/shell.py | 142 +++++++++++++++++ 7 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 cinderclient/tests/unit/v3/test_group_types.py create mode 100644 cinderclient/v3/group_types.py diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py index c71575e14..f3057101e 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.7" +MAX_VERSION = "3.11" _SUBSTITUTIONS = {} diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py index 50b44d73b..9366b5858 100644 --- a/cinderclient/tests/unit/v3/fakes.py +++ b/cinderclient/tests/unit/v3/fakes.py @@ -184,3 +184,67 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient): tenant_id='0fa851f6668144cf9cd8c8419c1646c1') return (200, {}, {'backups': backup}) + + # + # GroupTypes + # + def get_group_types(self, **kw): + return (200, {}, { + 'group_types': [{'id': 1, + 'name': 'test-type-1', + 'description': 'test_type-1-desc', + 'group_specs': {}}, + {'id': 2, + 'name': 'test-type-2', + 'description': 'test_type-2-desc', + 'group_specs': {}}]}) + + def get_group_types_1(self, **kw): + return (200, {}, {'group_type': {'id': 1, + 'name': 'test-type-1', + 'description': 'test_type-1-desc', + 'group_specs': {u'key': u'value'}}}) + + def get_group_types_2(self, **kw): + return (200, {}, {'group_type': {'id': 2, + 'name': 'test-type-2', + 'description': 'test_type-2-desc', + 'group_specs': {}}}) + + def get_group_types_3(self, **kw): + return (200, {}, {'group_type': {'id': 3, + 'name': 'test-type-3', + 'description': 'test_type-3-desc', + 'group_specs': {}, + 'is_public': False}}) + + def get_group_types_default(self, **kw): + return self.get_group_types_1() + + def post_group_types(self, body, **kw): + return (202, {}, {'group_type': {'id': 3, + 'name': 'test-type-3', + 'description': 'test_type-3-desc', + 'group_specs': {}}}) + + def post_group_types_1_group_specs(self, body, **kw): + assert list(body) == ['group_specs'] + return (200, {}, {'group_specs': {'k': 'v'}}) + + def delete_group_types_1_group_specs_k(self, **kw): + return(204, {}, None) + + def delete_group_types_1_group_specs_m(self, **kw): + return(204, {}, None) + + def delete_group_types_1(self, **kw): + return (202, {}, None) + + def delete_group_types_3_group_specs_k(self, **kw): + return(204, {}, None) + + def delete_group_types_3(self, **kw): + return (202, {}, None) + + def put_group_types_1(self, **kw): + return self.get_group_types_1() diff --git a/cinderclient/tests/unit/v3/test_group_types.py b/cinderclient/tests/unit/v3/test_group_types.py new file mode 100644 index 000000000..8904fb38a --- /dev/null +++ b/cinderclient/tests/unit/v3/test_group_types.py @@ -0,0 +1,99 @@ +# 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 cinderclient.v3 import group_types +from cinderclient.tests.unit import utils +from cinderclient.tests.unit.v3 import fakes + +cs = fakes.FakeClient() + + +class GroupTypesTest(utils.TestCase): + + def test_list_group_types(self): + tl = cs.group_types.list() + cs.assert_called('GET', '/group_types?is_public=None') + self._assert_request_id(tl) + for t in tl: + self.assertIsInstance(t, group_types.GroupType) + + def test_list_group_types_not_public(self): + t1 = cs.group_types.list(is_public=None) + cs.assert_called('GET', '/group_types?is_public=None') + self._assert_request_id(t1) + + def test_create(self): + t = cs.group_types.create('test-type-3', 'test-type-3-desc') + cs.assert_called('POST', '/group_types', + {'group_type': { + 'name': 'test-type-3', + 'description': 'test-type-3-desc', + 'is_public': True + }}) + self.assertIsInstance(t, group_types.GroupType) + self._assert_request_id(t) + + def test_create_non_public(self): + t = cs.group_types.create('test-type-3', 'test-type-3-desc', False) + cs.assert_called('POST', '/group_types', + {'group_type': { + 'name': 'test-type-3', + 'description': 'test-type-3-desc', + 'is_public': False + }}) + self.assertIsInstance(t, group_types.GroupType) + self._assert_request_id(t) + + def test_update(self): + t = cs.group_types.update('1', 'test_type_1', 'test_desc_1', False) + cs.assert_called('PUT', + '/group_types/1', + {'group_type': {'name': 'test_type_1', + 'description': 'test_desc_1', + 'is_public': False}}) + self.assertIsInstance(t, group_types.GroupType) + self._assert_request_id(t) + + def test_get(self): + t = cs.group_types.get('1') + cs.assert_called('GET', '/group_types/1') + self.assertIsInstance(t, group_types.GroupType) + self._assert_request_id(t) + + def test_default(self): + t = cs.group_types.default() + cs.assert_called('GET', '/group_types/default') + self.assertIsInstance(t, group_types.GroupType) + self._assert_request_id(t) + + def test_set_key(self): + t = cs.group_types.get(1) + res = t.set_keys({'k': 'v'}) + cs.assert_called('POST', + '/group_types/1/group_specs', + {'group_specs': {'k': 'v'}}) + self._assert_request_id(res) + + def test_unset_keys(self): + t = cs.group_types.get(1) + res = t.unset_keys(['k']) + cs.assert_called('DELETE', '/group_types/1/group_specs/k') + self._assert_request_id(res) + + def test_delete(self): + t = cs.group_types.delete(1) + cs.assert_called('DELETE', '/group_types/1') + self._assert_request_id(t) diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index c56911dbf..a24e23846 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -133,3 +133,41 @@ class ShellTest(utils.TestCase): self.run_command, '--os-volume-api-version 3.8 ' 'backup-update --name new-name 1234') + + def test_group_type_list(self): + self.run_command('--os-volume-api-version 3.11 group-type-list') + self.assert_called_anytime('GET', '/group_types?is_public=None') + + def test_group_type_show(self): + self.run_command('--os-volume-api-version 3.11 ' + 'group-type-show 1') + self.assert_called('GET', '/group_types/1') + + def test_group_type_create(self): + self.run_command('--os-volume-api-version 3.11 ' + 'group-type-create test-type-1') + self.assert_called('POST', '/group_types') + + def test_group_type_create_public(self): + expected = {'group_type': {'name': 'test-type-1', + 'description': 'test_type-1-desc', + 'is_public': True}} + self.run_command('--os-volume-api-version 3.11 ' + 'group-type-create test-type-1 ' + '--description=test_type-1-desc ' + '--is-public=True') + self.assert_called('POST', '/group_types', body=expected) + + def test_group_type_create_private(self): + expected = {'group_type': {'name': 'test-type-3', + 'description': 'test_type-3-desc', + 'is_public': False}} + self.run_command('--os-volume-api-version 3.11 ' + 'group-type-create test-type-3 ' + '--description=test_type-3-desc ' + '--is-public=False') + self.assert_called('POST', '/group_types', body=expected) + + def test_group_specs_list(self): + self.run_command('--os-volume-api-version 3.11 group-specs-list') + self.assert_called('GET', '/group_types?is_public=None') diff --git a/cinderclient/v3/client.py b/cinderclient/v3/client.py index 616ec25cc..f91301793 100644 --- a/cinderclient/v3/client.py +++ b/cinderclient/v3/client.py @@ -22,6 +22,7 @@ from cinderclient.v3 import cgsnapshots from cinderclient.v3 import clusters from cinderclient.v3 import consistencygroups from cinderclient.v3 import capabilities +from cinderclient.v3 import group_types from cinderclient.v3 import limits from cinderclient.v3 import pools from cinderclient.v3 import qos_specs @@ -70,6 +71,7 @@ class Client(object): self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) + self.group_types = group_types.GroupTypeManager(self) self.volume_type_access = \ volume_type_access.VolumeTypeAccessManager(self) self.volume_encryption_types = \ diff --git a/cinderclient/v3/group_types.py b/cinderclient/v3/group_types.py new file mode 100644 index 000000000..70c5575d8 --- /dev/null +++ b/cinderclient/v3/group_types.py @@ -0,0 +1,148 @@ +# Copyright (c) 2016 EMC Corporation +# +# 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 Type interface.""" + +from cinderclient import base + + +class GroupType(base.Resource): + """A Group Type is the type of group to be created.""" + def __repr__(self): + return "" % self.name + + @property + def is_public(self): + """ + Provide a user-friendly accessor to is_public + """ + return self._info.get("is_public", + self._info.get("is_public", 'N/A')) + + def get_keys(self): + """Get group specs from a group type. + + :param type: The :class:`GroupType` to get specs from + """ + _resp, body = self.manager.api.client.get( + "/group_types/%s/group_specs" % + base.getid(self)) + return body["group_specs"] + + def set_keys(self, metadata): + """Set group specs on a group type. + + :param type : The :class:`GroupType` to set spec on + :param metadata: A dict of key/value pairs to be set + """ + body = {'group_specs': metadata} + return self.manager._create( + "/group_types/%s/group_specs" % base.getid(self), + body, + "group_specs", + return_raw=True) + + def unset_keys(self, keys): + """Unset specs on a group type. + + :param type_id: The :class:`GroupType` to unset spec on + :param keys: A list of keys to be unset + """ + + for k in keys: + resp = self.manager._delete( + "/group_types/%s/group_specs/%s" % ( + base.getid(self), k)) + if resp: + return resp + + +class GroupTypeManager(base.ManagerWithFind): + """Manage :class:`GroupType` resources.""" + resource_class = GroupType + + def list(self, search_opts=None, is_public=None): + """Lists all group types. + + :rtype: list of :class:`GroupType`. + """ + query_string = '' + if not is_public: + query_string = '?is_public=%s' % is_public + return self._list("/group_types%s" % (query_string), "group_types") + + def get(self, group_type): + """Get a specific group type. + + :param group_type: The ID of the :class:`GroupType` to get. + :rtype: :class:`GroupType` + """ + return self._get("/group_types/%s" % base.getid(group_type), + "group_type") + + def default(self): + """Get the default group type. + + :rtype: :class:`GroupType` + """ + return self._get("/group_types/default", "group_type") + + def delete(self, group_type): + """Deletes a specific group_type. + + :param group_type: The name or ID of the :class:`GroupType` to get. + """ + return self._delete("/group_types/%s" % base.getid(group_type)) + + def create(self, name, description=None, is_public=True): + """Creates a group type. + + :param name: Descriptive name of the group type + :param description: Description of the the group type + :param is_public: Group type visibility + :rtype: :class:`GroupType` + """ + + body = { + "group_type": { + "name": name, + "description": description, + "is_public": is_public, + } + } + + return self._create("/group_types", body, "group_type") + + def update(self, group_type, name=None, description=None, is_public=None): + """Update the name and/or description for a group type. + + :param group_type: The ID of the :class:`GroupType` to update. + :param name: Descriptive name of the group type. + :param description: Description of the the group type. + :rtype: :class:`GroupType` + """ + + body = { + "group_type": { + "name": name, + "description": description + } + } + if is_public is not None: + body["group_type"]["is_public"] = is_public + + return self._update("/group_types/%s" % base.getid(group_type), + body, response_key="group_type") diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py index e5a5ea148..9036c6c89 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -73,6 +73,11 @@ def _find_vtype(cs, vtype): return utils.find_resource(cs.volume_types, vtype) +def _find_gtype(cs, gtype): + """Gets a group type by name or ID.""" + return utils.find_resource(cs.group_types, gtype) + + def _find_backup(cs, backup): """Gets a backup by name or ID.""" return utils.find_resource(cs.backups, backup) @@ -873,6 +878,10 @@ def _print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public']) +def _print_group_type_list(gtypes): + utils.print_list(gtypes, ['ID', 'Name', 'Description']) + + @utils.service_type('volumev3') def do_type_list(cs, args): """Lists available 'volume types'. (Admin only will see private types)""" @@ -880,6 +889,14 @@ def do_type_list(cs, args): _print_volume_type_list(vtypes) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +def do_group_type_list(cs, args): + """Lists available 'group types'. (Admin only will see private types)""" + gtypes = cs.group_types.list() + _print_group_type_list(gtypes) + + @utils.service_type('volumev3') def do_type_default(cs, args): """List the default volume type.""" @@ -887,6 +904,14 @@ def do_type_default(cs, args): _print_volume_type_list([vtype]) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +def do_group_type_default(cs, args): + """List the default group type.""" + gtype = cs.group_types.default() + _print_group_type_list([gtype]) + + @utils.arg('volume_type', metavar='', help='Name or ID of the volume type.') @@ -901,6 +926,21 @@ def do_type_show(cs, args): utils.print_dict(info, formatters=['extra_specs']) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +@utils.arg('group_type', + metavar='', + help='Name or ID of the group type.') +def do_group_type_show(cs, args): + """Show group type details.""" + gtype = _find_gtype(cs, args.group_type) + info = dict() + info.update(gtype._info) + + info.pop('links', None) + utils.print_dict(info, formatters=['group_specs']) + + @utils.arg('id', metavar='', help='ID of the volume type.') @@ -922,6 +962,28 @@ def do_type_update(cs, args): _print_volume_type_list([vtype]) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +@utils.arg('id', + metavar='', + help='ID of the group type.') +@utils.arg('--name', + metavar='', + help='Name of the group type.') +@utils.arg('--description', + metavar='', + help='Description of the group type.') +@utils.arg('--is-public', + metavar='', + help='Make type accessible to the public or not.') +def do_group_type_update(cs, args): + """Updates group type name, description, and/or is_public.""" + is_public = strutils.bool_from_string(args.is_public) + gtype = cs.group_types.update(args.id, args.name, args.description, + is_public) + _print_group_type_list([gtype]) + + @utils.service_type('volumev3') def do_extra_specs_list(cs, args): """Lists current volume types and extra specs.""" @@ -929,6 +991,14 @@ def do_extra_specs_list(cs, args): utils.print_list(vtypes, ['ID', 'Name', 'extra_specs']) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +def do_group_specs_list(cs, args): + """Lists current group types and specs.""" + gtypes = cs.group_types.list() + utils.print_list(gtypes, ['ID', 'Name', 'group_specs']) + + @utils.arg('name', metavar='', help='Name of new volume type.') @@ -947,6 +1017,25 @@ def do_type_create(cs, args): _print_volume_type_list([vtype]) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +@utils.arg('name', + metavar='', + help='Name of new group type.') +@utils.arg('--description', + metavar='', + help='Description of new group type.') +@utils.arg('--is-public', + metavar='', + default=True, + help='Make type accessible to the public (default true).') +def do_group_type_create(cs, args): + """Creates a group type.""" + is_public = strutils.bool_from_string(args.is_public) + gtype = cs.group_types.create(args.name, args.description, is_public) + _print_group_type_list([gtype]) + + @utils.arg('vol_type', metavar='', nargs='+', help='Name or ID of volume type or types to delete.') @@ -968,6 +1057,28 @@ def do_type_delete(cs, args): "specified types.") +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +@utils.arg('group_type', + metavar='', nargs='+', + help='Name or ID of group type or types to delete.') +def do_group_type_delete(cs, args): + """Deletes group type or types.""" + failure_count = 0 + for group_type in args.group_type: + try: + gtype = _find_group_type(cs, group_type) + cs.group_types.delete(gtype) + print("Request to delete group type %s has been accepted." + % (group_type)) + except Exception as e: + failure_count += 1 + print("Delete for group type %s failed: %s" % (group_type, e)) + if failure_count == len(args.group_type): + raise exceptions.CommandError("Unable to delete any of the " + "specified types.") + + @utils.arg('vtype', metavar='', help='Name or ID of volume type.') @@ -993,6 +1104,32 @@ def do_type_key(cs, args): vtype.unset_keys(list(keypair)) +@utils.service_type('volumev3') +@api_versions.wraps('3.11') +@utils.arg('gtype', + metavar='', + help='Name or ID of group type.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help='The action. Valid values are "set" or "unset."') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='The group specs key and value pair to set or unset. ' + 'For unset, specify only the key.') +def do_group_type_key(cs, args): + """Sets or unsets group_spec for a group type.""" + gtype = _find_group_type(cs, args.gtype) + keypair = _extract_metadata(args) + + if args.action == 'set': + gtype.set_keys(keypair) + elif args.action == 'unset': + gtype.unset_keys(list(keypair)) + + @utils.arg('--volume-type', metavar='', required=True, help='Filter results by volume type name or ID.') @utils.service_type('volumev3') @@ -1250,6 +1387,11 @@ def _find_volume_type(cs, vtype): return utils.find_resource(cs.volume_types, vtype) +def _find_group_type(cs, gtype): + """Gets a group type by name or ID.""" + return utils.find_resource(cs.group_types, gtype) + + @utils.arg('volume', metavar='', help='Name or ID of volume to snapshot.')