Skip to content

Commit

Permalink
The change allows loading several service plugins along with core plu…
Browse files Browse the repository at this point in the history
…gin.

The following functionality changes were made:
1. Multiple plugins are loaded one per type
2. QuantumManager now holds dictionary {plugin_type: plugin_instance}
   Core plugin is stored there as well
3. Extensions are checked against all loaded plugins
4. Service plugins are specified by service_plugins option in quantum.conf file
5. Provide basic interface for service plugins
6. Introduce dummy service plugin as example and PoC
7. Service plugin's REST calls get corresponding plugin's common prefix
8. Add UTs for new extension framework functionality and for QuantumManager

Implements: blueprint quantum-service-framework
Change-Id: I1d00d6f848937410bccd91c852ff0871a86d7bb8
  • Loading branch information
eugene64 committed Nov 20, 2012
1 parent a06fd81 commit 925b418
Show file tree
Hide file tree
Showing 13 changed files with 419 additions and 37 deletions.
3 changes: 3 additions & 0 deletions etc/quantum.conf
Expand Up @@ -40,6 +40,9 @@ bind_port = 9696
# Quantum plugin provider module
core_plugin = quantum.plugins.sample.SamplePlugin.FakePlugin

# Advanced service modules
# service_plugins =

# Paste configuration file
api_paste_config = api-paste.ini

Expand Down
2 changes: 2 additions & 0 deletions quantum/common/config.py
Expand Up @@ -41,6 +41,8 @@
cfg.StrOpt('auth_strategy', default='keystone'),
cfg.StrOpt('core_plugin',
default='quantum.plugins.sample.SamplePlugin.FakePlugin'),
cfg.ListOpt('service_plugins',
default=[]),
cfg.StrOpt('base_mac', default="fa:16:3e:00:00:00"),
cfg.IntOpt('mac_generation_retries', default=16),
cfg.BoolOpt('allow_bulk', default=True),
Expand Down
59 changes: 32 additions & 27 deletions quantum/extensions/extensions.py
Expand Up @@ -267,26 +267,30 @@ def __init__(self, application,

# extended resources
for resource in self.ext_mgr.get_resources():
path_prefix = resource.path_prefix
if resource.parent:
path_prefix = (resource.path_prefix +
"/%s/{%s_id}" %
(resource.parent["collection_name"],
resource.parent["member_name"]))

LOG.debug(_('Extended resource: %s'),
resource.collection)
for action, method in resource.collection_actions.iteritems():
path_prefix = ""
parent = resource.parent
conditions = dict(method=[method])
path = "/%s/%s" % (resource.collection, action)
if parent:
path_prefix = "/%s/{%s_id}" % (parent["collection_name"],
parent["member_name"])
with mapper.submapper(controller=resource.controller,
action=action,
path_prefix=path_prefix,
conditions=conditions) as submap:
submap.connect(path)
submap.connect("%s.:(format)" % path)

mapper.resource(resource.collection, resource.collection,
controller=resource.controller,
member=resource.member_actions,
parent_resource=resource.parent)
parent_resource=resource.parent,
path_prefix=path_prefix)

