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
|
@ -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
|
filter_scheduler
|
||||||
multinic
|
multinic
|
||||||
rpc
|
rpc
|
||||||
|
hooks
|
||||||
|
|
||||||
Other Resources
|
Other Resources
|
||||||
---------------
|
---------------
|
||||||
|
|
|
@ -40,6 +40,7 @@ from nova.consoleauth import rpcapi as consoleauth_rpcapi
|
||||||
from nova import crypto
|
from nova import crypto
|
||||||
from nova.db import base
|
from nova.db import base
|
||||||
from nova import exception
|
from nova import exception
|
||||||
|
from nova import hooks
|
||||||
from nova.image import glance
|
from nova.image import glance
|
||||||
from nova import network
|
from nova import network
|
||||||
from nova import notifications
|
from nova import notifications
|
||||||
|
@ -788,6 +789,7 @@ class API(base.Base):
|
||||||
if block_device_mapping:
|
if block_device_mapping:
|
||||||
check_policy(context, 'create:attach_volume', target)
|
check_policy(context, 'create:attach_volume', target)
|
||||||
|
|
||||||
|
@hooks.add_hook("create_instance")
|
||||||
def create(self, context, instance_type,
|
def create(self, context, instance_type,
|
||||||
image_href, kernel_id=None, ramdisk_id=None,
|
image_href, kernel_id=None, ramdisk_id=None,
|
||||||
min_count=None, max_count=None,
|
min_count=None, max_count=None,
|
||||||
|
|
|
@ -53,6 +53,7 @@ from nova.compute import vm_states
|
||||||
from nova import conductor
|
from nova import conductor
|
||||||
import nova.context
|
import nova.context
|
||||||
from nova import exception
|
from nova import exception
|
||||||
|
from nova import hooks
|
||||||
from nova.image import glance
|
from nova.image import glance
|
||||||
from nova import manager
|
from nova import manager
|
||||||
from nova import network
|
from nova import network
|
||||||
|
@ -1007,6 +1008,7 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||||
self.volume_api.delete(context, volume)
|
self.volume_api.delete(context, volume)
|
||||||
# NOTE(vish): bdms will be deleted on instance destroy
|
# NOTE(vish): bdms will be deleted on instance destroy
|
||||||
|
|
||||||
|
@hooks.add_hook("delete_instance")
|
||||||
def _delete_instance(self, context, instance, bdms):
|
def _delete_instance(self, context, instance, bdms):
|
||||||
"""Delete an instance on this host."""
|
"""Delete an instance on this host."""
|
||||||
instance_uuid = instance['uuid']
|
instance_uuid = instance['uuid']
|
||||||
|
|
|
@ -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()
|
|
@ -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-quantumclient>=2.1
|
||||||
python-glanceclient>=0.5.0,<2
|
python-glanceclient>=0.5.0,<2
|
||||||
python-keystoneclient>=0.2.0
|
python-keystoneclient>=0.2.0
|
||||||
|
stevedore>=0.7
|
||||||
|
|
Loading…
Reference in New Issue