diff options
| author | Jenkins <jenkins@review.openstack.org> | 2012-12-10 18:42:15 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2012-12-10 18:42:15 +0000 |
| commit | 734d9fb22b56c2fea9831c6ceba37a671d76b707 (patch) | |
| tree | a5f25dbde8b81d110557b209b4157dfa82489597 /nova | |
| parent | 5bfe4a7438a0bd500f109d1efd4dbf9ec63f3883 (diff) | |
| parent | e937a53065377ecb7c30372ce880bd3d358f4572 (diff) | |
Merge "Add generic customization hooks via decorator."
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/compute/api.py | 2 | ||||
| -rw-r--r-- | nova/compute/manager.py | 2 | ||||
| -rw-r--r-- | nova/hooks.py | 96 | ||||
| -rw-r--r-- | nova/tests/test_hooks.py | 87 |
4 files changed, 187 insertions, 0 deletions
diff --git a/nova/compute/api.py b/nova/compute/api.py index 07a3a3afe..8747f0d99 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 @@ -799,6 +800,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 9dc6f2c3b..54ed90de5 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 @@ -1026,6 +1027,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 000000000..8a9c77e73 --- /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 000000000..39be582c9 --- /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) |
