neutron-lib/neutron_lib/utils/runtime.py
Boden R a37d43018b rehome rpc and related plumbing
As shown in the history of this patch, along with the work in [1], we've
discussed rehoming neutron.common.rpc into lib a number of times before.
One of the main reasons we've decided not to rehome in the past is that
we'd hoped the neutron.common.rcp.BackingOffClient and related plumbing
could be put into oslo.messaging. However, it's 2 years later and that
still hasn't happened [2][3].

This patch proposes we just move forward with the rehome so that we can
begin to untangle the consumers [4]. There's no reason we can't
reiterate on this code in lib; it's no more difficult than in neutron.

This patch includes rehoming of:
- neutron.common.rpc, the only difference in the lib version is that
we dynamically add all neutron_lib.exceptions by default (_DFT_EXMODS).
- neutron.common.exceptions, but exceptions are broken out into their
respective exception modules rather than lumping all together in a
generic single module.
- The fake notifier and RPC fixture, without any real changes.
- A new runtime util method to dynamically load all modules for a
package.

For a sample neutron consumption patch see [5] that was tested with
PS10 herein.

[1] https://review.openstack.org/#/q/project:openstack/neutron-lib+message:rpc
[2] https://review.openstack.org/#/c/407722/
[3] https://bugs.launchpad.net/oslo.messaging/+bug/1667445
[4] http://codesearch.openstack.org/?q=from%20neutron.common%20import%20rpc
[5] https://review.openstack.org/#/c/579989/

Change-Id: I0052ba65973a993e088943056879bc6e982bd0b5
2018-07-12 13:13:21 -06:00

145 lines
4.9 KiB
Python

# 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 pkgutil
import sys
from oslo_concurrency import lockutils
from oslo_log import log as logging
from oslo_utils import importutils
from stevedore import driver
from stevedore import enabled
from neutron_lib._i18n import _
LOG = logging.getLogger(__name__)
SYNCHRONIZED_PREFIX = 'neutron-'
# common synchronization decorator
synchronized = lockutils.synchronized_with_prefix(SYNCHRONIZED_PREFIX)
class NamespacedPlugins(object):
"""Wraps a stevedore plugin namespace to load/access its plugins."""
def __init__(self, namespace):
self.namespace = namespace
self._extension_manager = None
self._extensions = {}
self.reload()
def reload(self):
"""Force a reload of the plugins for this instances namespace.
:returns: None.
"""
self._extensions = {}
self._extension_manager = enabled.EnabledExtensionManager(
self.namespace, lambda x: True,
invoke_on_load=False)
if not self._extension_manager.names():
LOG.debug("No plugins found in namespace: ", self.namespace)
return
self._extension_manager.map(self._add_extension)
def _add_extension(self, ext):
if ext.name in self._extensions:
msg = _("Plugin '%(p)s' already in namespace: %(ns)s") % {
'p': ext.name, 'ns': self.namespace}
raise KeyError(msg)
LOG.debug("Loaded plugin '%s' from namespace: %s",
ext.name, self.namespace)
self._extensions[ext.name] = ext
def _assert_plugin_loaded(self, plugin_name):
if plugin_name not in self._extensions:
msg = _("Plugin '%(p)s' not in namespace: %(ns)s") % {
'p': plugin_name, 'ns': self.namespace}
raise KeyError(msg)
def get_plugin_class(self, plugin_name):
"""Gets a reference to a loaded plugin's class.
:param plugin_name: The name of the plugin to get the class for.
:returns: A reference to the loaded plugin's class.
:raises: KeyError if plugin_name is not loaded.
"""
self._assert_plugin_loaded(plugin_name)
return self._extensions[plugin_name].plugin
def new_plugin_instance(self, plugin_name, *args, **kwargs):
"""Create a new instance of a plugin.
:param plugin_name: The name of the plugin to instantiate.
:param args: Any args to pass onto the constructor.
:param kwargs: Any kwargs to pass onto the constructor.
:returns: A new instance of plugin_name.
:raises: KeyError if plugin_name is not loaded.
"""
self._assert_plugin_loaded(plugin_name)
return self.get_plugin_class(plugin_name)(*args, **kwargs)
@property
def loaded_plugin_names(self):
return self._extensions.keys()
def load_class_by_alias_or_classname(namespace, name):
"""Load a class using stevedore alias or the class name.
:param namespace: The namespace where the alias is defined.
:param name: The alias or class name of the class to be loaded.
:returns: Class if it can be loaded.
:raises ImportError: if class cannot be loaded.
"""
if not name:
LOG.error("Alias or class name is not set")
raise ImportError(_("Class not found."))
try:
# Try to resolve class by alias
mgr = driver.DriverManager(
namespace, name, warn_on_missing_entrypoint=False)
class_to_load = mgr.driver
except RuntimeError:
e1_info = sys.exc_info()
# Fallback to class name
try:
class_to_load = importutils.import_class(name)
except (ImportError, ValueError):
LOG.error("Error loading class by alias",
exc_info=e1_info)
LOG.error("Error loading class by class name",
exc_info=True)
raise ImportError(_("Class not found."))
return class_to_load
def list_package_modules(package_name):
"""Get a list of the modules for a given package.
:param package_name: The package name to get modules for.
:returns: A list of module objects for the said package name.
"""
pkg_mod = importutils.import_module(package_name)
modules = [pkg_mod]
for mod in pkgutil.walk_packages(pkg_mod.__path__):
_, mod_name, _ = mod
fq_name = pkg_mod.__name__ + "." + mod_name
modules.append(importutils.import_module(fq_name))
return modules