diff --git a/keystoneauth1/access/service_catalog.py b/keystoneauth1/access/service_catalog.py index 3c3a7894..184569c6 100644 --- a/keystoneauth1/access/service_catalog.py +++ b/keystoneauth1/access/service_catalog.py @@ -126,6 +126,13 @@ class ServiceCatalog(object): catalog.append(service) return catalog + def _get_interface_list(self, interface): + if not interface: + return [] + if not isinstance(interface, list): + interface = [interface] + return [self.normalize_interface(i) for i in interface] + @positional() def get_endpoints_data(self, service_type=None, interface=None, region_name=None, service_name=None, @@ -139,9 +146,25 @@ class ServiceCatalog(object): be skipped. This allows compatibility with services that existed before the name was available in the catalog. + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + + :param string service_type: Service type of the endpoint. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. + :param string region_name: Region of the endpoint. + :param string service_name: The assigned name of the service. + :param string service_id: The identifier of a service. + :param string endpoint_id: The identifier of an endpoint. + + :returns: a list of matching EndpointData objects + :rtype: list(`keystoneauth1.discover.EndpointData`) + :returns: a dict, keyed by service_type, of lists of EndpointData """ - interface = self.normalize_interface(interface) + interfaces = self._get_interface_list(interface) matching_endpoints = {} @@ -161,12 +184,14 @@ class ServiceCatalog(object): matching_endpoints.setdefault(service['type'], []) for endpoint in service.get('endpoints', []): - if interface and interface != endpoint['interface']: + if interfaces and endpoint['interface'] not in interfaces: continue if region_name and region_name != endpoint['region_name']: continue if endpoint_id and endpoint_id != endpoint['id']: continue + if not endpoint['url']: + continue matching_endpoints[service['type']].append( discover.EndpointData( @@ -179,7 +204,23 @@ class ServiceCatalog(object): endpoint_id=endpoint['id'], raw_endpoint=endpoint['raw_endpoint'])) - return matching_endpoints + if not interfaces: + return matching_endpoints + + ret = {} + for service_type, endpoints in matching_endpoints.items(): + if not endpoints: + ret[service_type] = [] + continue + matches_by_interface = {} + for endpoint in endpoints: + matches_by_interface.setdefault(endpoint.interface, []) + matches_by_interface[endpoint.interface].append(endpoint) + best_interface = [i for i in interfaces + if i in matches_by_interface.keys()][0] + ret[service_type] = matches_by_interface[best_interface] + + return ret @positional() def get_endpoints(self, service_type=None, interface=None, @@ -215,11 +256,14 @@ class ServiceCatalog(object): endpoint attribute. If no attribute is given, return the first endpoint of the specified type. + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + :param string service_type: Service type of the endpoint. - :param string interface: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, admin or - adminURL + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. @@ -246,11 +290,14 @@ class ServiceCatalog(object): endpoint attribute. If no attribute is given, return the url of the first endpoint of the specified type. + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + :param string service_type: Service type of the endpoint. - :param string interface: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, admin or - adminURL + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. @@ -281,7 +328,9 @@ class ServiceCatalog(object): `admin` or 'adminURL` :param string service_type: Service type of the endpoint. - :param string interface: Type of endpoint. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. @@ -309,7 +358,9 @@ class ServiceCatalog(object): `admin` or 'adminURL` :param string service_type: Service type of the endpoint. - :param string interface: Type of endpoint. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. diff --git a/keystoneauth1/identity/base.py b/keystoneauth1/identity/base.py index 790f495a..8740d3a1 100644 --- a/keystoneauth1/identity/base.py +++ b/keystoneauth1/identity/base.py @@ -171,16 +171,21 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin): version, min_version and max_version can all be given either as a string or a tuple. + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. - :param string interface: The exposure of the endpoint. Should be - `public`, `internal`, `admin`, or `auth`. - `auth` is special here to use the `auth_url` - rather than a URL extracted from the service - catalog. Defaults to `public`. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. Can also be + `keystoneauth1.plugin.AUTH_INTERFACE` to indicate + that the auth_url should be used instead of the + value in the catalog. (optional, defaults to public) :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. @@ -310,16 +315,21 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin): version, min_version and max_version can all be given either as a string or a tuple. + Valid interface types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. - :param string interface: The exposure of the endpoint. Should be - `public`, `internal`, `admin`, or `auth`. - `auth` is special here to use the `auth_url` - rather than a URL extracted from the service - catalog. Defaults to `public`. + :param interface: Type of endpoint. Can be a single value or a list + of values. If it's a list of values, they will be + looked for in order of preference. Can also be + `keystoneauth1.plugin.AUTH_INTERFACE` to indicate + that the auth_url should be used instead of the + value in the catalog. (optional, defaults to public) :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. diff --git a/keystoneauth1/tests/unit/identity/test_identity_common.py b/keystoneauth1/tests/unit/identity/test_identity_common.py index 1401f3b1..fd54e424 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_common.py +++ b/keystoneauth1/tests/unit/identity/test_identity_common.py @@ -62,6 +62,9 @@ class CommonIdentityTests(object): TEST_VOLUME_V3_CATALOG_ADMIN = ( TEST_VOLUME_V3_SERVICE_ADMIN + '/{project_id}') + TEST_BAREMETAL_BASE = 'http://ironic' + TEST_BAREMETAL_INTERNAL = TEST_BAREMETAL_BASE + '/internal' + TEST_PASS = uuid.uuid4().hex def setUp(self): @@ -522,61 +525,26 @@ class CommonIdentityTests(object): self.assertEqual(v3_compute, v3_data.service_url) self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url) - def test_get_versioned_data_project_id(self): - - # need to construct list this way for relative - disc = fixture.DiscoveryList(v2=False, v3=False) - # The version discovery dict will not have a project_id - disc.add_microversion( - href=self.TEST_VOLUME_V3_SERVICE_PUBLIC, - id='v3.0', status='CURRENT', - min_version='3.0', max_version='3.20') - # Adding a v2 version to a service named volumev3 is not - # an error. The service itself is cinder and has more than - # one major version. - disc.add_microversion( - href=self.TEST_VOLUME_V2_SERVICE_PUBLIC, - id='v2.0', status='SUPPORTED') - - # We should only try to fetch the non-project_id url and only - # once - resps = [{'json': disc}, {'status_code': 500}] - self.requests_mock.get(self.TEST_VOLUME_V3_SERVICE_PUBLIC, resps) - - body = 'SUCCESS' - self.stub_url('GET', ['path'], text=body) + def test_interface_list(self): a = self.create_auth_plugin() s = session.Session(auth=a) - v2_catalog_url = self.TEST_VOLUME_V2_CATALOG_PUBLIC.format( - project_id=self.project_id) - v3_catalog_url = self.TEST_VOLUME_V3_CATALOG_PUBLIC.format( - project_id=self.project_id) + ep = s.get_endpoint(service_type='baremetal', + interface=['internal', 'public']) + self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL) - data = a.get_endpoint_data(session=s, - service_type='volumev3', - interface='public') - self.assertEqual(v3_catalog_url, data.url) + ep = s.get_endpoint(service_type='baremetal', + interface=['public', 'internal']) + self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL) - v3_data = data.get_versioned_data( - s, version='3.0', project_id=self.project_id) - self.assertEqual(v3_catalog_url, v3_data.url) - self.assertEqual(v3_catalog_url, v3_data.service_url) - self.assertEqual(v3_catalog_url, v3_data.catalog_url) - self.assertEqual((3, 0), v3_data.min_microversion) - self.assertEqual((3, 20), v3_data.max_microversion) + ep = s.get_endpoint(service_type='compute', + interface=['internal', 'public']) + self.assertEqual(ep, self.TEST_COMPUTE_INTERNAL) - v2_data = data.get_versioned_data( - s, version='2.0', project_id=self.project_id) - # Even though we never requested volumev2 from the catalog, we should - # wind up re-constructing it via version discovery and re-appending - # the project_id to the URL - self.assertEqual(v2_catalog_url, v2_data.url) - self.assertEqual(v2_catalog_url, v2_data.service_url) - self.assertEqual(v3_catalog_url, v2_data.catalog_url) - self.assertEqual(None, v2_data.min_microversion) - self.assertEqual(None, v2_data.max_microversion) + ep = s.get_endpoint(service_type='compute', + interface=['public', 'internal']) + self.assertEqual(ep, self.TEST_COMPUTE_PUBLIC) def test_get_versioned_data_compute_project_id(self): @@ -782,6 +750,11 @@ class V3(CommonIdentityTests, utils.TestCase): internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs), region=region) + svc = token.add_service('baremetal') + svc.add_standard_endpoints( + internal=self.TEST_BAREMETAL_INTERNAL, + region=region) + return token def stub_auth(self, subject_token=None, **kwargs): @@ -850,6 +823,12 @@ class V2(CommonIdentityTests, utils.TestCase): internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs), region=region) + svc = token.add_service('baremetal') + svc.add_endpoint( + public=None, admin=None, + internal=self.TEST_BAREMETAL_INTERNAL, + region=region) + return token def stub_auth(self, **kwargs):