From b4813fb83cd7ae547dd7bd5f706af54bd8308ee3 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Mon, 12 Oct 2020 13:50:53 +0200 Subject: [PATCH] Add support for updating Block Storage Volume type extra_spec attributes While the volume_types/ endpoint returns extra_specs and accepts it on creation it doesn't support updating them through volume_types. It has to be updated through the volume_types/extra_specs endpoint. Change-Id: I5f9d5bcb102c5fd9fe60eb03e218c66f9c49592c --- openstack/block_storage/v3/_proxy.py | 30 ++++++++ openstack/block_storage/v3/type.py | 55 ++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 22 ++++++ .../tests/unit/block_storage/v3/test_type.py | 76 +++++++++++++++++++ ...d-volume-type-update-b84f50b7fa3b061d.yaml | 3 + 5 files changed, 186 insertions(+) create mode 100644 releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 9b30d8b0c..15aaf8ce8 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -175,6 +175,36 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): """ return self._update(_type.Type, type, **attrs) + def update_type_extra_specs(self, type, **attrs): + """Update the extra_specs for a type + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param dict attrs: The extra_spec attributes to update on the + type represented by ``value``. + + :returns: A dict containing updated extra_specs + + """ + res = self._get_resource(_type.Type, type) + extra_specs = res.set_extra_specs(self, **attrs) + result = _type.Type.existing(id=res.id, extra_specs=extra_specs) + return result + + def delete_type_extra_specs(self, type, keys): + """Delete the extra_specs for a type + + Note: This method will do a HTTP DELETE request for every key in keys. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param keys: The keys to delete + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.delete_extra_specs(self, keys) + def get_type_encryption(self, volume_type_id): """Get the encryption details of a volume type diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index b3b9326c0..42fe16afc 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/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 Type(resource.Resource): @@ -39,6 +41,59 @@ class Type(resource.Resource): #: a private volume-type. *Type: bool* is_public = resource.Body('os-volume-type-access:is_public', type=bool) + def _extra_specs(self, method, key=None, delete=False, + extra_specs=None): + extra_specs = extra_specs or {} + for k, v in extra_specs.items(): + if not isinstance(v, str): + raise ValueError("The value for %s (%s) must be " + "a text string" % (k, v)) + + if key is not None: + url = utils.urljoin(self.base_path, self.id, "extra_specs", key) + else: + url = utils.urljoin(self.base_path, self.id, "extra_specs") + + kwargs = {} + if extra_specs: + kwargs["json"] = {"extra_specs": extra_specs} + + response = method(url, headers={}, **kwargs) + + # ensure Cinder API has not returned us an error + exceptions.raise_from_response(response) + # DELETE doesn't return a JSON body while everything else does. + return response.json() if not delete else None + + def set_extra_specs(self, session, **extra_specs): + """Update extra_specs + + This call will replace only the extra_specs with the same keys + given here. Other keys will not be modified. + + :param session: The session to use for this request. + :param kwargs extra_specs: key/value extra_specs pairs to be update on + this volume type. All keys and values + """ + if not extra_specs: + return dict() + + result = self._extra_specs(session.post, extra_specs=extra_specs) + return result["extra_specs"] + + def delete_extra_specs(self, session, keys): + """Delete extra_specs + + Note: This method will do a HTTP DELETE request for every key in keys. + + :param session: The session to use for this request. + :param list keys: The keys to delete. + + :rtype: ``None`` + """ + for key in keys: + self._extra_specs(session.delete, key=key, delete=True) + class TypeEncryption(resource.Resource): resource_key = "encryption" diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index e9ffdb1aa..0b72231a6 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -75,6 +75,28 @@ class TestVolumeProxy(test_proxy_base.TestProxyBase): def test_type_update(self): self.verify_update(self.proxy.update_type, type.Type) + def test_type_extra_specs_update(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify2( + "openstack.block_storage.v3.type.Type.set_extra_specs", + self.proxy.update_type_extra_specs, + method_args=[id], + method_kwargs=kwargs, + method_result=type.Type.existing(id=id, + extra_specs=kwargs), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=kwargs) + + def test_type_extra_specs_delete(self): + self._verify2( + "openstack.block_storage.v3.type.Type.delete_extra_specs", + self.proxy.delete_type_extra_specs, + expected_result=None, + method_args=["value", "key"], + expected_args=[self.proxy, "key"]) + def test_type_encryption_get(self): self.verify_get(self.proxy.get_type_encryption, type.TypeEncryption, diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 5fcd8380e..e92fb6726 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + from openstack.tests.unit import base +from openstack import exceptions from openstack.block_storage.v3 import type FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" @@ -27,6 +30,10 @@ TYPE = { class TestType(base.TestCase): + def setUp(self): + super(TestType, self).setUp() + self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} + def test_basic(self): sot = type.Type(**TYPE) self.assertEqual("volume_type", sot.resource_key) @@ -48,3 +55,72 @@ class TestType(base.TestCase): self.assertEqual(TYPE["extra_specs"], sot.extra_specs) self.assertEqual(TYPE["name"], sot.name) self.assertEqual(TYPE["description"], sot.description) + + def test_set_extra_specs(self): + response = mock.Mock() + response.status_code = 200 + response.json.return_value = self.extra_specs_result + sess = mock.Mock() + sess.post.return_value = response + + sot = type.Type(id=FAKE_ID) + + set_specs = {"lol": "rofl"} + + result = sot.set_extra_specs(sess, **set_specs) + + self.assertEqual(result, self.extra_specs_result["extra_specs"]) + sess.post.assert_called_once_with("types/" + FAKE_ID + "/extra_specs", + headers={}, + json={"extra_specs": set_specs}) + + def test_set_extra_specs_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.post.return_value = response + + sot = type.Type(id=FAKE_ID) + + set_specs = {"lol": "rofl"} + + self.assertRaises( + exceptions.BadRequestException, + sot.set_extra_specs, + sess, + **set_specs) + + def test_delete_extra_specs(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 200 + sess.delete.return_value = response + + sot = type.Type(id=FAKE_ID) + + key = "hey" + + sot.delete_extra_specs(sess, [key]) + + sess.delete.assert_called_once_with( + "types/" + FAKE_ID + "/extra_specs/" + key, + headers={}, + ) + + def test_delete_extra_specs_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.delete.return_value = response + + sot = type.Type(id=FAKE_ID) + + key = "hey" + + self.assertRaises( + exceptions.BadRequestException, + sot.delete_extra_specs, + sess, + [key]) diff --git a/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml b/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml new file mode 100644 index 000000000..3e9f3630a --- /dev/null +++ b/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for updating block storage volume type objects.