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:
parent
18bc8abd03
commit
e937a53065
57
doc/source/devref/hooks.rst
Normal file
57
doc/source/devref/hooks.rst
Normal 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):
|
||||
....
|
@ -43,6 +43,7 @@ Background Concepts for Nova
|
||||
filter_scheduler
|
||||
multinic
|
||||
rpc
|
||||
hooks
|
||||
|
||||
Other Resources
|
||||
---------------
|
||||
|
@ -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,
|
||||
|
@ -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
96
nova/hooks.py
Normal 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
87
nova/tests/test_hooks.py
Normal 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)
|
@ -23,3 +23,4 @@ python-cinderclient
|
||||
python-quantumclient>=2.1
|
||||
python-glanceclient>=0.5.0,<2
|
||||
python-keystoneclient>=0.2.0
|
||||
stevedore>=0.7
|
||||
|
Loading…
x
Reference in New Issue
Block a user