Decouple notifications from DI

This appears to have been a marriage of convenience. There is no real
reason to combine these two concepts.

bp object-dependency-lifecycle

Change-Id: I2dead4f66675f73a9ebc1551f257218a085211f0
This commit is contained in:
David Stanek 2015-03-05 01:39:00 +00:00
parent 2bf413ecda
commit 893a10144a
5 changed files with 74 additions and 41 deletions

View File

@ -30,7 +30,6 @@ import traceback
import six
from keystone.i18n import _
from keystone import notifications
_REGISTRY = {}
@ -94,44 +93,10 @@ def provider(name):
"""
def wrapper(cls):
def wrapped(init):
def register_event_callbacks(self):
# NOTE(morganfainberg): A provider who has an implicit
# dependency on other providers may utilize the event callback
# mechanism to react to any changes in those providers. This is
# performed at the .provider() mechanism so that we can ensure
# that the callback is only ever called once and guaranteed
# to be on the properly configured and instantiated backend.
if not hasattr(self, 'event_callbacks'):
return
if not isinstance(self.event_callbacks, dict):
msg = _('event_callbacks must be a dict')
raise ValueError(msg)
for event in self.event_callbacks:
if not isinstance(self.event_callbacks[event], dict):
msg = _('event_callbacks[%s] must be a dict') % event
raise ValueError(msg)
for resource_type in self.event_callbacks[event]:
# Make sure we register the provider for each event it
# cares to call back.
callbacks = self.event_callbacks[event][resource_type]
if not callbacks:
continue
if not hasattr(callbacks, '__iter__'):
# ensure the callback information is a list
# allowing multiple callbacks to exist
callbacks = [callbacks]
notifications.register_event_callback(event,
resource_type,
callbacks)
def __wrapped_init__(self, *args, **kwargs):
"""Initialize the wrapped object and add it to the registry."""
init(self, *args, **kwargs)
_set_provider(name, self)
register_event_callbacks(self)
resolve_future_dependencies(__provider_name=name)
return __wrapped_init__

View File

@ -24,6 +24,7 @@ from keystone import notifications
LOG = log.getLogger(__name__)
@notifications.listener # NOTE(dstanek): only needed if using event_callbacks
@dependency.provider('example_api')
class ExampleManager(manager.Manager):
"""Example Manager.
@ -47,8 +48,8 @@ class ExampleManager(manager.Manager):
# project_created_callback will be invoked whenever a new project is
# created.
# This information is used when the @dependency.provider decorator acts
# on the class.
# This information is used when the @notifications.listener decorator
# acts on the class.
self.event_callbacks = {
notifications.ACTIONS.deleted: {
'project': [self.project_deleted_callback],

View File

@ -364,6 +364,7 @@ def exception_translated(exception_type):
return _exception_translated
@notifications.listener
@dependency.provider('identity_api')
@dependency.requires('assignment_api', 'credential_api', 'id_mapping_api',
'resource_api', 'revoke_api')

View File

@ -15,6 +15,7 @@
"""Notifications module for OpenStack Identity Service resources"""
import collections
import functools
import inspect
import logging
import socket
@ -292,6 +293,72 @@ def register_event_callback(event, resource_type, callbacks):
LOG.debug(msg, {'callback': callback_str, 'event': event_str})
def listener(cls):
"""A class decorator to declare a class to be a notification listener.
A notification listener must specify the event(s) it is interested in by
defining a ``event_callbacks`` attribute or property. ``event_callbacks``
is a dictionary where the key is the type of event and the value is a
dictionary containing a mapping of resource types to callback(s).
``keystone.notifications.ACTIONS`` contains constants for the currently
supported events. There is currently no single place to find constants for
the resource types.
Example::
@listener
class Something(object):
event_callbacks = {
notifications.ACTIONS.created: {
'user': self._user_created_callback,
},
notifications.ACTIONS.deleted: {
'project': [
self._project_deleted_callback,
self._do_cleanup,
]
},
}
"""
def init_wrapper(init):
@functools.wraps(init)
def __new_init__(self, *args, **kwargs):
init(self, *args, **kwargs)
if not hasattr(self, 'event_callbacks'):
msg = _("%r object has no attribute 'event_callbacks'")
raise AttributeError(msg % self.__class__.__name)
_register_event_callbacks(self)
return __new_init__
def _register_event_callbacks(self):
if not isinstance(self.event_callbacks, dict):
msg = _('event_callbacks must be a dict')
raise ValueError(msg)
for event in self.event_callbacks:
if not isinstance(self.event_callbacks[event], dict):
msg = _('event_callbacks[%s] must be a dict') % event
raise ValueError(msg)
for resource_type in self.event_callbacks[event]:
# Make sure we register the provider for each event it
# cares to call back.
callbacks = self.event_callbacks[event][resource_type]
if not callbacks:
continue
if not hasattr(callbacks, '__iter__'):
# ensure the callback information is a list
# allowing multiple callbacks to exist
callbacks = [callbacks]
register_event_callback(event, resource_type, callbacks)
cls.__init__ = init_wrapper(cls.__init__)
return cls
def notify_event_callbacks(service, resource_type, operation, payload):
"""Sends a notification to registered extensions."""
if operation in _SUBSCRIBERS:

View File

@ -24,7 +24,6 @@ from pycadf import cadftype
from pycadf import eventfactory
from pycadf import resource as cadfresource
from keystone.common import dependency
from keystone import notifications
from keystone.tests import unit
from keystone.tests.unit import test_v3
@ -695,7 +694,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase):
def test_provider_event_callbacks_subscription(self):
callback_called = []
@dependency.provider('foo_api')
@notifications.listener
class Foo(object):
def __init__(self):
self.event_callbacks = {
@ -712,7 +711,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase):
self.assertEqual([True], callback_called)
def test_invalid_event_callbacks(self):
@dependency.provider('foo_api')
@notifications.listener
class Foo(object):
def __init__(self):
self.event_callbacks = 'bogus'
@ -720,7 +719,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase):
self.assertRaises(ValueError, Foo)
def test_invalid_event_callbacks_event(self):
@dependency.provider('foo_api')
@notifications.listener
class Foo(object):
def __init__(self):
self.event_callbacks = {CREATED_OPERATION: 'bogus'}