From 2963dc152560c22c672b840c5e98a01a15fb083c Mon Sep 17 00:00:00 2001 From: Ronald De Rose Date: Fri, 22 Apr 2016 02:00:28 +0000 Subject: [PATCH] Move the catalog abstract base class and common code out of core This patch moves the catalog abstract base class and common code out of core, and into backends/base.py This removes dependencies where backend code references code in the core. The reasoning being that the core should know about the backend interface, but the backends should not know anything about the core (separation of concerns). And part of the risk here is a potential for circular dependencies. Change-Id: I87edf8cf660fabbc7253e6b1abc7354eef34151d Partial-Bug: #1563101 --- keystone/catalog/backends/base.py | 533 ++++++++++++++++ keystone/catalog/backends/sql.py | 10 +- keystone/catalog/backends/templated.py | 7 +- keystone/catalog/controllers.py | 6 +- keystone/catalog/core.py | 592 +----------------- keystone/common/utils.py | 71 ++- .../endpoint_filter/backends/catalog_sql.py | 4 +- keystone/tests/unit/catalog/test_backends.py | 4 +- keystone/tests/unit/catalog/test_core.py | 18 +- 9 files changed, 639 insertions(+), 606 deletions(-) create mode 100644 keystone/catalog/backends/base.py diff --git a/keystone/catalog/backends/base.py b/keystone/catalog/backends/base.py new file mode 100644 index 0000000000..4ad896b090 --- /dev/null +++ b/keystone/catalog/backends/base.py @@ -0,0 +1,533 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone import exception + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class CatalogDriverV8(object): + """Interface description for the Catalog driver.""" + + def _get_list_limit(self): + return CONF.catalog.list_limit or CONF.list_limit + + def _ensure_no_circle_in_hierarchical_regions(self, region_ref): + if region_ref.get('parent_region_id') is None: + return + + root_region_id = region_ref['id'] + parent_region_id = region_ref['parent_region_id'] + + while parent_region_id: + # NOTE(wanghong): check before getting parent region can ensure no + # self circle + if parent_region_id == root_region_id: + raise exception.CircularRegionHierarchyError( + parent_region_id=parent_region_id) + parent_region = self.get_region(parent_region_id) + parent_region_id = parent_region.get('parent_region_id') + + @abc.abstractmethod + def create_region(self, region_ref): + """Create a new region. + + :raises keystone.exception.Conflict: If the region already exists. + :raises keystone.exception.RegionNotFound: If the parent region + is invalid. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_regions(self, hints): + """List all regions. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of region_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_region(self, region_id): + """Get region by id. + + :returns: region_ref dict + :raises keystone.exception.RegionNotFound: If the region doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_region(self, region_id, region_ref): + """Update region by id. + + :returns: region_ref dict + :raises keystone.exception.RegionNotFound: If the region doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_region(self, region_id): + """Delete an existing region. + + :raises keystone.exception.RegionNotFound: If the region doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_service(self, service_id, service_ref): + """Create a new service. + + :raises keystone.exception.Conflict: If a duplicate service exists. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_services(self, hints): + """List all services. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of service_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_service(self, service_id): + """Get service by id. + + :returns: service_ref dict + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_service(self, service_id, service_ref): + """Update service by id. + + :returns: service_ref dict + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_service(self, service_id): + """Delete an existing service. + + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_endpoint(self, endpoint_id, endpoint_ref): + """Create a new endpoint for a service. + + :raises keystone.exception.Conflict: If a duplicate endpoint exists. + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint(self, endpoint_id): + """Get endpoint by id. + + :returns: endpoint_ref dict + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints(self, hints): + """List all endpoints. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of endpoint_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_endpoint(self, endpoint_id, endpoint_ref): + """Get endpoint by id. + + :returns: endpoint_ref dict + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint(self, endpoint_id): + """Delete an endpoint for a service. + + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_catalog(self, user_id, tenant_id): + """Retrieve and format the current service catalog. + + Example:: + + { 'RegionOne': + {'compute': { + 'adminURL': u'http://host:8774/v1.1/tenantid', + 'internalURL': u'http://host:8774/v1.1/tenant_id', + 'name': 'Compute Service', + 'publicURL': u'http://host:8774/v1.1/tenantid'}, + 'ec2': { + 'adminURL': 'http://host:8773/services/Admin', + 'internalURL': 'http://host:8773/services/Cloud', + 'name': 'EC2 Service', + 'publicURL': 'http://host:8773/services/Cloud'}} + + :returns: A nested dict representing the service catalog or an + empty dict. + :raises keystone.exception.NotFound: If the endpoint doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + def get_v3_catalog(self, user_id, tenant_id): + """Retrieve and format the current V3 service catalog. + + The default implementation builds the V3 catalog from the V2 catalog. + + Example:: + + [ + { + "endpoints": [ + { + "interface": "public", + "id": "--endpoint-id--", + "region": "RegionOne", + "url": "http://external:8776/v1/--project-id--" + }, + { + "interface": "internal", + "id": "--endpoint-id--", + "region": "RegionOne", + "url": "http://internal:8776/v1/--project-id--" + }], + "id": "--service-id--", + "type": "volume" + }] + + :returns: A list representing the service catalog or an empty list + :raises keystone.exception.NotFound: If the endpoint doesn't exist. + + """ + v2_catalog = self.get_catalog(user_id, tenant_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 + + @abc.abstractmethod + def add_endpoint_to_project(self, endpoint_id, project_id): + """Create an endpoint to project association. + + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param project_id: identity of the project to be associated with + :type project_id: string + :raises: keystone.exception.Conflict: If the endpoint was already + added to project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_from_project(self, endpoint_id, project_id): + """Remove an endpoint to project association. + + :param endpoint_id: identity of endpoint to remove + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint was not found + in the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_endpoint_in_project(self, endpoint_id, project_id): + """Check if an endpoint is associated with a project. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint was not found + in the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints_for_project(self, project_id): + """List all endpoints associated with a project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: a list of identity endpoint ids or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_for_endpoint(self, endpoint_id): + """List all projects associated with an endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: a list of projects or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_endpoint(self, endpoint_id): + """Remove all the endpoints to project association with endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def delete_association_by_project(self, project_id): + """Remove all the endpoints to project association with project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def create_endpoint_group(self, endpoint_group): + """Create an endpoint group. + + :param endpoint_group: endpoint group to create + :type endpoint_group: dictionary + :raises: keystone.exception.Conflict: If a duplicate endpoint group + already exists. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group(self, endpoint_group_id): + """Get an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + """Update an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param endpoint_group: A full or partial endpoint_group + :type endpoint_group: dictionary + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group(self, endpoint_group_id): + """Delete an endpoint group. + + :param endpoint_group_id: identity of endpoint group to delete + :type endpoint_group_id: string + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + """Add an endpoint group to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.Conflict: If the endpoint group was already + added to the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + """Get endpoint group to project association. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint group to the + project association was not found. + :returns: a project endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups(self): + """List all endpoint groups. + + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups_for_project(self, project_id): + """List all endpoint group to project associations for a project. + + :param project_id: identity of project to associate + :type project_id: string + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + """List all projects associated with endpoint group. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + """Remove an endpoint to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.NotFound: If endpoint group project + association was not found. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group_association_by_project(self, project_id): + """Remove endpoint group to project associations. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone/catalog/backends/sql.py b/keystone/catalog/backends/sql.py index ee7e66ba5f..d0a04c370a 100644 --- a/keystone/catalog/backends/sql.py +++ b/keystone/catalog/backends/sql.py @@ -19,10 +19,10 @@ from oslo_config import cfg import sqlalchemy from sqlalchemy.sql import true -from keystone import catalog -from keystone.catalog import core +from keystone.catalog.backends import base from keystone.common import driver_hints from keystone.common import sql +from keystone.common import utils from keystone import exception from keystone.i18n import _ @@ -81,7 +81,7 @@ class Endpoint(sql.ModelBase, sql.DictBase): extra = sql.Column(sql.JsonBlob()) -class Catalog(catalog.CatalogDriverV8): +class Catalog(base.CatalogDriverV8): # Regions def list_regions(self, hints): with sql.session_for_read() as session: @@ -289,7 +289,7 @@ class Catalog(catalog.CatalogDriverV8): if not endpoint.service['enabled']: continue try: - formatted_url = core.format_url( + formatted_url = utils.format_url( endpoint['url'], substitutions, silent_keyerror_failures=silent_keyerror_failures) if formatted_url is not None: @@ -351,7 +351,7 @@ class Catalog(catalog.CatalogDriverV8): del endpoint['enabled'] endpoint['region'] = endpoint['region_id'] try: - formatted_url = core.format_url( + formatted_url = utils.format_url( endpoint['url'], d, silent_keyerror_failures=silent_keyerror_failures) if formatted_url: diff --git a/keystone/catalog/backends/templated.py b/keystone/catalog/backends/templated.py index 60b6c874a3..d6924bcdcb 100644 --- a/keystone/catalog/backends/templated.py +++ b/keystone/catalog/backends/templated.py @@ -19,7 +19,8 @@ from oslo_config import cfg from oslo_log import log import six -from keystone.catalog import core +from keystone.catalog.backends import base +from keystone.common import utils from keystone import exception from keystone.i18n import _LC @@ -56,7 +57,7 @@ def parse_templates(template_lines): return o -class Catalog(core.Driver): +class Catalog(base.CatalogDriverV8): """A backend that generates endpoints for the Catalog based on templates. It is usually configured via config entries that look like: @@ -231,7 +232,7 @@ class Catalog(core.Driver): service_data = {} try: for k, v in service_ref.items(): - formatted_value = core.format_url( + formatted_value = utils.format_url( v, substitutions, silent_keyerror_failures=silent_keyerror_failures) if formatted_value: diff --git a/keystone/catalog/controllers.py b/keystone/catalog/controllers.py index 1b051e01b4..aa7a6aa481 100644 --- a/keystone/catalog/controllers.py +++ b/keystone/catalog/controllers.py @@ -17,10 +17,10 @@ import uuid import six -from keystone.catalog import core from keystone.catalog import schema from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import validation from keystone.common import wsgi from keystone import exception @@ -144,7 +144,7 @@ class Endpoint(controller.V2Controller): for interface in INTERFACES: interface_url = endpoint.get(interface + 'url') if interface_url: - core.check_endpoint_url(interface_url) + utils.check_endpoint_url(interface_url) initiator = notifications._get_request_audit_info(context) @@ -348,7 +348,7 @@ class EndpointV3(controller.V3Controller): @controller.protected() @validation.validated(schema.endpoint_create, 'endpoint') def create_endpoint(self, context, endpoint): - core.check_endpoint_url(endpoint['url']) + utils.check_endpoint_url(endpoint['url']) ref = self._assign_unique_id(self._normalize_dict(endpoint)) ref = self._validate_endpoint_region(ref, context) initiator = notifications._get_request_audit_info(context) diff --git a/keystone/catalog/core.py b/keystone/catalog/core.py index c5ca3db5e3..b43b7ce784 100644 --- a/keystone/catalog/core.py +++ b/keystone/catalog/core.py @@ -15,32 +15,24 @@ """Main entry point into the Catalog service.""" -import abc -import itertools - from oslo_cache import core as oslo_cache from oslo_config import cfg from oslo_log import log -import six +from oslo_log import versionutils +from keystone.catalog.backends import base from keystone.common import cache from keystone.common import dependency from keystone.common import driver_hints from keystone.common import manager -from keystone.common import utils from keystone import exception from keystone.i18n import _ -from keystone.i18n import _LE from keystone import notifications CONF = cfg.CONF LOG = log.getLogger(__name__) -WHITELISTED_PROPERTIES = [ - 'tenant_id', 'project_id', 'user_id', - 'public_bind_host', 'admin_bind_host', - 'compute_host', 'admin_port', 'public_port', - 'public_endpoint', 'admin_endpoint', ] + # This is a general cache region for catalog administration (CRUD operations). MEMOIZE = cache.get_memoization_decorator(group='catalog') @@ -55,70 +47,6 @@ MEMOIZE_COMPUTED_CATALOG = cache.get_memoization_decorator( region=COMPUTED_CATALOG_REGION) -def format_url(url, substitutions, silent_keyerror_failures=None): - """Format a user-defined URL with the given substitutions. - - :param string url: the URL to be formatted - :param dict substitutions: the dictionary used for substitution - :param list silent_keyerror_failures: keys for which we should be silent - if there is a KeyError exception on substitution attempt - :returns: a formatted URL - - """ - substitutions = utils.WhiteListedItemFilter( - WHITELISTED_PROPERTIES, - substitutions) - allow_keyerror = silent_keyerror_failures or [] - try: - result = url.replace('$(', '%(') % substitutions - except AttributeError: - LOG.error(_LE('Malformed endpoint - %(url)r is not a string'), - {"url": url}) - raise exception.MalformedEndpoint(endpoint=url) - except KeyError as e: - if not e.args or e.args[0] not in allow_keyerror: - LOG.error(_LE("Malformed endpoint %(url)s - unknown key " - "%(keyerror)s"), - {"url": url, - "keyerror": e}) - raise exception.MalformedEndpoint(endpoint=url) - else: - result = None - except TypeError as e: - LOG.error(_LE("Malformed endpoint '%(url)s'. The following type error " - "occurred during string substitution: %(typeerror)s"), - {"url": url, - "typeerror": e}) - raise exception.MalformedEndpoint(endpoint=url) - except ValueError as e: - LOG.error(_LE("Malformed endpoint %s - incomplete format " - "(are you missing a type notifier ?)"), url) - raise exception.MalformedEndpoint(endpoint=url) - return result - - -def check_endpoint_url(url): - """Check substitution of url. - - The invalid urls are as follows: - urls with substitutions that is not in the whitelist - - Check the substitutions in the URL to make sure they are valid - and on the whitelist. - - :param str url: the URL to validate - :rtype: None - :raises keystone.exception.URLValidationError: if the URL is invalid - """ - # check whether the property in the path is exactly the same - # with that in the whitelist below - substitutions = dict(zip(WHITELISTED_PROPERTIES, itertools.repeat(''))) - try: - url.replace('$(', '%(') % substitutions - except (KeyError, TypeError, ValueError): - raise exception.URLValidationError(url) - - @dependency.provider('catalog_api') @dependency.requires('resource_api') class Manager(manager.Manager): @@ -384,511 +312,13 @@ class Manager(manager.Manager): return filtered_endpoints -@six.add_metaclass(abc.ABCMeta) -class CatalogDriverV8(object): - """Interface description for the Catalog driver.""" +@versionutils.deprecated( + versionutils.deprecated.NEWTON, + what='keystone.catalog.CatalogDriverV8', + in_favor_of='keystone.catalog.backends.base.CatalogDriverV8', + remove_in=+1) +class CatalogDriverV8(base.CatalogDriverV8): + pass - def _get_list_limit(self): - return CONF.catalog.list_limit or CONF.list_limit - def _ensure_no_circle_in_hierarchical_regions(self, region_ref): - if region_ref.get('parent_region_id') is None: - return - - root_region_id = region_ref['id'] - parent_region_id = region_ref['parent_region_id'] - - while parent_region_id: - # NOTE(wanghong): check before getting parent region can ensure no - # self circle - if parent_region_id == root_region_id: - raise exception.CircularRegionHierarchyError( - parent_region_id=parent_region_id) - parent_region = self.get_region(parent_region_id) - parent_region_id = parent_region.get('parent_region_id') - - @abc.abstractmethod - def create_region(self, region_ref): - """Create a new region. - - :raises keystone.exception.Conflict: If the region already exists. - :raises keystone.exception.RegionNotFound: If the parent region - is invalid. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_regions(self, hints): - """List all regions. - - :param hints: contains the list of filters yet to be satisfied. - Any filters satisfied here will be removed so that - the caller will know if any filters remain. - - :returns: list of region_refs or an empty list. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_region(self, region_id): - """Get region by id. - - :returns: region_ref dict - :raises keystone.exception.RegionNotFound: If the region doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_region(self, region_id, region_ref): - """Update region by id. - - :returns: region_ref dict - :raises keystone.exception.RegionNotFound: If the region doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_region(self, region_id): - """Delete an existing region. - - :raises keystone.exception.RegionNotFound: If the region doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_service(self, service_id, service_ref): - """Create a new service. - - :raises keystone.exception.Conflict: If a duplicate service exists. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_services(self, hints): - """List all services. - - :param hints: contains the list of filters yet to be satisfied. - Any filters satisfied here will be removed so that - the caller will know if any filters remain. - - :returns: list of service_refs or an empty list. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_service(self, service_id): - """Get service by id. - - :returns: service_ref dict - :raises keystone.exception.ServiceNotFound: If the service doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_service(self, service_id, service_ref): - """Update service by id. - - :returns: service_ref dict - :raises keystone.exception.ServiceNotFound: If the service doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_service(self, service_id): - """Delete an existing service. - - :raises keystone.exception.ServiceNotFound: If the service doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_endpoint(self, endpoint_id, endpoint_ref): - """Create a new endpoint for a service. - - :raises keystone.exception.Conflict: If a duplicate endpoint exists. - :raises keystone.exception.ServiceNotFound: If the service doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_endpoint(self, endpoint_id): - """Get endpoint by id. - - :returns: endpoint_ref dict - :raises keystone.exception.EndpointNotFound: If the endpoint doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_endpoints(self, hints): - """List all endpoints. - - :param hints: contains the list of filters yet to be satisfied. - Any filters satisfied here will be removed so that - the caller will know if any filters remain. - - :returns: list of endpoint_refs or an empty list. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_endpoint(self, endpoint_id, endpoint_ref): - """Get endpoint by id. - - :returns: endpoint_ref dict - :raises keystone.exception.EndpointNotFound: If the endpoint doesn't - exist. - :raises keystone.exception.ServiceNotFound: If the service doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_endpoint(self, endpoint_id): - """Delete an endpoint for a service. - - :raises keystone.exception.EndpointNotFound: If the endpoint doesn't - exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_catalog(self, user_id, tenant_id): - """Retrieve and format the current service catalog. - - Example:: - - { 'RegionOne': - {'compute': { - 'adminURL': u'http://host:8774/v1.1/tenantid', - 'internalURL': u'http://host:8774/v1.1/tenant_id', - 'name': 'Compute Service', - 'publicURL': u'http://host:8774/v1.1/tenantid'}, - 'ec2': { - 'adminURL': 'http://host:8773/services/Admin', - 'internalURL': 'http://host:8773/services/Cloud', - 'name': 'EC2 Service', - 'publicURL': 'http://host:8773/services/Cloud'}} - - :returns: A nested dict representing the service catalog or an - empty dict. - :raises keystone.exception.NotFound: If the endpoint doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - def get_v3_catalog(self, user_id, tenant_id): - """Retrieve and format the current V3 service catalog. - - The default implementation builds the V3 catalog from the V2 catalog. - - Example:: - - [ - { - "endpoints": [ - { - "interface": "public", - "id": "--endpoint-id--", - "region": "RegionOne", - "url": "http://external:8776/v1/--project-id--" - }, - { - "interface": "internal", - "id": "--endpoint-id--", - "region": "RegionOne", - "url": "http://internal:8776/v1/--project-id--" - }], - "id": "--service-id--", - "type": "volume" - }] - - :returns: A list representing the service catalog or an empty list - :raises keystone.exception.NotFound: If the endpoint doesn't exist. - - """ - v2_catalog = self.get_catalog(user_id, tenant_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 - - @abc.abstractmethod - def add_endpoint_to_project(self, endpoint_id, project_id): - """Create an endpoint to project association. - - :param endpoint_id: identity of endpoint to associate - :type endpoint_id: string - :param project_id: identity of the project to be associated with - :type project_id: string - :raises: keystone.exception.Conflict: If the endpoint was already - added to project. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def remove_endpoint_from_project(self, endpoint_id, project_id): - """Remove an endpoint to project association. - - :param endpoint_id: identity of endpoint to remove - :type endpoint_id: string - :param project_id: identity of the project associated with - :type project_id: string - :raises keystone.exception.NotFound: If the endpoint was not found - in the project. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def check_endpoint_in_project(self, endpoint_id, project_id): - """Check if an endpoint is associated with a project. - - :param endpoint_id: identity of endpoint to check - :type endpoint_id: string - :param project_id: identity of the project associated with - :type project_id: string - :raises keystone.exception.NotFound: If the endpoint was not found - in the project. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_endpoints_for_project(self, project_id): - """List all endpoints associated with a project. - - :param project_id: identity of the project to check - :type project_id: string - :returns: a list of identity endpoint ids or an empty list. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_projects_for_endpoint(self, endpoint_id): - """List all projects associated with an endpoint. - - :param endpoint_id: identity of endpoint to check - :type endpoint_id: string - :returns: a list of projects or an empty list. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_association_by_endpoint(self, endpoint_id): - """Remove all the endpoints to project association with endpoint. - - :param endpoint_id: identity of endpoint to check - :type endpoint_id: string - :returns: None - - """ - raise exception.NotImplemented() - - @abc.abstractmethod - def delete_association_by_project(self, project_id): - """Remove all the endpoints to project association with project. - - :param project_id: identity of the project to check - :type project_id: string - :returns: None - - """ - raise exception.NotImplemented() - - @abc.abstractmethod - def create_endpoint_group(self, endpoint_group): - """Create an endpoint group. - - :param endpoint_group: endpoint group to create - :type endpoint_group: dictionary - :raises: keystone.exception.Conflict: If a duplicate endpoint group - already exists. - :returns: an endpoint group representation. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_endpoint_group(self, endpoint_group_id): - """Get an endpoint group. - - :param endpoint_group_id: identity of endpoint group to retrieve - :type endpoint_group_id: string - :raises keystone.exception.NotFound: If the endpoint group was not - found. - :returns: an endpoint group representation. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_endpoint_group(self, endpoint_group_id, endpoint_group): - """Update an endpoint group. - - :param endpoint_group_id: identity of endpoint group to retrieve - :type endpoint_group_id: string - :param endpoint_group: A full or partial endpoint_group - :type endpoint_group: dictionary - :raises keystone.exception.NotFound: If the endpoint group was not - found. - :returns: an endpoint group representation. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_endpoint_group(self, endpoint_group_id): - """Delete an endpoint group. - - :param endpoint_group_id: identity of endpoint group to delete - :type endpoint_group_id: string - :raises keystone.exception.NotFound: If the endpoint group was not - found. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def add_endpoint_group_to_project(self, endpoint_group_id, project_id): - """Add an endpoint group to project association. - - :param endpoint_group_id: identity of endpoint to associate - :type endpoint_group_id: string - :param project_id: identity of project to associate - :type project_id: string - :raises keystone.exception.Conflict: If the endpoint group was already - added to the project. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_endpoint_group_in_project(self, endpoint_group_id, project_id): - """Get endpoint group to project association. - - :param endpoint_group_id: identity of endpoint group to retrieve - :type endpoint_group_id: string - :param project_id: identity of project to associate - :type project_id: string - :raises keystone.exception.NotFound: If the endpoint group to the - project association was not found. - :returns: a project endpoint group representation. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_endpoint_groups(self): - """List all endpoint groups. - - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_endpoint_groups_for_project(self, project_id): - """List all endpoint group to project associations for a project. - - :param project_id: identity of project to associate - :type project_id: string - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_projects_associated_with_endpoint_group(self, endpoint_group_id): - """List all projects associated with endpoint group. - - :param endpoint_group_id: identity of endpoint to associate - :type endpoint_group_id: string - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def remove_endpoint_group_from_project(self, endpoint_group_id, - project_id): - """Remove an endpoint to project association. - - :param endpoint_group_id: identity of endpoint to associate - :type endpoint_group_id: string - :param project_id: identity of project to associate - :type project_id: string - :raises keystone.exception.NotFound: If endpoint group project - association was not found. - :returns: None. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_endpoint_group_association_by_project(self, project_id): - """Remove endpoint group to project associations. - - :param project_id: identity of the project to check - :type project_id: string - :returns: None - - """ - raise exception.NotImplemented() # pragma: no cover - -Driver = manager.create_legacy_driver(CatalogDriverV8) +Driver = manager.create_legacy_driver(base.CatalogDriverV8) diff --git a/keystone/common/utils.py b/keystone/common/utils.py index 17f9463ff8..6c4a5a435c 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -20,6 +20,7 @@ import calendar import collections import grp import hashlib +import itertools import os import pwd import uuid @@ -40,8 +41,12 @@ from keystone.i18n import _, _LE, _LW CONF = cfg.CONF - LOG = log.getLogger(__name__) +WHITELISTED_PROPERTIES = [ + 'tenant_id', 'project_id', 'user_id', + 'public_bind_host', 'admin_bind_host', + 'compute_host', 'admin_port', 'public_port', + 'public_endpoint', 'admin_endpoint', ] # NOTE(stevermar): This UUID must stay the same, forever, across @@ -596,3 +601,67 @@ def remove_standard_port(url): o = o._replace(netloc=host) return moves.urllib.parse.urlunparse(o) + + +def format_url(url, substitutions, silent_keyerror_failures=None): + """Format a user-defined URL with the given substitutions. + + :param string url: the URL to be formatted + :param dict substitutions: the dictionary used for substitution + :param list silent_keyerror_failures: keys for which we should be silent + if there is a KeyError exception on substitution attempt + :returns: a formatted URL + + """ + substitutions = WhiteListedItemFilter( + WHITELISTED_PROPERTIES, + substitutions) + allow_keyerror = silent_keyerror_failures or [] + try: + result = url.replace('$(', '%(') % substitutions + except AttributeError: + LOG.error(_LE('Malformed endpoint - %(url)r is not a string'), + {"url": url}) + raise exception.MalformedEndpoint(endpoint=url) + except KeyError as e: + if not e.args or e.args[0] not in allow_keyerror: + LOG.error(_LE("Malformed endpoint %(url)s - unknown key " + "%(keyerror)s"), + {"url": url, + "keyerror": e}) + raise exception.MalformedEndpoint(endpoint=url) + else: + result = None + except TypeError as e: + LOG.error(_LE("Malformed endpoint '%(url)s'. The following type error " + "occurred during string substitution: %(typeerror)s"), + {"url": url, + "typeerror": e}) + raise exception.MalformedEndpoint(endpoint=url) + except ValueError as e: + LOG.error(_LE("Malformed endpoint %s - incomplete format " + "(are you missing a type notifier ?)"), url) + raise exception.MalformedEndpoint(endpoint=url) + return result + + +def check_endpoint_url(url): + """Check substitution of url. + + The invalid urls are as follows: + urls with substitutions that is not in the whitelist + + Check the substitutions in the URL to make sure they are valid + and on the whitelist. + + :param str url: the URL to validate + :rtype: None + :raises keystone.exception.URLValidationError: if the URL is invalid + """ + # check whether the property in the path is exactly the same + # with that in the whitelist below + substitutions = dict(zip(WHITELISTED_PROPERTIES, itertools.repeat(''))) + try: + url.replace('$(', '%(') % substitutions + except (KeyError, TypeError, ValueError): + raise exception.URLValidationError(url) diff --git a/keystone/contrib/endpoint_filter/backends/catalog_sql.py b/keystone/contrib/endpoint_filter/backends/catalog_sql.py index ad39d04587..1c39336bd3 100644 --- a/keystone/contrib/endpoint_filter/backends/catalog_sql.py +++ b/keystone/contrib/endpoint_filter/backends/catalog_sql.py @@ -15,8 +15,8 @@ from oslo_config import cfg from keystone.catalog.backends import sql -from keystone.catalog import core as catalog_core from keystone.common import dependency +from keystone.common import utils CONF = cfg.CONF @@ -56,7 +56,7 @@ class EndpointFilterCatalog(sql.Catalog): del endpoint['legacy_endpoint_id'] # Include deprecated region for backwards compatibility endpoint['region'] = endpoint['region_id'] - endpoint['url'] = catalog_core.format_url( + endpoint['url'] = utils.format_url( endpoint['url'], substitutions) # populate filtered endpoints if 'endpoints' in services[service_id]: diff --git a/keystone/tests/unit/catalog/test_backends.py b/keystone/tests/unit/catalog/test_backends.py index 55898015ad..79730bcdc1 100644 --- a/keystone/tests/unit/catalog/test_backends.py +++ b/keystone/tests/unit/catalog/test_backends.py @@ -17,7 +17,7 @@ import mock from six.moves import range from testtools import matchers -from keystone.catalog import core +from keystone.catalog.backends import base from keystone.common import driver_hints from keystone import exception from keystone.tests import unit @@ -187,7 +187,7 @@ class CatalogTests(object): region_two['id'], {'parent_region_id': region_four['id']}) - @mock.patch.object(core.CatalogDriverV8, + @mock.patch.object(base.CatalogDriverV8, "_ensure_no_circle_in_hierarchical_regions") def test_circular_regions_can_be_deleted(self, mock_ensure_on_circle): # turn off the enforcement so that cycles can be created for the test diff --git a/keystone/tests/unit/catalog/test_core.py b/keystone/tests/unit/catalog/test_core.py index b04b0bb703..be8ceb1a90 100644 --- a/keystone/tests/unit/catalog/test_core.py +++ b/keystone/tests/unit/catalog/test_core.py @@ -12,7 +12,7 @@ import uuid -from keystone.catalog import core +from keystone.common import utils from keystone import exception from keystone.tests import unit @@ -25,33 +25,33 @@ class FormatUrlTests(unit.BaseTestCase): project_id = uuid.uuid4().hex values = {'public_bind_host': 'server', 'admin_port': 9090, 'tenant_id': 'A', 'user_id': 'B', 'project_id': project_id} - actual_url = core.format_url(url_template, values) + actual_url = utils.format_url(url_template, values) expected_url = 'http://server:9090/A/B/%s' % (project_id,) self.assertEqual(expected_url, actual_url) def test_raises_malformed_on_missing_key(self): self.assertRaises(exception.MalformedEndpoint, - core.format_url, + utils.format_url, "http://$(public_bind_host)s/$(public_port)d", {"public_bind_host": "1"}) def test_raises_malformed_on_wrong_type(self): self.assertRaises(exception.MalformedEndpoint, - core.format_url, + utils.format_url, "http://$(public_bind_host)d", {"public_bind_host": "something"}) def test_raises_malformed_on_incomplete_format(self): self.assertRaises(exception.MalformedEndpoint, - core.format_url, + utils.format_url, "http://$(public_bind_host)", {"public_bind_host": "1"}) def test_formatting_a_non_string(self): def _test(url_template): self.assertRaises(exception.MalformedEndpoint, - core.format_url, + utils.format_url, url_template, {}) @@ -67,7 +67,7 @@ class FormatUrlTests(unit.BaseTestCase): values = {'public_bind_host': 'server', 'public_port': 9090, 'tenant_id': 'A', 'user_id': 'B', 'admin_token': 'C'} self.assertRaises(exception.MalformedEndpoint, - core.format_url, + utils.format_url, url_template, values) @@ -82,7 +82,7 @@ class FormatUrlTests(unit.BaseTestCase): '$(tenant_id)s/$(user_id)s') values = {'public_bind_host': 'server', 'admin_port': 9090, 'user_id': 'B'} - self.assertIsNone(core.format_url(url_template, values, + self.assertIsNone(utils.format_url(url_template, values, silent_keyerror_failures=['tenant_id'])) def test_substitution_with_allowed_project_keyerror(self): @@ -96,5 +96,5 @@ class FormatUrlTests(unit.BaseTestCase): '$(project_id)s/$(user_id)s') values = {'public_bind_host': 'server', 'admin_port': 9090, 'user_id': 'B'} - self.assertIsNone(core.format_url(url_template, values, + self.assertIsNone(utils.format_url(url_template, values, silent_keyerror_failures=['project_id']))