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
This commit is contained in:
parent
bc2b8bf1be
commit
6f8c235a92
@ -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
|
||||
#
|
||||
|
@ -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.
|
||||
|
42
cinderclient/tests/v2/test_type_access.py
Normal file
42
cinderclient/tests/v2/test_type_access.py
Normal file
@ -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}})
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
51
cinderclient/v2/volume_type_access.py
Normal file
51
cinderclient/v2/volume_type_access.py
Normal file
@ -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)
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user