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:
Mathieu Gagné 2014-07-26 17:41:26 -04:00
parent bc2b8bf1be
commit 6f8c235a92
8 changed files with 279 additions and 9 deletions

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

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

@ -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,
}
}