Merge "Support for microversions in base Resource"

This commit is contained in:
Zuul 2018-07-27 23:15:51 +00:00 committed by Gerrit Code Review
commit 52cf40bbe5
7 changed files with 335 additions and 43 deletions

View File

@ -322,6 +322,11 @@ class Resource(object):
#: Is this a detailed version of another Resource
detail_for = None
#: Maximum microversion to use for getting/creating/updating the Resource
_max_microversion = None
#: API microversion (string or None) this Resource was loaded with
microversion = None
def __init__(self, _synchronized=False, **attrs):
"""The base resource
@ -330,6 +335,7 @@ class Resource(object):
:meth:`~openstack.resource.Resource.existing`.
"""
self.microversion = attrs.pop('microversion', None)
# NOTE: _collect_attrs modifies **attrs in place, removing
# items as they match up with any of the body, header,
# or uri mappings.
@ -388,6 +394,7 @@ class Resource(object):
layer when updating instances that may have already
been created.
"""
self.microversion = attrs.pop('microversion', None)
body, header, uri = self._collect_attrs(attrs)
self._body.update(body)
@ -729,6 +736,44 @@ class Resource(object):
" instance of an openstack.proxy.Proxy object or at the very least"
" a raw keystoneauth1.adapter.Adapter.")
@classmethod
def _get_microversion_for_list(cls, session):
"""Get microversion to use when listing resources.
The base version uses the following logic:
1. If the session has a default microversion for the current service,
just use it.
2. If ``self._max_microversion`` is not ``None``, use minimum between
it and the maximum microversion supported by the server.
3. Otherwise use ``None``.
Subclasses can override this method if more complex logic is needed.
:param session: :class`keystoneauth1.adapter.Adapter`
:return: microversion as string or ``None``
"""
if session.default_microversion:
return session.default_microversion
return utils.maximum_supported_microversion(session,
cls._max_microversion)
def _get_microversion_for(self, session, action):
"""Get microversion to use for the given action.
The base version uses :meth:`_get_microversion_for_list`.
Subclasses can override this method if more complex logic is needed.
:param session: :class`keystoneauth1.adapter.Adapter`
:param action: One of "get", "update", "create", "delete". Unused in
the base implementation.
:return: microversion as string or ``None``
"""
if action not in ('get', 'update', 'create', 'delete'):
raise ValueError('Invalid action: %s' % action)
return self._get_microversion_for_list(session)
def create(self, session, prepend_key=True):
"""Create a remote resource based on this instance.
@ -746,20 +791,24 @@ class Resource(object):
raise exceptions.MethodNotSupported(self, "create")
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'create')
if self.create_method == 'PUT':
request = self._prepare_request(requires_id=True,
prepend_key=prepend_key)
response = session.put(request.url,
json=request.body, headers=request.headers)
json=request.body, headers=request.headers,
microversion=microversion)
elif self.create_method == 'POST':
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
response = session.post(request.url,
json=request.body, headers=request.headers)
json=request.body, headers=request.headers,
microversion=microversion)
else:
raise exceptions.ResourceFailure(
msg="Invalid create method: %s" % self.create_method)
self.microversion = microversion
self._translate_response(response)
return self
@ -779,11 +828,13 @@ class Resource(object):
request = self._prepare_request(requires_id=requires_id)
session = self._get_session(session)
response = session.get(request.url)
microversion = self._get_microversion_for(session, 'get')
response = session.get(request.url, microversion=microversion)
kwargs = {}
if error_message:
kwargs['error_message'] = error_message
self.microversion = microversion
self._translate_response(response, **kwargs)
return self
@ -803,9 +854,12 @@ class Resource(object):
request = self._prepare_request()
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'get')
response = session.head(request.url,
headers={"Accept": ""})
headers={"Accept": ""},
microversion=microversion)
self.microversion = microversion
self._translate_response(response, has_body=False)
return self
@ -834,20 +888,25 @@ class Resource(object):
request = self._prepare_request(prepend_key=prepend_key)
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'update')
if self.update_method == 'PATCH':
response = session.patch(
request.url, json=request.body, headers=request.headers)
request.url, json=request.body, headers=request.headers,
microversion=microversion)
elif self.update_method == 'POST':
response = session.post(
request.url, json=request.body, headers=request.headers)
request.url, json=request.body, headers=request.headers,
microversion=microversion)
elif self.update_method == 'PUT':
response = session.put(
request.url, json=request.body, headers=request.headers)
request.url, json=request.body, headers=request.headers,
microversion=microversion)
else:
raise exceptions.ResourceFailure(
msg="Invalid update method: %s" % self.update_method)
self.microversion = microversion
self._translate_response(response, has_body=has_body)
return self
@ -866,9 +925,11 @@ class Resource(object):
request = self._prepare_request()
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'delete')
response = session.delete(request.url,
headers={"Accept": ""})
headers={"Accept": ""},
microversion=microversion)
kwargs = {}
if error_message:
kwargs['error_message'] = error_message
@ -910,6 +971,7 @@ class Resource(object):
if not cls.allow_list:
raise exceptions.MethodNotSupported(cls, "list")
session = cls._get_session(session)
microversion = cls._get_microversion_for_list(session)
cls._query_mapping._validate(params, base_path=cls.base_path)
query_params = cls._query_mapping._transpose(params)
@ -925,7 +987,8 @@ class Resource(object):
response = session.get(
uri,
headers={"Accept": "application/json"},
params=query_params.copy())
params=query_params.copy(),
microversion=microversion)
exceptions.raise_from_response(response)
data = response.json()
@ -950,7 +1013,7 @@ class Resource(object):
# argument and is practically a reserved word.
raw_resource.pop("self", None)
value = cls.existing(**raw_resource)
value = cls.existing(microversion=microversion, **raw_resource)
marker = value.id
yield value
total_yielded += 1

