Allow passing explicit microversions to Resource methods

Sometimes users may want a specific behavior of a certain microversion
rather than just the most recent supported one. For example, Ironic only
supports creating nodes directly in the "available" state before 1.11.

Change-Id: I2458650a9ce30440b5e29b940eaf1df60239ff32
This commit is contained in:
Dmitry Tantsur 2022-07-11 15:06:44 +02:00
parent 6bc56b0eb6
commit 915da1e576
3 changed files with 130 additions and 20 deletions

View File

@ -317,13 +317,14 @@ class Object(_base.BaseResource):
self._translate_response(response, has_body=False)
return self
def _raw_delete(self, session):
def _raw_delete(self, session, microversion=None):
if not self.allow_delete:
raise exceptions.MethodNotSupported(self, "delete")
request = self._prepare_request()
session = self._get_session(session)
microversion = self._get_microversion(session, action='delete')
if microversion is None:
microversion = self._get_microversion(session, action='delete')
if self.is_static_large_object is None:
# Fetch metadata to determine SLO flag

View File

@ -1384,7 +1384,15 @@ class Resource(dict):
return actual
def create(self, session, prepend_key=True, base_path=None, **params):
def create(
self,
session,
prepend_key=True,
base_path=None,
*,
microversion=None,
**params
):
"""Create a remote resource based on this instance.
:param session: The session to use for making this request.
@ -1394,6 +1402,7 @@ class Resource(dict):
True.
:param str base_path: Base part of the URI for creating resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str microversion: API version to override the negotiated one.
:param dict params: Additional params to pass.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1403,7 +1412,8 @@ class Resource(dict):
raise exceptions.MethodNotSupported(self, 'create')
session = self._get_session(session)
microversion = self._get_microversion(session, action='create')
if microversion is None:
microversion = self._get_microversion(session, action='create')
requires_id = (
self.create_requires_id
if self.create_requires_id is not None
@ -1464,6 +1474,8 @@ class Resource(dict):
data,
prepend_key=True,
base_path=None,
*,
microversion=None,
**params,
):
"""Create multiple remote resources based on this class and data.
@ -1476,6 +1488,7 @@ class Resource(dict):
True.
:param str base_path: Base part of the URI for creating resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str microversion: API version to override the negotiated one.
:param dict params: Additional params to pass.
:return: A generator of :class:`Resource` objects.
@ -1493,7 +1506,8 @@ class Resource(dict):
raise ValueError('Invalid data passed: %s' % data)
session = cls._get_session(session)
microversion = cls._get_microversion(session, action='create')
if microversion is None:
microversion = cls._get_microversion(session, action='create')
requires_id = (
cls.create_requires_id
if cls.create_requires_id is not None
@ -1566,6 +1580,8 @@ class Resource(dict):
base_path=None,
error_message=None,
skip_cache=False,
*,
microversion=None,
**params,
):
"""Get a remote resource based on this instance.
@ -1580,6 +1596,7 @@ class Resource(dict):
requested object does not exist.
:param bool skip_cache: A boolean indicating whether optional API
cache should be skipped for this invocation.
:param str microversion: API version to override the negotiated one.
:param dict params: Additional parameters that can be consumed.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1594,7 +1611,8 @@ class Resource(dict):
requires_id=requires_id, base_path=base_path
)
session = self._get_session(session)
microversion = self._get_microversion(session, action='fetch')
if microversion is None:
microversion = self._get_microversion(session, action='fetch')
response = session.get(
request.url,
microversion=microversion,
@ -1609,13 +1627,14 @@ class Resource(dict):
self._translate_response(response, **kwargs)
return self
def head(self, session, base_path=None):
def head(self, session, base_path=None, *, microversion=None):
"""Get headers from a remote resource based on this instance.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param str base_path: Base part of the URI for fetching resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str microversion: API version to override the negotiated one.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1627,7 +1646,8 @@ class Resource(dict):
raise exceptions.MethodNotSupported(self, 'head')
session = self._get_session(session)
microversion = self._get_microversion(session, action='fetch')
if microversion is None:
microversion = self._get_microversion(session, action='fetch')
request = self._prepare_request(base_path=base_path)
response = session.head(request.url, microversion=microversion)
@ -1650,6 +1670,8 @@ class Resource(dict):
has_body=True,
retry_on_conflict=None,
base_path=None,
*,
microversion=None,
**kwargs,
):
"""Commit the state of the instance to the remote resource.
@ -1663,6 +1685,7 @@ class Resource(dict):
CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults.
:param str base_path: Base part of the URI for modifying resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str microversion: API version to override the negotiated one.
:param dict kwargs: Parameters that will be passed to
_prepare_request()
@ -1688,7 +1711,8 @@ class Resource(dict):
request = self._prepare_request(
prepend_key=prepend_key, base_path=base_path, **kwargs
)
microversion = self._get_microversion(session, action='commit')
if microversion is None:
microversion = self._get_microversion(session, action='commit')
return self._commit(
session,
@ -1774,6 +1798,8 @@ class Resource(dict):
has_body=True,
retry_on_conflict=None,
base_path=None,
*,
microversion=None,
):
"""Patch the remote resource.
@ -1792,6 +1818,7 @@ class Resource(dict):
CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults.
:param str base_path: Base part of the URI for modifying resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str microversion: API version to override the negotiated one.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1810,7 +1837,8 @@ class Resource(dict):
request = self._prepare_request(
prepend_key=prepend_key, base_path=base_path, patch=True
)
microversion = self._get_microversion(session, action='patch')
if microversion is None:
microversion = self._get_microversion(session, action='patch')
if patch:
request.body += self._convert_patch(patch)
@ -1823,11 +1851,13 @@ class Resource(dict):
retry_on_conflict=retry_on_conflict,
)
def delete(self, session, error_message=None, **kwargs):
def delete(self, session, error_message=None, *, microversion=None,
**kwargs):
"""Delete the remote resource based on this instance.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param str microversion: API version to override the negotiated one.
:param dict kwargs: Parameters that will be passed to
_prepare_request()
@ -1838,7 +1868,8 @@ class Resource(dict):
the resource was not found.
"""
response = self._raw_delete(session, **kwargs)
response = self._raw_delete(session, microversion=microversion,
**kwargs)
kwargs = {}
if error_message:
kwargs['error_message'] = error_message
@ -1846,13 +1877,14 @@ class Resource(dict):
self._translate_response(response, has_body=False, **kwargs)
return self
def _raw_delete(self, session, **kwargs):
def _raw_delete(self, session, microversion=None, **kwargs):
if not self.allow_delete:
raise exceptions.MethodNotSupported(self, 'delete')
request = self._prepare_request(**kwargs)
session = self._get_session(session)
microversion = self._get_microversion(session, action='delete')
if microversion is None:
microversion = self._get_microversion(session, action='delete')
return session.delete(
request.url, headers=request.headers, microversion=microversion
@ -1865,6 +1897,8 @@ class Resource(dict):
paginated=True,
base_path=None,
allow_unknown_params=False,
*,
microversion=None,
**params,
):
"""This method is a generator which yields resource objects.
@ -1884,6 +1918,7 @@ class Resource(dict):
unknown query parameters. This allows getting list of 'filters' and
passing everything known to the server. ``False`` will result in
validation exception when unknown query parameters are passed.
:param str microversion: API version to override the negotiated one.
:param dict params: These keyword arguments are passed through the
:meth:`~openstack.resource.QueryParamter._transpose` method
to find if any of them match expected query parameters to be sent
@ -1903,7 +1938,8 @@ class Resource(dict):
raise exceptions.MethodNotSupported(cls, 'list')
session = cls._get_session(session)
microversion = cls._get_microversion(session, action='list')
if microversion is None:
microversion = cls._get_microversion(session, action='list')
if base_path is None:
base_path = cls.base_path
@ -2076,6 +2112,8 @@ class Resource(dict):
name_or_id,
ignore_missing=True,
list_base_path=None,
*,
microversion=None,
**params,
):
"""Find a resource by its name or id.
@ -2090,6 +2128,7 @@ class Resource(dict):
returned when attempting to find a nonexistent resource.
:param str list_base_path: base_path to be used when need listing
resources.
:param str microversion: API version to override the negotiated one.
:param dict params: Any additional parameters to be passed into
underlying methods, such as to
:meth:`~openstack.resource.Resource.existing` in order to pass on
@ -2108,7 +2147,7 @@ class Resource(dict):
match = cls.existing(
id=name_or_id, connection=session._get_connection(), **params
)
return match.fetch(session, **params)
return match.fetch(session, microversion=microversion, **params)
except (exceptions.NotFoundException, exceptions.BadRequestException):
# NOTE(gtema): There are few places around openstack that return
# 400 if we try to GET resource and it doesn't exist.

