Support a list of interface values

Sometimes, especially in places like service-to-service defaults, it's
very helpful to express a list of values. For instance, when thinking
about nova connecting to ironic, nova would like to have the default
value of "interface" be ['internal', 'public'] - which is to say, use
internal if it's there, but otherwise use public. This use case is covered
in the API-WG specs on discoverability.

Change-Id: I9102155c2d4ef1ef8bbb1d0fa26a5b5838108a4c
This commit is contained in:
Monty Taylor 2017-06-24 16:18:58 -04:00
parent 46054f42d4
commit 2b949de8e9
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
3 changed files with 111 additions and 71 deletions

View File

@ -126,6 +126,13 @@ class ServiceCatalog(object):
catalog.append(service) catalog.append(service)
return catalog 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() @positional()
def get_endpoints_data(self, service_type=None, interface=None, def get_endpoints_data(self, service_type=None, interface=None,
region_name=None, service_name=None, region_name=None, service_name=None,
@ -139,9 +146,25 @@ class ServiceCatalog(object):
be skipped. This allows compatibility with services that existed be skipped. This allows compatibility with services that existed
before the name was available in the catalog. 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 :returns: a dict, keyed by service_type, of lists of EndpointData
""" """
interface = self.normalize_interface(interface) interfaces = self._get_interface_list(interface)
matching_endpoints = {} matching_endpoints = {}
@ -161,12 +184,14 @@ class ServiceCatalog(object):
matching_endpoints.setdefault(service['type'], []) matching_endpoints.setdefault(service['type'], [])
for endpoint in service.get('endpoints', []): for endpoint in service.get('endpoints', []):
if interface and interface != endpoint['interface']: if interfaces and endpoint['interface'] not in interfaces:
continue continue
if region_name and region_name != endpoint['region_name']: if region_name and region_name != endpoint['region_name']:
continue continue
if endpoint_id and endpoint_id != endpoint['id']: if endpoint_id and endpoint_id != endpoint['id']:
continue continue
if not endpoint['url']:
continue
matching_endpoints[service['type']].append( matching_endpoints[service['type']].append(
discover.EndpointData( discover.EndpointData(
@ -179,7 +204,23 @@ class ServiceCatalog(object):
endpoint_id=endpoint['id'], endpoint_id=endpoint['id'],
raw_endpoint=endpoint['raw_endpoint'])) 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() @positional()
def get_endpoints(self, service_type=None, interface=None, 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 attribute. If no attribute is given, return the first
endpoint of the specified type. 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 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
Possible values: public or publicURL, of values. If it's a list of values, they will be
internal or internalURL, admin or looked for in order of preference.
adminURL
:param string region_name: Region of the endpoint. :param string region_name: Region of the endpoint.
:param string service_name: The assigned name of the service. :param string service_name: The assigned name of the service.
:param string service_id: The identifier of a 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 endpoint attribute. If no attribute is given, return the url of the
first endpoint of the specified type. 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 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
Possible values: public or publicURL, of values. If it's a list of values, they will be
internal or internalURL, admin or looked for in order of preference.
adminURL
:param string region_name: Region of the endpoint. :param string region_name: Region of the endpoint.
:param string service_name: The assigned name of the service. :param string service_name: The assigned name of the service.
:param string service_id: The identifier of a service. :param string service_id: The identifier of a service.
@ -281,7 +328,9 @@ class ServiceCatalog(object):
`admin` or 'adminURL` `admin` or 'adminURL`
:param string service_type: Service type of the endpoint. :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 region_name: Region of the endpoint.
:param string service_name: The assigned name of the service. :param string service_name: The assigned name of the service.
:param string service_id: The identifier of a service. :param string service_id: The identifier of a service.
@ -309,7 +358,9 @@ class ServiceCatalog(object):
`admin` or 'adminURL` `admin` or 'adminURL`
:param string service_type: Service type of the endpoint. :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 region_name: Region of the endpoint.
:param string service_name: The assigned name of the service. :param string service_name: The assigned name of the service.
:param string service_id: The identifier of a service. :param string service_id: The identifier of a service.

View File

@ -171,16 +171,21 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
version, min_version and max_version can all be given either as a version, min_version and max_version can all be given either as a
string or a tuple. 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. :param session: A session object that can be used for communication.
:type session: keystoneauth1.session.Session :type session: keystoneauth1.session.Session
:param string service_type: The type of service to lookup the endpoint :param string service_type: The type of service to lookup the endpoint
for. This plugin will return None (failure) for. This plugin will return None (failure)
if service_type is not provided. if service_type is not provided.
:param string interface: The exposure of the endpoint. Should be :param interface: Type of endpoint. Can be a single value or a list
`public`, `internal`, `admin`, or `auth`. of values. If it's a list of values, they will be
`auth` is special here to use the `auth_url` looked for in order of preference. Can also be
rather than a URL extracted from the service `keystoneauth1.plugin.AUTH_INTERFACE` to indicate
catalog. Defaults to `public`. 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. :param string region_name: The region the endpoint should exist in.
(optional) (optional)
:param string service_name: The name of the service in the catalog. :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 version, min_version and max_version can all be given either as a
string or a tuple. 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. :param session: A session object that can be used for communication.
:type session: keystoneauth1.session.Session :type session: keystoneauth1.session.Session
:param string service_type: The type of service to lookup the endpoint :param string service_type: The type of service to lookup the endpoint
for. This plugin will return None (failure) for. This plugin will return None (failure)
if service_type is not provided. if service_type is not provided.
:param string interface: The exposure of the endpoint. Should be :param interface: Type of endpoint. Can be a single value or a list
`public`, `internal`, `admin`, or `auth`. of values. If it's a list of values, they will be
`auth` is special here to use the `auth_url` looked for in order of preference. Can also be
rather than a URL extracted from the service `keystoneauth1.plugin.AUTH_INTERFACE` to indicate
catalog. Defaults to `public`. 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. :param string region_name: The region the endpoint should exist in.
(optional) (optional)
:param string service_name: The name of the service in the catalog. :param string service_name: The name of the service in the catalog.

View File

@ -62,6 +62,9 @@ class CommonIdentityTests(object):
TEST_VOLUME_V3_CATALOG_ADMIN = ( TEST_VOLUME_V3_CATALOG_ADMIN = (
TEST_VOLUME_V3_SERVICE_ADMIN + '/{project_id}') TEST_VOLUME_V3_SERVICE_ADMIN + '/{project_id}')
TEST_BAREMETAL_BASE = 'http://ironic'
TEST_BAREMETAL_INTERNAL = TEST_BAREMETAL_BASE + '/internal'
TEST_PASS = uuid.uuid4().hex TEST_PASS = uuid.uuid4().hex
def setUp(self): def setUp(self):
@ -522,61 +525,26 @@ class CommonIdentityTests(object):
self.assertEqual(v3_compute, v3_data.service_url) self.assertEqual(v3_compute, v3_data.service_url)
self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url) self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url)
def test_get_versioned_data_project_id(self): def test_interface_list(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)
a = self.create_auth_plugin() a = self.create_auth_plugin()
s = session.Session(auth=a) s = session.Session(auth=a)
v2_catalog_url = self.TEST_VOLUME_V2_CATALOG_PUBLIC.format( ep = s.get_endpoint(service_type='baremetal',
project_id=self.project_id) interface=['internal', 'public'])
v3_catalog_url = self.TEST_VOLUME_V3_CATALOG_PUBLIC.format( self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL)
project_id=self.project_id)
data = a.get_endpoint_data(session=s, ep = s.get_endpoint(service_type='baremetal',
service_type='volumev3', interface=['public', 'internal'])
interface='public') self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL)
self.assertEqual(v3_catalog_url, data.url)
v3_data = data.get_versioned_data( ep = s.get_endpoint(service_type='compute',
s, version='3.0', project_id=self.project_id) interface=['internal', 'public'])
self.assertEqual(v3_catalog_url, v3_data.url) self.assertEqual(ep, self.TEST_COMPUTE_INTERNAL)
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)
v2_data = data.get_versioned_data( ep = s.get_endpoint(service_type='compute',
s, version='2.0', project_id=self.project_id) interface=['public', 'internal'])
# Even though we never requested volumev2 from the catalog, we should self.assertEqual(ep, self.TEST_COMPUTE_PUBLIC)
# 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)
def test_get_versioned_data_compute_project_id(self): 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), internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs),
region=region) region=region)
svc = token.add_service('baremetal')
svc.add_standard_endpoints(
internal=self.TEST_BAREMETAL_INTERNAL,
region=region)
return token return token
def stub_auth(self, subject_token=None, **kwargs): 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), internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs),
region=region) region=region)
svc = token.add_service('baremetal')
svc.add_endpoint(
public=None, admin=None,
internal=self.TEST_BAREMETAL_INTERNAL,
region=region)
return token return token
def stub_auth(self, **kwargs): def stub_auth(self, **kwargs):