diff --git a/keystone/catalog/backends/base.py b/keystone/catalog/backends/base.py index 03e805ec5b..5b86f383bc 100644 --- a/keystone/catalog/backends/base.py +++ b/keystone/catalog/backends/base.py @@ -240,8 +240,6 @@ class CatalogDriverBase(provider_api.ProviderAPIMixin, object): def get_v3_catalog(self, user_id, project_id): """Retrieve and format the current V3 service catalog. - The default implementation builds the V3 catalog from the V2 catalog. - Example:: [ @@ -267,38 +265,7 @@ class CatalogDriverBase(provider_api.ProviderAPIMixin, object): :raises keystone.exception.NotFound: If the endpoint doesn't exist. """ - v2_catalog = self.get_catalog(user_id, project_id) - v3_catalog = [] - - for region_name, region in v2_catalog.items(): - for service_type, service in region.items(): - service_v3 = { - 'type': service_type, - 'endpoints': [] - } - - for attr, value in service.items(): - # Attributes that end in URL are interfaces. In the V2 - # catalog, these are internalURL, publicURL, and adminURL. - # For example, .publicURL= in the V2 - # catalog becomes the V3 interface for the service: - # { 'interface': 'public', 'url': '', 'region': - # 'region: '' } - if attr.endswith('URL'): - v3_interface = attr[:-len('URL')] - service_v3['endpoints'].append({ - 'interface': v3_interface, - 'region': region_name, - 'url': value, - }) - continue - - # Other attributes are copied to the service. - service_v3[attr] = value - - v3_catalog.append(service_v3) - - return v3_catalog + raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod def add_endpoint_to_project(self, endpoint_id, project_id): diff --git a/keystone/catalog/backends/templated.py b/keystone/catalog/backends/templated.py index 5a7a519a43..6fbf282b72 100644 --- a/keystone/catalog/backends/templated.py +++ b/keystone/catalog/backends/templated.py @@ -241,6 +241,53 @@ class Catalog(base.CatalogDriverBase): return catalog + def get_v3_catalog(self, user_id, project_id): + """Retrieve and format the current V3 service catalog. + + This implementation builds the V3 catalog from the V2 catalog. + + :param user_id: The id of the user who has been authenticated for + creating service catalog. + :param project_id: The id of the project. 'project_id' will be None in + the case this being called to create a catalog to go in a domain + scoped token. In this case, any endpoint that requires a project_id + as part of their URL will be skipped. + + :returns: A list representing the service catalog or an empty list + + """ + v2_catalog = self.get_catalog(user_id, project_id) + v3_catalog = {} + + for region_name, region in v2_catalog.items(): + for service_type, service in region.items(): + if service_type not in v3_catalog: + v3_catalog[service_type] = { + 'type': service_type, + 'endpoints': [] + } + + for attr, value in service.items(): + # Attributes that end in URL are interfaces. In the V2 + # catalog, these are internalURL, publicURL, and adminURL. + # For example, .publicURL= in the V2 + # catalog becomes the V3 interface for the service: + # { 'interface': 'public', 'url': '', 'region': + # 'region: '' } + if attr.endswith('URL'): + v3_interface = attr[:-len('URL')] + v3_catalog[service_type]['endpoints'].append({ + 'interface': v3_interface, + 'region': region_name, + 'url': value, + }) + continue + + # Other attributes are copied to the service. + v3_catalog[service_type][attr] = value + + return list(v3_catalog.values()) + def add_endpoint_to_project(self, endpoint_id, project_id): raise exception.NotImplemented() diff --git a/keystone/tests/unit/default_catalog_multi_region.templates b/keystone/tests/unit/default_catalog_multi_region.templates new file mode 100644 index 0000000000..096deb6a12 --- /dev/null +++ b/keystone/tests/unit/default_catalog_multi_region.templates @@ -0,0 +1,27 @@ +# config for templated.Catalog, using camelCase because I don't want to do +# translations for keystone compat +catalog.RegionOne.identity.publicURL = http://region-one:$(public_port)s/v3 +catalog.RegionOne.identity.adminURL = http://region-one:$(admin_port)s/v3 +catalog.RegionOne.identity.internalURL = http://region-one:$(admin_port)s/v3 +catalog.RegionOne.identity.name = 'Identity Service' +catalog.RegionOne.identity.id = 1 + +# fake compute service for now to help novaclient tests work +catalog.RegionOne.compute.publicURL = http://region-one:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.adminURL = http://region-one:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.internalURL = http://region-one:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.name = 'Compute Service' +catalog.RegionOne.compute.id = 2 + +# second region for multi-region testing +catalog.RegionTwo.identity.publicURL = http://region-two:$(public_port)s/v3 +catalog.RegionTwo.identity.adminURL = http://region-two:$(admin_port)s/v3 +catalog.RegionTwo.identity.internalURL = http://region-two:$(admin_port)s/v3 +catalog.RegionTwo.identity.name = 'Identity Service' +catalog.RegionTwo.identity.id = 1 + +catalog.RegionTwo.compute.publicURL = http://region-two:8774/v1.1/$(tenant_id)s +catalog.RegionTwo.compute.adminURL = http://region-two:8774/v1.1/$(tenant_id)s +catalog.RegionTwo.compute.internalURL = http://region-two:8774/v1.1/$(tenant_id)s +catalog.RegionTwo.compute.name = 'Compute Service' +catalog.RegionTwo.compute.id = 2 \ No newline at end of file diff --git a/keystone/tests/unit/test_backend_templated.py b/keystone/tests/unit/test_backend_templated.py index e8aae0f627..2c7468da1f 100644 --- a/keystone/tests/unit/test_backend_templated.py +++ b/keystone/tests/unit/test_backend_templated.py @@ -129,6 +129,64 @@ class TestTemplatedCatalog(unit.TestCase, catalog_tests.CatalogTests): 'id': '1'}] self.assert_catalogs_equal(exp_catalog, catalog_ref) + def test_get_multi_region_v3_catalog(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + catalog_api = PROVIDERS.catalog_api + + # Load the multi-region catalog. + catalog_api._load_templates( + unit.dirs.tests('default_catalog_multi_region.templates')) + + catalog_ref = catalog_api.get_v3_catalog(user_id, project_id) + exp_catalog = [ + {'endpoints': [ + {'interface': 'admin', + 'region': 'RegionOne', + 'url': 'http://region-one:8774/v1.1/%s' % project_id}, + {'interface': 'public', + 'region': 'RegionOne', + 'url': 'http://region-one:8774/v1.1/%s' % project_id}, + {'interface': 'internal', + 'region': 'RegionOne', + 'url': 'http://region-one:8774/v1.1/%s' % project_id}, + {'interface': 'admin', + 'region': 'RegionTwo', + 'url': 'http://region-two:8774/v1.1/%s' % project_id}, + {'interface': 'public', + 'region': 'RegionTwo', + 'url': 'http://region-two:8774/v1.1/%s' % project_id}, + {'interface': 'internal', + 'region': 'RegionTwo', + 'url': 'http://region-two:8774/v1.1/%s' % project_id}], + 'type': 'compute', + 'name': "'Compute Service'", + 'id': '2'}, + {'endpoints': [ + {'interface': 'admin', + 'region': 'RegionOne', + 'url': 'http://region-one:35357/v3'}, + {'interface': 'public', + 'region': 'RegionOne', + 'url': 'http://region-one:5000/v3'}, + {'interface': 'internal', + 'region': 'RegionOne', + 'url': 'http://region-one:35357/v3'}, + {'interface': 'admin', + 'region': 'RegionTwo', + 'url': 'http://region-two:35357/v3'}, + {'interface': 'public', + 'region': 'RegionTwo', + 'url': 'http://region-two:5000/v3'}, + {'interface': 'internal', + 'region': 'RegionTwo', + 'url': 'http://region-two:35357/v3'}], + 'type': 'identity', + 'name': "'Identity Service'", + 'id': '1'}] + self.assert_catalogs_equal(exp_catalog, catalog_ref) + def test_get_catalog_ignores_endpoints_with_invalid_urls(self): user_id = uuid.uuid4().hex project_id = None diff --git a/releasenotes/notes/bug-1703666-b8a990f2bf5b62f0.yaml b/releasenotes/notes/bug-1703666-b8a990f2bf5b62f0.yaml new file mode 100644 index 0000000000..7ceeefaa7c --- /dev/null +++ b/releasenotes/notes/bug-1703666-b8a990f2bf5b62f0.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + [`bug 1703666 `_] + Fixing multi-region support for the templated v3 catalog by making sure + that the catalog contains only one definition per endpoint, and that + each region is listed under that endpoint. Previously each region + and endpoint would have had its own definition.