From 6f8c235a92b683448d4afbc96f3f4711a96fab8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= <mgagne@iweb.com> Date: Sat, 26 Jul 2014 17:41:26 -0400 Subject: [PATCH] Add support for os-volume-type-access extension This change adds the ability to manage volume type access: - Create non-public volume type - List volume type access - Add a project access - Remove a project access This change also adds the is_public flag to volume type list. Note: The volume type access extension is only implemented in the Cinder API v2. DocImpact: Add volume type access extension support Implements: blueprint private-volume-types Change-Id: Ife966120d9250be8d8149cdec9c1a53405d37027 --- cinderclient/tests/v2/fakes.py | 41 ++++++++++++++++ cinderclient/tests/v2/test_shell.py | 49 +++++++++++++++++++ cinderclient/tests/v2/test_type_access.py | 42 ++++++++++++++++ cinderclient/tests/v2/test_types.py | 24 +++++++++- cinderclient/v2/client.py | 3 ++ cinderclient/v2/shell.py | 58 +++++++++++++++++++++-- cinderclient/v2/volume_type_access.py | 51 ++++++++++++++++++++ cinderclient/v2/volume_types.py | 20 ++++++-- 8 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 cinderclient/tests/v2/test_type_access.py create mode 100644 cinderclient/v2/volume_type_access.py diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 18dc30588..430d76c1a 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -95,6 +95,13 @@ def _stub_cgsnapshot(**kwargs): return cgsnapshot +def _stub_type_access(**kwargs): + access = {'volume_type_id': '11111111-1111-1111-1111-111111111111', + 'project_id': '00000000-0000-0000-0000-000000000000'} + access.update(kwargs) + return access + + def _self_href(base_uri, tenant_id, backup_id): return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id) @@ -559,25 +566,37 @@ class FakeHTTPClient(base_client.HTTPClient): # # VolumeTypes # + def get_types(self, **kw): return (200, {}, { 'volume_types': [{'id': 1, 'name': 'test-type-1', + 'description': 'test_type-1-desc', 'extra_specs': {}}, {'id': 2, 'name': 'test-type-2', + 'description': 'test_type-2-desc', 'extra_specs': {}}]}) def get_types_1(self, **kw): return (200, {}, {'volume_type': {'id': 1, 'name': 'test-type-1', + 'description': 'test_type-1-desc', 'extra_specs': {}}}) def get_types_2(self, **kw): return (200, {}, {'volume_type': {'id': 2, 'name': 'test-type-2', + 'description': 'test_type-2-desc', 'extra_specs': {}}}) + def get_types_3(self, **kw): + return (200, {}, {'volume_type': {'id': 3, + 'name': 'test-type-3', + 'description': 'test_type-3-desc', + 'extra_specs': {}, + 'os-volume-type-access:is_public': False}}) + def get_types_default(self, **kw): return self.get_types_1() @@ -587,6 +606,19 @@ class FakeHTTPClient(base_client.HTTPClient): 'description': 'test_type-3-desc', 'extra_specs': {}}}) + def post_types_3_action(self, body, **kw): + _body = None + resp = 202 + assert len(list(body)) == 1 + action = list(body)[0] + if action == 'addProjectAccess': + assert 'project' in body['addProjectAccess'] + elif action == 'removeProjectAccess': + assert 'project' in body['removeProjectAccess'] + else: + raise AssertionError('Unexpected action: %s' % action) + return (resp, {}, _body) + def post_types_1_extra_specs(self, body, **kw): assert list(body) == ['extra_specs'] return (200, {}, {'extra_specs': {'k': 'v'}}) @@ -600,6 +632,15 @@ class FakeHTTPClient(base_client.HTTPClient): def put_types_1(self, **kw): return self.get_types_1() + # + # VolumeAccess + # + + def get_types_3_os_volume_type_access(self, **kw): + return (200, {}, {'volume_type_access': [ + _stub_type_access() + ]}) + # # VolumeEncryptionTypes # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 9aceed6ec..b9ab7a4c2 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -298,6 +298,55 @@ class ShellTest(utils.TestCase): self.assert_called_anytime('POST', '/snapshots/5678/action', body=expected) + def test_type_list(self): + self.run_command('type-list') + self.assert_called_anytime('GET', '/types') + + def test_type_list_all(self): + self.run_command('type-list --all') + self.assert_called_anytime('GET', '/types?is_public=None') + + def test_type_create(self): + self.run_command('type-create test-type-1') + self.assert_called('POST', '/types') + + def test_type_create_public(self): + expected = {'volume_type': {'name': 'test-type-1', + 'description': 'test_type-1-desc', + 'os-volume-type-access:is_public': True}} + self.run_command('type-create test-type-1 ' + '--description=test_type-1-desc ' + '--is-public=True') + self.assert_called('POST', '/types', body=expected) + + def test_type_create_private(self): + expected = {'volume_type': {'name': 'test-type-3', + 'description': 'test_type-3-desc', + 'os-volume-type-access:is_public': False}} + self.run_command('type-create test-type-3 ' + '--description=test_type-3-desc ' + '--is-public=False') + self.assert_called('POST', '/types', body=expected) + + def test_type_access_list(self): + self.run_command('type-access-list --volume-type 3') + self.assert_called('GET', '/types/3/os-volume-type-access') + + def test_type_access_add_project(self): + expected = {'addProjectAccess': {'project': '101'}} + self.run_command('type-access-add --volume-type 3 --project-id 101') + self.assert_called_anytime('GET', '/types/3') + self.assert_called('POST', '/types/3/action', + body=expected) + + def test_type_access_remove_project(self): + expected = {'removeProjectAccess': {'project': '101'}} + self.run_command('type-access-remove ' + '--volume-type 3 --project-id 101') + self.assert_called_anytime('GET', '/types/3') + self.assert_called('POST', '/types/3/action', + body=expected) + def test_encryption_type_list(self): """ Test encryption-type-list shell command. diff --git a/cinderclient/tests/v2/test_type_access.py b/cinderclient/tests/v2/test_type_access.py new file mode 100644 index 000000000..bf9a6b282 --- /dev/null +++ b/cinderclient/tests/v2/test_type_access.py @@ -0,0 +1,42 @@ +# Copyright (c) 2013 OpenStack Foundation +# +# 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.v2 import volume_type_access +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes + +cs = fakes.FakeClient() + +PROJECT_UUID = '11111111-1111-1111-111111111111' + + +class TypeAccessTest(utils.TestCase): + + def test_list(self): + access = cs.volume_type_access.list(volume_type='3') + cs.assert_called('GET', '/types/3/os-volume-type-access') + for a in access: + self.assertTrue(isinstance(a, volume_type_access.VolumeTypeAccess)) + + def test_add_project_access(self): + cs.volume_type_access.add_project_access('3', PROJECT_UUID) + cs.assert_called('POST', '/types/3/action', + {'addProjectAccess': {'project': PROJECT_UUID}}) + + def test_remove_project_access(self): + cs.volume_type_access.remove_project_access('3', PROJECT_UUID) + cs.assert_called('POST', '/types/3/action', + {'removeProjectAccess': {'project': PROJECT_UUID}}) diff --git a/cinderclient/tests/v2/test_types.py b/cinderclient/tests/v2/test_types.py index efe027ff3..108cbbab3 100644 --- a/cinderclient/tests/v2/test_types.py +++ b/cinderclient/tests/v2/test_types.py @@ -22,15 +22,35 @@ cs = fakes.FakeClient() class TypesTest(utils.TestCase): + def test_list_types(self): tl = cs.volume_types.list() cs.assert_called('GET', '/types') for t in tl: self.assertIsInstance(t, volume_types.VolumeType) + def test_list_types_not_public(self): + cs.volume_types.list(is_public=None) + cs.assert_called('GET', '/types?is_public=None') + def test_create(self): - t = cs.volume_types.create('test-type-3') - cs.assert_called('POST', '/types') + t = cs.volume_types.create('test-type-3', 'test-type-3-desc') + cs.assert_called('POST', '/types', + {'volume_type': { + 'name': 'test-type-3', + 'description': 'test-type-3-desc', + 'os-volume-type-access:is_public': True + }}) + self.assertIsInstance(t, volume_types.VolumeType) + + def test_create_non_public(self): + t = cs.volume_types.create('test-type-3', 'test-type-3-desc', False) + cs.assert_called('POST', '/types', + {'volume_type': { + 'name': 'test-type-3', + 'description': 'test-type-3-desc', + 'os-volume-type-access:is_public': False + }}) self.assertIsInstance(t, volume_types.VolumeType) def test_update(self): diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 88d51ceec..4619ea270 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -25,6 +25,7 @@ from cinderclient.v2 import services from cinderclient.v2 import volumes from cinderclient.v2 import volume_snapshots from cinderclient.v2 import volume_types +from cinderclient.v2 import volume_type_access from cinderclient.v2 import volume_encryption_types from cinderclient.v2 import volume_backups from cinderclient.v2 import volume_backups_restore @@ -61,6 +62,8 @@ class Client(object): self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) + self.volume_type_access = \ + volume_type_access.VolumeTypeAccessManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) self.qos_specs = qos_specs.QoSSpecsManager(self) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index a0f697746..c61ba4a33 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -696,13 +696,21 @@ def do_snapshot_reset_state(cs, args): def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name', 'Description']) + utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public']) @utils.service_type('volumev2') +@utils.arg('--all', + dest='all', + action='store_true', + default=False, + help='Display all volume types (Admin only).') def do_type_list(cs, args): """Lists available 'volume types'.""" - vtypes = cs.volume_types.list() + if args.all: + vtypes = cs.volume_types.list(is_public=None) + else: + vtypes = cs.volume_types.list() _print_volume_type_list(vtypes) @@ -739,10 +747,15 @@ def do_extra_specs_list(cs, args): @utils.arg('--description', metavar='<description>', help="Description of new volume type.") +@utils.arg('--is-public', + metavar='<is-public>', + help="Make type accessible to the public (default true).", + default=True) @utils.service_type('volumev2') def do_type_create(cs, args): """Creates a volume type.""" - vtype = cs.volume_types.create(args.name, args.description) + is_public = strutils.bool_from_string(args.is_public) + vtype = cs.volume_types.create(args.name, args.description, is_public) _print_volume_type_list([vtype]) @@ -780,6 +793,45 @@ def do_type_key(cs, args): vtype.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('volumev2') +def do_type_access_list(cs, args): + """Print access information about the given volume type.""" + volume_type = _find_volume_type(cs, args.volume_type) + if volume_type.is_public: + raise exceptions.CommandError("Failed to get access list " + "for public volume type.") + access_list = cs.volume_type_access.list(volume_type) + + columns = ['Volume_type_ID', 'Project_ID'] + utils.print_list(access_list, columns) + + +@utils.arg('--volume-type', metavar='<volume_type>', required=True, + help="Volume type name or ID to add access for the given project.") +@utils.arg('--project-id', metavar='<project_id>', required=True, + help='Project ID to add volume type access for.') +@utils.service_type('volumev2') +def do_type_access_add(cs, args): + """Adds volume type access for the given project.""" + vtype = _find_volume_type(cs, args.volume_type) + cs.volume_type_access.add_project_access(vtype, args.project_id) + + +@utils.arg('--volume-type', metavar='<volume_type>', required=True, + help=('Volume type name or ID to remove access ' + 'for the given project.')) +@utils.arg('--project-id', metavar='<project_id>', required=True, + help='Project ID to remove volume type access for.') +@utils.service_type('volumev2') +def do_type_access_remove(cs, args): + """Removes volume type access for the given project.""" + vtype = _find_volume_type(cs, args.volume_type) + cs.volume_type_access.remove_project_access( + vtype, args.project_id) + + @utils.service_type('volumev2') def do_endpoints(cs, args): """Discovers endpoints registered by authentication service.""" diff --git a/cinderclient/v2/volume_type_access.py b/cinderclient/v2/volume_type_access.py new file mode 100644 index 000000000..d604041a5 --- /dev/null +++ b/cinderclient/v2/volume_type_access.py @@ -0,0 +1,51 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. + +"""Volume type access interface.""" + +from cinderclient import base + + +class VolumeTypeAccess(base.Resource): + def __repr__(self): + return "<VolumeTypeAccess: %s>" % self.project_id + + +class VolumeTypeAccessManager(base.ManagerWithFind): + """ + Manage :class:`VolumeTypeAccess` resources. + """ + resource_class = VolumeTypeAccess + + def list(self, volume_type): + return self._list( + '/types/%s/os-volume-type-access' % base.getid(volume_type), + 'volume_type_access') + + def add_project_access(self, volume_type, project): + """Add a project to the given volume type access list.""" + info = {'project': project} + self._action('addProjectAccess', volume_type, info) + + def remove_project_access(self, volume_type, project): + """Remove a project from the given volume type access list.""" + info = {'project': project} + self._action('removeProjectAccess', volume_type, info) + + def _action(self, action, volume_type, info, **kwargs): + """Perform a volume type action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/types/%s/action' % base.getid(volume_type) + return self.api.client.post(url, body=body) diff --git a/cinderclient/v2/volume_types.py b/cinderclient/v2/volume_types.py index 6b167d0fb..34eb7ebe2 100644 --- a/cinderclient/v2/volume_types.py +++ b/cinderclient/v2/volume_types.py @@ -24,6 +24,13 @@ class VolumeType(base.Resource): def __repr__(self): return "<VolumeType: %s>" % self.name + @property + def is_public(self): + """ + Provide a user-friendly accessor to os-volume-type-access:is_public + """ + return self._info.get("os-volume-type-access:is_public", 'N/A') + def get_keys(self): """Get extra specs from a volume type. @@ -70,12 +77,15 @@ class VolumeTypeManager(base.ManagerWithFind): """Manage :class:`VolumeType` resources.""" resource_class = VolumeType - def list(self, search_opts=None): + def list(self, search_opts=None, is_public=True): """Lists all volume types. :rtype: list of :class:`VolumeType`. """ - return self._list("/types", "volume_types") + query_string = '' + if not is_public: + query_string = '?is_public=%s' % is_public + return self._list("/types%s" % (query_string), "volume_types") def get(self, volume_type): """Get a specific volume type. @@ -99,18 +109,20 @@ class VolumeTypeManager(base.ManagerWithFind): """ self._delete("/types/%s" % base.getid(volume_type)) - def create(self, name, description=None): + def create(self, name, description=None, is_public=True): """Creates a volume type. :param name: Descriptive name of the volume type :param description: Description of the the volume type + :param is_public: Volume type visibility :rtype: :class:`VolumeType` """ body = { "volume_type": { "name": name, - "description": description + "description": description, + "os-volume-type-access:is_public": is_public, } }