# 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
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),
# 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,
_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):
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
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__
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__)
"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
+# 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.
--- /dev/null
+# 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.
--- /dev/null
+# 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",
+}
--- /dev/null
+# 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.
--- /dev/null
+# 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.
--- /dev/null
+# 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"
--- /dev/null
+# 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
PluginAwareExtensionManager,
)
from quantum.openstack.common import jsonutils
+from quantum.plugins.common import constants
from quantum.tests.unit import BaseTest
from quantum.tests.unit.extension_stubs import (
ExtensionExpectingPluginInterface,
import quantum.tests.unit.extensions
from quantum import wsgi
+
LOG = logging.getLogger('quantum.tests.test_extensions')
ROOTDIR = os.path.dirname(os.path.dirname(__file__))
def custom_collection_action(self, request, **kwargs):
return {'collection': 'value'}
+ class DummySvcPlugin(wsgi.Controller):
+ def get_plugin_type(self):
+ return constants.DUMMY
+
+ def index(self, request, **kwargs):
+ return "resource index"
+
+ def custom_member_action(self, request, **kwargs):
+ return {'member_action': 'value'}
+
+ def collection_action(self, request, **kwargs):
+ return {'collection': 'value'}
+
+ def show(self, request, id):
+ return {'data': {'id': id}}
+
def test_exceptions_notimplemented(self):
controller = self.ResourceExtensionController()
member = {'notimplemented_function': "GET"}
show_response = test_app.get("/tweedles/25266")
self.assertEqual({'data': {'id': "25266"}}, show_response.json)
+ def test_resource_gets_prefix_of_plugin(self):
+ class DummySvcPlugin(wsgi.Controller):
+ def index(self, request):
+ return ""
+
+ def get_plugin_type(self):
+ return constants.DUMMY
+
+ res_ext = extensions.ResourceExtension(
+ 'tweedles', DummySvcPlugin(), path_prefix="/dummy_svc")
+ test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
+ index_response = test_app.get("/dummy_svc/tweedles")
+ self.assertEqual(200, index_response.status_int)
+
def test_resource_extension_with_custom_member_action(self):
controller = self.ResourceExtensionController()
member = {'custom_member_action': "GET"}
self.assertEqual(jsonutils.loads(response.body)['member_action'],
"value")
+ def test_resource_ext_with_custom_member_action_gets_plugin_prefix(self):
+ controller = self.DummySvcPlugin()
+ member = {'custom_member_action': "GET"}
+ collections = {'collection_action': "GET"}
+ res_ext = extensions.ResourceExtension('tweedles', controller,
+ path_prefix="/dummy_svc",
+ member_actions=member,
+ collection_actions=collections)
+ test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
+
+ response = test_app.get("/dummy_svc/tweedles/1/custom_member_action")
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(jsonutils.loads(response.body)['member_action'],
+ "value")
+
+ response = test_app.get("/dummy_svc/tweedles/collection_action")
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(jsonutils.loads(response.body)['collection'],
+ "value")
+
+ def test_plugin_prefix_with_parent_resource(self):
+ controller = self.DummySvcPlugin()
+ parent = dict(member_name="tenant",
+ collection_name="tenants")
+ member = {'custom_member_action': "GET"}
+ collections = {'collection_action': "GET"}
+ res_ext = extensions.ResourceExtension('tweedles', controller, parent,
+ path_prefix="/dummy_svc",
+ member_actions=member,
+ collection_actions=collections)
+ test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
+
+ index_response = test_app.get("/dummy_svc/tenants/1/tweedles")
+ self.assertEqual(200, index_response.status_int)
+
+ response = test_app.get("/dummy_svc/tenants/1/"
+ "tweedles/1/custom_member_action")
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(jsonutils.loads(response.body)['member_action'],
+ "value")
+
+ response = test_app.get("/dummy_svc/tenants/2/"
+ "tweedles/collection_action")
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(jsonutils.loads(response.body)['collection'],
+ "value")
+
def test_resource_extension_for_get_custom_collection_action(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_action': "GET"}
response = test_app.get("/tweedles/custom_collection_action")
self.assertEqual(200, response.status_int)
+ LOG.debug(jsonutils.loads(response.body))
self.assertEqual(jsonutils.loads(response.body)['collection'], "value")
def test_resource_extension_for_put_custom_collection_action(self):
def test_unsupported_extensions_are_not_loaded(self):
stub_plugin = StubPlugin(supported_extensions=["e1", "e3"])
- ext_mgr = PluginAwareExtensionManager('', stub_plugin)
+ ext_mgr = PluginAwareExtensionManager('',
+ {constants.CORE: stub_plugin})
ext_mgr.add_extension(StubExtension("e1"))
ext_mgr.add_extension(StubExtension("e2"))
"""
pass
- ext_mgr = PluginAwareExtensionManager('', ExtensionUnawarePlugin())
+ ext_mgr = PluginAwareExtensionManager('',
+ {constants.CORE:
+ ExtensionUnawarePlugin()})
ext_mgr.add_extension(StubExtension("e1"))
self.assertFalse("e1" in ext_mgr.extensions)
def test_extensions_not_loaded_for_plugin_without_expected_interface(self):
- class PluginWithoutExpectedInterface(object):
+ class PluginWithoutExpectedIface(object):
"""
Plugin does not implement get_foo method as expected by extension
"""
supported_extension_aliases = ["supported_extension"]
ext_mgr = PluginAwareExtensionManager('',
- PluginWithoutExpectedInterface())
+ {constants.CORE:
+ PluginWithoutExpectedIface()})
ext_mgr.add_extension(
ExtensionExpectingPluginInterface("supported_extension"))
def get_foo(self, bar=None):
pass
ext_mgr = PluginAwareExtensionManager('',
- PluginWithExpectedInterface())
+ {constants.CORE:
+ PluginWithExpectedInterface()})
ext_mgr.add_extension(
ExtensionExpectingPluginInterface("supported_extension"))
"""
pass
stub_plugin = StubPlugin(supported_extensions=["e1"])
- ext_mgr = PluginAwareExtensionManager('', stub_plugin)
+ ext_mgr = PluginAwareExtensionManager('', {constants.CORE:
+ stub_plugin})
ext_mgr.add_extension(ExtensionForQuamtumPluginInterface("e1"))
self.assertTrue("e1" in ext_mgr.extensions)
return None
stub_plugin = StubPlugin(supported_extensions=["e1"])
- ext_mgr = PluginAwareExtensionManager('', stub_plugin)
+ ext_mgr = PluginAwareExtensionManager('', {constants.CORE:
+ stub_plugin})
ext_mgr.add_extension(ExtensionWithNoNeedForPluginInterface("e1"))
self.assertTrue("e1" in ext_mgr.extensions)
+ def test_extension_loaded_for_non_core_plugin(self):
+ class NonCorePluginExtenstion(StubExtension):
+ def get_plugin_interface(self):
+ return None
+
+ stub_plugin = StubPlugin(supported_extensions=["e1"])
+ ext_mgr = PluginAwareExtensionManager('', {constants.DUMMY:
+ stub_plugin})
+ ext_mgr.add_extension(NonCorePluginExtenstion("e1"))
+
+ self.assertTrue("e1" in ext_mgr.extensions)
+
class ExtensionControllerTest(unittest.TestCase):
extension_manager = (extension_manager or
PluginAwareExtensionManager(
extensions_path,
- FakePluginWithExtension()))
+ {constants.CORE: FakePluginWithExtension()}))
config_file = 'quantum.conf.test'
args = ['--config-file', etcdir(config_file)]
config.parse(args=args)
--- /dev/null
+# Copyright (c) 2012 OpenStack, LLC.
+#
+# 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 logging
+import types
+import unittest2
+
+from quantum.common import config
+from quantum.common.test_lib import test_config
+from quantum.manager import QuantumManager
+from quantum.openstack.common import cfg
+from quantum.plugins.common import constants
+from quantum.plugins.services.dummy.dummy_plugin import QuantumDummyPlugin
+
+
+LOG = logging.getLogger(__name__)
+DB_PLUGIN_KLASS = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2'
+
+
+class QuantumManagerTestCase(unittest2.TestCase):
+ def setUp(self):
+ super(QuantumManagerTestCase, self).setUp()
+
+ def tearDown(self):
+ unittest2.TestCase.tearDown(self)
+ cfg.CONF.reset()
+ QuantumManager._instance = None
+
+ def test_service_plugin_is_loaded(self):
+ cfg.CONF.set_override("core_plugin",
+ test_config.get('plugin_name_v2',
+ DB_PLUGIN_KLASS))
+ cfg.CONF.set_override("service_plugins",
+ ["quantum.plugins.services."
+ "dummy.dummy_plugin.QuantumDummyPlugin"])
+ QuantumManager._instance = None
+ mgr = QuantumManager.get_instance()
+ plugin = mgr.get_service_plugins()[constants.DUMMY]
+
+ self.assertTrue(
+ isinstance(plugin,
+ (QuantumDummyPlugin, types.ClassType)),
+ "loaded plugin should be of type QuantumDummyPlugin")
+
+ def test_multiple_plugins_specified_for_service_type(self):
+ cfg.CONF.set_override("service_plugins",
+ ["quantum.plugins.services."
+ "dummy.dummy_plugin.QuantumDummyPlugin",
+ "quantum.plugins.services."
+ "dummy.dummy_plugin.QuantumDummyPlugin"])
+ QuantumManager._instance = None
+
+ try:
+ QuantumManager.get_instance().get_service_plugins()
+ self.assertTrue(False,
+ "Shouldn't load multiple plugins "
+ "for the same type")
+ except Exception as e:
+ LOG.debug(str(e))