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
This commit is contained in:
Mark Chappell 2020-10-12 13:50:53 +02:00
parent cc7369c743
commit b4813fb83c
5 changed files with 186 additions and 0 deletions

View File

@ -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

View File

@ -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"

View File

@ -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,

View File

@ -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])

View File

@ -0,0 +1,3 @@
---
features:
- Add support for updating block storage volume type objects.