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
This commit is contained in:
Jamie Lennox
2014-04-28 12:20:46 +10:00
parent 3305c7be4b
commit ec57b35bc8
5 changed files with 198 additions and 1 deletions

View File

@@ -22,6 +22,7 @@ raw data specified in version discovery responses.
""" """
import logging import logging
import re
from keystoneclient import exceptions from keystoneclient import exceptions
from keystoneclient import utils from keystoneclient import utils
@@ -262,3 +263,56 @@ class Discover(object):
""" """
data = self.data_for(version, **kwargs) data = self.data_for(version, **kwargs)
return data['url'] if data else None 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)

View File

@@ -201,8 +201,15 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
# defaulting to the most recent version. # defaulting to the most recent version.
return url 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: try:
disc = self.get_discovery(session, url, authenticated=False) disc = self.get_discovery(session, hacked_url, authenticated=False)
except (exceptions.DiscoveryFailure, except (exceptions.DiscoveryFailure,
exceptions.HTTPError, exceptions.HTTPError,
exceptions.ConnectionError): exceptions.ConnectionError):

View File

@@ -266,3 +266,34 @@ class Discover(_discover.Discover):
""" """
version_data = self._calculate_version(version, unstable) version_data = self._calculate_version(version, unstable)
return self._create_client(version_data, **kwargs) 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)

View File

@@ -286,3 +286,71 @@ class V2(CommonIdentityTests, utils.TestCase):
def stub_auth(self, **kwargs): def stub_auth(self, **kwargs):
self.stub_url('POST', ['tokens'], **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)

View File

@@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import re
import uuid import uuid
import six import six
@@ -772,6 +773,42 @@ class DiscoverQueryTests(utils.TestCase):
self.assertEqual(1, len(versions)) 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): class DiscoverUtils(utils.TestCase):
def test_version_number(self): def test_version_number(self):