From f4c8a1831402cd4d48b2876e0b223f855020d896 Mon Sep 17 00:00:00 2001 From: Amir Sadoughi Date: Thu, 5 Sep 2013 01:38:41 -0500 Subject: [PATCH] Added check on plugin.supported_extension_aliases Added check to neutron.api.extensions.PluginAwareExtensionManager which raises an exception when an alias in the plugin's `supported_extension_aliases` list is not found in the set of loaded extension aliases. If an alias is missing, it means the extension for that alias has not been loaded, has not been found, and the file is missing from paths listed in `oslo.config.CONF.api_extensions_path`. This guards against a common class of bugs in plugins, such as typographical errors in the `supported_extension_aliases` property. Plugin changes: * bigswitch.plugin: Moves api_extensions_path override to plugin's __init__ method, similar to other plugins. * cisco.n1kv.n1kv_neutron_plugin: Removes "policy_profile_binding" and "network_profile_binding" as they don't exist in Neutron currently. Removed override of api_extensions_path as it is loaded through cisco.network_plugin. * cisco.network_plugin: Renames "Cisco Credential" to "credential". Adds api_extension_path override to plugin's __init__ method. * metaplugin.meta_neutron_plugin: Avoids alias of empty string when cfg.CONF.META.supported_extension_aliases is an empty string. * midonet.plugin: Fixes regression of 98e16a06 from 715b16ac. * nec.nec_plugin: Extended override of api_extensions_path to append nec extensions path to existing configured path. * nicira.NeutronPlugin: Extended override of api_extensions_path to append NXP_EXT_PATH to existing configured path. Fixes: bug 1225080 Change-Id: Idcaade221d83c611fcbd87b503b2c8377d106962 --- neutron/api/extensions.py | 18 +++++ neutron/common/exceptions.py | 4 ++ neutron/plugins/bigswitch/plugin.py | 12 ++-- .../plugins/cisco/n1kv/n1kv_neutron_plugin.py | 9 --- neutron/plugins/cisco/network_plugin.py | 6 +- .../plugins/metaplugin/meta_neutron_plugin.py | 11 +-- neutron/plugins/midonet/plugin.py | 2 +- .../plugins/nec/extensions/packetfilter.py | 5 -- .../plugins/nec/extensions/router_provider.py | 3 +- neutron/plugins/nec/nec_plugin.py | 9 ++- neutron/plugins/nicira/NeutronPlugin.py | 5 +- .../tests/unit/cisco/n1kv/test_n1kv_plugin.py | 5 +- neutron/tests/unit/nicira/test_networkgw.py | 5 ++ neutron/tests/unit/test_extensions.py | 71 ++++++++++++------- 14 files changed, 100 insertions(+), 65 deletions(-) diff --git a/neutron/api/extensions.py b/neutron/api/extensions.py index 5919d8396..751c062ad 100644 --- a/neutron/api/extensions.py +++ b/neutron/api/extensions.py @@ -18,6 +18,7 @@ from abc import ABCMeta import imp +import itertools import os from oslo.config import cfg @@ -578,6 +579,7 @@ class PluginAwareExtensionManager(ExtensionManager): def __init__(self, path, plugins): self.plugins = plugins super(PluginAwareExtensionManager, self).__init__(path) + self.check_if_plugin_extensions_loaded() def _check_extension(self, extension): """Check if an extension is supported by any plugin.""" @@ -616,6 +618,16 @@ class PluginAwareExtensionManager(ExtensionManager): NeutronManager.get_service_plugins()) return cls._instance + def check_if_plugin_extensions_loaded(self): + """Check if an extension supported by a plugin has been loaded.""" + plugin_extensions = set(itertools.chain.from_iterable([ + getattr(plugin, "supported_extension_aliases", []) + for plugin in self.plugins.values()])) + missing_aliases = plugin_extensions - set(self.extensions) + if missing_aliases: + raise exceptions.ExtensionsNotFound( + extensions=list(missing_aliases)) + class RequestExtension(object): """Extend requests and responses of core Neutron OpenStack API controllers. @@ -662,3 +674,9 @@ def get_extensions_path(): paths = ':'.join([cfg.CONF.api_extensions_path, paths]) return paths + + +def append_api_extensions_path(paths): + paths = [cfg.CONF.api_extensions_path] + paths + cfg.CONF.set_override('api_extensions_path', + ':'.join([p for p in paths if p])) diff --git a/neutron/common/exceptions.py b/neutron/common/exceptions.py index df49df858..a46e17dfc 100644 --- a/neutron/common/exceptions.py +++ b/neutron/common/exceptions.py @@ -261,6 +261,10 @@ class InvalidExtensionEnv(BadRequest): message = _("Invalid extension environment: %(reason)s") +class ExtensionsNotFound(NotFound): + message = _("Extensions not found: %(extensions)s") + + class InvalidContentType(NeutronException): message = _("Invalid content type %(content_type)s") diff --git a/neutron/plugins/bigswitch/plugin.py b/neutron/plugins/bigswitch/plugin.py index b5e272e73..ab2e7bf7c 100644 --- a/neutron/plugins/bigswitch/plugin.py +++ b/neutron/plugins/bigswitch/plugin.py @@ -48,11 +48,11 @@ import base64 import copy import httplib import json -import os import socket from oslo.config import cfg +from neutron.api import extensions as neutron_extensions from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.common import constants as const from neutron.common import exceptions @@ -77,17 +77,12 @@ from neutron.openstack.common import importutils from neutron.openstack.common import log as logging from neutron.openstack.common import rpc from neutron.plugins.bigswitch.db import porttracker_db +from neutron.plugins.bigswitch import extensions from neutron.plugins.bigswitch import routerrule_db from neutron.plugins.bigswitch.version import version_string_with_vcs LOG = logging.getLogger(__name__) -# Include the BigSwitch Extensions path in the api_extensions -EXTENSIONS_PATH = os.path.join(os.path.dirname(__file__), 'extensions') -if not cfg.CONF.api_extensions_path: - cfg.CONF.set_override('api_extensions_path', - EXTENSIONS_PATH) - restproxy_opts = [ cfg.StrOpt('servers', default='localhost:8800', help=_("A comma separated list of BigSwitch or Floodlight " @@ -450,6 +445,9 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # init DB, proxy's persistent store defaults to in-memory sql-lite DB db.configure_db() + # Include the BigSwitch Extensions path in the api_extensions + neutron_extensions.append_api_extensions_path(extensions.__path__) + # 'servers' is the list of network controller REST end-points # (used in order specified till one suceeds, and it is sticky # till next failure). Use 'server_auth' to encode api-key diff --git a/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py b/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py index 6a21d1315..1e695912d 100644 --- a/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py +++ b/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py @@ -21,8 +21,6 @@ import eventlet -from oslo.config import cfg as q_conf - from neutron.agent import securitygroups_rpc as sg_rpc from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api @@ -148,8 +146,6 @@ class N1kvNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2, # bulk operations. __native_bulk_support = False supported_extension_aliases = ["provider", "agent", - "policy_profile_binding", - "network_profile_binding", "n1kv_profile", "network_profile", "policy_profile", "external-net", "router", "credential"] @@ -164,11 +160,6 @@ class N1kvNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2, n1kv_db_v2.initialize() c_cred.Store.initialize() self._initialize_network_ranges() - # If no api_extensions_path is provided set the following - if not q_conf.CONF.api_extensions_path: - q_conf.CONF.set_override( - 'api_extensions_path', - 'extensions:neutron/plugins/cisco/extensions') self._setup_vsm() self._setup_rpc() diff --git a/neutron/plugins/cisco/network_plugin.py b/neutron/plugins/cisco/network_plugin.py index f64822f51..c7b0ea57f 100644 --- a/neutron/plugins/cisco/network_plugin.py +++ b/neutron/plugins/cisco/network_plugin.py @@ -22,6 +22,7 @@ import logging from sqlalchemy import orm import webob.exc as wexc +from neutron.api import extensions as neutron_extensions from neutron.api.v2 import base from neutron.common import exceptions as exc from neutron.db import db_base_plugin_v2 @@ -31,13 +32,14 @@ from neutron.plugins.cisco.common import cisco_constants as const from neutron.plugins.cisco.common import cisco_exceptions as cexc from neutron.plugins.cisco.common import config from neutron.plugins.cisco.db import network_db_v2 as cdb +from neutron.plugins.cisco import extensions LOG = logging.getLogger(__name__) class PluginV2(db_base_plugin_v2.NeutronDbPluginV2): """Meta-Plugin with v2 API support for multiple sub-plugins.""" - supported_extension_aliases = ["Cisco Credential", "Cisco qos"] + supported_extension_aliases = ["credential", "Cisco qos"] _methods_to_delegate = ['create_network', 'delete_network', 'update_network', 'get_network', 'get_networks', @@ -81,6 +83,8 @@ class PluginV2(db_base_plugin_v2.NeutronDbPluginV2): self.supported_extension_aliases.extend( self._model.supported_extension_aliases) + neutron_extensions.append_api_extensions_path(extensions.__path__) + # Extend the fault map self._extend_fault_map() diff --git a/neutron/plugins/metaplugin/meta_neutron_plugin.py b/neutron/plugins/metaplugin/meta_neutron_plugin.py index 6731d204b..fb4ad8877 100644 --- a/neutron/plugins/metaplugin/meta_neutron_plugin.py +++ b/neutron/plugins/metaplugin/meta_neutron_plugin.py @@ -51,11 +51,12 @@ class MetaPluginV2(db_base_plugin_v2.NeutronDbPluginV2, def __init__(self, configfile=None): LOG.debug(_("Start initializing metaplugin")) - self.supported_extension_aliases = \ - cfg.CONF.META.supported_extension_aliases.split(',') - self.supported_extension_aliases += ['flavor', 'external-net', - 'router', 'ext-gw-mode', - 'extraroute'] + self.supported_extension_aliases = ['flavor', 'external-net', + 'router', 'ext-gw-mode', + 'extraroute'] + if cfg.CONF.META.supported_extension_aliases: + cfg_aliases = cfg.CONF.META.supported_extension_aliases.split(',') + self.supported_extension_aliases += cfg_aliases # Ignore config option overapping def _is_opt_registered(opts, opt): diff --git a/neutron/plugins/midonet/plugin.py b/neutron/plugins/midonet/plugin.py index ca2cb9c18..230fe41cf 100644 --- a/neutron/plugins/midonet/plugin.py +++ b/neutron/plugins/midonet/plugin.py @@ -204,7 +204,7 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, securitygroups_db.SecurityGroupDbMixin): supported_extension_aliases = ['external-net', 'router', 'security-group', - 'agent' 'dhcp_agent_scheduler', 'binding'] + 'agent', 'dhcp_agent_scheduler', 'binding'] __native_bulk_support = False def __init__(self): diff --git a/neutron/plugins/nec/extensions/packetfilter.py b/neutron/plugins/nec/extensions/packetfilter.py index 52591a64b..b487dc965 100644 --- a/neutron/plugins/nec/extensions/packetfilter.py +++ b/neutron/plugins/nec/extensions/packetfilter.py @@ -122,7 +122,6 @@ PACKET_FILTER_ATTR_MAP = {COLLECTION: PACKET_FILTER_ATTR_PARAMS} class Packetfilter(extensions.ExtensionDescriptor): - @classmethod def get_name(cls): return ALIAS @@ -157,10 +156,6 @@ class Packetfilter(extensions.ExtensionDescriptor): COLLECTION, resource, attr_map=PACKET_FILTER_ATTR_PARAMS) return [pf_ext] - def update_attributes_map(self, attributes): - super(Packetfilter, self).update_attributes_map( - attributes, extension_attrs_map=PACKET_FILTER_ATTR_MAP) - def get_extended_resources(self, version): if version == "2.0": return PACKET_FILTER_ATTR_MAP diff --git a/neutron/plugins/nec/extensions/router_provider.py b/neutron/plugins/nec/extensions/router_provider.py index d893a4c18..102e23218 100644 --- a/neutron/plugins/nec/extensions/router_provider.py +++ b/neutron/plugins/nec/extensions/router_provider.py @@ -14,7 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -from neutron.api import extensions from neutron.api.v2 import attributes from neutron.openstack.common import log as logging @@ -33,7 +32,7 @@ ROUTER_PROVIDER_ATTRIBUTE = { } -class Router_provider(extensions.ExtensionDescriptor): +class Router_provider(object): @classmethod def get_name(cls): return "Router Provider" diff --git a/neutron/plugins/nec/nec_plugin.py b/neutron/plugins/nec/nec_plugin.py index 5d38a485f..8fbc59c95 100644 --- a/neutron/plugins/nec/nec_plugin.py +++ b/neutron/plugins/nec/nec_plugin.py @@ -17,6 +17,7 @@ # @author: Akihiro MOTOKI from neutron.agent import securitygroups_rpc as sg_rpc +from neutron.api import extensions as neutron_extensions from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.api.v2 import attributes as attrs from neutron.common import constants as const @@ -46,6 +47,7 @@ from neutron.plugins.nec.common import config from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec.db import api as ndb from neutron.plugins.nec.db import router as rdb +from neutron.plugins.nec import extensions from neutron.plugins.nec import nec_router from neutron.plugins.nec import ofc_manager from neutron.plugins.nec import packet_filter @@ -104,11 +106,8 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, self.ofc = ofc_manager.OFCManager() self.base_binding_dict = self._get_base_binding_dict() portbindings_base.register_port_dict_function() - # Set the plugin default extension path - # if no api_extensions_path is specified. - if not config.CONF.api_extensions_path: - config.CONF.set_override('api_extensions_path', - 'neutron/plugins/nec/extensions') + + neutron_extensions.append_api_extensions_path(extensions.__path__) self.setup_rpc() self.l3_rpc_notifier = nec_router.L3AgentNotifyAPI() diff --git a/neutron/plugins/nicira/NeutronPlugin.py b/neutron/plugins/nicira/NeutronPlugin.py index ab3319bc4..05081318a 100644 --- a/neutron/plugins/nicira/NeutronPlugin.py +++ b/neutron/plugins/nicira/NeutronPlugin.py @@ -27,6 +27,7 @@ from oslo.config import cfg from sqlalchemy.orm import exc as sa_exc import webob.exc +from neutron.api import extensions as neutron_extensions from neutron.api.v2 import attributes as attr from neutron.api.v2 import base from neutron.common import constants @@ -181,9 +182,7 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin, 'default': self._nvp_delete_port} } - # If no api_extensions_path is provided set the following - if not cfg.CONF.api_extensions_path: - cfg.CONF.set_override('api_extensions_path', NVP_EXT_PATH) + neutron_extensions.append_api_extensions_path([NVP_EXT_PATH]) self.nvp_opts = cfg.CONF.NVP self.nvp_sync_opts = cfg.CONF.NVP_SYNC self.cluster = create_nvp_cluster(cfg.CONF, diff --git a/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py b/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py index 59b2a1f99..298074c7d 100644 --- a/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py +++ b/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py @@ -18,9 +18,9 @@ # @author: Abhishek Raut, Cisco Systems Inc. from mock import patch -import os from oslo.config import cfg +from neutron.api import extensions as neutron_extensions from neutron.api.v2 import attributes from neutron.common.test_lib import test_config from neutron import context @@ -204,8 +204,7 @@ class N1kvPluginTestCase(test_plugin.NeutronDbPluginV2TestCase): n1kv_neutron_plugin.N1kvNeutronPluginV2._setup_vsm = _fake_setup_vsm test_config['plugin_name_v2'] = self._plugin_name - cfg.CONF.set_override('api_extensions_path', - os.path.dirname(extensions.__file__)) + neutron_extensions.append_api_extensions_path(extensions.__path__) self.addCleanup(cfg.CONF.reset) ext_mgr = NetworkProfileTestExtensionManager() test_config['extension_manager'] = ext_mgr diff --git a/neutron/tests/unit/nicira/test_networkgw.py b/neutron/tests/unit/nicira/test_networkgw.py index e8fcd32a0..db6fda6c7 100644 --- a/neutron/tests/unit/nicira/test_networkgw.py +++ b/neutron/tests/unit/nicira/test_networkgw.py @@ -32,6 +32,7 @@ from neutron.db import db_base_plugin_v2 from neutron import manager from neutron.plugins.nicira.dbexts import nicira_networkgw_db from neutron.plugins.nicira.extensions import nvp_networkgw as networkgw +from neutron.plugins.nicira.NeutronPlugin import NVP_EXT_PATH from neutron import quota from neutron.tests import base from neutron.tests.unit import test_api_v2 @@ -630,6 +631,10 @@ class TestNetworkGatewayPlugin(db_base_plugin_v2.NeutronDbPluginV2, supported_extension_aliases = ["network-gateway"] + def __init__(self, **args): + super(TestNetworkGatewayPlugin, self).__init__(**args) + extensions.append_api_extensions_path([NVP_EXT_PATH]) + def delete_port(self, context, id, nw_gw_port_check=True): if nw_gw_port_check: port = self._get_port(context, id) diff --git a/neutron/tests/unit/test_extensions.py b/neutron/tests/unit/test_extensions.py index b2ae1ec3b..4b0c639a6 100644 --- a/neutron/tests/unit/test_extensions.py +++ b/neutron/tests/unit/test_extensions.py @@ -17,12 +17,14 @@ import os +import mock import routes import webob import webtest from neutron.api import extensions from neutron.common import config +from neutron.common import exceptions from neutron.db import db_base_plugin_v2 from neutron.openstack.common import jsonutils from neutron.openstack.common import log as logging @@ -447,15 +449,17 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): def test_unsupported_extensions_are_not_loaded(self): stub_plugin = ext_stubs.StubPlugin(supported_extensions=["e1", "e3"]) plugin_info = {constants.CORE: stub_plugin} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension(ext_stubs.StubExtension("e1")) - ext_mgr.add_extension(ext_stubs.StubExtension("e2")) - ext_mgr.add_extension(ext_stubs.StubExtension("e3")) + ext_mgr.add_extension(ext_stubs.StubExtension("e1")) + ext_mgr.add_extension(ext_stubs.StubExtension("e2")) + ext_mgr.add_extension(ext_stubs.StubExtension("e3")) - self.assertIn("e1", ext_mgr.extensions) - self.assertNotIn("e2", ext_mgr.extensions) - self.assertIn("e3", ext_mgr.extensions) + self.assertIn("e1", ext_mgr.extensions) + self.assertNotIn("e2", ext_mgr.extensions) + self.assertIn("e3", ext_mgr.extensions) def test_extensions_are_not_loaded_for_plugins_unaware_of_extensions(self): class ExtensionUnawarePlugin(object): @@ -478,11 +482,13 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): supported_extension_aliases = ["supported_extension"] plugin_info = {constants.CORE: PluginWithoutExpectedIface()} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension( - ext_stubs.ExtensionExpectingPluginInterface("supported_extension")) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + ext_mgr.add_extension(ext_stubs.ExtensionExpectingPluginInterface( + "supported_extension")) - self.assertNotIn("e1", ext_mgr.extensions) + self.assertNotIn("e1", ext_mgr.extensions) def test_extensions_are_loaded_for_plugin_with_expected_interface(self): @@ -494,11 +500,13 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): pass plugin_info = {constants.CORE: PluginWithExpectedInterface()} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension( - ext_stubs.ExtensionExpectingPluginInterface("supported_extension")) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + ext_mgr.add_extension(ext_stubs.ExtensionExpectingPluginInterface( + "supported_extension")) - self.assertIn("supported_extension", ext_mgr.extensions) + self.assertIn("supported_extension", ext_mgr.extensions) def test_extensions_expecting_neutron_plugin_interface_are_loaded(self): class ExtensionForQuamtumPluginInterface(ext_stubs.StubExtension): @@ -509,10 +517,13 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): pass stub_plugin = ext_stubs.StubPlugin(supported_extensions=["e1"]) plugin_info = {constants.CORE: stub_plugin} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension(ExtensionForQuamtumPluginInterface("e1")) - self.assertIn("e1", ext_mgr.extensions) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + ext_mgr.add_extension(ExtensionForQuamtumPluginInterface("e1")) + + self.assertIn("e1", ext_mgr.extensions) def test_extensions_without_need_for__plugin_interface_are_loaded(self): class ExtensionWithNoNeedForPluginInterface(ext_stubs.StubExtension): @@ -525,10 +536,12 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): stub_plugin = ext_stubs.StubPlugin(supported_extensions=["e1"]) plugin_info = {constants.CORE: stub_plugin} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension(ExtensionWithNoNeedForPluginInterface("e1")) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + ext_mgr.add_extension(ExtensionWithNoNeedForPluginInterface("e1")) - self.assertIn("e1", ext_mgr.extensions) + self.assertIn("e1", ext_mgr.extensions) def test_extension_loaded_for_non_core_plugin(self): class NonCorePluginExtenstion(ext_stubs.StubExtension): @@ -537,10 +550,20 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase): stub_plugin = ext_stubs.StubPlugin(supported_extensions=["e1"]) plugin_info = {constants.DUMMY: stub_plugin} - ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) - ext_mgr.add_extension(NonCorePluginExtenstion("e1")) + with mock.patch("neutron.api.extensions.PluginAwareExtensionManager." + "check_if_plugin_extensions_loaded"): + ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info) + ext_mgr.add_extension(NonCorePluginExtenstion("e1")) - self.assertIn("e1", ext_mgr.extensions) + self.assertIn("e1", ext_mgr.extensions) + + def test_unloaded_supported_extensions_raises_exception(self): + stub_plugin = ext_stubs.StubPlugin( + supported_extensions=["unloaded_extension"]) + plugin_info = {constants.CORE: stub_plugin} + self.assertRaises(exceptions.ExtensionsNotFound, + extensions.PluginAwareExtensionManager, + '', plugin_info) class ExtensionControllerTest(testlib_api.WebTestCase):