From a3c1690231f8ca98e53ce26e6916de26a71eab48 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 18 Jul 2018 10:10:11 +0200 Subject: [PATCH] Support for microversions in base Resource This change allows a Resource to negotiate the highest microversion supported by both the client (openstacksdk) and the server. It is done by overriding Class._max_microversion to a non-None value. The microversion used to load or update a Resource is stored on it as the "microversion" attribute. Change-Id: I78703bece7c1a3898e4a5b60cd7c2912cf4b5595 --- openstack/resource.py | 83 ++++++- .../tests/unit/compute/v2/test_limits.py | 1 + openstack/tests/unit/image/v2/test_image.py | 5 +- .../tests/unit/network/v2/test_floating_ip.py | 5 +- openstack/tests/unit/test_resource.py | 222 +++++++++++++++--- openstack/tests/unit/test_utils.py | 30 +++ openstack/utils.py | 32 +++ 7 files changed, 335 insertions(+), 43 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 9742d212d..cc8371049 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -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 diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 81ded5765..90144875a 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -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) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index a898b096a..d1367f3d0 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -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) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 2c80684d5..b708dd5bc 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -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) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1f7eff912..be5e18c26 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -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) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index aa947ffa1..8b7e5dc81 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -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')) diff --git a/openstack/utils.py b/openstack/utils.py index 4f67f3603..fafa47e92 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -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)