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
This commit is contained in:
Ronald De Rose 2016-04-22 02:00:28 +00:00
parent f7b33213f1
commit 2963dc1525
9 changed files with 639 additions and 606 deletions

View File

@ -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, <region_name>.publicURL=<URL> in the V2
# catalog becomes the V3 interface for the service:
# { 'interface': 'public', 'url': '<URL>', 'region':
# 'region: '<region_name>' }
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

View File

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

View File

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

View File

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

View File

@ -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, <region_name>.publicURL=<URL> in the V2
# catalog becomes the V3 interface for the service:
# { 'interface': 'public', 'url': '<URL>', 'region':
# 'region: '<region_name>' }
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)

View File

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

View File

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

View File

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

View File

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