diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index d1e3ecf31..73ff27cfd 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -70,6 +70,10 @@ Group Type Operations :noindex: :members: create_group_type, delete_group_type, update_group_type, get_group_type, find_group_type, group_types, + fetch_group_type_group_specs, create_group_type_group_specs, + get_group_type_group_specs_property, + update_group_type_group_specs_property, + delete_group_type_group_specs_property Type Operations ^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c1df06ce5..5570f8188 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1082,6 +1082,66 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): return self._update( _group_type.GroupType, group_type, **attrs) + def fetch_group_type_group_specs(self, group_type): + """Lists group specs of a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + + :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType` + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.fetch_group_specs(self) + + def create_group_type_group_specs(self, group_type, group_specs): + """Create group specs for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param dict group_specs: dict of extra specs + + :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType` + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.create_group_specs(self, specs=group_specs) + + def get_group_type_group_specs_property(self, group_type, prop): + """Retrieve a group spec property for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + + :returns: String value of the requested property. + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.get_group_specs_property(self, prop) + + def update_group_type_group_specs_property(self, group_type, prop, val): + """Update a group spec property for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + :param str val: Property value. + + :returns: String value of the requested property. + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.update_group_specs_property(self, prop, val) + + def delete_group_type_group_specs_property(self, group_type, prop): + """Delete a group spec property from a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + + :returns: None + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.delete_group_specs_property(self, prop) + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): """Show quota set information for the project diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py index 89f18e4f9..4f23528d2 100644 --- a/openstack/block_storage/v3/group_type.py +++ b/openstack/block_storage/v3/group_type.py @@ -10,7 +10,9 @@ # 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 GroupType(resource.Resource): @@ -31,6 +33,90 @@ class GroupType(resource.Resource): #: The group type description. description = resource.Body("description") #: Contains the specifications for a group type. - group_specs = resource.Body("group_specs", type=dict) + group_specs = resource.Body("group_specs", type=dict, default={}) #: Whether the group type is publicly visible. is_public = resource.Body("is_public", type=bool) + + def fetch_group_specs(self, session): + """Fetch group_specs of the group type. + + These are returned by default if the user has suitable permissions + (i.e. you're an admin) but by default you also need the same + permissions to access this API. That means this function is kind of + useless. However, that is how the API was designed and it is + theoretically possible that people will have modified their policy to + allow this but not the other so we provide this anyway. + + :param session: The session to use for making this request. + :returns: An updated version of this object. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('group_specs', {}) + self._update(group_specs=specs) + return self + + def create_group_specs(self, session, specs): + """Creates group specs for the group type. + + This will override whatever specs are already present on the group + type. + + :param session: The session to use for making this request. + :param specs: A dict of group specs to set on the group type. + :returns: An updated version of this object. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') + microversion = self._get_microversion_for(session, 'create') + response = session.post( + url, json={'group_specs': specs}, microversion=microversion, + ) + exceptions.raise_from_response(response) + specs = response.json().get('group_specs', {}) + self._update(group_specs=specs) + return self + + def get_group_specs_property(self, session, prop): + """Retrieve a group spec property of the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to update. + :returns: The value of the group spec property. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def update_group_specs_property(self, session, prop, val): + """Update a group spec property of the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to update. + :param val: The value to set for the group spec property. + :returns: The updated value of the group spec property. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'commit') + response = session.put( + url, json={prop: val}, microversion=microversion + ) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def delete_group_specs_property(self, session, prop): + """Delete a group spec property from the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to delete. + :returns: None + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'delete') + response = session.delete(url, microversion=microversion) + exceptions.raise_from_response(response) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py new file mode 100644 index 000000000..33e37ffba --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -0,0 +1,103 @@ +# 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.block_storage.v3 import group_type as _group_type +from openstack.tests.functional.block_storage.v3 import base + + +class TestGroup(base.BaseBlockStorageTest): + # TODO(stephenfin): We should use setUpClass here for MOAR SPEED!!! + def setUp(self): + super().setUp() + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') + + 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) + + def test_group_type(self): + # get + group_type = self.conn.block_storage.get_group_type(self.group_type) + self.assertEqual(self.group_type.name, group_type.name) + + # find + group_type = self.conn.block_storage.find_group_type( + self.group_type.name, + ) + self.assertEqual(self.group_type.id, group_type.id) + + # list + group_types = list(self.conn.block_storage.group_types()) + # other tests may have created group types and there can be defaults so + # we don't assert that this is the *only* group type present + self.assertIn(self.group_type.id, {g.id for g in group_types}) + + # update + group_type_name = self.getUniqueString() + group_type_description = self.getUniqueString() + group_type = self.conn.block_storage.update_group_type( + self.group_type, + name=group_type_name, + description=group_type_description, + ) + self.assertIsInstance(group_type, _group_type.GroupType) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual(group_type_name, group_type.name) + self.assertEqual(group_type_description, group_type.description) + + def test_group_type_group_specs(self): + # create + group_type = self.conn.block_storage.create_group_type_group_specs( + self.group_type, + {'foo': 'bar', 'acme': 'buzz'}, + ) + self.assertIsInstance(group_type, _group_type.GroupType) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual( + {'foo': 'bar', 'acme': 'buzz'}, group_type.group_specs + ) + + # get + spec = self.conn.block_storage.get_group_type_group_specs_property( + self.group_type, + 'foo', + ) + self.assertEqual('bar', spec) + + # update + spec = self.conn.block_storage.update_group_type_group_specs_property( + self.group_type, + 'foo', + 'baz', + ) + self.assertEqual('baz', spec) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual( + {'foo': 'baz', 'acme': 'buzz'}, group_type.group_specs + ) + + # delete + self.conn.block_storage.delete_group_type_group_specs_property( + self.group_type, + 'foo', + ) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual({'acme': 'buzz'}, group_type.group_specs) diff --git a/openstack/tests/functional/block_storage/v3/test_group_type.py b/openstack/tests/functional/block_storage/v3/test_group_type.py deleted file mode 100644 index 0ca5252a8..000000000 --- a/openstack/tests/functional/block_storage/v3/test_group_type.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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.block_storage.v3 import group_type as _group_type -from openstack.tests.functional.block_storage.v3 import base - - -class TestGroupType(base.BaseBlockStorageTest): - - def setUp(self): - super(TestGroupType, self).setUp() - - self.GROUP_TYPE_NAME = self.getUniqueString() - self.GROUP_TYPE_ID = None - - group_type = self.conn.block_storage.create_group_type( - name=self.GROUP_TYPE_NAME) - self.assertIsInstance(group_type, _group_type.GroupType) - self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) - self.GROUP_TYPE_ID = group_type.id - - def tearDown(self): - group_type = self.conn.block_storage.delete_group_type( - self.GROUP_TYPE_ID, ignore_missing=False) - self.assertIsNone(group_type) - super(TestGroupType, self).tearDown() - - def test_get(self): - group_type = self.conn.block_storage.get_group_type(self.GROUP_TYPE_ID) - self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) diff --git a/openstack/tests/unit/block_storage/v3/test_group_type.py b/openstack/tests/unit/block_storage/v3/test_group_type.py index 541339dcb..842e4de43 100644 --- a/openstack/tests/unit/block_storage/v3/test_group_type.py +++ b/openstack/tests/unit/block_storage/v3/test_group_type.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.block_storage.v3 import group_type from openstack.tests.unit import base @@ -18,13 +22,16 @@ GROUP_TYPE = { "name": "grp-type-001", "description": "group type 001", "is_public": True, - "group_specs": { - "consistent_group_snapshot_enabled": " False" - } + "group_specs": {"consistent_group_snapshot_enabled": " False"}, } class TestGroupType(base.TestCase): + def setUp(self): + super().setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) def test_basic(self): resource = group_type.GroupType() @@ -44,3 +51,93 @@ class TestGroupType(base.TestCase): self.assertEqual(GROUP_TYPE["description"], resource.description) self.assertEqual(GROUP_TYPE["is_public"], resource.is_public) self.assertEqual(GROUP_TYPE["group_specs"], resource.group_specs) + + def test_fetch_group_specs(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'group_specs': {'a': 'b', 'c': 'd'}} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.fetch_group_specs(self.sess) + + self.sess.get.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs", + microversion=self.sess.default_microversion, + ) + + self.assertEqual(resp.body['group_specs'], rsp.group_specs) + self.assertIsInstance(rsp, group_type.GroupType) + + def test_create_group_specs(self): + sot = group_type.GroupType(**GROUP_TYPE) + specs = {'a': 'b', 'c': 'd'} + resp = mock.Mock() + resp.body = {'group_specs': specs} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + rsp = sot.create_group_specs(self.sess, specs) + + self.sess.post.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs", + json={'group_specs': specs}, + microversion=self.sess.default_microversion, + ) + + self.assertEqual(resp.body['group_specs'], rsp.group_specs) + self.assertIsInstance(rsp, group_type.GroupType) + + def test_get_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'a': 'b'} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_group_specs_property(self.sess, 'a') + + self.sess.get.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + microversion=self.sess.default_microversion, + ) + + self.assertEqual('b', rsp) + + def test_update_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'a': 'b'} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.put = mock.Mock(return_value=resp) + + rsp = sot.update_group_specs_property(self.sess, 'a', 'b') + + self.sess.put.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + json={'a': 'b'}, + microversion=self.sess.default_microversion, + ) + + self.assertEqual('b', rsp) + + def test_delete_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.delete = mock.Mock(return_value=resp) + + rsp = sot.delete_group_specs_property(self.sess, 'a') + + self.sess.delete.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + microversion=self.sess.default_microversion, + ) + + self.assertIsNone(rsp) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index f2fa6b324..790f6cc34 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -169,6 +169,47 @@ class TestGroupType(TestVolumeProxy): def test_group_type_update(self): self.verify_update(self.proxy.update_group_type, group_type.GroupType) + def test_group_type_fetch_group_specs(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.fetch_group_specs", # noqa: E501 + self.proxy.fetch_group_type_group_specs, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_group_type_create_group_specs(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.create_group_specs", # noqa: E501 + self.proxy.create_group_type_group_specs, + method_args=["value", {'a': 'b'}], + expected_args=[self.proxy], + expected_kwargs={"specs": {'a': 'b'}}, + ) + + def test_group_type_get_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.get_group_specs_property", # noqa: E501 + self.proxy.get_group_type_group_specs_property, + method_args=["value", "prop"], + expected_args=[self.proxy, "prop"], + ) + + def test_group_type_update_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.update_group_specs_property", # noqa: E501 + self.proxy.update_group_type_group_specs_property, + method_args=["value", "prop", "val"], + expected_args=[self.proxy, "prop", "val"], + ) + + def test_group_type_delete_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.delete_group_specs_property", # noqa: E501 + self.proxy.delete_group_type_group_specs_property, + method_args=["value", "prop"], + expected_args=[self.proxy, "prop"], + ) + class TestExtension(TestVolumeProxy): def test_extensions(self): diff --git a/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml b/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml new file mode 100644 index 000000000..50ae352a0 --- /dev/null +++ b/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for creating, updating and deleting group type group specs for + the block storage service.