From feb3660344782107212c2da39afbe2e230f88cd4 Mon Sep 17 00:00:00 2001 From: Nader Lahouti Date: Sat, 19 Apr 2014 16:01:05 -0700 Subject: [PATCH] Support for extensions in ML2 The current ML2 plugin supports only extensions defined in the plugin and it does not support any extensions in the mechanism drivers. The changes in this commit allows mechanism drivers to define new extensions. Change-Id: I28da19fabf6de2e9f0d687f875aaaa24c8bbc4f0 Implements: blueprint extensions-in-ml2 --- etc/neutron/plugins/ml2/ml2_conf.ini | 5 + neutron/api/extensions.py | 2 +- neutron/db/db_base_plugin_v2.py | 2 + neutron/plugins/ml2/config.py | 5 + neutron/plugins/ml2/driver_api.py | 155 ++++++++++++++++++ neutron/plugins/ml2/managers.py | 104 ++++++++++++ neutron/plugins/ml2/plugin.py | 40 +++++ neutron/tests/unit/ml2/extensions/__init__.py | 0 .../unit/ml2/extensions/test_extension.py | 69 ++++++++ .../unit/ml2/test_extension_driver_api.py | 66 ++++++++ setup.cfg | 3 +- 11 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 neutron/tests/unit/ml2/extensions/__init__.py create mode 100644 neutron/tests/unit/ml2/extensions/test_extension.py create mode 100644 neutron/tests/unit/ml2/test_extension_driver_api.py diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index 54722df91d8..6325e714eee 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 e6706033964..9c825cbd504 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 9f8d7bf3fbf..0b203094e02 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 afce63045e0..9591e6b19e2 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 779ebdf92d2..1fe5f8381fb 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 d679e7dd5b2..084829e5837 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 7f60ba5ce66..f25c5124104 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 00000000000..e69de29bb2d 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 00000000000..c48c0e7d06b --- /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 00000000000..f2367b4d96c --- /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 d006b09a6f6..707545704a9 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