summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxing-yang <xing.yang@emc.com>2016-05-14 17:36:13 -0400
committerxing-yang <xing.yang@emc.com>2016-07-14 03:36:39 -0400
commit37ac58cc62447d463d1977c79f9bba49e02523b2 (patch)
treea8a5e0c4cd39fd723b4d41b01ea20e99cc075d34
parentf802d1ae1add84a3cd590de596d7835c1fce79b3 (diff)
downloadpython-cinderclient-37ac58cc62447d463d1977c79f9bba49e02523b2.tar.gz
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 <group type uuid> --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
-rw-r--r--cinderclient/api_versions.py2
-rw-r--r--cinderclient/tests/unit/v3/fakes.py64
-rw-r--r--cinderclient/tests/unit/v3/test_group_types.py99
-rw-r--r--cinderclient/tests/unit/v3/test_shell.py38
-rw-r--r--cinderclient/v3/client.py2
-rw-r--r--cinderclient/v3/group_types.py148
-rw-r--r--cinderclient/v3/shell.py142
7 files changed, 494 insertions, 1 deletions
diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py
index c71575e..f305710 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 50b44d7..9366b58 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 0000000..8904fb3
--- /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 c56911d..a24e238 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 616ec25..f913017 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 0000000..70c5575
--- /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 "<GroupType: %s>" % 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 e5a5ea1..9036c6c 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)"""
@@ -881,12 +890,28 @@ def do_type_list(cs, args):
@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."""
vtype = cs.volume_types.default()
_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='<volume_type>',
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='<group_type>',
+ 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='<id>',
help='ID of the volume type.')
@@ -923,12 +963,42 @@ def do_type_update(cs, args):
@utils.service_type('volumev3')
+@api_versions.wraps('3.11')
+@utils.arg('id',
+ metavar='<id>',
+ help='ID of the group type.')
+@utils.arg('--name',
+ metavar='<name>',
+ help='Name of the group type.')
+@utils.arg('--description',
+ metavar='<description>',
+ help='Description of the group type.')
+@utils.arg('--is-public',
+ metavar='<is-public>',
+ 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."""
vtypes = cs.volume_types.list()
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='<name>',
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='<name>',
+ help='Name of new group type.')
+@utils.arg('--description',
+ metavar='<description>',
+ help='Description of new group type.')
+@utils.arg('--is-public',
+ metavar='<is-public>',
+ 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='<vol_type>', 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='<group_type>', 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='<vtype>',
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='<gtype>',
+ help='Name or ID of group type.')
+@utils.arg('action',
+ metavar='<action>',
+ choices=['set', 'unset'],
+ help='The action. Valid values are "set" or "unset."')
+@utils.arg('metadata',
+ metavar='<key=value>',
+ 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='<volume_type>', 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='<volume>',
help='Name or ID of volume to snapshot.')