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']))