]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
The change allows loading several service plugins along with core plugin.
authorEugene Nikanorov <enikanorov@mirantis.com>
Thu, 8 Nov 2012 14:44:02 +0000 (18:44 +0400)
committerEugene Nikanorov <enikanorov@mirantis.com>
Tue, 20 Nov 2012 05:37:34 +0000 (09:37 +0400)
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

13 files changed:
etc/quantum.conf
quantum/common/config.py
quantum/extensions/extensions.py
quantum/manager.py
quantum/plugins/__init__.py
quantum/plugins/common/__init__.py [new file with mode: 0644]
quantum/plugins/common/constants.py [new file with mode: 0644]
quantum/plugins/services/__init__.py [new file with mode: 0644]
quantum/plugins/services/dummy/__init__.py [new file with mode: 0644]
quantum/plugins/services/dummy/dummy_plugin.py [new file with mode: 0644]
quantum/plugins/services/service_base.py [new file with mode: 0644]
quantum/tests/unit/test_extensions.py
quantum/tests/unit/test_quantum_manager.py [new file with mode: 0644]

index 802d8bb44b7e957b1c893b7c67a3ea905513ce92..9c837232a43ca54808b1ba0fdbc7e2798ab74c34 100644 (file)
@@ -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
 
index 12c6cb4f29832c59d85217c06a5bf5e26575cebb..5f99fc40484c47149f489fd87f7a268236d775f5 100644 (file)
@@ -41,6 +41,8 @@ core_opts = [
     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),
index b7af3b36c1e54493873eb28c83ad709f3d868ba8..7909345419c070439f58cda726666d53ec6d8e61 100644 (file)
@@ -267,26 +267,30 @@ class ExtensionMiddleware(wsgi.Middleware):
 
         # 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,
@@ -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):
@@ -582,7 +586,7 @@ class PluginAwareExtensionManager(ExtensionManager):
                     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
 
 
@@ -612,13 +616,14 @@ class ActionExtension(object):
 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__
index bb4981a86b203dfa7ba5a4fe72c9ff6327835356..5d494a9f76a123c4bc9b752c2d1c70d8f3309efb 100644 (file)
@@ -27,6 +27,7 @@ from quantum.common.exceptions import ClassNotFound
 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__)
@@ -58,8 +59,52 @@ class QuantumManager(object):
                             "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
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cbf4a4506085659966f497ad9a671f605c40e36f 100644 (file)
@@ -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.
diff --git a/quantum/plugins/common/__init__.py b/quantum/plugins/common/__init__.py
new file mode 100644 (file)
index 0000000..cbf4a45
--- /dev/null
@@ -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.
diff --git a/quantum/plugins/common/constants.py b/quantum/plugins/common/constants.py
new file mode 100644 (file)
index 0000000..59da9d8
--- /dev/null
@@ -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",
+}
diff --git a/quantum/plugins/services/__init__.py b/quantum/plugins/services/__init__.py
new file mode 100644 (file)
index 0000000..cbf4a45
--- /dev/null
@@ -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.
diff --git a/quantum/plugins/services/dummy/__init__.py b/quantum/plugins/services/dummy/__init__.py
new file mode 100644 (file)
index 0000000..cbf4a45
--- /dev/null
@@ -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.
diff --git a/quantum/plugins/services/dummy/dummy_plugin.py b/quantum/plugins/services/dummy/dummy_plugin.py
new file mode 100644 (file)
index 0000000..8b85fdb
--- /dev/null
@@ -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"
diff --git a/quantum/plugins/services/service_base.py b/quantum/plugins/services/service_base.py
new file mode 100644 (file)
index 0000000..dfa074d
--- /dev/null
@@ -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
index b0d49e064654445859473eadfedd9dbd9c9e38e9..8a72b4c069c57c1b8fdc5b98a98252a72ef0d68b 100644 (file)
@@ -33,6 +33,7 @@ from quantum.extensions.extensions import (
     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,
@@ -43,6 +44,7 @@ from quantum.tests.unit.extension_stubs import (
 import quantum.tests.unit.extensions
 from quantum import wsgi
 
+
 LOG = logging.getLogger('quantum.tests.test_extensions')
 
 ROOTDIR = os.path.dirname(os.path.dirname(__file__))
@@ -93,6 +95,22 @@ class ResourceExtensionTest(unittest.TestCase):
         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"}
@@ -122,6 +140,20 @@ class ResourceExtensionTest(unittest.TestCase):
         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"}
@@ -134,6 +166,53 @@ class ResourceExtensionTest(unittest.TestCase):
         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"}
@@ -143,6 +222,7 @@ class ResourceExtensionTest(unittest.TestCase):
 
         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):
@@ -354,7 +434,8 @@ class PluginAwareExtensionManagerTest(unittest.TestCase):
 
     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"))
@@ -372,21 +453,24 @@ class PluginAwareExtensionManagerTest(unittest.TestCase):
             """
             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"))
 
@@ -403,7 +487,8 @@ class PluginAwareExtensionManagerTest(unittest.TestCase):
             def get_foo(self, bar=None):
                 pass
         ext_mgr = PluginAwareExtensionManager('',
-                                              PluginWithExpectedInterface())
+                                              {constants.CORE:
+                                               PluginWithExpectedInterface()})
         ext_mgr.add_extension(
             ExtensionExpectingPluginInterface("supported_extension"))
 
@@ -417,7 +502,8 @@ class PluginAwareExtensionManagerTest(unittest.TestCase):
             """
             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)
@@ -432,11 +518,24 @@ class PluginAwareExtensionManagerTest(unittest.TestCase):
                 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):
 
@@ -483,7 +582,7 @@ def setup_extensions_middleware(extension_manager=None):
     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)
diff --git a/quantum/tests/unit/test_quantum_manager.py b/quantum/tests/unit/test_quantum_manager.py
new file mode 100644 (file)
index 0000000..a07f694
--- /dev/null
@@ -0,0 +1,71 @@
+# 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))