View File

@ -148,6 +148,7 @@ class TestLimits(base.TestCase):
def test_get(self):
sess = mock.Mock(spec=adapter.Adapter)
sess.default_microversion = None
resp = mock.Mock()
sess.get.return_value = resp
resp.json.return_value = copy.deepcopy(LIMITS_BODY)

View File

@ -106,6 +106,7 @@ class TestImage(base.TestCase):
self.resp.json = mock.Mock(return_value=self.resp.body)
self.sess = mock.Mock(spec=adapter.Adapter)
self.sess.post = mock.Mock(return_value=self.resp)
self.sess.default_microversion = None
def test_basic(self):
sot = image.Image()
@ -266,7 +267,7 @@ class TestImage(base.TestCase):
self.sess.get.assert_has_calls(
[mock.call('images/IDENTIFIER/file',
stream=False),
mock.call('images/IDENTIFIER',)])
mock.call('images/IDENTIFIER', microversion=None)])
self.assertEqual(rv, resp1.content)
@ -292,7 +293,7 @@ class TestImage(base.TestCase):
self.sess.get.assert_has_calls(
[mock.call('images/IDENTIFIER/file',
stream=False),
mock.call('images/IDENTIFIER',)])
mock.call('images/IDENTIFIER', microversion=None)])
self.assertEqual(rv, resp1.content)

View File

@ -76,6 +76,7 @@ class TestFloatingIP(base.TestCase):
def test_find_available(self):
mock_session = mock.Mock(spec=adapter.Adapter)
mock_session.get_filter = mock.Mock(return_value={})
mock_session.default_microversion = None
data = {'id': 'one', 'floating_ip_address': '10.0.0.1'}
fake_response = mock.Mock()
body = {floating_ip.FloatingIP.resources_key: [data]}
@ -89,10 +90,12 @@ class TestFloatingIP(base.TestCase):
mock_session.get.assert_called_with(
floating_ip.FloatingIP.base_path,
headers={'Accept': 'application/json'},
params={})
params={},
microversion=None)
def test_find_available_nada(self):
mock_session = mock.Mock(spec=adapter.Adapter)
mock_session.default_microversion = None
fake_response = mock.Mock()
body = {floating_ip.FloatingIP.resources_key: []}
fake_response.json = mock.Mock(return_value=body)

View File

