Driver registry

Uses automatic dependency injection to provide controllers with driver
interfaces (identity_api, token_api, etc).

See tests/test_injection.py for a self-contained example.

Change-Id: I255087de534292fbf57a45b19f97488f831f607c
This commit is contained in:
Dolph Mathews 2012-12-19 10:04:21 -06:00
parent ac2d92ca2e
commit 03eb2801a3
21 changed files with 297 additions and 126 deletions

View File

@ -17,20 +17,16 @@
import uuid
from keystone.catalog import core
from keystone.common import controller
from keystone.common import wsgi
from keystone.common import dependency
from keystone import exception
from keystone import identity
from keystone import policy
from keystone import token
INTERFACES = ['public', 'internal', 'admin']
@dependency.requires('catalog_api')
class Service(controller.V2Controller):
def get_services(self, context):
self.assert_admin(context)
service_list = self.catalog_api.list_services(context)
@ -55,6 +51,7 @@ class Service(controller.V2Controller):
return {'OS-KSADM:service': new_service_ref}
@dependency.requires('catalog_api')
class Endpoint(controller.V2Controller):
def get_endpoints(self, context):
"""Merge matching v3 endpoint refs into legacy refs."""
@ -115,6 +112,7 @@ class Endpoint(controller.V2Controller):
raise exception.EndpointNotFound(endpoint_id=endpoint_id)
@dependency.requires('catalog_api')
class ServiceV3(controller.V3Controller):
@controller.protected
def create_service(self, context, service):
@ -147,6 +145,7 @@ class ServiceV3(controller.V3Controller):
return self.catalog_api.delete_service(context, service_id)
@dependency.requires('catalog_api')
class EndpointV3(controller.V3Controller):
@controller.protected
def create_endpoint(self, context, endpoint):

View File