View File

@ -1512,15 +1512,19 @@ class TestResourceActions(base.TestCase):
def _test_create(self, cls, requires_id=False, prepend_key=False,
microversion=None, base_path=None, params=None,
id_marked_dirty=True):
id_marked_dirty=True, explicit_microversion=None):
id = "id" if requires_id else None
sot = cls(id=id)
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
params = params or {}
kwargs = params.copy()
if explicit_microversion is not None:
kwargs['microversion'] = explicit_microversion
microversion = explicit_microversion
result = sot.create(self.session, prepend_key=prepend_key,
base_path=base_path, **params)
base_path=base_path, **kwargs)
id_is_dirty = ('id' in sot._body._dirty)
self.assertEqual(id_marked_dirty, id_is_dirty)
@ -1575,6 +1579,17 @@ class TestResourceActions(base.TestCase):
self._test_create(Test, requires_id=True, prepend_key=True,
microversion='1.42')
def test_put_create_with_explicit_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'PUT'
_max_microversion = '1.99'
self._test_create(Test, requires_id=True, prepend_key=True,
explicit_microversion='1.42')
def test_put_create_with_params(self):
class Test(resource.Resource):
service = self.service_name
@ -1663,6 +1678,29 @@ class TestResourceActions(base.TestCase):
sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, sot)
def test_fetch_with_explicit_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_fetch = True
_max_microversion = '1.99'
sot = Test(id='id')
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.fetch(self.session, microversion='1.42')
sot._prepare_request.assert_called_once_with(
requires_id=True, base_path=None)
self.session.get.assert_called_once_with(
self.request.url, microversion='1.42', params={},
skip_cache=False)
self.assertEqual(sot.microversion, '1.42')
sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, sot)
def test_fetch_not_requires_id(self):
result = self.sot.fetch(self.session, False)
@ -1739,16 +1777,21 @@ class TestResourceActions(base.TestCase):
def _test_commit(self, commit_method='PUT', prepend_key=True,
has_body=True, microversion=None,
commit_args=None, expected_args=None, base_path=None):
commit_args=None, expected_args=None, base_path=None,
explicit_microversion=None):
self.sot.commit_method = commit_method
# Need to make sot look dirty so we can attempt an update
self.sot._body = mock.Mock()
self.sot._body.dirty = mock.Mock(return_value={"x": "y"})
commit_args = commit_args or {}
if explicit_microversion is not None:
commit_args['microversion'] = explicit_microversion
microversion = explicit_microversion
self.sot.commit(self.session, prepend_key=prepend_key,
has_body=has_body, base_path=base_path,
**(commit_args or {}))
**commit_args)
self.sot._prepare_request.assert_called_once_with(
prepend_key=prepend_key, base_path=base_path)
@ -1810,6 +1853,10 @@ class TestResourceActions(base.TestCase):
commit_args={'retry_on_conflict': False},
expected_args={'retriable_status_codes': {503}})
def test_commit_put_explicit_microversion(self):
self._test_commit(commit_method='PUT', prepend_key=True, has_body=True,
explicit_microversion='1.42')
def test_commit_not_dirty(self):
self.sot._body = mock.Mock()
self.sot._body.dirty = dict()
@ -1910,6 +1957,29 @@ class TestResourceActions(base.TestCase):
self.response, has_body=False)
self.assertEqual(result, sot)
def test_delete_with_explicit_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_delete = True
_max_microversion = '1.99'
sot = Test(id='id')
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.delete(self.session, microversion='1.42')
sot._prepare_request.assert_called_once_with()
self.session.delete.assert_called_once_with(
self.request.url,
headers='headers',
microversion='1.42')
sot._translate_response.assert_called_once_with(
self.response, has_body=False)
self.assertEqual(result, sot)
# NOTE: As list returns a generator, testing it requires consuming
# the generator. Wrap calls to self.sot.list in a `list`
# and then test the results as a list of responses.