Merge "Basic volume_type access"
This commit is contained in:
commit
e4dbb42695
@ -311,3 +311,35 @@ A volume from cinder.
|
||||
is_encrypted=bool(),
|
||||
can_multiattach=bool(),
|
||||
properties=dict())
|
||||
|
||||
|
||||
VolumeType
|
||||
------
|
||||
|
||||
A volume type from cinder.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
VolumeType = dict(
|
||||
location=Location(),
|
||||
id=str(),
|
||||
name=str(),
|
||||
description=str() or None,
|
||||
is_public=bool(),
|
||||
qos_specs_id=str() or None,
|
||||
extra_specs=dict(),
|
||||
properties=dict())
|
||||
|
||||
|
||||
VolumeTypeAccess
|
||||
------
|
||||
|
||||
A volume type access from cinder.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
VolumeTypeAccess = dict(
|
||||
location=Location(),
|
||||
volume_type_id=str(),
|
||||
project_id=str(),
|
||||
properties=dict())
|
||||
|
4
releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml
Normal file
4
releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Add support for listing volume types.
|
||||
- Add support for managing volume type access.
|
@ -614,6 +614,56 @@ class Normalizer(object):
|
||||
|
||||
return ret
|
||||
|
||||
def _normalize_volume_type_access(self, volume_type_access):
|
||||
|
||||
volume_type_access = volume_type_access.copy()
|
||||
|
||||
volume_type_id = volume_type_access.pop('volume_type_id')
|
||||
project_id = volume_type_access.pop('project_id')
|
||||
ret = munch.Munch(
|
||||
location=self.current_location,
|
||||
project_id=project_id,
|
||||
volume_type_id=volume_type_id,
|
||||
properties=volume_type_access.copy(),
|
||||
)
|
||||
return ret
|
||||
|
||||
def _normalize_volume_type_accesses(self, volume_type_accesses):
|
||||
ret = []
|
||||
for volume_type_access in volume_type_accesses:
|
||||
ret.append(self._normalize_volume_type_access(volume_type_access))
|
||||
return ret
|
||||
|
||||
def _normalize_volume_type(self, volume_type):
|
||||
|
||||
volume_type = volume_type.copy()
|
||||
|
||||
volume_id = volume_type.pop('id')
|
||||
description = volume_type.pop('description', None)
|
||||
name = volume_type.pop('name', None)
|
||||
old_is_public = volume_type.pop('os-volume-type-access:is_public',
|
||||
False)
|
||||
is_public = volume_type.pop('is_public', old_is_public)
|
||||
qos_specs_id = volume_type.pop('qos_specs_id', None)
|
||||
extra_specs = volume_type.pop('extra_specs', {})
|
||||
ret = munch.Munch(
|
||||
location=self.current_location,
|
||||
is_public=is_public,
|
||||
id=volume_id,
|
||||
name=name,
|
||||
description=description,
|
||||
qos_specs_id=qos_specs_id,
|
||||
extra_specs=extra_specs,
|
||||
properties=volume_type.copy(),
|
||||
)
|
||||
return ret
|
||||
|
||||
def _normalize_volume_types(self, volume_types):
|
||||
ret = []
|
||||
for volume in volume_types:
|
||||
ret.append(self._normalize_volume_type(volume))
|
||||
return ret
|
||||
|
||||
def _normalize_volumes(self, volumes):
|
||||
"""Normalize the structure of volumes
|
||||
|
||||
|
@ -301,6 +301,28 @@ class ImageSnapshotCreate(task_manager.Task):
|
||||
return client.nova_client.servers.create_image(**self.args)
|
||||
|
||||
|
||||
class VolumeTypeList(task_manager.Task):
|
||||
def main(self, client):
|
||||
return client.cinder_client.volume_types.list()
|
||||
|
||||
|
||||
class VolumeTypeAccessList(task_manager.Task):
|
||||
def main(self, client):
|
||||
return client.cinder_client.volume_type_access.list(**self.args)
|
||||
|
||||
|
||||
class VolumeTypeAccessAdd(task_manager.Task):
|
||||
def main(self, client):
|
||||
return client.cinder_client.volume_type_access.add_project_access(
|
||||
**self.args)
|
||||
|
||||
|
||||
class VolumeTypeAccessRemove(task_manager.Task):
|
||||
def main(self, client):
|
||||
return client.cinder_client.volume_type_access.remove_project_access(
|
||||
**self.args)
|
||||
|
||||
|
||||
class VolumeCreate(task_manager.Task):
|
||||
def main(self, client):
|
||||
return client.cinder_client.volumes.create(**self.args)
|
||||
|
@ -1471,6 +1471,11 @@ class OpenStackCloud(_normalize.Normalizer):
|
||||
return _utils._filter_list(
|
||||
volume_backups, name_or_id, filters)
|
||||
|
||||
def search_volume_types(
|
||||
self, name_or_id=None, filters=None, get_extra=True):
|
||||
volume_types = self.list_volume_types(get_extra=get_extra)
|
||||
return _utils._filter_list(volume_types, name_or_id, filters)
|
||||
|
||||
def search_flavors(self, name_or_id=None, filters=None, get_extra=True):
|
||||
flavors = self.list_flavors(get_extra=get_extra)
|
||||
return _utils._filter_list(flavors, name_or_id, filters)
|
||||
@ -1637,6 +1642,17 @@ class OpenStackCloud(_normalize.Normalizer):
|
||||
return self._normalize_volumes(
|
||||
self.manager.submit_task(_tasks.VolumeList()))
|
||||
|
||||
@_utils.cache_on_arguments()
|
||||
def list_volume_types(self, get_extra=True):
|
||||
"""List all available volume types.
|
||||
|
||||
:returns: A list of volume ``munch.Munch``.
|
||||
|
||||
"""
|
||||
with _utils.shade_exceptions("Error fetching volume_type list"):
|
||||
return self._normalize_volume_types(
|
||||
self.manager.submit_task(_tasks.VolumeTypeList()))
|
||||
|
||||
@_utils.cache_on_arguments()
|
||||
def list_flavors(self, get_extra=True):
|
||||
"""List all available flavors.
|
||||
@ -2379,6 +2395,31 @@ class OpenStackCloud(_normalize.Normalizer):
|
||||
"""
|
||||
return _utils._get_entity(self.search_volumes, name_or_id, filters)
|
||||
|
||||
def get_volume_type(self, name_or_id, filters=None):
|
||||
"""Get a volume type by name or ID.
|
||||
|
||||
:param name_or_id: Name or ID of the volume.
|
||||
:param filters:
|
||||
A dictionary of meta data to use for further filtering. Elements
|
||||
of this dictionary may, themselves, be dictionaries. Example::
|
||||
|
||||
{
|
||||
'last_name': 'Smith',
|
||||
'other': {
|
||||
'gender': 'Female'
|
||||
}
|
||||
}
|
||||
OR
|
||||
A string containing a jmespath expression for further filtering.
|
||||
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
|
||||
|
||||
:returns: A volume ``munch.Munch`` or None if no matching volume is
|
||||
found.
|
||||
|
||||
"""
|
||||
return _utils._get_entity(
|
||||
self.search_volume_types, name_or_id, filters)
|
||||
|
||||
def get_flavor(self, name_or_id, filters=None, get_extra=True):
|
||||
"""Get a flavor by name or ID.
|
||||
|
||||
|
@ -1977,6 +1977,70 @@ class OperatorCloud(openstackcloud.OpenStackCloud):
|
||||
return self.manager.submit_task(_tasks.AggregateRemoveHost(
|
||||
aggregate=aggregate['id'], host=host_name))
|
||||
|
||||
def get_volume_type_access(self, name_or_id):
|
||||
"""Return a list of volume_type_access.
|
||||
|
||||
:param name_or_id: Name or ID of the volume type.
|
||||
|
||||
:raises: OpenStackCloudException on operation error.
|
||||
"""
|
||||
volume_type = self.get_volume_type(name_or_id)
|
||||
if not volume_type:
|
||||
raise OpenStackCloudException(
|
||||
"VolumeType not found: %s" % name_or_id)
|
||||
|
||||
with _utils.shade_exceptions(
|
||||
"Unable to get volume type access {name}".format(
|
||||
name=name_or_id)):
|
||||
return self._normalize_volume_type_accesses(
|
||||
self.manager.submit_task(
|
||||
_tasks.VolumeTypeAccessList(volume_type=volume_type))
|
||||
)
|
||||
|
||||
def add_volume_type_access(self, name_or_id, project_id):
|
||||
"""Grant access on a volume_type to a project.
|
||||
|
||||
:param name_or_id: ID or name of a volume_type
|
||||
:param project_id: A project id
|
||||
|
||||
NOTE: the call works even if the project does not exist.
|
||||
|
||||
:raises: OpenStackCloudException on operation error.
|
||||
"""
|
||||
volume_type = self.get_volume_type(name_or_id)
|
||||
if not volume_type:
|
||||
raise OpenStackCloudException(
|
||||
"VolumeType not found: %s" % name_or_id)
|
||||
with _utils.shade_exceptions(
|
||||
"Unable to authorize {project} "
|
||||
"to use volume type {name}".format(
|
||||
name=name_or_id, project=project_id
|
||||
)):
|
||||
self.manager.submit_task(
|
||||
_tasks.VolumeTypeAccessAdd(
|
||||
volume_type=volume_type, project=project_id))
|
||||
|
||||
def remove_volume_type_access(self, name_or_id, project_id):
|
||||
"""Revoke access on a volume_type to a project.
|
||||
|
||||
:param name_or_id: ID or name of a volume_type
|
||||
:param project_id: A project id
|
||||
|
||||
:raises: OpenStackCloudException on operation error.
|
||||
"""
|
||||
volume_type = self.get_volume_type(name_or_id)
|
||||
if not volume_type:
|
||||
raise OpenStackCloudException(
|
||||
"VolumeType not found: %s" % name_or_id)
|
||||
with _utils.shade_exceptions(
|
||||
"Unable to revoke {project} "
|
||||
"to use volume type {name}".format(
|
||||
name=name_or_id, project=project_id
|
||||
)):
|
||||
self.manager.submit_task(
|
||||
_tasks.VolumeTypeAccessRemove(
|
||||
volume_type=volume_type, project=project_id))
|
||||
|
||||
def set_compute_quotas(self, name_or_id, **kwargs):
|
||||
""" Set a quota in a project
|
||||
|
||||
|
114
shade/tests/functional/test_volume_type.py
Normal file
114
shade/tests/functional/test_volume_type.py
Normal file
@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
|
||||
"""
|
||||
test_volume
|
||||
----------------------------------
|
||||
|
||||
Functional tests for `shade` block storage methods.
|
||||
"""
|
||||
import testtools
|
||||
from shade.exc import OpenStackCloudException
|
||||
from shade.tests.functional import base
|
||||
|
||||
|
||||
class TestVolumeType(base.BaseFunctionalTestCase):
|
||||
|
||||
def _assert_project(self, volume_name_or_id, project_id, allowed=True):
|
||||
acls = self.operator_cloud.get_volume_type_access(volume_name_or_id)
|
||||
allowed_projects = [x.get('project_id') for x in acls]
|
||||
self.assertEqual(allowed, project_id in allowed_projects)
|
||||
|
||||
def setUp(self):
|
||||
super(TestVolumeType, self).setUp()
|
||||
if not self.demo_cloud.has_service('volume'):
|
||||
self.skipTest('volume service not supported by cloud')
|
||||
self.operator_cloud.cinder_client.volume_types.create(
|
||||
'test-volume-type', is_public=False)
|
||||
|
||||
def tearDown(self):
|
||||
ret = self.operator_cloud.get_volume_type('test-volume-type')
|
||||
if ret.get('id'):
|
||||
self.operator_cloud.cinder_client.volume_types.delete(ret.id)
|
||||
super(TestVolumeType, self).tearDown()
|
||||
|
||||
def test_list_volume_types(self):
|
||||
volume_types = self.operator_cloud.list_volume_types()
|
||||
self.assertTrue(volume_types)
|
||||
self.assertTrue(any(
|
||||
x for x in volume_types if x.name == 'test-volume-type'))
|
||||
|
||||
def test_add_remove_volume_type_access(self):
|
||||
volume_type = self.operator_cloud.get_volume_type('test-volume-type')
|
||||
self.assertEqual('test-volume-type', volume_type.name)
|
||||
|
||||
self.operator_cloud.add_volume_type_access(
|
||||
'test-volume-type',
|
||||
self.operator_cloud.current_project_id)
|
||||
self._assert_project(
|
||||
'test-volume-type', self.operator_cloud.current_project_id,
|
||||
allowed=True)
|
||||
|
||||
self.operator_cloud.remove_volume_type_access(
|
||||
'test-volume-type',
|
||||
self.operator_cloud.current_project_id)
|
||||
self._assert_project(
|
||||
'test-volume-type', self.operator_cloud.current_project_id,
|
||||
allowed=False)
|
||||
|
||||
def test_add_volume_type_access_missing_project(self):
|
||||
# Project id is not valitaded and it may not exist.
|
||||
self.operator_cloud.add_volume_type_access(
|
||||
'test-volume-type',
|
||||
'00000000000000000000000000000000')
|
||||
|
||||
self.operator_cloud.remove_volume_type_access(
|
||||
'test-volume-type',
|
||||
'00000000000000000000000000000000')
|
||||
|
||||
def test_add_volume_type_access_missing_volume(self):
|
||||
with testtools.ExpectedException(
|
||||
OpenStackCloudException,
|
||||
"VolumeType not found.*"
|
||||
):
|
||||
self.operator_cloud.add_volume_type_access(
|
||||
'MISSING_VOLUME_TYPE',
|
||||
self.operator_cloud.current_project_id)
|
||||
|
||||
def test_remove_volume_type_access_missing_volume(self):
|
||||
with testtools.ExpectedException(
|
||||
OpenStackCloudException,
|
||||
"VolumeType not found.*"
|
||||
):
|
||||
self.operator_cloud.remove_volume_type_access(
|
||||
'MISSING_VOLUME_TYPE',
|
||||
self.operator_cloud.current_project_id)
|
||||
|
||||
def test_add_volume_type_access_bad_project(self):
|
||||
with testtools.ExpectedException(
|
||||
OpenStackCloudException,
|
||||
"Unable to authorize.*"
|
||||
):
|
||||
self.operator_cloud.add_volume_type_access(
|
||||
'test-volume-type',
|
||||
'BAD_PROJECT_ID')
|
||||
|
||||
def test_remove_volume_type_access_missing_project(self):
|
||||
with testtools.ExpectedException(
|
||||
OpenStackCloudException,
|
||||
"Unable to revoke.*"
|
||||
):
|
||||
self.operator_cloud.remove_volume_type_access(
|
||||
'test-volume-type',
|
||||
'00000000000000000000000000000000')
|
122
shade/tests/unit/test_volume_access.py
Normal file
122
shade/tests/unit/test_volume_access.py
Normal file
@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
import shade
|
||||
from shade.tests.unit import base
|
||||
|
||||
|
||||
class TestVolumeAccess(base.TestCase):
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_list_volume_types(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description',
|
||||
name='name', is_public=False)
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
|
||||
self.assertTrue(self.cloud.list_volume_types())
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_get_volume_type(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description', name='name',
|
||||
is_public=False)
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
|
||||
volume_type_got = self.cloud.get_volume_type('name')
|
||||
self.assertEqual(volume_type_got.id, volume_type['id'])
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_get_volume_type_access(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description', name='name',
|
||||
is_public=False)
|
||||
volume_type_access = [
|
||||
dict(volume_type_id='voltype01', name='name', project_id='prj01'),
|
||||
dict(volume_type_id='voltype01', name='name', project_id='prj02')
|
||||
]
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
mock_cinder.volume_type_access.list.return_value = volume_type_access
|
||||
|
||||
self.assertEqual(
|
||||
len(self.op_cloud.get_volume_type_access('name')), 2)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_remove_volume_type_access(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description', name='name',
|
||||
is_public=False)
|
||||
project_001 = dict(volume_type_id='voltype01', name='name',
|
||||
project_id='prj01')
|
||||
project_002 = dict(volume_type_id='voltype01', name='name',
|
||||
project_id='prj02')
|
||||
volume_type_access = [project_001, project_002]
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
mock_cinder.volume_type_access.list.return_value = volume_type_access
|
||||
|
||||
def _fake_remove(*args, **kwargs):
|
||||
volume_type_access.pop()
|
||||
|
||||
mock_cinder.volume_type_access.remove_project_access.side_effect = \
|
||||
_fake_remove
|
||||
|
||||
self.assertEqual(
|
||||
len(self.op_cloud.get_volume_type_access(
|
||||
volume_type['name'])), 2)
|
||||
self.op_cloud.remove_volume_type_access(
|
||||
volume_type['name'], project_001['project_id'])
|
||||
|
||||
self.assertEqual(
|
||||
len(self.op_cloud.get_volume_type_access('name')), 1)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_add_volume_type_access(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description', name='name',
|
||||
is_public=False)
|
||||
project_001 = dict(volume_type_id='voltype01', name='name',
|
||||
project_id='prj01')
|
||||
project_002 = dict(volume_type_id='voltype01', name='name',
|
||||
project_id='prj02')
|
||||
volume_type_access = [project_001]
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
mock_cinder.volume_type_access.list.return_value = volume_type_access
|
||||
mock_cinder.volume_type_access.add_project_access.return_value = None
|
||||
|
||||
def _fake_add(*args, **kwargs):
|
||||
volume_type_access.append(project_002)
|
||||
|
||||
mock_cinder.volume_type_access.add_project_access.side_effect = \
|
||||
_fake_add
|
||||
|
||||
self.op_cloud.add_volume_type_access(
|
||||
volume_type['name'], project_002['project_id'])
|
||||
self.assertEqual(
|
||||
len(self.op_cloud.get_volume_type_access('name')), 2)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
|
||||
def test_add_volume_type_access_missing(self, mock_cinder):
|
||||
volume_type = dict(
|
||||
id='voltype01', description='volume type description', name='name',
|
||||
is_public=False)
|
||||
project_001 = dict(volume_type_id='voltype01', name='name',
|
||||
project_id='prj01')
|
||||
mock_cinder.volume_types.list.return_value = [volume_type]
|
||||
with testtools.ExpectedException(shade.OpenStackCloudException,
|
||||
"VolumeType not found: MISSING"):
|
||||
self.op_cloud.add_volume_type_access(
|
||||
"MISSING", project_001['project_id'])
|
Loading…
Reference in New Issue
Block a user