Basic volume_type access

Change-Id: I86405e7272c4101f6f03841ff65deec057fbed0b
This commit is contained in:
Roberto Polli 2016-12-23 13:03:07 +01:00
parent 197ca1b8c7
commit 0f706a7ded
8 changed files with 449 additions and 0 deletions

View File

@ -274,3 +274,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())

View File

@ -0,0 +1,4 @@
---
features:
- Add support for listing volume types.
- Add support for managing volume type access.

View File

@ -578,6 +578,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

View File

@ -296,6 +296,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)

View File

@ -1457,6 +1457,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)
@ -1623,6 +1628,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.
@ -2326,6 +2342,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.

View File

@ -1960,6 +1960,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

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

View 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'])