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)
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.

View File

@ -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.

View File

@ -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):