diff --git a/keystone/common/dependency.py b/keystone/common/dependency.py index 14a68f1995..28992e73e1 100644 --- a/keystone/common/dependency.py +++ b/keystone/common/dependency.py @@ -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__ diff --git a/keystone/contrib/example/core.py b/keystone/contrib/example/core.py index fc3c9fab1e..e3d1b457eb 100644 --- a/keystone/contrib/example/core.py +++ b/keystone/contrib/example/core.py @@ -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], diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 4afc24c834..75e6af6f56 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -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') diff --git a/keystone/notifications.py b/keystone/notifications.py index 34d6eabe97..e58a9a7ce4 100644 --- a/keystone/notifications.py +++ b/keystone/notifications.py @@ -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: diff --git a/keystone/tests/unit/common/test_notifications.py b/keystone/tests/unit/common/test_notifications.py index 997cdba5fd..eebcb4d2b2 100644 --- a/keystone/tests/unit/common/test_notifications.py +++ b/keystone/tests/unit/common/test_notifications.py @@ -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'}