@ -17,6 +17,7 @@
"""Main entry point into the Catalog service."""
from keystone.common import dependency
from keystone.common import logging
from keystone.common import manager
from keystone import config
@ -51,6 +52,7 @@ def format_url(url, data):
return result
@dependency.provider('catalog_api')
class Manager(manager.Manager):
"""Default pivot point for the Catalog backend.

View File

@ -13,13 +13,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystone.catalog import controllers
from keystone.common import router
from keystone.common import wsgi
def append_v3_routers(mapper, routers, apis):
routers.append(router.Router(controllers.ServiceV3(**apis),
def append_v3_routers(mapper, routers):
routers.append(router.Router(controllers.ServiceV3(),
'services', 'service'))
routers.append(router.Router(controllers.EndpointV3(**apis),
routers.append(router.Router(controllers.EndpointV3(),
'endpoints', 'endpoint'))

View File

@ -1,6 +1,7 @@
import uuid
import functools
from keystone.common import dependency
from keystone.common import logging
from keystone.common import wsgi
from keystone import exception
@ -55,15 +56,10 @@ def protected(f):
return wrapper
@dependency.requires('identity_api', 'policy_api', 'token_api')
class V2Controller(wsgi.Application):
"""Base controller class for Identity API v2."""
def __init__(self, catalog_api, identity_api, policy_api, token_api):
self.catalog_api = catalog_api
self.identity_api = identity_api
self.policy_api = policy_api
self.token_api = token_api
super(V2Controller, self).__init__()
pass
class V3Controller(V2Controller):

View File

@ -0,0 +1,67 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC
#
# 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.
REGISTRY = {}
class UnresolvableDependencyException(Exception):
def __init__(self, name):
msg = 'Unregistered dependency: %s' % name
super(UnresolvableDependencyException, self).__init__(msg)
def provider(name):
"""Register the wrapped dependency provider under the specified name."""
def wrapper(cls):
def wrapped(init):
def __wrapped_init__(self, *args, **kwargs):
"""Initialize the wrapped object and add it to the registry."""
init(self, *args, **kwargs)
REGISTRY[name] = self
return __wrapped_init__
cls.__init__ = wrapped(cls.__init__)
return cls
return wrapper
def requires(*dependencies):
"""Inject specified dependencies from the registry into the instance."""
def wrapper(self, *args, **kwargs):
"""Inject each dependency from the registry."""
self.__wrapped_init__(*args, **kwargs)
for dependency in self._dependencies:
if dependency not in REGISTRY:
raise UnresolvableDependencyException(dependency)
setattr(self, dependency, REGISTRY[dependency])
def wrapped(cls):
"""Note the required dependencies on the object for later injection.
The dependencies of the parent class are combined with that of the
child class to create a new set of dependencies.
"""
existing_dependencies = getattr(cls, '_dependencies', set())
cls._dependencies = existing_dependencies.union(dependencies)
if not hasattr(cls, '__wrapped_init__'):
cls.__wrapped_init__ = cls.__init__
cls.__init__ = wrapper
return cls
return wrapped

View File

@ -16,8 +16,6 @@
from keystone import catalog
from keystone.common import wsgi
from keystone import identity
from keystone import policy
from keystone import token
class CrudExtension(wsgi.ExtensionRouter):
@ -28,16 +26,11 @@ class CrudExtension(wsgi.ExtensionRouter):
"""
def add_routes(self, mapper):
apis = dict(catalog_api=catalog.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager())
tenant_controller = identity.controllers.Tenant(**apis)
user_controller = identity.controllers.User(**apis)
role_controller = identity.controllers.Role(**apis)
service_controller = catalog.controllers.Service(**apis)
endpoint_controller = catalog.controllers.Endpoint(**apis)
tenant_controller = identity.controllers.Tenant()
user_controller = identity.controllers.User()
role_controller = identity.controllers.Role()
service_controller = catalog.controllers.Service()
endpoint_controller = catalog.controllers.Endpoint()
# Tenant Operations
mapper.connect(

View File

@ -36,20 +36,20 @@ glance to list images needed to perform the requested task.
import uuid
from keystone import catalog
from keystone.common import controller
from keystone.common import dependency
from keystone.common import manager
from keystone.common import utils
from keystone.common import wsgi
from keystone import config
from keystone import exception
from keystone import identity
from keystone import policy
from keystone import token
CONF = config.CONF
@dependency.provider('ec2_api')
class Manager(manager.Manager):
"""Default pivot point for the EC2 Credentials backend.
@ -95,15 +95,8 @@ class Ec2Extension(wsgi.ExtensionRouter):
conditions=dict(method=['DELETE']))
class Ec2Controller(wsgi.Application):
def __init__(self):
self.catalog_api = catalog.Manager()
self.identity_api = identity.Manager()
self.token_api = token.Manager()
self.policy_api = policy.Manager()
self.ec2_api = Manager()
super(Ec2Controller, self).__init__()
@dependency.requires('catalog_api', 'ec2_api')
class Ec2Controller(controller.V2Controller):
def check_signature(self, creds_ref, credentials):
signer = utils.Ec2Signer(creds_ref['secret'])
signature = signer.generate(credentials)

View File

@ -18,13 +18,9 @@ import copy
import uuid
from keystone import exception
from keystone.common import controller
from keystone.common import logging
from keystone.common import wsgi
from keystone import catalog
from keystone import identity
from keystone import policy
from keystone import token
LOG = logging.getLogger(__name__)
@ -81,12 +77,7 @@ class CrudExtension(wsgi.ExtensionRouter):
"""
def add_routes(self, mapper):
apis = dict(catalog_api=catalog.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager())
user_controller = UserController(**apis)
user_controller = UserController()
mapper.connect('/OS-KSCRUD/users/{user_id}',
controller=user_controller,

View File

@ -22,11 +22,7 @@ import uuid
from keystone.common import controller
from keystone.common import logging
from keystone.common import wsgi
from keystone import exception
from keystone.identity import core
from keystone import policy
from keystone import token
LOG = logging.getLogger(__name__)

View File

@ -16,14 +16,9 @@
"""Main entry point into the Identity service."""
import urllib
import urlparse
import uuid
from keystone.common import controller
from keystone.common import dependency
from keystone.common import logging
from keystone.common import manager
from keystone.common import wsgi
from keystone import config
from keystone import exception
@ -51,6 +46,7 @@ def filter_user(user_ref):
return user_ref
@dependency.provider('identity_api')
class Manager(manager.Manager):
"""Default pivot point for the Identity backend.

View File

@ -20,12 +20,8 @@ from keystone.common import router
class Public(wsgi.ComposableRouter):
def __init__(self, apis):
self.apis = apis
super(Public, self).__init__()
def add_routes(self, mapper):
tenant_controller = controllers.Tenant(**self.apis)
tenant_controller = controllers.Tenant()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_tenants_for_token',
@ -33,13 +29,9 @@ class Public(wsgi.ComposableRouter):
class Admin(wsgi.ComposableRouter):
def __init__(self, apis):
self.apis = apis
super(Admin, self).__init__()
def add_routes(self, mapper):
# Tenant Operations
tenant_controller = controllers.Tenant(**self.apis)
tenant_controller = controllers.Tenant()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_all_tenants',
@ -50,14 +42,14 @@ class Admin(wsgi.ComposableRouter):
conditions=dict(method=['GET']))
# User Operations
user_controller = controllers.User(**self.apis)
user_controller = controllers.User()
mapper.connect('/users/{user_id}',
controller=user_controller,
action='get_user',
conditions=dict(method=['GET']))
# Role Operations
roles_controller = controllers.Role(**self.apis)
roles_controller = controllers.Role()
mapper.connect('/tenants/{tenant_id}/users/{user_id}/roles',
controller=roles_controller,
action='get_user_roles',
@ -68,11 +60,11 @@ class Admin(wsgi.ComposableRouter):
conditions=dict(method=['GET']))
def append_v3_routers(mapper, routers, apis):
def append_v3_routers(mapper, routers):
routers.append(
router.Router(controllers.DomainV3(**apis),
router.Router(controllers.DomainV3(),
'domains', 'domain'))
project_controller = controllers.ProjectV3(**apis)
project_controller = controllers.ProjectV3()
routers.append(
router.Router(project_controller,
'projects', 'project'))
@ -81,12 +73,12 @@ def append_v3_routers(mapper, routers, apis):
action='list_user_projects',
conditions=dict(method=['GET']))
routers.append(
router.Router(controllers.UserV3(**apis),
router.Router(controllers.UserV3(),
'users', 'user'))
routers.append(
router.Router(controllers.CredentialV3(**apis),
router.Router(controllers.CredentialV3(),
'credentials', 'credential'))
role_controller = controllers.RoleV3(**apis)
role_controller = controllers.RoleV3()
routers.append(router.Router(role_controller, 'roles', 'role'))
mapper.connect('/projects/{project_id}/users/{user_id}/roles/{role_id}',
controller=role_controller,

View File

@ -17,6 +17,7 @@
"""Main entry point into the Policy service."""
from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone import exception
@ -25,6 +26,7 @@ from keystone import exception
CONF = config.CONF
@dependency.provider('policy_api')
class Manager(manager.Manager):
"""Default pivot point for the Policy backend.

View File

@ -17,6 +17,6 @@ from keystone.policy import controllers
from keystone.common import router
def append_v3_routers(mapper, routers, apis):
policy_controller = controllers.PolicyV3(**apis)
def append_v3_routers(mapper, routers):
policy_controller = controllers.PolicyV3()
routers.append(router.Router(policy_controller, 'policies', 'policy'))

View File

@ -14,17 +14,18 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
The only types of routers in this file should be ComposingRouters.
The routers for the submodules should be in the module specific router files
for example, the Composable Router for identity belongs in
keystone/identity/routers.py
The only types of routers in this file should be ``ComposingRouters``.
The routers for the backends should be in the backend-specific router modules.
For example, the ``ComposableRouter`` for ``identity`` belongs in::
keystone.identity.routers
"""
from keystone.common import wsgi
from keystone import catalog
from keystone import controllers
from keystone import exception
class Extension(wsgi.ComposableRouter):

View File

@ -17,22 +17,22 @@
import routes
from keystone import catalog
from keystone.contrib import ec2
from keystone.common import logging
from keystone.common import wsgi
from keystone import exception
from keystone import identity
from keystone import policy
from keystone import routers
from keystone import token
LOG = logging.getLogger(__name__)
def _apis():
return dict(catalog_api=catalog.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager())
DRIVERS = dict(
catalog_api=catalog.Manager(),
ec2_api=ec2.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager())
@logging.fail_gracefully
@ -40,10 +40,10 @@ def public_app_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
return wsgi.ComposingRouter(routes.Mapper(),
[identity.routers.Public(_apis()),
token.routers.Router(_apis()),
routers.Version('public'),
routers.Extension(False)])
[identity.routers.Public(),
token.routers.Router(),
routers.Version('public'),
routers.Extension(False)])
@logging.fail_gracefully
@ -51,8 +51,8 @@ def admin_app_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
return wsgi.ComposingRouter(routes.Mapper(),
[identity.routers.Admin(_apis()),
token.routers.Router(_apis()),
[identity.routers.Admin(),
token.routers.Router(),
routers.Version('admin'),
routers.Extension()])
@ -80,6 +80,6 @@ def v3_app_factory(global_conf, **local_conf):
mapper = routes.Mapper()
v3routers = []
for module in [catalog, identity, policy]:
module.routers.append_v3_routers(mapper, v3routers, _apis())
#TODO put token routes here
module.routers.append_v3_routers(mapper, v3routers)
# TODO(ayoung): put token routes here
return wsgi.ComposingRouter(mapper, v3routers)

View File

@ -31,7 +31,10 @@ from keystone.common import logging
from keystone.common import utils
from keystone.common import wsgi
from keystone import config
from keystone.openstack.common import importutils
from keystone import catalog
from keystone import identity
from keystone import policy
from keystone import token
do_monkeypatch = not os.getenv('STANDARD_THREADS')
@ -44,6 +47,7 @@ VENDOR = os.path.join(ROOTDIR, 'vendor')
TESTSDIR = os.path.join(ROOTDIR, 'tests')
ETCDIR = os.path.join(ROOTDIR, 'etc')
CONF = config.CONF
DRIVERS = {}
cd = os.chdir
@ -61,6 +65,14 @@ def testsdir(*p):
return os.path.join(TESTSDIR, *p)
def initialize_drivers():
DRIVERS['catalog_api'] = catalog.Manager()
DRIVERS['identity_api'] = identity.Manager()
DRIVERS['policy_api'] = policy.Manager()
DRIVERS['token_api'] = token.Manager()
return DRIVERS
def checkout_vendor(repo, rev):
# TODO(termie): this function is a good target for some optimizations :PERF
name = repo.split('/')[-1]
@ -202,11 +214,9 @@ class TestCase(NoModule, unittest.TestCase):
CONF.set_override(k, v)
def load_backends(self):
"""Hacky shortcut to load the backends for data manipulation."""
self.identity_api = importutils.import_object(CONF.identity.driver)
self.token_api = importutils.import_object(CONF.token.driver)
self.catalog_api = importutils.import_object(CONF.catalog.driver)
self.policy_api = importutils.import_object(CONF.policy.driver)
"""Create shortcut references to each driver for data manipulation."""
for name, manager in initialize_drivers().iteritems():
setattr(self, name, manager.driver)
def load_fixtures(self, fixtures):
"""Hacky basic and naive fixture loading based on a python module.

View File

@ -4,6 +4,7 @@ import json
from keystone import config
from keystone.common import cms
from keystone.common import controller
from keystone.common import dependency
from keystone.common import logging
from keystone import exception
from keystone.openstack.common import timeutils
@ -18,6 +19,7 @@ class ExternalAuthNotApplicable(Exception):
pass
@dependency.requires('catalog_api')
class Auth(controller.V2Controller):
def ca_cert(self, context, auth=None):
ca_file = open(config.CONF.signing.ca_certs, 'r')

View File

@ -18,8 +18,9 @@
import datetime
from keystone.common import manager
from keystone.common import cms
from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone import exception
from keystone.openstack.common import timeutils
@ -54,6 +55,7 @@ def default_expire_time():
return timeutils.utcnow() + expire_delta
@dependency.provider('token_api')
class Manager(manager.Manager):
"""Default pivot point for the Token backend.

View File

@ -18,12 +18,8 @@ from keystone.token import controllers
class Router(wsgi.ComposableRouter):
def __init__(self, apis):
self.apis = apis
super(Router, self).__init__()
def add_routes(self, mapper):
token_controller = controllers.Auth(**self.apis)
token_controller = controllers.Auth()
mapper.connect('/tokens',
controller=token_controller,
action='authenticate',

View File

@ -17,13 +17,9 @@ import uuid
import default_fixtures
from keystone import catalog
from keystone import config
from keystone import exception
from keystone import identity
from keystone.identity.backends import kvs as kvs_identity
from keystone.openstack.common import timeutils
from keystone import policy
from keystone import test
from keystone import token
@ -56,15 +52,11 @@ class AuthTest(test.TestCase):
def setUp(self):
super(AuthTest, self).setUp()
# load_fixtures checks for 'identity_api' to be defined
self.identity_api = kvs_identity.Identity()
CONF.identity.driver = 'keystone.identity.backends.kvs.Identity'
self.load_backends()
self.load_fixtures(default_fixtures)
self.api = token.controllers.Auth(
catalog_api=catalog.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager())
self.api = token.controllers.Auth()
def assertEqualTokens(self, a, b):
"""Assert that two tokens are equal.

141
tests/test_injection.py Normal file
View File

@ -0,0 +1,141 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC
#
# 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 unittest2 as unittest
import uuid
from keystone.common import dependency
class TestDependencyInjection(unittest.TestCase):
def test_dependency_injection(self):
class Interface(object):
def do_work(self):
assert False
@dependency.provider('first_api')
class FirstImplementation(Interface):
def do_work(self):
return True
@dependency.provider('second_api')
class SecondImplementation(Interface):
def do_work(self):
return True
@dependency.requires('first_api', 'second_api')
class Consumer(object):
def do_work_with_dependencies(self):
assert self.first_api.do_work()
assert self.second_api.do_work()
# initialize dependency providers
first_api = FirstImplementation()
second_api = SecondImplementation()
# ... sometime later, initialize a dependency consumer
consumer = Consumer()
# the expected dependencies should be available to the consumer
self.assertIs(consumer.first_api, first_api)
self.assertIs(consumer.second_api, second_api)
self.assertIsInstance(consumer.first_api, Interface)
self.assertIsInstance(consumer.second_api, Interface)
consumer.do_work_with_dependencies()
def test_dependency_configuration(self):
@dependency.provider('api')
class Configurable(object):
def __init__(self, value=None):
self.value = value
def get_value(self):
return self.value
@dependency.requires('api')
class Consumer(object):
def get_value(self):
return self.api.get_value()
# initialize dependency providers
api = Configurable(value=True)
# ... sometime later, initialize a dependency consumer
consumer = Consumer()
# the expected dependencies should be available to the consumer
self.assertIs(consumer.api, api)
self.assertIsInstance(consumer.api, Configurable)
self.assertTrue(consumer.get_value())
def test_inherited_dependency(self):
class Interface(object):
def do_work(self):
assert False
@dependency.provider('first_api')
class FirstImplementation(Interface):
def do_work(self):
return True
@dependency.provider('second_api')
class SecondImplementation(Interface):
def do_work(self):
return True
@dependency.requires('first_api')
class ParentConsumer(object):
def do_work_with_dependencies(self):
assert self.first_api.do_work()
@dependency.requires('second_api')
class ChildConsumer(ParentConsumer):
def do_work_with_dependencies(self):
assert self.second_api.do_work()
super(ChildConsumer, self).do_work_with_dependencies()
# initialize dependency providers
first_api = FirstImplementation()
second_api = SecondImplementation()
# ... sometime later, initialize a dependency consumer
consumer = ChildConsumer()
# dependencies should be naturally inherited
self.assertEqual(
ParentConsumer._dependencies,
set(['first_api']))
self.assertEqual(
ChildConsumer._dependencies,
set(['first_api', 'second_api']))
self.assertEqual(
consumer._dependencies,
set(['first_api', 'second_api']))
# the expected dependencies should be available to the consumer
self.assertIs(consumer.first_api, first_api)
self.assertIs(consumer.second_api, second_api)
self.assertIsInstance(consumer.first_api, Interface)
self.assertIsInstance(consumer.second_api, Interface)
consumer.do_work_with_dependencies()
def test_unresolvable_dependency(self):
@dependency.requires(uuid.uuid4().hex)
class Consumer(object):
pass
with self.assertRaises(dependency.UnresolvableDependencyException):
Consumer()