summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-12-10 18:42:15 +0000
committerGerrit Code Review <review@openstack.org>2012-12-10 18:42:15 +0000
commit734d9fb22b56c2fea9831c6ceba37a671d76b707 (patch)
treea5f25dbde8b81d110557b209b4157dfa82489597 /nova
parent5bfe4a7438a0bd500f109d1efd4dbf9ec63f3883 (diff)
parente937a53065377ecb7c30372ce880bd3d358f4572 (diff)
Merge "Add generic customization hooks via decorator."
Diffstat (limited to 'nova')
-rw-r--r--nova/compute/api.py2
-rw-r--r--nova/compute/manager.py2
-rw-r--r--nova/hooks.py96
-rw-r--r--nova/tests/test_hooks.py87
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)