# extended actions
action_controllers = self._action_ext_controllers(application,
Expand Down Expand Up @@ -534,44 +538,44 @@ class PluginAwareExtensionManager(ExtensionManager):

_instance = None

def __init__(self, path, plugin):
self.plugin = plugin
def __init__(self, path, plugins):
self.plugins = plugins
super(PluginAwareExtensionManager, self).__init__(path)

def _check_extension(self, extension):
"""Checks if plugin supports extension and implements the
"""Checks if any of plugins supports extension and implements the
extension contract."""
extension_is_valid = super(PluginAwareExtensionManager,
self)._check_extension(extension)
return (extension_is_valid and
self._plugin_supports(extension) and
self._plugin_implements_interface(extension))
self._plugins_support(extension) and
self._plugins_implement_interface(extension))

def _plugin_supports(self, extension):
def _plugins_support(self, extension):
alias = extension.get_alias()
supports_extension = (hasattr(self.plugin,
"supported_extension_aliases") and
alias in self.plugin.supported_extension_aliases)
supports_extension = any((hasattr(plugin,
"supported_extension_aliases") and
alias in plugin.supported_extension_aliases)
for plugin in self.plugins.values())
plugin_provider = cfg.CONF.core_plugin
if not supports_extension and plugin_provider in ENABLED_EXTS:
supports_extension = (alias in
ENABLED_EXTS[plugin_provider]['ext_alias'])
if not supports_extension:
LOG.warn("extension %s not supported by plugin %s",
alias, self.plugin)
LOG.warn(_("extension %s not supported by any of loaded plugins" %
alias))
return supports_extension

def _plugin_implements_interface(self, extension):
def _plugins_implement_interface(self, extension):
if(not hasattr(extension, "get_plugin_interface") or
extension.get_plugin_interface() is None):
return True
plugin_has_interface = isinstance(self.plugin,
extension.get_plugin_interface())
if not plugin_has_interface:
LOG.warn("plugin %s does not implement extension's"
"plugin interface %s" % (self.plugin,
extension.get_alias()))
return plugin_has_interface
for plugin in self.plugins.values():
if isinstance(plugin, extension.get_plugin_interface()):
return True
LOG.warn(_("Loaded plugins do not implement extension %s interface"
% extension.get_alias()))
return False

@classmethod
def get_instance(cls):
Expand All @@ -582,7 +586,7 @@ def get_instance(cls):
LOG.debug('loading model %s', model)
model_class = importutils.import_class(model)
cls._instance = cls(get_extensions_path(),
QuantumManager.get_plugin())
QuantumManager.get_service_plugins())
return cls._instance


Expand Down Expand Up @@ -612,13 +616,14 @@ def __init__(self, collection, action_name, handler):
class ResourceExtension(object):
"""Add top level resources to the OpenStack API in Quantum."""

def __init__(self, collection, controller, parent=None,
def __init__(self, collection, controller, parent=None, path_prefix="",
collection_actions={}, member_actions={}):
self.collection = collection
self.controller = controller
self.parent = parent
self.collection_actions = collection_actions
self.member_actions = member_actions
self.path_prefix = path_prefix


# Returns the extention paths from a config entry and the __path__
Expand Down
49 changes: 47 additions & 2 deletions quantum/manager.py
Expand Up @@ -27,6 +27,7 @@
from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
from quantum.openstack.common import log as logging
from quantum.plugins.common import constants


LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -58,8 +59,52 @@ def __init__(self, options=None, config_file=None):
"Example: pip install quantum-sample-plugin")
self.plugin = plugin_klass()

# core plugin as a part of plugin collection simplifies
# checking extensions
# TODO (enikanorov): make core plugin the same as
# the rest of service plugins
self.service_plugins = {constants.CORE: self.plugin}
self._load_service_plugins()

def _load_service_plugins(self):
plugin_providers = cfg.CONF.service_plugins
LOG.debug(_("Loading service plugins: %s" % plugin_providers))
for provider in plugin_providers:
if provider == '':
continue
try:
LOG.info(_("Loading Plugin: %s" % provider))
plugin_class = importutils.import_class(provider)
except ClassNotFound:
LOG.exception(_("Error loading plugin"))
raise Exception(_("Plugin not found."))
plugin_inst = plugin_class()

# only one implementation of svc_type allowed
# specifying more than one plugin
# for the same type is a fatal exception
if plugin_inst.get_plugin_type() in self.service_plugins:
raise Exception(_("Multiple plugins for service "
"%s were configured" %
plugin_inst.get_plugin_type()))

self.service_plugins[plugin_inst.get_plugin_type()] = plugin_inst

LOG.debug(_("Successfully loaded %(type)s plugin. "
"Description: %(desc)s"),
{"type": plugin_inst.get_plugin_type(),
"desc": plugin_inst.get_plugin_description()})

@classmethod
def get_plugin(cls):
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance.plugin
return cls._instance

@classmethod
def get_plugin(cls):
return cls.get_instance().plugin

@classmethod
def get_service_plugins(cls):
return cls.get_instance().service_plugins
16 changes: 16 additions & 0 deletions quantum/plugins/__init__.py
@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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.
16 changes: 16 additions & 0 deletions quantum/plugins/common/__init__.py
@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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.
26 changes: 26 additions & 0 deletions quantum/plugins/common/constants.py
@@ -0,0 +1,26 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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.

# service type constants:
CORE = "CORE"
DUMMY = "DUMMY"


COMMON_PREFIXES = {
CORE: "",
DUMMY: "/dummy_svc",
}
16 changes: 16 additions & 0 deletions quantum/plugins/services/__init__.py
@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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.
16 changes: 16 additions & 0 deletions quantum/plugins/services/dummy/__init__.py
@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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.
32 changes: 32 additions & 0 deletions quantum/plugins/services/dummy/dummy_plugin.py
@@ -0,0 +1,32 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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 quantum.plugins.common import constants
from quantum.plugins.services.service_base import ServicePluginBase


class QuantumDummyPlugin(ServicePluginBase):
supported_extension_aliases = []

def __init__(self):
pass

def get_plugin_type(self):
return constants.DUMMY

def get_plugin_description(self):
return "Quantum Dummy Plugin"
35 changes: 35 additions & 0 deletions quantum/plugins/services/service_base.py
@@ -0,0 +1,35 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# 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 abc


class ServicePluginBase(object):
""" defines base interface for any Advanced Service plugin """
__metaclass__ = abc.ABCMeta
supported_extension_aliases = []

@abc.abstractmethod
def get_plugin_type(self):
""" returns one of predefine service types. see
quantum/plugins/common/constants.py """
pass

@abc.abstractmethod
def get_plugin_description(self):
""" returns string description of the plugin """
pass

0 comments on commit 925b418

Please sign in to comment.