@ -976,8 +976,14 @@ class TestResourceActions(base.TestCase):
self.session.post = mock.Mock(return_value=self.response)
self.session.delete = mock.Mock(return_value=self.response)
self.session.head = mock.Mock(return_value=self.response)
self.session.default_microversion = None
def _test_create(self, cls, requires_id=False, prepend_key=False):
self.endpoint_data = mock.Mock(max_microversion='1.99',
min_microversion=None)
self.session.get_endpoint_data.return_value = self.endpoint_data
def _test_create(self, cls, requires_id=False, prepend_key=False,
microversion=None):
id = "id" if requires_id else None
sot = cls(id=id)
sot._prepare_request = mock.Mock(return_value=self.request)
@ -990,12 +996,15 @@ class TestResourceActions(base.TestCase):
if requires_id:
self.session.put.assert_called_once_with(
self.request.url,
json=self.request.body, headers=self.request.headers)
json=self.request.body, headers=self.request.headers,
microversion=microversion)
else:
self.session.post.assert_called_once_with(
self.request.url,
json=self.request.body, headers=self.request.headers)
json=self.request.body, headers=self.request.headers,
microversion=microversion)
self.assertEqual(sot.microversion, microversion)
sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, sot)
@ -1008,6 +1017,17 @@ class TestResourceActions(base.TestCase):
self._test_create(Test, requires_id=True, prepend_key=True)
def test_put_create_with_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'PUT'
_max_microversion = '1.42'
self._test_create(Test, requires_id=True, prepend_key=True,
microversion='1.42')
def test_post_create(self):
class Test(resource.Resource):
service = self.service_name
@ -1022,17 +1042,39 @@ class TestResourceActions(base.TestCase):
self.sot._prepare_request.assert_called_once_with(requires_id=True)
self.session.get.assert_called_once_with(
self.request.url,)
self.request.url, microversion=None)
self.assertIsNone(self.sot.microversion)
self.sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, self.sot)
def test_get_with_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_get = True
_max_microversion = '1.42'
sot = Test(id='id')
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.get(self.session)
sot._prepare_request.assert_called_once_with(requires_id=True)
self.session.get.assert_called_once_with(
self.request.url, microversion='1.42')
self.assertEqual(sot.microversion, '1.42')
sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, sot)
def test_get_not_requires_id(self):
result = self.sot.get(self.session, False)
self.sot._prepare_request.assert_called_once_with(requires_id=False)
self.session.get.assert_called_once_with(
self.request.url,)
self.request.url, microversion=None)
self.sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, self.sot)
@ -1043,14 +1085,40 @@ class TestResourceActions(base.TestCase):
self.sot._prepare_request.assert_called_once_with()
self.session.head.assert_called_once_with(
self.request.url,
headers={"Accept": ""})
headers={"Accept": ""},
microversion=None)
self.assertIsNone(self.sot.microversion)
self.sot._translate_response.assert_called_once_with(
self.response, has_body=False)
self.assertEqual(result, self.sot)
def test_head_with_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_head = True
_max_microversion = '1.42'
sot = Test(id='id')
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.head(self.session)
sot._prepare_request.assert_called_once_with()
self.session.head.assert_called_once_with(
self.request.url,
headers={"Accept": ""},
microversion='1.42')
self.assertEqual(sot.microversion, '1.42')
sot._translate_response.assert_called_once_with(
self.response, has_body=False)
self.assertEqual(result, sot)
def _test_update(self, update_method='PUT', prepend_key=True,
has_body=True):
has_body=True, microversion=None):
self.sot.update_method = update_method
# Need to make sot look dirty so we can attempt an update
@ -1066,16 +1134,20 @@ class TestResourceActions(base.TestCase):
if update_method == 'PATCH':
self.session.patch.assert_called_once_with(
self.request.url,
json=self.request.body, headers=self.request.headers)
json=self.request.body, headers=self.request.headers,
microversion=microversion)
elif update_method == 'POST':
self.session.post.assert_called_once_with(
self.request.url,
json=self.request.body, headers=self.request.headers)
json=self.request.body, headers=self.request.headers,
microversion=microversion)
elif update_method == 'PUT':
self.session.put.assert_called_once_with(
self.request.url,
json=self.request.body, headers=self.request.headers)
json=self.request.body, headers=self.request.headers,
microversion=microversion)
self.assertEqual(self.sot.microversion, microversion)
self.sot._translate_response.assert_called_once_with(
self.response, has_body=has_body)
@ -1102,12 +1174,36 @@ class TestResourceActions(base.TestCase):
self.sot._prepare_request.assert_called_once_with()
self.session.delete.assert_called_once_with(
self.request.url,
headers={"Accept": ""})
headers={"Accept": ""},
microversion=None)
self.sot._translate_response.assert_called_once_with(
self.response, has_body=False)
self.assertEqual(result, self.sot)
def test_delete_with_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_delete = True
_max_microversion = '1.42'
sot = Test(id='id')
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.delete(self.session)
sot._prepare_request.assert_called_once_with()
self.session.delete.assert_called_once_with(
self.request.url,
headers={"Accept": ""},
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.
@ -1123,7 +1219,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_once_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
self.assertEqual([], result)
@ -1159,7 +1256,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_once_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
self.assertEqual(1, len(results))
self.assertEqual(id_value, results[0].id)
@ -1185,7 +1283,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_once_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
self.assertEqual(1, len(results))
self.assertEqual(id_value, results[0].id)
@ -1219,11 +1318,13 @@ class TestResourceActions(base.TestCase):
self.assertEqual(ids[1], results[1].id)
self.assertEqual(
mock.call('base_path',
headers={'Accept': 'application/json'}, params={}),
headers={'Accept': 'application/json'}, params={},
microversion=None),
self.session.get.mock_calls[0])
self.assertEqual(
mock.call('https://example.com/next-url',
headers={'Accept': 'application/json'}, params={}),
headers={'Accept': 'application/json'}, params={},
microversion=None),
self.session.get.mock_calls[1])
self.assertEqual(2, len(self.session.get.call_args_list))
self.assertIsInstance(results[0], self.test_class)
@ -1253,15 +1354,64 @@ class TestResourceActions(base.TestCase):
self.assertEqual(ids[1], results[1].id)
self.assertEqual(
mock.call('base_path',
headers={'Accept': 'application/json'}, params={}),
headers={'Accept': 'application/json'}, params={},
microversion=None),
self.session.get.mock_calls[0])
self.assertEqual(
mock.call('https://example.com/next-url',
headers={'Accept': 'application/json'}, params={}),
headers={'Accept': 'application/json'}, params={},
microversion=None),
self.session.get.mock_calls[2])
self.assertEqual(2, len(self.session.get.call_args_list))
self.assertIsInstance(results[0], self.test_class)
def test_list_response_paginated_with_microversions(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
resources_key = 'resources'
allow_list = True
_max_microversion = '1.42'
ids = [1, 2]
mock_response = mock.Mock()
mock_response.status_code = 200
mock_response.links = {}
mock_response.json.return_value = {
"resources": [{"id": ids[0]}],
"resources_links": [{
"href": "https://example.com/next-url",
"rel": "next",
}]
}
mock_response2 = mock.Mock()
mock_response2.status_code = 200
mock_response2.links = {}
mock_response2.json.return_value = {
"resources": [{"id": ids[1]}],
}
self.session.get.side_effect = [mock_response, mock_response2]
results = list(Test.list(self.session, paginated=True))
self.assertEqual(2, len(results))
self.assertEqual(ids[0], results[0].id)
self.assertEqual(ids[1], results[1].id)
self.assertEqual(
mock.call('base_path',
headers={'Accept': 'application/json'}, params={},
microversion='1.42'),
self.session.get.mock_calls[0])
self.assertEqual(
mock.call('https://example.com/next-url',
headers={'Accept': 'application/json'}, params={},
microversion='1.42'),
self.session.get.mock_calls[1])
self.assertEqual(2, len(self.session.get.call_args_list))
self.assertIsInstance(results[0], Test)
self.assertEqual('1.42', results[0].microversion)
def test_list_multi_page_response_not_paginated(self):
ids = [1, 2]
mock_response = mock.Mock()
@ -1453,20 +1603,23 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
result1 = next(results)
self.assertEqual(result1.id, ids[1])
self.session.get.assert_called_with(
'https://example.com/next-url',
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
self.assertRaises(StopIteration, next, results)
self.session.get.assert_called_with(
'https://example.com/next-url',
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
def test_list_multi_page_no_early_termination(self):
# This tests verifies that multipages are not early terminated.
@ -1508,7 +1661,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={"limit": 3})
params={"limit": 3},
microversion=None)
# Second page contains another two items
result2 = next(results)
@ -1518,7 +1672,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={"limit": 3, "marker": 2})
params={"limit": 3, "marker": 2},
microversion=None)
# Ensure we're done after those four items
self.assertRaises(StopIteration, next, results)
@ -1527,7 +1682,8 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={"limit": 3, "marker": 4})
params={"limit": 3, "marker": 4},
microversion=None)
# Ensure we made three calls to get this done
self.assertEqual(3, len(self.session.get.call_args_list))
@ -1564,14 +1720,16 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={"limit": 2})
params={"limit": 2},
microversion=None)
result2 = next(results)
self.assertEqual(result2.id, ids[2])
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={'limit': 2, 'marker': 2})
params={'limit': 2, 'marker': 2},
microversion=None)
# Ensure we're done after those three items
self.assertRaises(StopIteration, next, results)
@ -1612,14 +1770,16 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
result2 = next(results)
self.assertEqual(result2.id, ids[2])
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={'marker': 2})
params={'marker': 2},
microversion=None)
# Ensure we're done after those three items
self.assertRaises(StopIteration, next, results)
@ -1658,14 +1818,16 @@ class TestResourceActions(base.TestCase):
self.session.get.assert_called_with(
self.base_path,
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
result2 = next(results)
self.assertEqual(result2.id, ids[2])
self.session.get.assert_called_with(
'https://example.com/next-url',
headers={"Accept": "application/json"},
params={})
params={},
microversion=None)
# Ensure we're done after those three items
self.assertRaises(StopIteration, next, results)

