From 79cd91e75580511171a3a61dc6f3c70e275f6348 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 13 Apr 2018 08:51:55 -0500 Subject: [PATCH] Implement service_type alias lookups The Service Types Authority has grown support for aliases, and the os-service-types library exposes the data. Add support for matching known aliases when matching endpoints for a user. Change-Id: Ie90c265cb17905981d877abfaaa52354a3e63692 --- keystoneauth1/access/service_catalog.py | 68 +++++++++++++++- keystoneauth1/discover.py | 2 + .../unit/access/test_v3_service_catalog.py | 81 +++++++++++++++++++ lower-constraints.txt | 1 + .../serice-type-aliases-249454829c57f39a.yaml | 5 ++ requirements.txt | 1 + 6 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml diff --git a/keystoneauth1/access/service_catalog.py b/keystoneauth1/access/service_catalog.py index 3aeb6ed4..45df284b 100644 --- a/keystoneauth1/access/service_catalog.py +++ b/keystoneauth1/access/service_catalog.py @@ -168,7 +168,8 @@ class ServiceCatalog(object): for service in self.normalize_catalog(): - if service_type and service_type != service['type']: + if service_type and not discover._SERVICE_TYPES.is_match( + service_type, service['type']): continue if (service_name and service['name'] and @@ -203,7 +204,7 @@ class ServiceCatalog(object): raw_endpoint=endpoint['raw_endpoint'])) if not interfaces: - return matching_endpoints + return self._endpoints_by_type(service_type, matching_endpoints) ret = {} for matched_service_type, endpoints in matching_endpoints.items(): @@ -218,7 +219,68 @@ class ServiceCatalog(object): if i in matches_by_interface.keys()][0] ret[matched_service_type] = matches_by_interface[best_interface] - return ret + return self._endpoints_by_type(service_type, ret) + + def _endpoints_by_type(self, requested, endpoints): + """Get the approrpriate endpoints from the list of given endpoints. + + Per the service type alias rules: + + If a user requests a service by its proper name and that matches, win. + + If a user requests a service by its proper name and only a single alias + matches, win. + + If a user requests a service by its proper name and more than one alias + matches, choose the first alias from the list given. + + Do the "first alias" match after the other filters, as they might limit + the number of choices for us otherwise. + + :param str requested: + The service_type as requested by the user. + :param dict sc: + A dictionary keyed by found service_type. Values are opaque to + this method. + + :returns: + Dict of service_type/endpoints filtered for the appropriate + service_type based on alias matching rules. + """ + if not requested or not discover._SERVICE_TYPES.is_known(requested): + # The user did not request a service we have any alias information + # about, or did not request a service, which means that we cannot + # further filter the list. + return endpoints + + if len(endpoints) < 2: + # There is at most one type found from the initial pass through + # the catalog. Nothing further to do. + return endpoints + + # At this point, the user has requested a type, we do know things + # about aliases for that type, and we've found more than one match. + # We must filter out additional types, otherwise clouds that register + # the same endpoint twice as part of a migration will confuse users. + + # Only return the one the user requested if there's an exact match + # and there is data for it. There might not be data for this match + # if there are other filters that excluded it from consideration + # after we accepted it as an alias. + if endpoints.get(requested): + return {requested: endpoints[requested]} + + # We've matched something that isn't exactly what the user requested. + # Look at the possible types for this service in order or priority and + # return the first match. + for alias in discover._SERVICE_TYPES.get_all_types(requested): + if endpoints.get(alias): + # Return the first one found in the order listed. + return {alias: endpoints[alias]} + + # We should never get here - it's a programming logic error on our + # part if we do. Raise this so that we can panic in unit tests. + raise ValueError("Programming error choosing an endpoint.") def get_endpoints(self, service_type=None, interface=None, region_name=None, service_name=None, diff --git a/keystoneauth1/discover.py b/keystoneauth1/discover.py index 222602d7..ccf53c96 100644 --- a/keystoneauth1/discover.py +++ b/keystoneauth1/discover.py @@ -24,6 +24,7 @@ raw data specified in version discovery responses. import copy import re +import os_service_types import six from six.moves import urllib @@ -34,6 +35,7 @@ from keystoneauth1 import exceptions _LOGGER = utils.get_logger(__name__) LATEST = float('inf') +_SERVICE_TYPES = os_service_types.ServiceTypes() def _str_or_latest(val): diff --git a/keystoneauth1/tests/unit/access/test_v3_service_catalog.py b/keystoneauth1/tests/unit/access/test_v3_service_catalog.py index 713586c3..4df17590 100644 --- a/keystoneauth1/tests/unit/access/test_v3_service_catalog.py +++ b/keystoneauth1/tests/unit/access/test_v3_service_catalog.py @@ -56,6 +56,27 @@ class ServiceCatalogTest(utils.TestCase): admin='http://glance.south.host/glanceapi/admin', region='South') + s = self.AUTH_RESPONSE_BODY.add_service('block-storage', name='cinder') + s.add_standard_endpoints( + public='http://cinder.north.host/cinderapi/public', + internal='http://cinder.north.host/cinderapi/internal', + admin='http://cinder.north.host/cinderapi/admin', + region='North') + + s = self.AUTH_RESPONSE_BODY.add_service('volumev2', name='cinder') + s.add_standard_endpoints( + public='http://cinder.south.host/cinderapi/public/v2', + internal='http://cinder.south.host/cinderapi/internal/v2', + admin='http://cinder.south.host/cinderapi/admin/v2', + region='South') + + s = self.AUTH_RESPONSE_BODY.add_service('volumev3', name='cinder') + s.add_standard_endpoints( + public='http://cinder.south.host/cinderapi/public/v3', + internal='http://cinder.south.host/cinderapi/internal/v3', + admin='http://cinder.south.host/cinderapi/admin/v3', + region='South') + self.north_endpoints = {'public': 'http://glance.north.host/glanceapi/public', 'internal': @@ -97,6 +118,66 @@ class ServiceCatalogTest(utils.TestCase): self.assertEqual(public_ep['compute'][0]['url'], "https://compute.north.host/novapi/public") + def test_service_catalog_alias_find_official(self): + auth_ref = access.create(auth_token=uuid.uuid4().hex, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + # Tests that we find the block-storage endpoint when we request + # the volume endpoint. + public_ep = sc.get_endpoints(service_type='volume', + interface='public', + region_name='North') + self.assertEqual(public_ep['block-storage'][0]['region'], 'North') + self.assertEqual(public_ep['block-storage'][0]['url'], + "http://cinder.north.host/cinderapi/public") + + def test_service_catalog_alias_find_exact_match(self): + auth_ref = access.create(auth_token=uuid.uuid4().hex, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + # Tests that we find the volumev3 endpoint when we request it. + public_ep = sc.get_endpoints(service_type='volumev3', + interface='public') + self.assertEqual(public_ep['volumev3'][0]['region'], 'South') + self.assertEqual(public_ep['volumev3'][0]['url'], + "http://cinder.south.host/cinderapi/public/v3") + + def test_service_catalog_alias_find_best_match(self): + auth_ref = access.create(auth_token=uuid.uuid4().hex, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + # Tests that we find the volumev3 endpoint when we request + # block-storage when only volumev2 and volumev3 are present since + # volumev3 comes first in the list. + public_ep = sc.get_endpoints(service_type='block-storage', + interface='public', + region_name='South') + self.assertEqual(public_ep['volumev3'][0]['region'], 'South') + self.assertEqual(public_ep['volumev3'][0]['url'], + "http://cinder.south.host/cinderapi/public/v3") + + def test_service_catalog_alias_all_by_name(self): + auth_ref = access.create(auth_token=uuid.uuid4().hex, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + # Tests that we find all the cinder endpoints since we request + # them by name and that no filtering related to aliases happens. + public_ep = sc.get_endpoints(service_name='cinder', + interface='public') + self.assertEqual(public_ep['volumev2'][0]['region'], 'South') + self.assertEqual(public_ep['volumev2'][0]['url'], + "http://cinder.south.host/cinderapi/public/v2") + self.assertEqual(public_ep['volumev3'][0]['region'], 'South') + self.assertEqual(public_ep['volumev3'][0]['url'], + "http://cinder.south.host/cinderapi/public/v3") + self.assertEqual(public_ep['block-storage'][0]['region'], 'North') + self.assertEqual(public_ep['block-storage'][0]['url'], + "http://cinder.north.host/cinderapi/public") + def test_service_catalog_regions(self): self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" auth_ref = access.create(auth_token=uuid.uuid4().hex, diff --git a/lower-constraints.txt b/lower-constraints.txt index a194d1ba..f5ee3ab9 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -37,6 +37,7 @@ oauthlib==0.6.2 openstack-requirements==1.2.0 openstackdocstheme==1.18.1 os-client-config==1.29.0 +os-service-types==1.2.0 os-testr==1.0.0 oslo.config==5.2.0 oslo.i18n==3.20.0 diff --git a/releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml b/releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml new file mode 100644 index 00000000..7486539e --- /dev/null +++ b/releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for service-type aliases as defined in the Service Types + Authority when doing catalog lookups. diff --git a/requirements.txt b/requirements.txt index 6e43b673..e88d1fff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ iso8601>=0.1.11 # MIT requests>=2.14.2 # Apache-2.0 six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 +os-service-types>=1.2.0 # Apache-2.0