From 46286b1cf9e647d69e639873a27c63c2f7ba10ad Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 Sep 2017 11:55:26 -0500 Subject: [PATCH] Add version discovery support to BaseAuthPlugin The new 'none' auth plugin and the old 'admin_token' plugin are subclasses of BaseAuthPluign, not BaseIdentityPlugin. That means if someone does: s = session.Session(noauth.NoAuth()) a = adapter.Adapter(s, endpoint_override='https://example.com') to get an Adapter on an endpoint using the none plugin, then does either: a.get_api_major_version() or: a.get_endpoint_data() it will fail because the none plugin doesn't have those methods. There is, however, nothing about those methods that necessarily needs authentication. That is, they can work just fine in contexts without a keystone token or without authentication of any sort. Ironic/Bifrost is specifically a usecase here, as standalone Ironic wants to use the 'none' plugin, but consuming the API still needs to get microversion info from the given endpoint. Add methods to BaseAuthPlugin that take less arguments since the ones about finding services in catalogs make zero sense in none/admin_token context. Change-Id: Id9bd19cca68206fc64d23b0eaa95aa3e5b01b676 --- keystoneauth1/identity/base.py | 1 - keystoneauth1/plugin.py | 63 ++++++++++++++- keystoneauth1/tests/unit/test_discovery.py | 89 ++++++++++++++++++++++ keystoneauth1/token_endpoint.py | 28 +++++++ 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/keystoneauth1/identity/base.py b/keystoneauth1/identity/base.py index 09d0e918..9cb5a18b 100644 --- a/keystoneauth1/identity/base.py +++ b/keystoneauth1/identity/base.py @@ -43,7 +43,6 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin): self.auth_ref = None self.reauthenticate = reauthenticate - self._discovery_cache = {} self._lock = threading.Lock() @abc.abstractmethod diff --git a/keystoneauth1/plugin.py b/keystoneauth1/plugin.py index 3551a0d3..7f91c85f 100644 --- a/keystoneauth1/plugin.py +++ b/keystoneauth1/plugin.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1 import discover + # NOTE(jamielennox): The AUTH_INTERFACE is a special value that can be # requested from get_endpoint. If a plugin receives this as the value of # 'interface' it should return the initial URL that was passed to the plugin. @@ -27,6 +29,9 @@ class BaseAuthPlugin(object): """ + def __init__(self): + self._discovery_cache = {} + def get_token(self, session, **kwargs): """Obtain a token. @@ -94,6 +99,58 @@ class BaseAuthPlugin(object): return {IDENTITY_AUTH_HEADER_NAME: token} + def get_endpoint_data(self, session, + endpoint_override=None, + discover_versions=True, + **kwargs): + """Return a valid endpoint data for a the service. + + :param session: A session object that can be used for communication. + :type session: keystoneauth1.session.Session + :param str endpoint_override: URL to use for version discovery. + :param bool discover_versions: Whether to get version metadata from + the version discovery document even + if it major api version info can be + inferred from the url. + (optional, defaults to True) + :param kwargs: Ignored. + + :raises keystoneauth1.exceptions.http.HttpError: An error from an + invalid HTTP response. + + :return: Valid EndpointData or None if not available. + :rtype: `keystoneauth1.discover.EndpointData` or None + """ + if not endpoint_override: + return None + endpoint_data = discover.EndpointData(catalog_url=endpoint_override) + + if endpoint_data.api_version and not discover_versions: + return endpoint_data + + return endpoint_data.get_versioned_data( + session, cache=self._discovery_cache, + discover_versions=discover_versions) + + def get_api_major_version(self, session, endpoint_override=None, **kwargs): + """Get the major API version from the endpoint. + + :param session: A session object that can be used for communication. + :type session: keystoneauth1.session.Session + :param str endpoint_override: URL to use for version discovery. + :param kwargs: Ignored. + + :raises keystoneauth1.exceptions.http.HttpError: An error from an + invalid HTTP response. + + :return: Valid EndpointData or None if not available. + :rtype: `keystoneauth1.discover.EndpointData` or None + """ + endpoint_data = self.get_endpoint_data( + session, endpoint_override=endpoint_override, + discover_versions=False, **kwargs) + return endpoint_data.api_version + def get_endpoint(self, session, **kwargs): """Return an endpoint for the client. @@ -114,7 +171,11 @@ class BaseAuthPlugin(object): service or None if not available. :rtype: string """ - return None + endpoint_data = self.get_endpoint_data( + session, discover_versions=False, **kwargs) + if not endpoint_data: + return None + return endpoint_data.url def get_connection_params(self, session, **kwargs): """Return any additional connection parameters required for the plugin. diff --git a/keystoneauth1/tests/unit/test_discovery.py b/keystoneauth1/tests/unit/test_discovery.py index ec5a15c4..d4ad7fdb 100644 --- a/keystoneauth1/tests/unit/test_discovery.py +++ b/keystoneauth1/tests/unit/test_discovery.py @@ -16,11 +16,14 @@ import re from testtools import matchers +from keystoneauth1 import adapter from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import fixture +from keystoneauth1 import noauth from keystoneauth1 import session from keystoneauth1.tests.unit import utils +from keystoneauth1 import token_endpoint BASE_HOST = 'http://keystone.example.com' @@ -556,6 +559,92 @@ class VersionDataTests(utils.TestCase): # Badly-formatted next_min_version test_exc({'next_min_version': 'bogus', 'not_before': '2019-07-01'}) + def test_endpoint_data_noauth_discover(self): + mock = self.requests_mock.get( + V3_URL, status_code=200, json=V3_VERSION_ENTRY) + plugin = noauth.NoAuth() + data = plugin.get_endpoint_data(self.session, endpoint_override=V3_URL) + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual( + plugin.get_api_major_version( + self.session, endpoint_override=V3_URL), + (3, 0)) + self.assertEqual( + plugin.get_endpoint(self.session, endpoint_override=V3_URL), + V3_URL) + + self.assertTrue(mock.called_once) + + def test_endpoint_data_noauth_no_discover(self): + plugin = noauth.NoAuth() + data = plugin.get_endpoint_data( + self.session, endpoint_override=V3_URL, discover_versions=False) + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual( + plugin.get_api_major_version( + self.session, endpoint_override=V3_URL), + (3, 0)) + self.assertEqual( + plugin.get_endpoint(self.session, endpoint_override=V3_URL), + V3_URL) + + def test_endpoint_data_noauth_adapter(self): + mock = self.requests_mock.get( + V3_URL, status_code=200, json=V3_VERSION_ENTRY) + + client = adapter.Adapter( + session.Session(noauth.NoAuth()), + endpoint_override=V3_URL) + data = client.get_endpoint_data() + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual(client.get_api_major_version(), (3, 0)) + self.assertEqual(client.get_endpoint(), V3_URL) + + self.assertTrue(mock.called_once) + + def test_endpoint_data_token_endpoint_discover(self): + mock = self.requests_mock.get( + V3_URL, status_code=200, json=V3_VERSION_ENTRY) + plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') + data = plugin.get_endpoint_data(self.session) + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) + self.assertEqual(plugin.get_endpoint(self.session), V3_URL) + + self.assertTrue(mock.called_once) + + def test_endpoint_data_token_endpoint_no_discover(self): + plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') + data = plugin.get_endpoint_data(self.session, discover_versions=False) + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) + self.assertEqual(plugin.get_endpoint(self.session), V3_URL) + + def test_endpoint_data_token_endpoint_adapter(self): + mock = self.requests_mock.get( + V3_URL, status_code=200, json=V3_VERSION_ENTRY) + plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') + + client = adapter.Adapter(session.Session(plugin)) + data = client.get_endpoint_data() + + self.assertEqual(data.api_version, (3, 0)) + self.assertEqual(data.url, V3_URL) + self.assertEqual(client.get_api_major_version(), (3, 0)) + self.assertEqual(client.get_endpoint(), V3_URL) + + self.assertTrue(mock.called_once) + def test_data_for_url(self): mock = self.requests_mock.get(V3_URL, status_code=200, diff --git a/keystoneauth1/token_endpoint.py b/keystoneauth1/token_endpoint.py index 675d4c13..9c90eb49 100644 --- a/keystoneauth1/token_endpoint.py +++ b/keystoneauth1/token_endpoint.py @@ -21,6 +21,7 @@ class Token(plugin.BaseAuthPlugin): """ def __init__(self, endpoint, token): + super(Token, self).__init__() # NOTE(jamielennox): endpoint is reserved for when plugins # can be used to provide that information self.endpoint = endpoint @@ -29,6 +30,33 @@ class Token(plugin.BaseAuthPlugin): def get_token(self, session): return self.token + def get_endpoint_data(self, session, + endpoint_override=None, + discover_versions=True, **kwargs): + """Return a valid endpoint data for a the service. + + :param session: A session object that can be used for communication. + :type session: keystoneauth1.session.Session + :param str endpoint_override: URL to use for version discovery other + than the endpoint stored in the plugin. + (optional, defaults to None) + :param bool discover_versions: Whether to get version metadata from + the version discovery document even + if it major api version info can be + inferred from the url. + (optional, defaults to True) + :param kwargs: Ignored. + + :raises keystoneauth1.exceptions.http.HttpError: An error from an + invalid HTTP response. + + :return: Valid EndpointData or None if not available. + :rtype: `keystoneauth1.discover.EndpointData` or None + """ + return super(Token, self).get_endpoint_data( + session, endpoint_override=endpoint_override or self.endpoint, + discover_versions=discover_versions, **kwargs) + def get_endpoint(self, session, **kwargs): """Return the supplied endpoint.