View File

@ -104,3 +104,33 @@ class Test_urljoin(base.TestCase):
result = utils.urljoin(root, *leaves)
self.assertEqual(result, "http://www.example.com/foo/")
class TestMaximumSupportedMicroversion(base.TestCase):
def setUp(self):
super(TestMaximumSupportedMicroversion, self).setUp()
self.adapter = mock.Mock(spec=['get_endpoint_data'])
self.endpoint_data = mock.Mock(spec=['min_microversion',
'max_microversion'],
min_microversion=None,
max_microversion='1.99')
self.adapter.get_endpoint_data.return_value = self.endpoint_data
def test_with_none(self):
self.assertIsNone(utils.maximum_supported_microversion(self.adapter,
None))
def test_with_value(self):
self.assertEqual('1.42',
utils.maximum_supported_microversion(self.adapter,
'1.42'))
def test_value_more_than_max(self):
self.assertEqual('1.99',
utils.maximum_supported_microversion(self.adapter,
'1.100'))
def test_value_less_than_min(self):
self.endpoint_data.min_microversion = '1.42'
self.assertIsNone(utils.maximum_supported_microversion(self.adapter,
'1.2'))

View File

@ -183,3 +183,35 @@ def pick_microversion(session, required):
if required is not None:
return discover.version_to_string(required)
def maximum_supported_microversion(adapter, client_maximum):
"""Determinte the maximum microversion supported by both client and server.
:param adapter: :class:`~keystoneauth1.adapter.Adapter` instance.
:param client_maximum: Maximum microversion supported by the client.
If ``None``, ``None`` is returned.
:returns: the maximum supported microversion as string or ``None``.
"""
if client_maximum is None:
return None
endpoint_data = adapter.get_endpoint_data()
if not endpoint_data.max_microversion:
return None
client_max = discover.normalize_version_number(client_maximum)
server_max = discover.normalize_version_number(
endpoint_data.max_microversion)
if endpoint_data.min_microversion:
server_min = discover.normalize_version_number(
endpoint_data.min_microversion)
if client_max < server_min:
# NOTE(dtantsur): we may want to raise in this case, but this keeps
# the current behavior intact.
return None
result = min(client_max, server_max)
return discover.version_to_string(result)