Add ability to specify a microversion in a request
The user now has the ability to know what microversions are available, but needs to be able to send a microversion header with their request. Add a microversion parameter to Session that will construct and send the header. The microversion header requires a service_type. One should be available but it's possible for it to be missing if someone is using an endpoint_override. Provide a parameter to let the user specify a service_type for the microversion call in such cases. Change-Id: I63cdd67701749630228f9496eda82b3c8747a608
This commit is contained in:
parent
24b09f4088
commit
218adc333e
@ -365,6 +365,25 @@ If the user knows a service does not support microversions and is merely
|
||||
curious as to which major version was discovered, `discover_versions` can be
|
||||
set to `False` to prevent fetching microversion metadata.
|
||||
|
||||
Requesting a Microversion
|
||||
-------------------------
|
||||
|
||||
A user who wants to specify a microversion for a given request can pass it to
|
||||
the ``microversion`` parameter of the `request` method on the
|
||||
:class:`keystoneauth1.session.Session` object, or the
|
||||
:class:`keystoneauth1.adapter.Adapter` object. This will cause `keystoneauth`
|
||||
to pass the appropriate header to the service informing the service of the
|
||||
microversion the user wants.
|
||||
|
||||
If the user is using a :class:`keystoneauth1.adapter.Adapter`, the
|
||||
`service_type`, which is a part of the data sent in the microversion header,
|
||||
will be taken from the Adapter's `service_type`.
|
||||
|
||||
If the user is using a :class:`keystoneauth1.session.Session`, the
|
||||
`service_type` will be taken from the `service_type` in `endpoint_filter`
|
||||
or alternately from the parameter `microversion_service_type` in case there
|
||||
is no `service_type` in `endpoint_filter` for some reason.
|
||||
|
||||
.. _API-WG Specs: http://specs.openstack.org/openstack/api-wg/
|
||||
.. _Consuming the Catalog: http://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html
|
||||
.. _Microversions: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery
|
||||
|
@ -27,8 +27,8 @@ class Adapter(object):
|
||||
particular client that is using the session. An adapter provides a wrapper
|
||||
of client local data around the global session object.
|
||||
|
||||
version, min_version and max_version can all be given either as a
|
||||
string or a tuple.
|
||||
version, min_version, max_version and default_microversion can all be
|
||||
given either as a string or a tuple.
|
||||
|
||||
:param session: The session object to wrap.
|
||||
:type session: keystoneauth1.session.Session
|
||||
@ -77,6 +77,11 @@ class Adapter(object):
|
||||
:param max_version: The maximum major version of a given API, intended to
|
||||
be used as the upper bound of a range with min_version.
|
||||
Mutually exclusive with version. (optional)
|
||||
:param default_microversion: The default microversion value to send
|
||||
with API requests. While microversions are
|
||||
a per-request feature, a user may know they
|
||||
want to default to sending a specific value.
|
||||
(optional)
|
||||
"""
|
||||
|
||||
client_name = None
|
||||
@ -90,7 +95,8 @@ class Adapter(object):
|
||||
additional_headers=None, client_name=None,
|
||||
client_version=None, allow_version_hack=None,
|
||||
global_request_id=None,
|
||||
min_version=None, max_version=None):
|
||||
min_version=None, max_version=None,
|
||||
default_microversion=None):
|
||||
if version and (min_version or max_version):
|
||||
raise TypeError(
|
||||
"version is mutually exclusive with min_version and"
|
||||
@ -116,6 +122,7 @@ class Adapter(object):
|
||||
self.allow_version_hack = allow_version_hack
|
||||
self.min_version = min_version
|
||||
self.max_version = max_version
|
||||
self.default_microversion = default_microversion
|
||||
|
||||
self.global_request_id = global_request_id
|
||||
|
||||
@ -159,6 +166,8 @@ class Adapter(object):
|
||||
kwargs.setdefault('logger', self.logger)
|
||||
if self.allow:
|
||||
kwargs.setdefault('allow', self.allow)
|
||||
if self.default_microversion is not None:
|
||||
kwargs.setdefault('microversion', self.default_microversion)
|
||||
|
||||
if isinstance(self.session, (session.Session, Adapter)):
|
||||
# these things are unsupported by keystoneclient's session so be
|
||||
|
@ -29,6 +29,7 @@ from six.moves import urllib
|
||||
|
||||
import keystoneauth1
|
||||
from keystoneauth1 import _utils as utils
|
||||
from keystoneauth1 import discover
|
||||
from keystoneauth1 import exceptions
|
||||
|
||||
try:
|
||||
@ -65,6 +66,22 @@ def _construct_session(session_obj=None):
|
||||
return session_obj
|
||||
|
||||
|
||||
def _mv_legacy_headers_for_service(mv_service_type):
|
||||
"""Workaround for services that predate standardization.
|
||||
|
||||
TODO(sdague): eventually convert this to using os-service-types
|
||||
and put the logic there. However, right now this is so little
|
||||
logic, inlining it for release is a better call.
|
||||
|
||||
"""
|
||||
headers = []
|
||||
if mv_service_type == "compute":
|
||||
headers.append("X-OpenStack-Nova-API-Version")
|
||||
elif mv_service_type == "baremetal":
|
||||
headers.append("X-OpenStack-Ironic-API-Version")
|
||||
return headers
|
||||
|
||||
|
||||
class _JSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, o):
|
||||
@ -405,13 +422,57 @@ class Session(object):
|
||||
|
||||
logger.debug(' '.join(string_parts))
|
||||
|
||||
@staticmethod
|
||||
def _set_microversion_headers(
|
||||
headers, microversion, service_type, endpoint_filter):
|
||||
# We're converting it to normalized version number for two reasons.
|
||||
# First, to validate it's a real version number. Second, so that in
|
||||
# the future we can pre-validate that it is within the range of
|
||||
# available microversions before we send the request.
|
||||
# TODO(mordred) Validate when we get the response back that
|
||||
# the server executed in the microversion we expected.
|
||||
# TODO(mordred) Validate that the requested microversion works
|
||||
# with the microversion range we found in discovery.
|
||||
microversion = discover.normalize_version_number(microversion)
|
||||
microversion = discover.version_to_string(microversion)
|
||||
if not service_type:
|
||||
if endpoint_filter and 'service_type' in endpoint_filter:
|
||||
service_type = endpoint_filter['service_type']
|
||||
else:
|
||||
raise TypeError(
|
||||
"microversion {microversion} was requested but no"
|
||||
" service_type information is available. Either provide a"
|
||||
" service_type in endpoint_filter or pass"
|
||||
" microversion_service_type as an argument.".format(
|
||||
microversion=discover.version_to_string(microversion)))
|
||||
|
||||
# TODO(mordred) cinder uses volume in its microversion header. This
|
||||
# logic should be handled in the future by os-service-types but for
|
||||
# now hard-code for cinder.
|
||||
if (service_type.startswith('volume')
|
||||
or service_type == 'block-storage'):
|
||||
service_type = 'volume'
|
||||
# TODO(mordred) Fix this as part of the service-types generalized
|
||||
# fix. Ironic does not yet support the new version header
|
||||
if service_type != 'baremetal':
|
||||
headers.setdefault('OpenStack-API-Version',
|
||||
'{service_type} {microversion}'.format(
|
||||
service_type=service_type,
|
||||
microversion=microversion))
|
||||
header_names = _mv_legacy_headers_for_service(service_type)
|
||||
for h in header_names:
|
||||
headers.setdefault(h, microversion)
|
||||
return headers
|
||||
|
||||
@positional()
|
||||
def request(self, url, method, json=None, original_ip=None,
|
||||
user_agent=None, redirect=None, authenticated=None,
|
||||
endpoint_filter=None, auth=None, requests_auth=None,
|
||||
raise_exc=True, allow_reauth=True, log=True,
|
||||
endpoint_override=None, connect_retries=0, logger=_logger,
|
||||
allow={}, client_name=None, client_version=None, **kwargs):
|
||||
allow={}, client_name=None, client_version=None,
|
||||
microversion=None, microversion_service_type=None,
|
||||
**kwargs):
|
||||
"""Send an HTTP request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
@ -479,6 +540,16 @@ class Session(object):
|
||||
:type logger: logging.Logger
|
||||
:param dict allow: Extra filters to pass when discovering API
|
||||
versions. (optional)
|
||||
:param microversion: Microversion to send for this request.
|
||||
microversion can be given as a string or a tuple.
|
||||
(optional)
|
||||
:param str microversion_service_type: The service_type to be sent in
|
||||
the microversion header, if a microversion is given.
|
||||
Defaults to the value of service_type from
|
||||
endpoint_filter if one exists. If endpoint_filter
|
||||
does not have a service_type, microversion is given and
|
||||
microversion_service_type is not provided, an exception
|
||||
will be raised.
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
:meth:`requests.Session.request` (such as `headers`).
|
||||
Except:
|
||||
@ -494,6 +565,10 @@ class Session(object):
|
||||
:returns: The response to the request.
|
||||
"""
|
||||
headers = kwargs.setdefault('headers', dict())
|
||||
if microversion:
|
||||
self._set_microversion_headers(
|
||||
headers, microversion, microversion_service_type,
|
||||
endpoint_filter)
|
||||
|
||||
if authenticated is None:
|
||||
authenticated = bool(auth or self.auth)
|
||||
|
@ -105,6 +105,85 @@ class SessionTests(utils.TestCase):
|
||||
self.assertEqual(resp.text, 'response')
|
||||
self.assertRequestBodyIs(json={'hello': 'world'})
|
||||
|
||||
def test_set_microversion_headers(self):
|
||||
|
||||
# String microversion, specified service type
|
||||
headers = {}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, '2.30', 'compute', None)
|
||||
self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30')
|
||||
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30')
|
||||
self.assertEqual(len(headers.keys()), 2)
|
||||
|
||||
# Tuple microversion, service type via endpoint_filter
|
||||
headers = {}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, (2, 30), None, {'service_type': 'compute'})
|
||||
self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30')
|
||||
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30')
|
||||
self.assertEqual(len(headers.keys()), 2)
|
||||
|
||||
# ironic microversion, specified service type
|
||||
headers = {}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, '2.30', 'baremetal', None)
|
||||
self.assertEqual(headers['X-OpenStack-Ironic-API-Version'], '2.30')
|
||||
self.assertEqual(len(headers.keys()), 1)
|
||||
|
||||
# volumev2 service-type - volume microversion
|
||||
headers = {}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, (2, 30), None, {'service_type': 'volumev2'})
|
||||
self.assertEqual(headers['OpenStack-API-Version'], 'volume 2.30')
|
||||
self.assertEqual(len(headers.keys()), 1)
|
||||
|
||||
# block-storage service-type - volume microversion
|
||||
headers = {}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, (2, 30), None, {'service_type': 'block-storage'})
|
||||
self.assertEqual(headers['OpenStack-API-Version'], 'volume 2.30')
|
||||
self.assertEqual(len(headers.keys()), 1)
|
||||
|
||||
# Headers already exist - no change
|
||||
headers = {
|
||||
'OpenStack-API-Version': 'compute 2.30',
|
||||
'X-OpenStack-Nova-API-Version': '2.30',
|
||||
}
|
||||
client_session.Session._set_microversion_headers(
|
||||
headers, (2, 31), None, {'service_type': 'volume'})
|
||||
self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30')
|
||||
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30')
|
||||
|
||||
# Normalization error
|
||||
self.assertRaises(TypeError,
|
||||
client_session.Session._set_microversion_headers,
|
||||
{}, 'bogus', 'service_type', None)
|
||||
# No service type in param or endpoint filter
|
||||
self.assertRaises(TypeError,
|
||||
client_session.Session._set_microversion_headers,
|
||||
{}, (2, 30), None, None)
|
||||
self.assertRaises(TypeError,
|
||||
client_session.Session._set_microversion_headers,
|
||||
{}, (2, 30), None, {'no_service_type': 'here'})
|
||||
|
||||
def test_microversion(self):
|
||||
# microversion not specified
|
||||
session = client_session.Session()
|
||||
self.stub_url('GET', text='response')
|
||||
resp = session.get(self.TEST_URL)
|
||||
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestNotInHeader('OpenStack-API-Version')
|
||||
|
||||
session = client_session.Session()
|
||||
self.stub_url('GET', text='response')
|
||||
resp = session.get(self.TEST_URL, microversion='2.30',
|
||||
microversion_service_type='compute',
|
||||
endpoint_filter={'endpoint': 'filter'})
|
||||
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('OpenStack-API-Version', 'compute 2.30')
|
||||
|
||||
def test_user_agent(self):
|
||||
session = client_session.Session()
|
||||
self.stub_url('GET', text='response')
|
||||
@ -1279,6 +1358,29 @@ class AdapterTest(utils.TestCase):
|
||||
last_token = self.requests_mock.last_request.headers['X-Auth-Token']
|
||||
self.assertEqual(token, last_token)
|
||||
|
||||
def test_default_microversion(self):
|
||||
sess = client_session.Session()
|
||||
url = 'http://url'
|
||||
|
||||
def validate(adap_kwargs, get_kwargs, exp_kwargs):
|
||||
with mock.patch.object(sess, 'request') as m:
|
||||
adapter.Adapter(sess, **adap_kwargs).get(url, **get_kwargs)
|
||||
m.assert_called_once_with(url, 'GET', endpoint_filter={},
|
||||
**exp_kwargs)
|
||||
|
||||
# No default_microversion in Adapter, no microversion in get()
|
||||
validate({}, {}, {})
|
||||
|
||||
# default_microversion in Adapter, no microversion in get()
|
||||
validate({'default_microversion': '1.2'}, {}, {'microversion': '1.2'})
|
||||
|
||||
# No default_microversion in Adapter, microversion specified in get()
|
||||
validate({}, {'microversion': '1.2'}, {'microversion': '1.2'})
|
||||
|
||||
# microversion in get() overrides default_microversion in Adapter
|
||||
validate({'default_microversion': '1.2'}, {'microversion': '1.5'},
|
||||
{'microversion': '1.5'})
|
||||
|
||||
|
||||
class TCPKeepAliveAdapterTest(utils.TestCase):
|
||||
|
||||
|
@ -109,6 +109,14 @@ class TestCase(testtools.TestCase):
|
||||
headers = self.requests_mock.last_request.headers
|
||||
self.assertEqual(headers.get(name), val)
|
||||
|
||||
def assertRequestNotInHeader(self, name):
|
||||
"""Verify that the last request made does not contain a header key.
|
||||
|
||||
The request must have already been made.
|
||||
"""
|
||||
headers = self.requests_mock.last_request.headers
|
||||
self.assertNotIn(name, headers)
|
||||
|
||||
|
||||
class TestResponse(requests.Response):
|
||||
"""Class used to wrap requests.Response.
|
||||
|
Loading…
x
Reference in New Issue
Block a user