Add the plugin framework from common; use and test.

For blueprint novaplugins.

Change-Id: Id4a5ae3ebb91f941956e2f73ecfd9ea1d290a235
This commit is contained in:
Andrew Bogott 2012-07-01 16:37:14 -05:00
parent 9a283c6b37
commit ece205982f
7 changed files with 390 additions and 1 deletions

View File

@ -56,6 +56,7 @@ This module provides Manager, a base class for managers.
from nova.db import base from nova.db import base
from nova import flags from nova import flags
from nova.openstack.common import log as logging from nova.openstack.common import log as logging
from nova.openstack.common.plugin import pluginmanager
from nova.openstack.common.rpc import dispatcher as rpc_dispatcher from nova.openstack.common.rpc import dispatcher as rpc_dispatcher
from nova.scheduler import rpcapi as scheduler_rpcapi from nova.scheduler import rpcapi as scheduler_rpcapi
from nova import version from nova import version
@ -138,8 +139,13 @@ class Manager(base.Base):
if not host: if not host:
host = FLAGS.host host = FLAGS.host
self.host = host self.host = host
self.load_plugins()
super(Manager, self).__init__(db_driver) super(Manager, self).__init__(db_driver)
def load_plugins(self):
pluginmgr = pluginmanager.PluginManager('nova', self.__class__)
pluginmgr.load_plugins()
def create_rpc_dispatcher(self): def create_rpc_dispatcher(self):
'''Get the rpc dispatcher for this manager. '''Get the rpc dispatcher for this manager.
@ -205,6 +211,10 @@ class SchedulerDependentManager(Manager):
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI() self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
super(SchedulerDependentManager, self).__init__(host, db_driver) super(SchedulerDependentManager, self).__init__(host, db_driver)
def load_plugins(self):
pluginmgr = pluginmanager.PluginManager('nova', self.service_name)
pluginmgr.load_plugins()
def update_service_capabilities(self, capabilities): def update_service_capabilities(self, capabilities):
"""Remember these capabilities to send on next periodic update.""" """Remember these capabilities to send on next periodic update."""
self.last_capabilities = capabilities self.last_capabilities = capabilities

View File

@ -0,0 +1,14 @@
# Copyright 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.

View File

@ -0,0 +1,93 @@
# Copyright 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.
from nova.openstack.common import log as logging
from nova.openstack.common.plugin import plugin
LOG = logging.getLogger(__name__)
class _CallbackNotifier(object):
"""Manages plugin-defined notification callbacks.
For each Plugin, a CallbackNotifier will be added to the
notification driver list. Calls to notify() with appropriate
messages will be hooked and prompt callbacks.
A callback should look like this:
def callback(context, message, user_data)
"""
def __init__(self):
self._callback_dict = {}
def _add_callback(self, event_type, callback, user_data):
callback_list = self._callback_dict.get(event_type, [])
callback_list.append({'function': callback,
'user_data': user_data})
self._callback_dict[event_type] = callback_list
def _remove_callback(self, callback):
for callback_list in self._callback_dict.values():
for entry in callback_list:
if entry['function'] == callback:
callback_list.remove(entry)
def notify(self, context, message):
if message.get('event_type') not in self._callback_dict:
return
for entry in self._callback_dict[message.get('event_type')]:
entry['function'](context, message, entry.get('user_data'))
def callbacks(self):
return self._callback_dict
class CallbackPlugin(plugin.Plugin):
""" Plugin with a simple callback interface.
This class is provided as a convenience for producing a simple
plugin that only watches a couple of events. For example, here's
a subclass which prints a line the first time an instance is created.
class HookInstanceCreation(CallbackPlugin):
def __init__(self, _service_name):
super(HookInstanceCreation, self).__init__()
self._add_callback(self.magic, 'compute.instance.create.start')
def magic(self):
print "An instance was created!"
self._remove_callback(self, self.magic)
"""
def __init__(self, service_name):
super(CallbackPlugin, self).__init__(service_name)
self._callback_notifier = _CallbackNotifier()
self._add_notifier(self._callback_notifier)
def _add_callback(self, callback, event_type, user_data=None):
"""Add callback for a given event notification.
Subclasses can call this as an alternative to implementing
a fullblown notify notifier.
"""
self._callback_notifier._add_callback(event_type, callback, user_data)
def _remove_callback(self, callback):
"""Remove all notification callbacks to specified function."""
self._callback_notifier._remove_callback(callback)

View File

@ -0,0 +1,87 @@
# Copyright 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.
from nova.openstack.common import log as logging
from nova.openstack.common.notifier import list_notifier
LOG = logging.getLogger(__name__)
class Plugin(object):
"""Defines an interface for adding functionality to an OpenStack service.
A plugin interacts with a service via the following pathways:
- An optional set of notifiers, managed by calling add_notifier()
or by overriding _notifiers()
- A set of api extensions, managed via add_api_extension_descriptor()
- Direct calls to service functions.
- Whatever else the plugin wants to do on its own.
This is the reference implementation.
"""
# The following functions are provided as convenience methods
# for subclasses. Subclasses should call them but probably not
# override them.
def _add_api_extension_descriptor(self, descriptor):
"""Subclass convenience method which adds an extension descriptor.
Subclass constructors should call this method when
extending a project's REST interface.
Note that once the api service has loaded, the
API extension set is more-or-less fixed, so
this should mainly be called by subclass constructors.
"""
self._api_extension_descriptors.append(descriptor)
def _add_notifier(self, notifier):
"""Subclass convenience method which adds a notifier.
Notifier objects should implement the function notify(message).
Each notifier receives a notify() call whenever an openstack
service broadcasts a notification.
Best to call this during construction. Notifiers are enumerated
and registered by the pluginmanager at plugin load time.
"""
self._notifiers.append(notifier)
# The following methods are called by OpenStack services to query
# plugin features. Subclasses should probably not override these.
def _notifiers(self):
"""Returns list of notifiers for this plugin."""
return self._notifiers
notifiers = property(_notifiers)
def _api_extension_descriptors(self):
"""Return a list of API extension descriptors.
Called by a project API during its load sequence.
"""
return self._api_extension_descriptors
api_extension_descriptors = property(_api_extension_descriptors)
# Most plugins will override this:
def __init__(self, service_name):
self._notifiers = []
self._api_extension_descriptors = []

