diff options
author | Jamie Lennox <jamielennox@redhat.com> | 2014-04-28 12:20:46 +1000 |
---|---|---|
committer | Jamie Lennox <jamielennox@redhat.com> | 2014-09-16 10:41:55 +1000 |
commit | ec57b35bc8edb933fe259db2b96c393874166dc0 (patch) | |
tree | 13d73119da90341a28ab2b340b0de3e95e3a1624 | |
parent | 3305c7be4b726de4dcc889006d0be30eb46d3ad9 (diff) | |
download | python-keystoneclient-ec57b35bc8edb933fe259db2b96c393874166dc0.tar.gz |
Versioned Endpoint hack for Sessions
To maintain compatibility we must allow people to specify a versioned
URL in the service catalog but allow the plugins to return a different
URL to users.
We need this to be a general approach as other services will likely have
a similar problem with their catalog.
The expectation here is that a client will register the catalog hack at
import time rather than for every request.
Closes-Bug: #1335726
Change-Id: I244f0ec3acca39fd1b2a2c5883abc06ec10eddc7
-rw-r--r-- | keystoneclient/_discover.py | 54 | ||||
-rw-r--r-- | keystoneclient/auth/identity/base.py | 9 | ||||
-rw-r--r-- | keystoneclient/discover.py | 31 | ||||
-rw-r--r-- | keystoneclient/tests/auth/test_identity_common.py | 68 | ||||
-rw-r--r-- | keystoneclient/tests/test_discovery.py | 37 |
5 files changed, 198 insertions, 1 deletions
diff --git a/keystoneclient/_discover.py b/keystoneclient/_discover.py index 7ea396f..c9c9792 100644 --- a/keystoneclient/_discover.py +++ b/keystoneclient/_discover.py @@ -22,6 +22,7 @@ raw data specified in version discovery responses. """ import logging +import re from keystoneclient import exceptions from keystoneclient import utils @@ -262,3 +263,56 @@ class Discover(object): """ data = self.data_for(version, **kwargs) return data['url'] if data else None + + +class _VersionHacks(object): + """A container to abstract the list of version hacks. + + This could be done as simply a dictionary but is abstracted like this to + make for easier testing. + """ + + def __init__(self): + self._discovery_data = {} + + def add_discover_hack(self, service_type, old, new=''): + """Add a new hack for a service type. + + :param str service_type: The service_type in the catalog. + :param re.RegexObject old: The pattern to use. + :param str new: What to replace the pattern with. + """ + hacks = self._discovery_data.setdefault(service_type, []) + hacks.append((old, new)) + + def get_discover_hack(self, service_type, url): + """Apply the catalog hacks and figure out an unversioned endpoint. + + :param str service_type: the service_type to look up. + :param str url: The original url that came from a service_catalog. + + :return: Either the unversioned url or the one from the catalog to try. + """ + for old, new in self._discovery_data.get(service_type, []): + new_string, number_of_subs_made = old.subn(new, url) + if number_of_subs_made > 0: + return new_string + + return url + + +_VERSION_HACKS = _VersionHacks() +_VERSION_HACKS.add_discover_hack('identity', re.compile('/v2.0/?$'), '/') + + +def get_catalog_discover_hack(service_type, url): + """Apply the catalog hacks and figure out an unversioned endpoint. + + This function is internal to keystoneclient. + + :param str service_type: the service_type to look up. + :param str url: The original url that came from a service_catalog. + + :return: Either the unversioned url or the one from the catalog to try. + """ + return _VERSION_HACKS.get_discover_hack(service_type, url) diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py index 8069e5c..10a9fe8 100644 --- a/keystoneclient/auth/identity/base.py +++ b/keystoneclient/auth/identity/base.py @@ -201,8 +201,15 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): # defaulting to the most recent version. return url + # NOTE(jamielennox): For backwards compatibility people might have a + # versioned endpoint in their catalog even though they want to use + # other endpoint versions. So we support a list of client defined + # situations where we can strip the version component from a URL before + # doing discovery. + hacked_url = _discover.get_catalog_discover_hack(service_type, url) + try: - disc = self.get_discovery(session, url, authenticated=False) + disc = self.get_discovery(session, hacked_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HTTPError, exceptions.ConnectionError): diff --git a/keystoneclient/discover.py b/keystoneclient/discover.py index 07de97d..982683a 100644 --- a/keystoneclient/discover.py +++ b/keystoneclient/discover.py @@ -266,3 +266,34 @@ class Discover(_discover.Discover): """ version_data = self._calculate_version(version, unstable) return self._create_client(version_data, **kwargs) + + +def add_catalog_discover_hack(service_type, old, new): + """Adds a version removal rule for a particular service. + + Originally deployments of OpenStack would contain a versioned endpoint in + the catalog for different services. E.g. an identity service might look + like ``http://localhost:5000/v2.0``. This is a problem when we want to use + a different version like v3.0 as there is no way to tell where it is + located. We cannot simply change all service catalogs either so there must + be a way to handle the older style of catalog. + + This function adds a rule for a given service type that if part of the URL + matches a given regular expression in *old* then it will be replaced with + the *new* value. This will replace all instances of old with new. It should + therefore contain a regex anchor. + + For example the included rule states:: + + add_catalog_version_hack('identity', re.compile('/v2.0/?$'), '/') + + so if the catalog retrieves an *identity* URL that ends with /v2.0 or + /v2.0/ then it should replace it simply with / to fix the user's catalog. + + :param str service_type: The service type as defined in the catalog that + the rule will apply to. + :param re.RegexObject old: The regular expression to search for and replace + if found. + :param str new: The new string to replace the pattern with. + """ + _discover._VERSION_HACKS.add_discover_hack(service_type, old, new) diff --git a/keystoneclient/tests/auth/test_identity_common.py b/keystoneclient/tests/auth/test_identity_common.py index 371fd18..9a369f7 100644 --- a/keystoneclient/tests/auth/test_identity_common.py +++ b/keystoneclient/tests/auth/test_identity_common.py @@ -286,3 +286,71 @@ class V2(CommonIdentityTests, utils.TestCase): def stub_auth(self, **kwargs): self.stub_url('POST', ['tokens'], **kwargs) + + +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def test_getting_endpoints(self): + disc = fixture.DiscoveryList(href=self.BASE_URL) + self.stub_url('GET', + ['/'], + base_url=self.BASE_URL, + json=disc) + + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + v2_auth = v2.Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V3_URL, endpoint) + + def test_returns_original_when_discover_fails(self): + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404) + + v2_auth = v2.Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V2_URL, endpoint) diff --git a/keystoneclient/tests/test_discovery.py b/keystoneclient/tests/test_discovery.py index 10f1c2f..811e65c 100644 --- a/keystoneclient/tests/test_discovery.py +++ b/keystoneclient/tests/test_discovery.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import re import uuid import six @@ -772,6 +773,42 @@ class DiscoverQueryTests(utils.TestCase): self.assertEqual(1, len(versions)) +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def setUp(self): + super(CatalogHackTests, self).setUp() + self.hacks = _discover._VersionHacks() + self.hacks.add_discover_hack(self.IDENTITY, + re.compile('/v2.0/?$'), + '/') + + def test_version_hacks(self): + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL)) + + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL + '/')) + + self.assertEqual(self.OTHER_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.OTHER_URL)) + + def test_ignored_non_service_type(self): + self.assertEqual(self.V2_URL, + self.hacks.get_discover_hack('other', self.V2_URL)) + + class DiscoverUtils(utils.TestCase): def test_version_number(self): |