summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2014-04-28 12:20:46 +1000
committerJamie Lennox <jamielennox@redhat.com>2014-09-16 10:41:55 +1000
commitec57b35bc8edb933fe259db2b96c393874166dc0 (patch)
tree13d73119da90341a28ab2b340b0de3e95e3a1624
parent3305c7be4b726de4dcc889006d0be30eb46d3ad9 (diff)
downloadpython-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.py54
-rw-r--r--keystoneclient/auth/identity/base.py9
-rw-r--r--keystoneclient/discover.py31
-rw-r--r--keystoneclient/tests/auth/test_identity_common.py68
-rw-r--r--keystoneclient/tests/test_discovery.py37
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):