diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index 54722df91d..6325e714ee 100644 --- a/etc/neutron/plugins/ml2/ml2_conf.ini +++ b/etc/neutron/plugins/ml2/ml2_conf.ini @@ -21,6 +21,11 @@ # Example: mechanism_drivers = openvswitch,brocade # Example: mechanism_drivers = linuxbridge,brocade +# (ListOpt) Ordered list of extension driver entrypoints +# to be loaded from the neutron.ml2.extension_drivers namespace. +# extension_drivers = +# Example: extension_drivers = anewextensiondriver + [ml2_type_flat] # (ListOpt) List of physical_network names with which flat networks # can be created. Use * to allow flat networks with arbitrary diff --git a/neutron/api/extensions.py b/neutron/api/extensions.py index e670603396..9c825cbd50 100644 --- a/neutron/api/extensions.py +++ b/neutron/api/extensions.py @@ -679,6 +679,6 @@ def get_extensions_path(): def append_api_extensions_path(paths): - paths = [cfg.CONF.api_extensions_path] + paths + paths = list(set([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/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 9f8d7bf3fb..0b203094e0 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -807,6 +807,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, for route in subnet['routes']], 'shared': subnet['shared'] } + # Call auxiliary extend functions, if any + self._apply_dict_extend_functions(attributes.SUBNETS, res, subnet) return self._fields(res, fields) def _make_port_dict(self, port, fields=None, diff --git a/neutron/plugins/ml2/config.py b/neutron/plugins/ml2/config.py index afce63045e..9591e6b19e 100644 --- a/neutron/plugins/ml2/config.py +++ b/neutron/plugins/ml2/config.py @@ -30,6 +30,11 @@ ml2_opts = [ help=_("An ordered list of networking mechanism driver " "entrypoints to be loaded from the " "neutron.ml2.mechanism_drivers namespace.")), + cfg.ListOpt('extension_drivers', + default=[], + help=_("An ordered list of extension driver " + "entrypoints to be loaded from the " + "neutron.ml2.extension_drivers namespace.")), ] diff --git a/neutron/plugins/ml2/driver_api.py b/neutron/plugins/ml2/driver_api.py index 779ebdf92d..1fe5f8381f 100644 --- a/neutron/plugins/ml2/driver_api.py +++ b/neutron/plugins/ml2/driver_api.py @@ -649,3 +649,158 @@ class MechanismDriver(object): that such state changes are eventually cleaned up. """ pass + + +@six.add_metaclass(abc.ABCMeta) +class ExtensionDriver(object): + """Define stable abstract interface for ML2 extension drivers. + + An extension driver extends the core resources implemented by the + ML2 plugin with additional attributes. Methods that process create + and update operations for these resources validate and persist + values for extended attributes supplied through the API. Other + methods extend the resource dictionaries returned from the API + operations with the values of the extended attributes. + """ + + @abc.abstractmethod + def initialize(self): + """Perform driver initialization. + + Called after all drivers have been loaded and the database has + been initialized. No abstract methods defined below will be + called prior to this method being called. + """ + pass + + @abc.abstractproperty + def extension_alias(self): + """Supported extension alias. + + Return the alias identifying the core API extension supported + by this driver. + """ + pass + + def process_create_network(self, session, data, result): + """Process extended attributes for create network. + + :param session: database session + :param data: dictionary of incoming network data + :param result: network dictionary to extend + + Called inside transaction context on session to validate and + persist any extended network attributes defined by this + driver. Extended attribute values must also be added to + result. + """ + pass + + def process_create_subnet(self, session, data, result): + """Process extended attributes for create subnet. + + :param session: database session + :param data: dictionary of incoming subnet data + :param result: subnet dictionary to extend + + Called inside transaction context on session to validate and + persist any extended subnet attributes defined by this + driver. Extended attribute values must also be added to + result. + """ + pass + + def process_create_port(self, session, data, result): + """Process extended attributes for create port. + + :param session: database session + :param data: dictionary of incoming port data + :param result: port dictionary to extend + + Called inside transaction context on session to validate and + persist any extended port attributes defined by this + driver. Extended attribute values must also be added to + result. + """ + pass + + def process_update_network(self, session, data, result): + """Process extended attributes for update network. + + :param session: database session + :param data: dictionary of incoming network data + :param result: network dictionary to extend + + Called inside transaction context on session to validate and + update any extended network attributes defined by this + driver. Extended attribute values, whether updated or not, + must also be added to result. + """ + pass + + def process_update_subnet(self, session, data, result): + """Process extended attributes for update subnet. + + :param session: database session + :param data: dictionary of incoming subnet data + :param result: subnet dictionary to extend + + Called inside transaction context on session to validate and + update any extended subnet attributes defined by this + driver. Extended attribute values, whether updated or not, + must also be added to result. + """ + pass + + def process_update_port(self, session, data, result): + """Process extended attributes for update port. + + :param session: database session + :param data: dictionary of incoming port data + :param result: port dictionary to extend + + Called inside transaction context on session to validate and + update any extended port attributes defined by this + driver. Extended attribute values, whether updated or not, + must also be added to result. + """ + pass + + def extend_network_dict(self, session, result): + """Add extended attributes to network dictionary. + + :param session: database session + :param result: network dictionary to extend + + Called inside transaction context on session to add any + extended attributes defined by this driver to a network + dictionary to be used for mechanism driver calls and/or + returned as the result of a network operation. + """ + pass + + def extend_subnet_dict(self, session, result): + """Add extended attributes to subnet dictionary. + + :param session: database session + :param result: subnet dictionary to extend + + Called inside transaction context on session to add any + extended attributes defined by this driver to a subnet + dictionary to be used for mechanism driver calls and/or + returned as the result of a subnet operation. + """ + pass + + def extend_port_dict(self, session, result): + """Add extended attributes to port dictionary. + + :param session: database session + :param result: port dictionary to extend + + Called inside transaction context on session to add any + extended attributes defined by this driver to a port + dictionary to be used for mechanism driver calls and/or + returned as the result of a port operation. + """ + pass diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index d679e7dd5b..084829e583 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -573,3 +573,107 @@ class MechanismManager(stevedore.named.NamedExtensionManager): LOG.warning(_("Failed to bind port %(port)s on host %(host)s"), {'port': context._port['id'], 'host': binding.host}) + + +class ExtensionManager(stevedore.named.NamedExtensionManager): + """Manage extension drivers using drivers.""" + + def __init__(self): + # Ordered list of extension drivers, defining + # the order in which the drivers are called. + self.ordered_ext_drivers = [] + + LOG.info(_("Configured extension driver names: %s"), + cfg.CONF.ml2.extension_drivers) + super(ExtensionManager, self).__init__('neutron.ml2.extension_drivers', + cfg.CONF.ml2.extension_drivers, + invoke_on_load=True, + name_order=True) + LOG.info(_("Loaded extension driver names: %s"), self.names()) + self._register_drivers() + + def _register_drivers(self): + """Register all extension drivers. + + This method should only be called once in the ExtensionManager + constructor. + """ + for ext in self: + self.ordered_ext_drivers.append(ext) + LOG.info(_("Registered extension drivers: %s"), + [driver.name for driver in self.ordered_ext_drivers]) + + def initialize(self): + # Initialize each driver in the list. + for driver in self.ordered_ext_drivers: + LOG.info(_("Initializing extension driver '%s'"), driver.name) + driver.obj.initialize() + + def extension_aliases(self): + exts = [] + for driver in self.ordered_ext_drivers: + alias = driver.obj.extension_alias + exts.append(alias) + LOG.info(_("Got %(alias)s extension from driver '%(drv)s'"), + {'alias': alias, 'drv': driver.name}) + return exts + + def _call_on_ext_drivers(self, method_name, session, data, result): + """Helper method for calling a method across all extension drivers.""" + for driver in self.ordered_ext_drivers: + try: + getattr(driver.obj, method_name)(session, data, result) + except Exception: + LOG.exception( + _("Extension driver '%(name)s' failed in %(method)s"), + {'name': driver.name, 'method': method_name} + ) + + def process_create_network(self, session, data, result): + """Notify all extension drivers during network creation.""" + self._call_on_ext_drivers("process_create_network", session, data, + result) + + def process_update_network(self, session, data, result): + """Notify all extension drivers during network update.""" + self._call_on_ext_drivers("process_update_network", session, data, + result) + + def process_create_subnet(self, session, data, result): + """Notify all extension drivers during subnet creation.""" + self._call_on_ext_drivers("process_create_subnet", session, data, + result) + + def process_update_subnet(self, session, data, result): + """Notify all extension drivers during subnet update.""" + self._call_on_ext_drivers("process_update_subnet", session, data, + result) + + def process_create_port(self, session, data, result): + """Notify all extension drivers during port creation.""" + self._call_on_ext_drivers("process_create_port", session, data, result) + + def process_update_port(self, session, data, result): + """Notify all extension drivers during port update.""" + self._call_on_ext_drivers("process_update_port", session, data, result) + + def extend_network_dict(self, session, result): + """Notify all extension drivers to extend network dictionary.""" + for driver in self.ordered_ext_drivers: + driver.obj.extend_network_dict(session, result) + LOG.info(_("Extended network dict for driver '%(drv)s'"), + {'drv': driver.name}) + + def extend_subnet_dict(self, session, result): + """Notify all extension drivers to extend subnet dictionary.""" + for driver in self.ordered_ext_drivers: + driver.obj.extend_subnet_dict(session, result) + LOG.info(_("Extended subnet dict for driver '%(drv)s'"), + {'drv': driver.name}) + + def extend_port_dict(self, session, result): + """Notify all extension drivers to extend port dictionary.""" + for driver in self.ordered_ext_drivers: + driver.obj.extend_port_dict(session, result) + LOG.info(_("Extended port dict for driver '%(drv)s'"), + {'drv': driver.name}) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 7f60ba5ce6..f25c512410 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -35,6 +35,7 @@ from neutron.common import utils from neutron.db import agents_db from neutron.db import agentschedulers_db from neutron.db import allowedaddresspairs_db as addr_pair_db +from neutron.db import api as db_api from neutron.db import db_base_plugin_v2 from neutron.db import dvr_mac_db from neutron.db import external_net_db @@ -110,6 +111,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, def supported_extension_aliases(self): if not hasattr(self, '_aliases'): aliases = self._supported_extension_aliases[:] + aliases += self.extension_manager.extension_aliases() sg_rpc.disable_security_group_extension_by_config(aliases) self._aliases = aliases return self._aliases @@ -117,9 +119,11 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, def __init__(self): # First load drivers, then initialize DB, then initialize drivers self.type_manager = managers.TypeManager() + self.extension_manager = managers.ExtensionManager() self.mechanism_manager = managers.MechanismManager() super(Ml2Plugin, self).__init__() self.type_manager.initialize() + self.extension_manager.initialize() self.mechanism_manager.initialize() # bulk support depends on the underlying drivers self.__native_bulk_support = self.mechanism_manager.native_bulk_support @@ -411,6 +415,31 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( attributes.PORTS, ['_ml2_extend_port_dict_binding']) + # Register extend dict methods for network and port resources. + # Each mechanism driver that supports extend attribute for the resources + # can add those attribute to the result. + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + attributes.NETWORKS, ['_ml2_md_extend_network_dict']) + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + attributes.PORTS, ['_ml2_md_extend_port_dict']) + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + attributes.SUBNETS, ['_ml2_md_extend_subnet_dict']) + + def _ml2_md_extend_network_dict(self, result, netdb): + session = db_api.get_session() + with session.begin(subtransactions=True): + self.extension_manager.extend_network_dict(session, result) + + def _ml2_md_extend_port_dict(self, result, portdb): + session = db_api.get_session() + with session.begin(subtransactions=True): + self.extension_manager.extend_port_dict(session, result) + + def _ml2_md_extend_subnet_dict(self, result, subnetdb): + session = db_api.get_session() + with session.begin(subtransactions=True): + self.extension_manager.extend_subnet_dict(session, result) + # Note - The following hook methods have "ml2" in their names so # that they are not called twice during unit tests due to global # registration of hooks in portbindings_db.py used by other @@ -460,6 +489,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, with session.begin(subtransactions=True): self._ensure_default_security_group(context, tenant_id) result = super(Ml2Plugin, self).create_network(context, network) + self.extension_manager.process_create_network(session, net_data, + result) self._process_l3_create(context, result, net_data) net_data['id'] = result['id'] self.type_manager.create_network_segments(context, net_data, @@ -487,6 +518,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, updated_network = super(Ml2Plugin, self).update_network(context, id, network) + self.extension_manager.process_update_network(session, network, + original_network) self._process_l3_update(context, updated_network, network['network']) self.type_manager._extend_network_dict_provider(context, @@ -627,6 +660,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, session = context.session with session.begin(subtransactions=True): result = super(Ml2Plugin, self).create_subnet(context, subnet) + self.extension_manager.process_create_subnet(session, subnet, + result) mech_context = driver_context.SubnetContext(self, context, result) self.mechanism_manager.create_subnet_precommit(mech_context) @@ -645,6 +680,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, original_subnet = super(Ml2Plugin, self).get_subnet(context, id) updated_subnet = super(Ml2Plugin, self).update_subnet( context, id, subnet) + self.extension_manager.process_update_subnet(session, subnet, + original_subnet) mech_context = driver_context.SubnetContext( self, context, updated_subnet, original_subnet=original_subnet) self.mechanism_manager.update_subnet_precommit(mech_context) @@ -737,6 +774,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, sgids = self._get_security_groups_on_port(context, port) dhcp_opts = port['port'].get(edo_ext.EXTRADHCPOPTS, []) result = super(Ml2Plugin, self).create_port(context, port) + self.extension_manager.process_create_port(session, attrs, result) self._process_port_create_security_group(context, result, sgids) network = self.get_network(context, result['network_id']) binding = db.add_port_binding(session, result['id']) @@ -791,6 +829,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, original_port = self._make_port_dict(port_db) updated_port = super(Ml2Plugin, self).update_port(context, id, port) + self.extension_manager.process_update_port(session, attrs, + original_port) if addr_pair.ADDRESS_PAIRS in port['port']: need_port_update_notify |= ( self.update_address_pairs_on_port(context, id, port, diff --git a/neutron/tests/unit/ml2/extensions/__init__.py b/neutron/tests/unit/ml2/extensions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/tests/unit/ml2/extensions/test_extension.py b/neutron/tests/unit/ml2/extensions/test_extension.py new file mode 100644 index 0000000000..c48c0e7d06 --- /dev/null +++ b/neutron/tests/unit/ml2/extensions/test_extension.py @@ -0,0 +1,69 @@ +# 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 neutron.api import extensions +from neutron.api.v2 import attributes as attr + + +EXTENDED_ATTRIBUTES_2_0 = { + 'networks': { + 'network_extension': {'allow_post': True, + 'allow_put': True, + 'default': attr.ATTR_NOT_SPECIFIED, + 'is_visible': True, + 'enforce_policy': True}, + }, + 'subnets': { + 'subnet_extension': {'allow_post': True, + 'allow_put': True, + 'default': attr.ATTR_NOT_SPECIFIED, + 'is_visible': True, + 'enforce_policy': True}, + }, + 'ports': { + 'port_extension': {'allow_post': True, + 'allow_put': True, + 'default': attr.ATTR_NOT_SPECIFIED, + 'is_visible': True, + 'enforce_policy': True}, + }, +} + + +class Test_extension(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "ML2 test extension" + + @classmethod + def get_alias(cls): + return "test_extension" + + @classmethod + def get_description(cls): + return _("Adds test attributes to core resources.") + + @classmethod + def get_namespace(cls): + return ("http://docs.openstack.org/ext/neutron/ml2/test/" + "test_extension/api/v1.0") + + @classmethod + def get_updated(cls): + return "2014-07-16T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/tests/unit/ml2/test_extension_driver_api.py b/neutron/tests/unit/ml2/test_extension_driver_api.py new file mode 100644 index 0000000000..f2367b4d96 --- /dev/null +++ b/neutron/tests/unit/ml2/test_extension_driver_api.py @@ -0,0 +1,66 @@ +# 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 neutron.api import extensions +from neutron.plugins.ml2 import config +from neutron.plugins.ml2 import driver_api as api +from neutron.tests.unit.ml2 import extensions as test_extensions +from neutron.tests.unit.ml2 import test_ml2_plugin + + +class ExtensionDriverTestCase(test_ml2_plugin.Ml2PluginV2TestCase): + + _extension_drivers = ['test'] + + def setUp(self): + config.cfg.CONF.set_override('extension_drivers', + self._extension_drivers, + group='ml2') + super(ExtensionDriverTestCase, self).setUp() + + def test_network_attr(self): + with self.network() as network: + ent = network['network'].get('network_extension') + self.assertIsNotNone(ent) + + def test_subnet_attr(self): + with self.subnet() as subnet: + ent = subnet['subnet'].get('subnet_extension') + self.assertIsNotNone(ent) + + def test_port_attr(self): + with self.port() as port: + ent = port['port'].get('port_extension') + self.assertIsNotNone(ent) + + +class TestExtensionDriver(api.ExtensionDriver): + _supported_extension_alias = 'test_extension' + + def initialize(self): + self.network_extension = 'Test_Network_Extension' + self.subnet_extension = 'Test_Subnet_Extension' + self.port_extension = 'Test_Port_Extension' + extensions.append_api_extensions_path(test_extensions.__path__) + + @property + def extension_alias(self): + return self._supported_extension_alias + + def process_create_network(self, session, data, result): + result['network_extension'] = self.network_extension + + def process_create_subnet(self, session, data, result): + result['subnet_extension'] = self.subnet_extension + + def process_create_port(self, session, data, result): + result['port_extension'] = self.port_extension diff --git a/setup.cfg b/setup.cfg index d006b09a6f..707545704a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -176,6 +176,8 @@ neutron.ml2.mechanism_drivers = fslsdn = neutron.plugins.ml2.drivers.mechanism_fslsdn:FslsdnMechanismDriver sriovnicswitch = neutron.plugins.ml2.drivers.mech_sriov.mech_driver:SriovNicSwitchMechanismDriver nuage = neutron.plugins.ml2.drivers.mech_nuage.driver:NuageMechanismDriver +neutron.ml2.extension_drivers = + test = neutron.tests.unit.ml2.test_extension_driver_api:TestExtensionDriver neutron.openstack.common.cache.backends = memory = neutron.openstack.common.cache._backends.memory:MemoryBackend # These are for backwards compat with Icehouse notification_driver configuration values @@ -186,7 +188,6 @@ oslo.messaging.notify.drivers = neutron.openstack.common.notifier.rpc_notifier = oslo.messaging.notify._impl_messaging:MessagingDriver neutron.openstack.common.notifier.test_notifier = oslo.messaging.notify._impl_test:TestDriver - [build_sphinx] all_files = 1 build-dir = doc/build