Add generic customization hooks via decorator.

Hooks add the ability to insert custom code around operations that
declare a named hook:

e.g.

@hooks.add_hooks('create_instance')
def create_instance(....):
   ....

The above hook allows Hook objects to be run 'pre' and 'post' the
execution of create_instance()

Hook objects are discovered via the setuptools entry point group
'nova.hooks'.

Change-Id: I3961df12ef415085de7459438967edacc34500c2
This commit is contained in:
Brian Elliott 2012-11-16 15:45:59 +00:00
parent 18bc8abd03
commit e937a53065
7 changed files with 246 additions and 0 deletions

View File

@ -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):
....

View File

@ -43,6 +43,7 @@ Background Concepts for Nova
filter_scheduler
multinic
rpc
hooks
Other Resources
---------------

View File

@ -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,

View File

@ -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']

96
nova/hooks.py Normal file
View File

@ -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()

87
nova/tests/test_hooks.py Normal file
View File

@ -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)

View File

@ -23,3 +23,4 @@ python-cinderclient
python-quantumclient>=2.1
python-glanceclient>=0.5.0,<2
python-keystoneclient>=0.2.0
stevedore>=0.7