diff --git a/doc/source/devref/hooks.rst b/doc/source/devref/hooks.rst new file mode 100644 index 000000000000..a53e373526d7 --- /dev/null +++ b/doc/source/devref/hooks.rst @@ -0,0 +1,57 @@ +Hooks +===== + +Hooks provide a mechanism to extend Nova with custom code through a plugin +mechanism. + +Named hooks are added to nova code via a decorator that will lazily load +plugin code matching the name. The loading works via setuptools +`entry points`_. + +.. _`entry points`: http://packages.python.org/distribute/pkg_resources.html#entry-points + +What are hooks good for? +------------------------ + +Hooks are good for anchoring your custom code to Nova internal APIs. + +What are hooks NOT good for? +---------------------------- + +Hooks should not be used when API stability is a key factor. Internal APIs may +change. Consider using a notification driver if this is important to you. + +Declaring hooks in the Nova codebase +------------------------------------ + +The following example declares a *resize_hook* around the *resize_instance* method:: + + from nova import hooks + + @hooks.add_hook("resize_hook") + def resize_instance(self, context, instance, a=1, b=2): + ... + +Hook objects can now be attached via entry points to the *resize_hook*. + +Adding hook object code +----------------------- + +1. Setup a Python package with a setup.py file. +2. Add the following to the setup.py setup call:: + + entry_points = [ + 'nova.hooks': [ + 'resize_hook': your_package.hooks.YourHookClass, + ] + ] + +3. *YourHookClass* should be an object with *pre* and/or *post* methods:: + + class YourHookClass(object): + + def pre(self, *args, **kwargs): + .... + + def post(self, rv, *args, **kwargs): + .... diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index 239848c62c66..0b7883f7b9fa 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -43,6 +43,7 @@ Background Concepts for Nova filter_scheduler multinic rpc + hooks Other Resources --------------- diff --git a/nova/compute/api.py b/nova/compute/api.py index c937b7cf5be6..b3b5f1f42539 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -40,6 +40,7 @@ from nova.consoleauth import rpcapi as consoleauth_rpcapi from nova import crypto from nova.db import base from nova import exception +from nova import hooks from nova.image import glance from nova import network from nova import notifications @@ -788,6 +789,7 @@ class API(base.Base): if block_device_mapping: check_policy(context, 'create:attach_volume', target) + @hooks.add_hook("create_instance") def create(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, min_count=None, max_count=None, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 99637fd6580f..1013ddaba423 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -53,6 +53,7 @@ from nova.compute import vm_states from nova import conductor import nova.context from nova import exception +from nova import hooks from nova.image import glance from nova import manager from nova import network @@ -1007,6 +1008,7 @@ class ComputeManager(manager.SchedulerDependentManager): self.volume_api.delete(context, volume) # NOTE(vish): bdms will be deleted on instance destroy + @hooks.add_hook("delete_instance") def _delete_instance(self, context, instance, bdms): """Delete an instance on this host.""" instance_uuid = instance['uuid'] diff --git a/nova/hooks.py b/nova/hooks.py new file mode 100644 index 000000000000..8a9c77e73630 --- /dev/null +++ b/nova/hooks.py @@ -0,0 +1,96 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 OpenStack, LLC. +# All Rights Reserved. +# +# 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. + +"""Decorator and config option definitions for adding custom code (hooks) +around callables. + +Any method may have the 'add_hook' decorator applied, which yields the +ability to invoke Hook objects before or after the method. (i.e. pre and +post) + +Hook objects are loaded by HookLoaders. Each named hook may invoke multiple +Hooks. + +Example Hook object: + +class MyHook(object): + def pre(self, *args, **kwargs): + # do stuff before wrapped callable runs + + def post(self, rv, *args, **kwargs): + # do stuff after wrapped callable runs + + +""" + +import functools + +import stevedore + +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) +NS = 'nova.hooks' + +_HOOKS = {} # hook name => hook manager + + +class HookManager(stevedore.hook.HookManager): + def __init__(self, name): + # invoke_on_load creates an instance of the Hook class + super(HookManager, self).__init__(NS, name, invoke_on_load=True) + + def run_pre(self, name, args, kwargs): + for e in self.extensions: + obj = e.obj + pre = getattr(obj, 'pre', None) + if pre: + LOG.debug(_("Running %(name)s pre-hook: %(obj)s") % locals()) + pre(*args, **kwargs) + + def run_post(self, name, rv, args, kwargs): + for e in reversed(self.extensions): + obj = e.obj + post = getattr(obj, 'post', None) + if post: + LOG.debug(_("Running %(name)s post-hook: %(obj)s") % locals()) + post(rv, *args, **kwargs) + + +def add_hook(name): + """Execute optional pre and post methods around the decorated + function. This is useful for customization around callables. + """ + + def outer(f): + @functools.wraps(f) + def inner(*args, **kwargs): + manager = _HOOKS.setdefault(name, HookManager(name)) + + manager.run_pre(name, args, kwargs) + rv = f(*args, **kwargs) + manager.run_post(name, rv, args, kwargs) + + return rv + + return inner + return outer + + +def reset(): + """Clear loaded hooks.""" + _HOOKS.clear() diff --git a/nova/tests/test_hooks.py b/nova/tests/test_hooks.py new file mode 100644 index 000000000000..39be582c97cd --- /dev/null +++ b/nova/tests/test_hooks.py @@ -0,0 +1,87 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 OpenStack, LLC. +# All Rights Reserved. +# +# 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. + +"""Tests for hook customization""" + +import stevedore + +from nova import hooks +from nova import test + + +class SampleHookA(object): + name = "a" + + def _add_called(self, op, kwargs): + called = kwargs.get('called', None) + if called is not None: + called.append(op + self.name) + + def pre(self, *args, **kwargs): + self._add_called("pre", kwargs) + + +class SampleHookB(SampleHookA): + name = "b" + + def post(self, rv, *args, **kwargs): + self._add_called("post", kwargs) + + +class MockEntryPoint(object): + + def __init__(self, cls): + self.cls = cls + + def load(self): + return self.cls + + +class HookTestCase(test.TestCase): + + def _mock_load_plugins(self, iload, iargs, ikwargs): + return [ + stevedore.extension.Extension('test_hook', + MockEntryPoint(SampleHookA), SampleHookA, SampleHookA()), + stevedore.extension.Extension('test_hook', + MockEntryPoint(SampleHookB), SampleHookB, SampleHookB()), + ] + + def setUp(self): + super(HookTestCase, self).setUp() + + hooks.reset() + + self.stubs.Set(stevedore.extension.ExtensionManager, '_load_plugins', + self._mock_load_plugins) + + @hooks.add_hook('test_hook') + def _hooked(self, a, b=1, c=2, called=None): + return 42 + + def test_basic(self): + self.assertEqual(42, self._hooked(1)) + + mgr = hooks._HOOKS['test_hook'] + self.assertEqual(2, len(mgr.extensions)) + self.assertEqual(SampleHookA, mgr.extensions[0].plugin) + self.assertEqual(SampleHookB, mgr.extensions[1].plugin) + + def test_order_of_execution(self): + called_order = [] + self._hooked(42, called=called_order) + self.assertEqual(['prea', 'preb', 'postb'], called_order) diff --git a/tools/pip-requires b/tools/pip-requires index e590f365e666..1fbfa6dafaa9 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -23,3 +23,4 @@ python-cinderclient python-quantumclient>=2.1 python-glanceclient>=0.5.0,<2 python-keystoneclient>=0.2.0 +stevedore>=0.7