From 0fa07d01c581ecfe53a701c26e365b62bd47b411 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Aug 2017 09:25:23 -0500 Subject: [PATCH] Add method to get the api major version Similar to get_endpoint, which knows it doesn't need full endpoint_data, if a user just wants to know what major version the discovery process wound up with, there are cases in which we do not need to fetch discovery documents. Provide an API call that a user can use when this is the information they need to avoid them having to play games with discover_versions settings. Change-Id: I204a45d1d139a90176bcc2ef8d46decd09b2cd5b --- keystoneauth1/adapter.py | 19 +++ keystoneauth1/identity/base.py | 129 ++++++++++++++++++ keystoneauth1/session.py | 16 +++ .../unit/identity/test_identity_common.py | 34 +++++ 4 files changed, 198 insertions(+) diff --git a/keystoneauth1/adapter.py b/keystoneauth1/adapter.py index eb7589b2..115ac6dd 100644 --- a/keystoneauth1/adapter.py +++ b/keystoneauth1/adapter.py @@ -246,6 +246,25 @@ class Adapter(object): return self.session.get_endpoint_data(auth or self.auth, **kwargs) + def get_api_major_version(self, auth=None, **kwargs): + """Get the major API version as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin on + the session. (optional) + :type auth: keystoneauth1.plugin.BaseAuthPlugin + + :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a + plugin is not available. + + :return: The major version of the API of the service discovered. + :rtype: tuple or None + """ + self._set_endpoint_filter_kwargs(kwargs) + if self.endpoint_override: + kwargs['endpoint_override'] = self.endpoint_override + + return self.session.get_api_major_version(auth or self.auth, **kwargs) + def invalidate(self, auth=None): """Invalidate an authentication plugin.""" return self.session.invalidate(auth or self.auth) diff --git a/keystoneauth1/identity/base.py b/keystoneauth1/identity/base.py index 3d70c1d8..5fbd6459 100644 --- a/keystoneauth1/identity/base.py +++ b/keystoneauth1/identity/base.py @@ -12,6 +12,7 @@ import abc import base64 +import functools import hashlib import json import threading @@ -380,6 +381,134 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin): allow_version_hack=allow_version_hack, **kwargs) return endpoint_data.url if endpoint_data else None + def get_api_major_version(self, session, service_type=None, interface=None, + region_name=None, service_name=None, + version=None, allow=None, + allow_version_hack=True, skip_discovery=False, + discover_versions=False, min_version=None, + max_version=None, **kwargs): + """Return the major API version for a service. + + If a valid token is not present then a new one will be fetched using + the session and kwargs. + + version, min_version and max_version can all be given either as a + string or a tuple. + + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + + :param session: A session object that can be used for communication. + :type session: keystoneauth1.session.Session + :param string service_type: The type of service to lookup the endpoint + for. This plugin will return None (failure) + if service_type is not provided. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. Can also be + `keystoneauth1.plugin.AUTH_INTERFACE` to indicate + that the auth_url should be used instead of the + value in the catalog. (optional, defaults to public) + :param string region_name: The region the endpoint should exist in. + (optional) + :param string service_name: The name of the service in the catalog. + (optional) + :param version: The minimum version number required for this + endpoint. (optional) + :param dict allow: Extra filters to pass when discovering API + versions. (optional) + :param bool allow_version_hack: Allow keystoneauth to hack up catalog + URLS to support older schemes. + (optional, default True) + :param bool skip_discovery: Whether to skip version discovery even + if a version has been given. This is useful + if endpoint_override or similar has been + given and grabbing additional information + about the endpoint is not useful. + :param bool discover_versions: Whether to get version metadata from + the version discovery document even + if it's not neccessary to fulfill the + major version request. Defaults to False + because get_endpoint doesn't need + metadata. (optional, defaults to False) + :param min_version: The minimum version that is acceptable. Mutually + exclusive with version. If min_version is given + with no max_version it is as if max version is + 'latest'. (optional) + :param max_version: The maximum version that is acceptable. Mutually + exclusive with version. If min_version is given + with no max_version it is as if max version is + 'latest'. (optional) + + :raises keystoneauth1.exceptions.http.HttpError: An error from an + invalid HTTP response. + + :return: The major version of the API of the service discovered. + :rtype: tuple or None + + .. note:: Implementation notes follow. Users should not need to wrap + their head around these implementation note. + `get_api_major_version` should do what is expected with the + least possible cost while still consistently returning a + value if possible. + + There are many cases when major version can be satisfied + without actually calling the discovery endpoint (like when the version + is in the url). If the user has a cloud with the versioned endpoint + ``https://volume.example.com/v3`` in the catalog for the + ``block-storage`` service and they do:: + + client = adapter.Adapter( + session, service_type='block-storage', min_version=2, + max_version=3) + volume_version = client.get_api_major_version() + + The version actually be returned with no api calls other than getting + the token. For that reason, :meth:`.get_api_major_version` first + calls :meth:`.get_endpoint_data` with ``discover_versions=False``. + + If their catalog has an unversioned endpoint + ``https://volume.example.com`` for the ``block-storage`` service + and they do this:: + + client = adapter.Adapter(session, service_type='block-storage') + + client is now set up to "use whatever is in the catalog". Since the + url doesn't have a version, :meth:`.get_endpoint_data` with + ``discover_versions=False`` will result in ``api_version=None``. + (No version was requested so it didn't need to do the round trip) + + In order to find out what version the endpoint actually is, we must + make a round trip. Therefore, if ``api_version`` is ``None`` after + the first call, :meth:`.get_api_major_version` will make a second + call to :meth:`.get_endpoint_data` with ``discover_versions=True``. + + """ + allow = allow or {} + # Explode `version` into min_version and max_version - everything below + # here uses the latter rather than the former. + min_version, max_version = discover._normalize_version_args( + version, min_version, max_version) + # Using functools.partial here just to reduce copy-pasta of params + get_endpoint_data = functools.partial( + self.get_endpoint_data, + session, service_type=service_type, interface=interface, + region_name=region_name, service_name=service_name, + allow=allow, min_version=min_version, max_version=max_version, + skip_discovery=skip_discovery, + allow_version_hack=allow_version_hack, **kwargs) + data = get_endpoint_data(discover_versions=discover_versions) + if (not data or not data.api_version) and not discover_versions: + # It's possible that no version was requested and the endpoint + # in the catalog has no version in the URL. A version has been + # requested, so now it's ok to run discovery. + + data = get_endpoint_data(discover_versions=True) + if not data: + return None + return data.api_version + def get_user_id(self, session, **kwargs): return self.get_access(session).user_id diff --git a/keystoneauth1/session.py b/keystoneauth1/session.py index b7e8eaf6..1be5ab34 100644 --- a/keystoneauth1/session.py +++ b/keystoneauth1/session.py @@ -963,6 +963,22 @@ class Session(object): auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_endpoint_data(self, **kwargs) + def get_api_major_version(self, auth=None, **kwargs): + """Get the major API version as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin on + the session. (optional) + :type auth: keystoneauth1.plugin.BaseAuthPlugin + + :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a + plugin is not available. + + :return: The major version of the API of the service discovered. + :rtype: tuple or None + """ + auth = self._auth_required(auth, 'determine endpoint URL') + return auth.get_api_major_version(self, **kwargs) + def get_auth_connection_params(self, auth=None, **kwargs): """Return auth connection params as provided by the auth plugin. diff --git a/keystoneauth1/tests/unit/identity/test_identity_common.py b/keystoneauth1/tests/unit/identity/test_identity_common.py index c9673fc5..5759a87c 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_common.py +++ b/keystoneauth1/tests/unit/identity/test_identity_common.py @@ -385,6 +385,40 @@ class CommonIdentityTests(object): self.assertEqual(v2_compute, url_v2) self.assertEqual(v3_compute, url_v3) + def test_discovering_version_no_discovery(self): + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + # Grab a version that can be returned without doing discovery + # This tests that it doesn't make a discovery call because we don't + # have a reqquest mock, and this will throw an exception if it tries + version = s.get_api_major_version( + service_type='volumev2', interface='admin') + self.assertEqual((2, 0), version) + + def test_discovering_version_with_discovery(self): + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + v2_compute = self.TEST_COMPUTE_ADMIN + '/v2.0' + v3_compute = self.TEST_COMPUTE_ADMIN + '/v3' + + disc = fixture.DiscoveryList(v2=False, v3=False) + disc.add_v2(v2_compute) + disc.add_v3(v3_compute) + + self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) + + # This needs to do version discovery to find the version + version = s.get_api_major_version( + service_type='compute', interface='admin') + self.assertEqual((3, 0), version) + self.assertEqual( + self.requests_mock.request_history[-1].url, + self.TEST_COMPUTE_ADMIN) + def test_direct_discovering_with_relative_link(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False)