Facilitate latest Rest API use
In order to provide insight into the remote API verison, we need the ability to negotiate upon the latest API version available, and then report what that version is. In order to understand if this has occured, we also need to provide insight into if version negotiation has occured. Adds logic to the session/http clients to faciltate version negotiation on the latest available version, and provide user insight into that verison. Change-Id: I813237eee4b122211f95558f677b25e0675569d5 Related-Bug: #1739440 Related-Bug: #1671145
This commit is contained in:
parent
a91f2e4623
commit
5b01c8f2ba
@ -66,6 +66,11 @@ def get_client(api_version, os_auth_token=None, ironic_url=None,
|
||||
:param ignored_kwargs: all the other params that are passed. Left for
|
||||
backwards compatibility. They are ignored.
|
||||
"""
|
||||
# TODO(TheJulia): At some point, we should consider possibly noting
|
||||
# the "latest" flag for os_ironic_api_version to cause the client to
|
||||
# auto-negotiate to the greatest available version, however we do not
|
||||
# have the ability yet for a caller to cap the version, and will hold
|
||||
# off doing so until then.
|
||||
os_service_type = os_service_type or 'baremetal'
|
||||
os_endpoint_type = os_endpoint_type or 'publicURL'
|
||||
project_id = (os_project_id or os_tenant_id)
|
||||
|
@ -44,7 +44,8 @@ from ironicclient import exc
|
||||
# http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa
|
||||
# for full details.
|
||||
DEFAULT_VER = '1.9'
|
||||
|
||||
LAST_KNOWN_API_VERSION = 35
|
||||
LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-ironicclient'
|
||||
@ -98,6 +99,17 @@ class VersionNegotiationMixin(object):
|
||||
param conn: A connection object
|
||||
param resp: The response object from http request
|
||||
"""
|
||||
def _query_server(conn):
|
||||
if (self.os_ironic_api_version and
|
||||
self.os_ironic_api_version != 'latest'):
|
||||
base_version = ("/v%s" %
|
||||
str(self.os_ironic_api_version).split('.')[0])
|
||||
else:
|
||||
base_version = API_VERSION
|
||||
return self._make_simple_request(conn, 'GET', base_version)
|
||||
|
||||
if not resp:
|
||||
resp = _query_server(conn)
|
||||
if self.api_version_select_state not in API_VERSION_SELECTED_STATES:
|
||||
raise RuntimeError(
|
||||
_('Error: self.api_version_select_state should be one of the '
|
||||
@ -110,22 +122,30 @@ class VersionNegotiationMixin(object):
|
||||
# the supported version range
|
||||
if not max_ver:
|
||||
LOG.debug('No version header in response, requesting from server')
|
||||
if self.os_ironic_api_version:
|
||||
base_version = ("/v%s" %
|
||||
str(self.os_ironic_api_version).split('.')[0])
|
||||
else:
|
||||
base_version = API_VERSION
|
||||
resp = self._make_simple_request(conn, 'GET', base_version)
|
||||
resp = _query_server(conn)
|
||||
min_ver, max_ver = self._parse_version_headers(resp)
|
||||
# Reset the maximum version that we permit
|
||||
if StrictVersion(max_ver) > StrictVersion(LATEST_VERSION):
|
||||
LOG.debug("Remote API version %(max_ver)s is greater than the "
|
||||
"version supported by ironicclient. Maximum available "
|
||||
"version is %(client_ver)s",
|
||||
{'max_ver': max_ver,
|
||||
'client_ver': LATEST_VERSION})
|
||||
max_ver = LATEST_VERSION
|
||||
|
||||
# If the user requested an explicit version or we have negotiated a
|
||||
# version and still failing then error now. The server could
|
||||
# support the version requested but the requested operation may not
|
||||
# be supported by the requested version.
|
||||
if self.api_version_select_state == 'user':
|
||||
# TODO(TheJulia): We should break this method into several parts,
|
||||
# such as a sanity check/error method.
|
||||
if (self.api_version_select_state == 'user' and
|
||||
self.os_ironic_api_version != 'latest'):
|
||||
raise exc.UnsupportedVersion(textwrap.fill(
|
||||
_("Requested API version %(req)s is not supported by the "
|
||||
"server or the requested operation is not supported by the "
|
||||
"requested version. Supported version range is %(min)s to "
|
||||
"server, client, or the requested operation is not "
|
||||
"supported by the requested version."
|
||||
"Supported version range is %(min)s to "
|
||||
"%(max)s")
|
||||
% {'req': self.os_ironic_api_version,
|
||||
'min': min_ver, 'max': max_ver}))
|
||||
@ -137,8 +157,11 @@ class VersionNegotiationMixin(object):
|
||||
% {'req': self.os_ironic_api_version,
|
||||
'min': min_ver, 'max': max_ver}))
|
||||
|
||||
negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version),
|
||||
StrictVersion(max_ver)))
|
||||
if self.os_ironic_api_version == 'latest':
|
||||
negotiated_ver = max_ver
|
||||
else:
|
||||
negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version),
|
||||
StrictVersion(max_ver)))
|
||||
if StrictVersion(negotiated_ver) < StrictVersion(min_ver):
|
||||
negotiated_ver = min_ver
|
||||
# server handles microversions, but doesn't support
|
||||
@ -310,6 +333,13 @@ class HTTPClient(VersionNegotiationMixin):
|
||||
Wrapper around request.Session.request to handle tasks such
|
||||
as setting headers and error handling.
|
||||
"""
|
||||
# NOTE(TheJulia): self.os_ironic_api_version is reset in
|
||||
# the self.negotiate_version() call if negotiation occurs.
|
||||
if (self.os_ironic_api_version and
|
||||
self.api_version_select_state == 'user' and
|
||||
self.os_ironic_api_version == 'latest'):
|
||||
self.negotiate_version(self.session, None)
|
||||
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
||||
@ -517,6 +547,14 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
|
||||
|
||||
@with_retries
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
|
||||
# NOTE(TheJulia): self.os_ironic_api_version is reset in
|
||||
# the self.negotiate_version() call if negotiation occurs.
|
||||
if (self.os_ironic_api_version and
|
||||
self.api_version_select_state == 'user' and
|
||||
self.os_ironic_api_version == 'latest'):
|
||||
self.negotiate_version(self.session, None)
|
||||
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('auth', self.auth)
|
||||
if isinstance(self.endpoint_override, six.string_types):
|
||||
|
@ -19,6 +19,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from ironicclient.common import http
|
||||
from osc_lib import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -26,8 +27,13 @@ LOG = logging.getLogger(__name__)
|
||||
CLIENT_CLASS = 'ironicclient.v1.client.Client'
|
||||
API_VERSION_OPTION = 'os_baremetal_api_version'
|
||||
API_NAME = 'baremetal'
|
||||
LAST_KNOWN_API_VERSION = 35
|
||||
LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION)
|
||||
# NOTE(TheJulia) Latest known version tracking has been moved
|
||||
# to the ironicclient/common/http.py file as the OSC committment
|
||||
# is latest known, and we should only store it in one location.
|
||||
LAST_KNOWN_API_VERSION = http.LAST_KNOWN_API_VERSION
|
||||
LATEST_VERSION = http.LATEST_VERSION
|
||||
|
||||
|
||||
API_VERSIONS = {
|
||||
'1.%d' % i: CLIENT_CLASS
|
||||
for i in range(1, LAST_KNOWN_API_VERSION + 1)
|
||||
|
@ -200,6 +200,29 @@ class VersionNegotiationMixinTest(utils.BaseTestCase):
|
||||
mock_save_data.assert_called_once_with(host=host, port=port,
|
||||
data=max_ver)
|
||||
|
||||
@mock.patch.object(filecache, 'save_data', autospec=True)
|
||||
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
||||
autospec=True)
|
||||
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
||||
autospec=True)
|
||||
def test_negotiate_version_server_user_latest(
|
||||
self, mock_pvh, mock_msr, mock_save_data):
|
||||
# have to retry with simple get
|
||||
mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')])
|
||||
mock_conn = mock.MagicMock()
|
||||
self.test_object.api_version_select_state = 'user'
|
||||
self.test_object.os_ironic_api_version = 'latest'
|
||||
result = self.test_object.negotiate_version(mock_conn, None)
|
||||
self.assertEqual(http.LATEST_VERSION, result)
|
||||
self.assertEqual('negotiated',
|
||||
self.test_object.api_version_select_state)
|
||||
self.assertEqual(http.LATEST_VERSION,
|
||||
self.test_object.os_ironic_api_version)
|
||||
|
||||
self.assertTrue(mock_msr.called)
|
||||
self.assertEqual(2, mock_pvh.call_count)
|
||||
self.assertEqual(1, mock_save_data.call_count)
|
||||
|
||||
def test_get_server(self):
|
||||
host = 'ironic-host'
|
||||
port = '6385'
|
||||
|
@ -59,6 +59,9 @@ class ClientTest(utils.BaseTestCase):
|
||||
region_name=kwargs.get('os_region_name'))
|
||||
if 'os_ironic_api_version' in kwargs:
|
||||
self.assertEqual(0, mock_retrieve_data.call_count)
|
||||
self.assertEqual(kwargs['os_ironic_api_version'],
|
||||
client.current_api_version)
|
||||
self.assertFalse(client.is_api_version_negotiated)
|
||||
else:
|
||||
mock_retrieve_data.assert_called_once_with(
|
||||
host='localhost',
|
||||
|
@ -49,6 +49,16 @@ class ClientTest(utils.BaseTestCase):
|
||||
os_ironic_api_version=os_ironic_api_version,
|
||||
api_version_select_state='default')
|
||||
|
||||
def test_client_user_api_version_latest_with_downgrade(self,
|
||||
http_client_mock):
|
||||
endpoint = 'http://ironic:6385'
|
||||
token = 'safe_token'
|
||||
os_ironic_api_version = 'latest'
|
||||
|
||||
self.assertRaises(ValueError, client.Client, endpoint,
|
||||
token=token, allow_api_version_downgrade=True,
|
||||
os_ironic_api_version=os_ironic_api_version)
|
||||
|
||||
@mock.patch.object(filecache, 'retrieve_data', autospec=True)
|
||||
def test_client_cache_api_version(self, cache_mock, http_client_mock):
|
||||
endpoint = 'http://ironic:6385'
|
||||
@ -93,3 +103,19 @@ class ClientTest(utils.BaseTestCase):
|
||||
self.assertIsInstance(cl.port, client.port.PortManager)
|
||||
self.assertIsInstance(cl.driver, client.driver.DriverManager)
|
||||
self.assertIsInstance(cl.chassis, client.chassis.ChassisManager)
|
||||
|
||||
def test_negotiate_api_version(self, http_client_mock):
|
||||
endpoint = 'http://ironic:6385'
|
||||
token = 'safe_token'
|
||||
os_ironic_api_version = 'latest'
|
||||
cl = client.Client(endpoint, token=token,
|
||||
os_ironic_api_version=os_ironic_api_version)
|
||||
|
||||
cl.negotiate_api_version()
|
||||
http_client_mock.assert_called_once_with(
|
||||
endpoint, api_version_select_state='user',
|
||||
os_ironic_api_version='latest', token=token)
|
||||
# TODO(TheJulia): We should verify that negotiate_version
|
||||
# is being called in the client and returns a version,
|
||||
# although mocking might need to be restrutured to
|
||||
# properly achieve that.
|
||||
|
@ -41,7 +41,17 @@ class Client(object):
|
||||
"""Initialize a new client for the Ironic v1 API."""
|
||||
allow_downgrade = kwargs.pop('allow_api_version_downgrade', False)
|
||||
if kwargs.get('os_ironic_api_version'):
|
||||
# TODO(TheJulia): We should sanity check os_ironic_api_version
|
||||
# against our maximum suported version, so the client fails
|
||||
# immediately upon an unsupported version being provided.
|
||||
# This logic should also likely live in common/http.py
|
||||
if allow_downgrade:
|
||||
if kwargs['os_ironic_api_version'] == 'latest':
|
||||
raise ValueError(
|
||||
"Invalid configuration defined. "
|
||||
"The os_ironic_api_versioncan not be set "
|
||||
"to 'latest' while allow_api_version_downgrade "
|
||||
"is set.")
|
||||
# NOTE(dtantsur): here we allow the HTTP client to negotiate a
|
||||
# lower version if the requested is too high
|
||||
kwargs['api_version_select_state'] = "default"
|
||||
@ -76,3 +86,26 @@ class Client(object):
|
||||
self.http_client)
|
||||
self.driver = driver.DriverManager(self.http_client)
|
||||
self.portgroup = portgroup.PortgroupManager(self.http_client)
|
||||
|
||||
@property
|
||||
def current_api_version(self):
|
||||
"""Return the current API version in use.
|
||||
|
||||
This returns the version of the REST API that the API client
|
||||
is presently set to request. This value may change as a result
|
||||
of API version negotiation.
|
||||
"""
|
||||
return self.http_client.os_ironic_api_version
|
||||
|
||||
@property
|
||||
def is_api_version_negotiated(self):
|
||||
"""Returns True if microversion negotiation has occured."""
|
||||
return self.http_client.api_version_select_state == 'negotiated'
|
||||
|
||||
def negotiate_api_version(self):
|
||||
"""Triggers negotiation with the remote API endpoint.
|
||||
|
||||
:returns: the negotiated API version.
|
||||
"""
|
||||
return self.http_client.negotiate_version(
|
||||
self.http_client.session, None)
|
||||
|
@ -0,0 +1,26 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Allows a python API user to pass ``latest`` to the client creation request
|
||||
for the ``os_ironic_api_version`` parameter. The version utilized for REST
|
||||
API requests will, as a result, be the highest available version
|
||||
understood by both the ironicclient library and the server.
|
||||
- |
|
||||
Adds base client properties to provide insight to a python API user of
|
||||
what the current REST API version that will be utilized, and if API
|
||||
version negotiation has occured.
|
||||
These new properties are ``client.current_api_version`` and
|
||||
``client.is_api_version_negotiated`` respectively.
|
||||
- |
|
||||
Adds additional base client method to allow a python API user to trigger
|
||||
version negotiation and return the negotiated version. This new method is
|
||||
``client.negotiate_api_version()``.
|
||||
other:
|
||||
- |
|
||||
The maximum supported version supported for negotiation is now defined
|
||||
in the ``common/http.py`` file. Any new feature added to the API client
|
||||
library must increment this version.
|
||||
- |
|
||||
The maximum known version supported by the ``OpenStackClient`` plugin is
|
||||
now defined by the maximum supported version for API negotiation as
|
||||
defined in the ``common/http.py`` file.
|
Loading…
Reference in New Issue
Block a user