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
This commit is contained in:
xing-yang
2016-05-14 17:36:13 -04:00
parent f802d1ae1a
commit 37ac58cc62
7 changed files with 494 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.
DEPRECATED_VERSIONS = {"1": "2"}
MAX_VERSION = "3.7"
MAX_VERSION = "3.11"
_SUBSTITUTIONS = {}

View File

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

View File

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

View File

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

View File

@@ -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 = \

View File

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

View File

@@ -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='<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.')
@@ -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='<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."""
@@ -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='<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.')