View File

@ -0,0 +1,95 @@
# Copyright 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.
import imp
import os
import pkg_resources
from nova.openstack.common import cfg
from nova.openstack.common import log as logging
from nova.openstack.common.notifier import list_notifier
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class PluginManager(object):
"""Manages plugin entrypoints and loading.
For a service to implement this plugin interface for callback purposes:
- Make use of the openstack-common notifier system
- Instantiate this manager in each process (passing in
project and service name)
For an API service to extend itself using this plugin interface,
it needs to query the plugin_extension_factory provided by
the already-instantiated PluginManager.
"""
def __init__(self, project_name, service_name):
""" Construct Plugin Manager; load and initialize plugins.
project_name (e.g. 'nova' or 'glance') is used
to construct the entry point that identifies plugins.
The service_name (e.g. 'compute') is passed on to
each plugin as a raw string for it to do what it will.
"""
self._project_name = project_name
self._service_name = service_name
self.plugins = []
def _force_use_list_notifier(self):
if (CONF.notification_driver !=
'nova.openstack.common.notifier.list_notifier'):
if not hasattr(CONF, "list_notifier_drivers"):
CONF.list_notifier_drivers = []
old_notifier = CONF.notification_driver
drvstring = 'nova.openstack.common.notifier.list_notifier'
CONF.notification_driver = drvstring
if old_notifier:
list_notifier.add_driver(old_notifier)
def load_plugins(self):
self.plugins = []
for entrypoint in pkg_resources.iter_entry_points('%s.plugin' %
self._project_name):
try:
pluginclass = entrypoint.load()
plugin = pluginclass(self._service_name)
self.plugins.append(plugin)
except Exception, exc:
LOG.error(_("Failed to load plugin %(plug)s: %(exc)s") %
{'plug': entrypoint, 'exc': exc})
# See if we need to turn on the list notifier
for plugin in self.plugins:
if plugin.notifiers:
self._force_use_list_notifier()
break
# Register individual notifiers.
for plugin in self.plugins:
for notifier in plugin.notifiers:
list_notifier.add_driver(notifier)
def plugin_extension_factory(self, ext_mgr):
for plugin in self.plugins:
descriptors = plugin.api_extension_descriptors
for descriptor in descriptors:
ext_mgr.load_extension(descriptor)

View File

@ -0,0 +1,90 @@
# Copyright 2011 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.
import pkg_resources
import nova
from nova.api.openstack.compute import extensions as computeextensions
from nova.api.openstack import extensions
from nova.openstack.common.plugin import plugin
from nova.openstack.common.plugin import pluginmanager
from nova import test
class StubController(object):
def i_am_the_stub(self):
pass
class StubControllerExtension(extensions.ExtensionDescriptor):
"""This is a docstring. We need it."""
name = 'stubextension'
alias = 'stubby'
def get_resources(self):
resources = []
res = extensions.ResourceExtension('testme',
StubController())
resources.append(res)
return resources
service_list = []
class TestPluginClass(plugin.Plugin):
def __init__(self, service_name):
super(TestPluginClass, self).__init__(service_name)
self._add_api_extension_descriptor(StubControllerExtension)
service_list.append(service_name)
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return TestPluginClass
class APITestCase(test.TestCase):
"""Test case for the plugin api extension interface"""
def test_add_extension(self):
def mock_load(_s):
return TestPluginClass()
def mock_iter_entry_points(_t):
return [MockEntrypoint("fake", "fake", ["fake"])]
self.stubs.Set(pkg_resources, 'iter_entry_points',
mock_iter_entry_points)
global service_list
service_list = []
# Marking out the default extension paths makes this test MUCH faster.
self.flags(osapi_compute_extension=[])
self.flags(osapi_volume_extension=[])
found = False
mgr = computeextensions.ExtensionManager()
for res in mgr.get_resources():
# We have to use this weird 'dir' check because
# the plugin framework muddies up the classname
# such that 'isinstance' doesn't work right.
if 'i_am_the_stub' in dir(res.controller):
found = True
self.assertTrue(found)
self.assertEqual(len(service_list), 1)
self.assertEqual(service_list[0], 'compute-extensions')

View File

@ -1,7 +1,7 @@
[DEFAULT] [DEFAULT]
# The list of modules to copy from openstack-common # The list of modules to copy from openstack-common
modules=cfg,context,excutils,gettextutils,importutils,iniparser,jsonutils,local,log,notifier,policy,setup,timeutils,rpc modules=cfg,context,excutils,gettextutils,importutils,iniparser,jsonutils,local,log,notifier,plugin,policy,setup,timeutils,rpc
# The base module to hold the copy of openstack.common # The base module to hold the copy of openstack.common
base=nova base=nova