Support auto-negotiated microversion

To improve the usability for those who are not familiar with
microversion, this patch adds microversion negotiation to pick up max
microversion supported both by client and by server.

This negotiation is enabled by specifying only major api version,
``--os-placement-api-version 1``. Note that the default microversion
remains to be ``1.0`` in this patch and will be switched to the
negotiated version in the following patch.

Change-Id: I2998ff0f3941bf226a942969adf75564a8d5a065
Story: #2005448
Task: #30497
This commit is contained in:
Tetsuro Nakamura 2019-11-22 12:08:41 +00:00
parent ee98b03938
commit 8ac8c8627c
5 changed files with 122 additions and 4 deletions

View File

@ -11,12 +11,15 @@
# under the License.
import contextlib
import logging
import keystoneauth1.exceptions.http as ks_exceptions
import osc_lib.exceptions as exceptions
import simplejson as json
import six
from osc_placement import version
_http_error_to_exc = {
cls.http_status: cls
@ -24,6 +27,9 @@ _http_error_to_exc = {
}
LOG = logging.getLogger(__name__)
@contextlib.contextmanager
def _wrap_http_exceptions():
"""Reraise osc-lib exceptions with detailed messages."""
@ -45,7 +51,7 @@ class SessionClient(object):
def __init__(self, session, ks_filter, api_version='1.0'):
self.session = session
self.ks_filter = ks_filter
self.api_version = api_version
self.negotiate_api_version(api_version)
def request(self, method, url, **kwargs):
version = kwargs.pop('version', None)
@ -60,3 +66,22 @@ class SessionClient(object):
headers=headers,
endpoint_filter=self.ks_filter,
**kwargs)
def negotiate_api_version(self, api_version):
"""Set api_version to self.
If negotiate version (only majorversion) is given, talk to server to
pick up max microversion supported both by client and by server.
"""
if api_version not in version.NEGOTIATE_VERSIONS:
self.api_version = api_version
return
client_ver = version.MAX_VERSION_NO_GAP
self.api_version = client_ver
resp = self.request('GET', '/', raise_exc=False)
if resp.status_code == 406:
server_ver = resp.json()['errors'][0]['max_version']
self.api_version = server_ver
LOG.debug('Microversion %s not supported in server. '
'Falling back to microversion %s',
client_ver, server_ver)

View File

@ -38,13 +38,13 @@ def make_client(instance):
'interface': instance.interface}
LOG.debug('Instantiating placement client: %s', client_class)
# TODO(rpodolyaka): add version negotiation
return client_class(session=instance.session,
ks_filter=ks_filter,
api_version=instance._api_version[API_NAME])
def build_option_parser(parser):
# ToDo(tetsuro): Make default a negotiate version
default = version.SUPPORTED_VERSIONS[0]
parser.add_argument(
'--os-placement-api-version',

View File

@ -17,9 +17,23 @@ import six
import keystoneauth1.exceptions.http as ks_exceptions
import osc_lib.exceptions as exceptions
import oslotest.base as base
import requests
import simplejson as json
import osc_placement.http as http
from osc_placement import http
from osc_placement import version
from oslo_serialization import jsonutils
class FakeResponse(requests.Response):
def __init__(self, status_code, content=None, headers=None):
super(FakeResponse, self).__init__()
self.status_code = status_code
if content:
self._content = content
if headers:
self.headers = headers
class TestSessionClient(base.BaseTestCase):
@ -50,3 +64,54 @@ class TestSessionClient(base.BaseTestCase):
exc = self.assertRaises(ks_exceptions.InternalServerError, go)
self.assertEqual(500, exc.http_status)
self.assertIn('Internal Server Error (HTTP 500)', six.text_type(exc))
def test_session_client_version(self):
session = mock.Mock()
ks_filter = {'service_type': 'placement',
'region_name': 'mock_region',
'interface': 'mock_interface'}
# 1. target to a specific version
target_version = '1.23'
client = http.SessionClient(
session, ks_filter, api_version=target_version)
self.assertEqual(client.api_version, target_version)
# validate that the server side is not called
session.request.assert_not_called()
# 2. negotiation succeeds and have the client's highest version
target_version = '1'
session.request.return_value = FakeResponse(200)
client = http.SessionClient(
session, ks_filter, api_version=target_version)
self.assertEqual(client.api_version, version.MAX_VERSION_NO_GAP)
# validate that the server side is called
expected_version = 'placement ' + version.MAX_VERSION_NO_GAP
expected_headers = {'OpenStack-API-Version': expected_version,
'Accept': 'application/json'}
session.request.assert_called_once_with(
'/', 'GET', endpoint_filter=ks_filter,
headers=expected_headers, raise_exc=False)
session.reset_mock()
# 3. negotiation fails and get the servers's highest version
mock_server_version = '1.10'
json_mock = {
"errors": [{"status": 406,
"title": "Not Acceptable",
"min_version": "1.0",
"max_version": mock_server_version}]
}
session.request.return_value = FakeResponse(
406, content=jsonutils.dump_as_bytes(json_mock))
client = http.SessionClient(
session, ks_filter, api_version=target_version)
self.assertEqual(client.api_version, mock_server_version)
# validate that the server side is called
session.request.assert_called_once_with(
'/', 'GET', endpoint_filter=ks_filter,
headers=expected_headers, raise_exc=False)

View File

@ -102,3 +102,22 @@ class TestVersion(base.BaseTestCase):
ValueError,
'Operation or argument is not supported',
t.check_version, version.lt('1.2'))
def test_max_version_consistency(self):
def _convert_to_tuple(str):
return tuple(map(int, str.split(".")))
versions = [
_convert_to_tuple(ver) for ver in version.SUPPORTED_MICROVERSIONS]
max_ver = _convert_to_tuple(version.MAX_VERSION_NO_GAP)
there_is_gap = False
for i in range(len(versions) - 1):
j = i + 1
if versions[j][1] - versions[i][1] != 1:
there_is_gap = True
self.assertEqual(max_ver, versions[i])
break
if not there_is_gap:
self.assertEqual(max_ver, versions[-1])

View File

@ -14,7 +14,10 @@ from distutils.version import StrictVersion
import operator
SUPPORTED_VERSIONS = [
NEGOTIATE_VERSIONS = [
'1', # Added for auto choice for appropriate version
]
SUPPORTED_MICROVERSIONS = [
'1.0',
'1.1',
'1.2',
@ -46,6 +49,12 @@ SUPPORTED_VERSIONS = [
'1.28', # Added for provider allocation (un)set (Ussuri)
'1.29',
]
SUPPORTED_VERSIONS = SUPPORTED_MICROVERSIONS + NEGOTIATE_VERSIONS
# The max microversion lower than which are all supported by this client.
# This is used to automatically pick up the microversion to use. Change this
# when you add a microversion to the `_SUPPORTED_VERSIONS` without a gap.
# TestVersion.test_max_version_consistency checks its consistency.
MAX_VERSION_NO_GAP = '1.29'
def _op(func, b, msg):