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:
		@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user