block storage: Add support for the Group resource
Introduce the Group resource, fill in its resources, and implement API calls to support the Cinder v3 API. Change-Id: Ied48b46eb76dfe6cbafa3f08ac8f5bfe78af4058
This commit is contained in:
parent
9e9fc98795
commit
71a8466f0f
@ -63,6 +63,14 @@ Capabilities Operations
|
||||
:noindex:
|
||||
:members: get_capabilities
|
||||
|
||||
Group Operations
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: openstack.block_storage.v3._proxy.Proxy
|
||||
:noindex:
|
||||
:members: create_group, create_group_from_source, delete_group, update_group,
|
||||
get_group, find_group, groups, reset_group_state
|
||||
|
||||
Group Type Operations
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -15,6 +15,7 @@ from openstack.block_storage.v3 import availability_zone
|
||||
from openstack.block_storage.v3 import backup as _backup
|
||||
from openstack.block_storage.v3 import capabilities as _capabilities
|
||||
from openstack.block_storage.v3 import extension as _extension
|
||||
from openstack.block_storage.v3 import group as _group
|
||||
from openstack.block_storage.v3 import group_type as _group_type
|
||||
from openstack.block_storage.v3 import limits as _limits
|
||||
from openstack.block_storage.v3 import quota_set as _quota_set
|
||||
@ -977,6 +978,121 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
|
||||
"""
|
||||
return self._get(_capabilities.Capabilities, host)
|
||||
|
||||
# ====== GROUPS ======
|
||||
def get_group(self, group_id, **attrs):
|
||||
"""Get a group
|
||||
|
||||
:param group_id: The ID of the group to get.
|
||||
:param dict attrs: Optional query parameters to be sent to limit the
|
||||
resources being returned.
|
||||
|
||||
:returns: A Group instance.
|
||||
:rtype: :class:`~openstack.block_storage.v3.group`
|
||||
"""
|
||||
return self._get(_group.Group, group_id, **attrs)
|
||||
|
||||
def find_group(self, name_or_id, ignore_missing=True, **attrs):
|
||||
"""Find a single group
|
||||
|
||||
:param name_or_id: The name or ID of a group.
|
||||
:param bool ignore_missing: When set to ``False``
|
||||
:class:`~openstack.exceptions.ResourceNotFound` will be raised
|
||||
when the group snapshot does not exist.
|
||||
|
||||
:returns: One :class:`~openstack.block_storage.v3.group.Group`
|
||||
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
||||
when no resource can be found.
|
||||
"""
|
||||
return self._find(
|
||||
_group.Group, name_or_id, ignore_missing=ignore_missing)
|
||||
|
||||
def groups(self, details=True, **query):
|
||||
"""Retrieve a generator of groups
|
||||
|
||||
:param bool details: When set to ``False``, no additional details will
|
||||
be returned. The default, ``True``, will cause additional details
|
||||
to be returned.
|
||||
:param dict query: Optional query parameters to be sent to limit the
|
||||
resources being returned:
|
||||
|
||||
* all_tenants: Shows details for all project.
|
||||
* sort: Comma-separated list of sort keys and optional sort
|
||||
directions.
|
||||
* limit: Returns a number of items up to the limit value.
|
||||
* offset: Used in conjunction with limit to return a slice of
|
||||
items. Specifies where to start in the list.
|
||||
* marker: The ID of the last-seen item.
|
||||
* list_volume: Show volume ids in this group.
|
||||
* detailed: If True, will list groups with details.
|
||||
* search_opts: Search options.
|
||||
|
||||
:returns: A generator of group objects.
|
||||
"""
|
||||
base_path = '/groups/detail' if details else '/groups'
|
||||
return self._list(_group.Group, base_path=base_path, **query)
|
||||
|
||||
def create_group(self, **attrs):
|
||||
"""Create a new group from attributes
|
||||
|
||||
:param dict attrs: Keyword arguments which will be used to create
|
||||
a :class:`~openstack.block_storage.v3.group.Group` comprised of
|
||||
the properties on the Group class.
|
||||
|
||||
:returns: The results of group creation.
|
||||
:rtype: :class:`~openstack.block_storage.v3.group.Group`.
|
||||
"""
|
||||
return self._create(_group.Group, **attrs)
|
||||
|
||||
def create_group_from_source(self, **attrs):
|
||||
"""Creates a new group from source
|
||||
|
||||
:param dict attrs: Keyword arguments which will be used to create
|
||||
a :class:`~openstack.block_storage.v3.group.Group` comprised of
|
||||
the properties on the Group class.
|
||||
|
||||
:returns: The results of group creation.
|
||||
:rtype: :class:`~openstack.block_storage.v3.group.Group`.
|
||||
"""
|
||||
return _group.Group.create_from_source(self, **attrs)
|
||||
|
||||
def reset_group_state(self, group, status):
|
||||
"""Reset group status
|
||||
|
||||
:param group: The :class:`~openstack.block_storage.v3.group.Group`
|
||||
to set the state.
|
||||
:param status: The status for a group.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
res = self._get_resource(_group.Group, group)
|
||||
return res.reset_status(self, status)
|
||||
|
||||
def delete_group(self, group, delete_volumes=False):
|
||||
"""Delete a group
|
||||
|
||||
:param group: The :class:`~openstack.block_storage.v3.group.Group` to
|
||||
delete.
|
||||
:param bool delete_volumes: When set to ``True``, volumes in group
|
||||
will be deleted.
|
||||
|
||||
:returns: ``None``.
|
||||
"""
|
||||
res = self._get_resource(_group.Group, group)
|
||||
res.delete(self, delete_volumes=delete_volumes)
|
||||
|
||||
def update_group(self, group, **attrs):
|
||||
"""Update a group
|
||||
|
||||
:param group: The value can be the ID of a group or a
|
||||
:class:`~openstack.block_storage.v3.group.Group` instance.
|
||||
:param dict attrs: The attributes to update on the group.
|
||||
|
||||
:returns: The updated group
|
||||
:rtype: :class:`~openstack.volume.v3.group.Group`
|
||||
"""
|
||||
return self._update(_group.Group, group, **attrs)
|
||||
|
||||
# ====== AVAILABILITY ZONES ======
|
||||
def availability_zones(self):
|
||||
"""Return a generator of availability zones
|
||||
|
||||
@ -987,6 +1103,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
|
||||
|
||||
return self._list(availability_zone.AvailabilityZone)
|
||||
|
||||
# ====== GROUP TYPE ======
|
||||
def get_group_type(self, group_type):
|
||||
"""Get a specific group type
|
||||
|
||||
|
89
openstack/block_storage/v3/group.py
Normal file
89
openstack/block_storage/v3/group.py
Normal file
@ -0,0 +1,89 @@
|
||||
# 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 openstack import exceptions
|
||||
from openstack import resource
|
||||
from openstack import utils
|
||||
|
||||
|
||||
class Group(resource.Resource):
|
||||
resource_key = "group"
|
||||
resources_key = "groups"
|
||||
base_path = "/groups"
|
||||
|
||||
# capabilities
|
||||
allow_fetch = True
|
||||
allow_create = True
|
||||
allow_delete = True
|
||||
allow_commit = True
|
||||
allow_list = True
|
||||
|
||||
availability_zone = resource.Body("availability_zone")
|
||||
created_at = resource.Body("created_at")
|
||||
description = resource.Body("description")
|
||||
group_snapshot_id = resource.Body("group_snapshot_id")
|
||||
group_type = resource.Body("group_type")
|
||||
project_id = resource.Body("project_id")
|
||||
replication_status = resource.Body("replication_status")
|
||||
source_group_id = resource.Body("source_group_id")
|
||||
status = resource.Body("status")
|
||||
volumes = resource.Body("volumes", type=list)
|
||||
volume_types = resource.Body("volume_types", type=list)
|
||||
|
||||
_max_microversion = "3.38"
|
||||
|
||||
def _action(self, session, body):
|
||||
"""Preform group actions given the message body."""
|
||||
session = self._get_session(session)
|
||||
microversion = self._get_microversion_for(session, 'create')
|
||||
url = utils.urljoin(self.base_path, self.id, 'action')
|
||||
response = session.post(url, json=body, microversion=microversion)
|
||||
exceptions.raise_from_response(response)
|
||||
return response
|
||||
|
||||
def delete(self, session, *, delete_volumes=False):
|
||||
"""Delete a group."""
|
||||
body = {'delete': {'delete-volumes': delete_volumes}}
|
||||
self._action(session, body)
|
||||
|
||||
def reset(self, session, status):
|
||||
"""Resets the status for a group."""
|
||||
body = {'reset_status': {'status': status}}
|
||||
self._action(session, body)
|
||||
|
||||
@classmethod
|
||||
def create_from_source(
|
||||
cls,
|
||||
session,
|
||||
group_snapshot_id,
|
||||
source_group_id,
|
||||
name=None,
|
||||
description=None,
|
||||
):
|
||||
"""Creates a new group from source."""
|
||||
session = cls._get_session(session)
|
||||
microversion = cls._get_microversion_for(cls, session, 'create')
|
||||
url = utils.urljoin(cls.base_path, 'action')
|
||||
body = {
|
||||
'create-from-src': {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'group_snapshot_id': group_snapshot_id,
|
||||
'source_group_id': source_group_id,
|
||||
}
|
||||
}
|
||||
response = session.post(url, json=body, microversion=microversion)
|
||||
exceptions.raise_from_response(response)
|
||||
|
||||
group = Group()
|
||||
group._translate_response(response=response)
|
||||
return group
|
@ -49,6 +49,8 @@ class Volume(resource.Resource, metadata.MetadataMixin):
|
||||
#: Extended replication status on this volume.
|
||||
extended_replication_status = resource.Body(
|
||||
"os-volume-replication:extended_status")
|
||||
#: The ID of the group that the volume belongs to.
|
||||
group_id = resource.Body("group_id")
|
||||
#: The volume's current back-end.
|
||||
host = resource.Body("os-vol-host-attr:host")
|
||||
#: The ID of the image from which you want to create the volume.
|
||||
|
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from openstack.block_storage.v3 import group as _group
|
||||
from openstack.block_storage.v3 import group_type as _group_type
|
||||
from openstack.tests.functional.block_storage.v3 import base
|
||||
|
||||
@ -22,20 +23,41 @@ class TestGroup(base.BaseBlockStorageTest):
|
||||
if not self.user_cloud.has_service('block-storage'):
|
||||
self.skipTest('block-storage service not supported by cloud')
|
||||
|
||||
# there will always be at least one volume type, i.e. the default one
|
||||
volume_types = list(self.conn.block_storage.types())
|
||||
self.volume_type = volume_types[0]
|
||||
|
||||
group_type_name = self.getUniqueString()
|
||||
self.group_type = self.conn.block_storage.create_group_type(
|
||||
name=group_type_name,
|
||||
)
|
||||
self.addCleanup(
|
||||
self.conn.block_storage.delete_group_type,
|
||||
self.group_type,
|
||||
)
|
||||
self.assertIsInstance(self.group_type, _group_type.GroupType)
|
||||
self.assertEqual(group_type_name, self.group_type.name)
|
||||
|
||||
group_name = self.getUniqueString()
|
||||
self.group = self.conn.block_storage.create_group(
|
||||
name=group_name,
|
||||
group_type=self.group_type.id,
|
||||
volume_types=[self.volume_type.id],
|
||||
)
|
||||
self.assertIsInstance(self.group, _group.Group)
|
||||
self.assertEqual(group_name, self.group.name)
|
||||
|
||||
def tearDown(self):
|
||||
# we do this in tearDown rather than via 'addCleanup' since we need to
|
||||
# wait for the deletion of the group before moving onto the deletion of
|
||||
# the group type
|
||||
self.conn.block_storage.delete_group(self.group, delete_volumes=True)
|
||||
self.conn.block_storage.wait_for_delete(self.group)
|
||||
|
||||
self.conn.block_storage.delete_group_type(self.group_type)
|
||||
self.conn.block_storage.wait_for_delete(self.group_type)
|
||||
|
||||
super().tearDown()
|
||||
|
||||
def test_group_type(self):
|
||||
# get
|
||||
group_type = self.conn.block_storage.get_group_type(self.group_type)
|
||||
group_type = self.conn.block_storage.get_group_type(self.group_type.id)
|
||||
self.assertEqual(self.group_type.name, group_type.name)
|
||||
|
||||
# find
|
||||
@ -101,3 +123,31 @@ class TestGroup(base.BaseBlockStorageTest):
|
||||
)
|
||||
group_type = self.conn.block_storage.get_group_type(self.group_type.id)
|
||||
self.assertEqual({'acme': 'buzz'}, group_type.group_specs)
|
||||
|
||||
def test_group(self):
|
||||
# get
|
||||
group = self.conn.block_storage.get_group(self.group.id)
|
||||
self.assertEqual(self.group.name, group.name)
|
||||
|
||||
# find
|
||||
group = self.conn.block_storage.find_group(self.group.name)
|
||||
self.assertEqual(self.group.id, group.id)
|
||||
|
||||
# list
|
||||
groups = self.conn.block_storage.groups()
|
||||
# other tests may have created groups and there can be defaults so we
|
||||
# don't assert that this is the *only* group present
|
||||
self.assertIn(self.group.id, {g.id for g in groups})
|
||||
|
||||
# update
|
||||
group_name = self.getUniqueString()
|
||||
group_description = self.getUniqueString()
|
||||
group = self.conn.block_storage.update_group(
|
||||
self.group,
|
||||
name=group_name,
|
||||
description=group_description,
|
||||
)
|
||||
self.assertIsInstance(group, _group.Group)
|
||||
group = self.conn.block_storage.get_group(self.group.id)
|
||||
self.assertEqual(group_name, group.name)
|
||||
self.assertEqual(group_description, group.description)
|
||||
|
135
openstack/tests/unit/block_storage/v3/test_group.py
Normal file
135
openstack/tests/unit/block_storage/v3/test_group.py
Normal file
@ -0,0 +1,135 @@
|
||||
# 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 copy
|
||||
from unittest import mock
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
|
||||
from openstack.block_storage.v3 import group
|
||||
from openstack.tests.unit import base
|
||||
|
||||
GROUP_ID = "6f519a48-3183-46cf-a32f-41815f813986"
|
||||
|
||||
GROUP = {
|
||||
"id": GROUP_ID,
|
||||
"status": "available",
|
||||
"availability_zone": "az1",
|
||||
"created_at": "2015-09-16T09:28:52.000000",
|
||||
"name": "first_group",
|
||||
"description": "my first group",
|
||||
"group_type": "29514915-5208-46ab-9ece-1cc4688ad0c1",
|
||||
"volume_types": ["c4daaf47-c530-4901-b28e-f5f0a359c4e6"],
|
||||
"volumes": ["a2cdf1ad-5497-4e57-bd7d-f573768f3d03"],
|
||||
"group_snapshot_id": None,
|
||||
"source_group_id": None,
|
||||
"project_id": "7ccf4863071f44aeb8f141f65780c51b"
|
||||
}
|
||||
|
||||
|
||||
class TestGroup(base.TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
resource = group.Group()
|
||||
self.assertEqual("group", resource.resource_key)
|
||||
self.assertEqual("groups", resource.resources_key)
|
||||
self.assertEqual("/groups", resource.base_path)
|
||||
self.assertTrue(resource.allow_create)
|
||||
self.assertTrue(resource.allow_fetch)
|
||||
self.assertTrue(resource.allow_delete)
|
||||
self.assertTrue(resource.allow_commit)
|
||||
self.assertTrue(resource.allow_list)
|
||||
|
||||
def test_make_resource(self):
|
||||
resource = group.Group(**GROUP)
|
||||
self.assertEqual(GROUP["id"], resource.id)
|
||||
self.assertEqual(GROUP["status"], resource.status)
|
||||
self.assertEqual(
|
||||
GROUP["availability_zone"], resource.availability_zone)
|
||||
self.assertEqual(GROUP["created_at"], resource.created_at)
|
||||
self.assertEqual(GROUP["name"], resource.name)
|
||||
self.assertEqual(GROUP["description"], resource.description)
|
||||
self.assertEqual(GROUP["group_type"], resource.group_type)
|
||||
self.assertEqual(GROUP["volume_types"], resource.volume_types)
|
||||
self.assertEqual(GROUP["volumes"], resource.volumes)
|
||||
self.assertEqual(
|
||||
GROUP["group_snapshot_id"], resource.group_snapshot_id)
|
||||
self.assertEqual(GROUP["source_group_id"], resource.source_group_id)
|
||||
self.assertEqual(GROUP["project_id"], resource.project_id)
|
||||
|
||||
|
||||
class TestGroupAction(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.resp = mock.Mock()
|
||||
self.resp.body = None
|
||||
self.resp.json = mock.Mock(return_value=self.resp.body)
|
||||
self.resp.headers = {}
|
||||
self.resp.status_code = 202
|
||||
|
||||
self.sess = mock.Mock(spec=adapter.Adapter)
|
||||
self.sess.get = mock.Mock()
|
||||
self.sess.post = mock.Mock(return_value=self.resp)
|
||||
self.sess.default_microversion = '3.38'
|
||||
|
||||
def test_delete(self):
|
||||
sot = group.Group(**GROUP)
|
||||
|
||||
self.assertIsNone(sot.delete(self.sess))
|
||||
|
||||
url = 'groups/%s/action' % GROUP_ID
|
||||
body = {'delete': {'delete-volumes': False}}
|
||||
self.sess.post.assert_called_with(
|
||||
url, json=body, microversion=sot._max_microversion)
|
||||
|
||||
def test_reset(self):
|
||||
sot = group.Group(**GROUP)
|
||||
|
||||
self.assertIsNone(sot.reset(self.sess, 'new_status'))
|
||||
|
||||
url = 'groups/%s/action' % GROUP_ID
|
||||
body = {'reset_status': {'status': 'new_status'}}
|
||||
self.sess.post.assert_called_with(
|
||||
url, json=body, microversion=sot._max_microversion,
|
||||
)
|
||||
|
||||
def test_create_from_source(self):
|
||||
resp = mock.Mock()
|
||||
resp.body = {'group': copy.deepcopy(GROUP)}
|
||||
resp.json = mock.Mock(return_value=resp.body)
|
||||
resp.headers = {}
|
||||
resp.status_code = 202
|
||||
|
||||
self.sess.post = mock.Mock(return_value=resp)
|
||||
|
||||
sot = group.Group.create_from_source(
|
||||
self.sess,
|
||||
group_snapshot_id='9a591346-e595-4bc1-94e7-08f264406b63',
|
||||
source_group_id='6c5259f6-42ed-4e41-8ffe-e1c667ae9dff',
|
||||
name='group_from_source',
|
||||
description='a group from source',
|
||||
)
|
||||
self.assertIsNotNone(sot)
|
||||
|
||||
url = 'groups/action'
|
||||
body = {
|
||||
'create-from-src': {
|
||||
'name': 'group_from_source',
|
||||
'description': 'a group from source',
|
||||
'group_snapshot_id': '9a591346-e595-4bc1-94e7-08f264406b63',
|
||||
'source_group_id': '6c5259f6-42ed-4e41-8ffe-e1c667ae9dff',
|
||||
},
|
||||
}
|
||||
self.sess.post.assert_called_with(
|
||||
url, json=body, microversion=sot._max_microversion,
|
||||
)
|
@ -16,6 +16,7 @@ from openstack.block_storage.v3 import _proxy
|
||||
from openstack.block_storage.v3 import backup
|
||||
from openstack.block_storage.v3 import capabilities
|
||||
from openstack.block_storage.v3 import extension
|
||||
from openstack.block_storage.v3 import group
|
||||
from openstack.block_storage.v3 import group_type
|
||||
from openstack.block_storage.v3 import limits
|
||||
from openstack.block_storage.v3 import quota_set
|
||||
@ -143,6 +144,43 @@ class TestResourceFilter(TestVolumeProxy):
|
||||
)
|
||||
|
||||
|
||||
class TestGroup(TestVolumeProxy):
|
||||
def test_group_get(self):
|
||||
self.verify_get(self.proxy.get_group, group.Group)
|
||||
|
||||
def test_group_find(self):
|
||||
self.verify_find(self.proxy.find_group, group.Group)
|
||||
|
||||
def test_groups(self):
|
||||
self.verify_list(self.proxy.groups, group.Group)
|
||||
|
||||
def test_group_create(self):
|
||||
self.verify_create(self.proxy.create_group, group.Group)
|
||||
|
||||
def test_group_create_from_source(self):
|
||||
self._verify(
|
||||
"openstack.block_storage.v3.group.Group.create_from_source",
|
||||
self.proxy.create_group_from_source,
|
||||
method_args=[],
|
||||
expected_args=[self.proxy],
|
||||
)
|
||||
|
||||
def test_group_delete(self):
|
||||
self._verify(
|
||||
"openstack.block_storage.v3.group.Group.delete",
|
||||
self.proxy.delete_group,
|
||||
method_args=['delete_volumes'],
|
||||
expected_args=[self.proxy],
|
||||
expected_kwargs={'delete_volumes': False},
|
||||
)
|
||||
|
||||
def test_group_update(self):
|
||||
self.verify_update(self.proxy.update_group, group.Group)
|
||||
|
||||
def reset_group_state(self):
|
||||
self._verify(self.proxy.reset_group_state, group.Group)
|
||||
|
||||
|
||||
class TestGroupType(TestVolumeProxy):
|
||||
def test_group_type_get(self):
|
||||
self.verify_get(self.proxy.get_group_type, group_type.GroupType)
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add support for groups to the block storage service.
|
Loading…
Reference in New Issue
Block a user