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:
parent
ee98b03938
commit
8ac8c8627c
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue