From ab44545a76718a7e97dbb7e2950655fe0129f1a5 Mon Sep 17 00:00:00 2001 From: Andre Pech Date: Sun, 7 Jul 2013 13:00:54 -0700 Subject: [PATCH] Initial Modular L2 Mechanism Driver implementation. Define the Mechanism Driver interface for create/update/delete operations on networks and ports. For each of these event, the Mechanism Driver provides one method that is called within the database transaction of the ml2 plugin method, and one that is called after the transaction is completed. Support for mechanism drivers is still a work-in-progress, and the interface is subject to change in future versions before the release of Havana. However this initial version should be sufficient to enable others to start defining their own mechanism drivers. Change-Id: Ife30215589792ee27df9897d3b2bc04392638266 Implements: blueprint ml2-mechanism-drivers Fixes: bug #1199977 Fixes: bug #1199978 DocImpact --- etc/neutron/plugins/ml2/ml2_conf.ini | 6 + neutron/plugins/ml2/README | 14 +- neutron/plugins/ml2/common/__init__.py | 14 + neutron/plugins/ml2/common/exceptions.py | 23 ++ neutron/plugins/ml2/config.py | 6 +- neutron/plugins/ml2/driver_api.py | 279 +++++++++++++++++- neutron/plugins/ml2/driver_context.py | 74 +++++ neutron/plugins/ml2/managers.py | 238 ++++++++++++++- neutron/plugins/ml2/plugin.py | 92 +++++- neutron/tests/unit/ml2/drivers/__init__.py | 14 + .../unit/ml2/drivers/mechanism_logger.py | 83 ++++++ .../tests/unit/ml2/drivers/mechanism_test.py | 80 +++++ neutron/tests/unit/ml2/test_ml2_plugin.py | 8 + setup.cfg | 3 + 14 files changed, 897 insertions(+), 37 deletions(-) create mode 100644 neutron/plugins/ml2/common/__init__.py create mode 100644 neutron/plugins/ml2/common/exceptions.py create mode 100644 neutron/plugins/ml2/driver_context.py create mode 100644 neutron/tests/unit/ml2/drivers/__init__.py create mode 100644 neutron/tests/unit/ml2/drivers/mechanism_logger.py create mode 100644 neutron/tests/unit/ml2/drivers/mechanism_test.py diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index a7fc6478fe..5dc20440b0 100644 --- a/etc/neutron/plugins/ml2/ml2_conf.ini +++ b/etc/neutron/plugins/ml2/ml2_conf.ini @@ -13,6 +13,12 @@ # tenant_network_types = local # Example: tenant_network_types = vlan,gre +# (ListOpt) Ordered list of networking mechanism driver entrypoints +# to be loaded from the neutron.ml2.mechanism_drivers namespace. +# mechanism_drivers = +# Example: mechanism_drivers = arista +# Example: mechanism_drivers = cisco,logger + [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/plugins/ml2/README b/neutron/plugins/ml2/README index 38d704dd42..30c0d375a1 100644 --- a/neutron/plugins/ml2/README +++ b/neutron/plugins/ml2/README @@ -31,13 +31,13 @@ openvswitch and linuxbridge plugins' L2 agents, and should also work with the hyperv L2 agent. A modular agent may be developed as a follow-on effort. -Support for mechanism drivers is currently skeletal. The -MechanismDriver interface is currently a stub, with details to be -defined in future versions. MechanismDrivers will be called both -inside and following DB transactions for network and port -create/update/delete operations. They will also be called to establish -a port binding, determining the VIF type and network segment to be -used. +Support for mechanism drivers is currently a work-in-progress in +pre-release Havana versions, and the interface is subject to change +before the release of Havana. MechanismDrivers are currently called +both inside and following DB transactions for network and port +create/update/delete operations. In a future version, they will also +called to establish a port binding, determining the VIF type and +network segment to be used. The database schema and driver APIs support multi-segment networks, but the client API for multi-segment networks is not yet implemented. diff --git a/neutron/plugins/ml2/common/__init__.py b/neutron/plugins/ml2/common/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/neutron/plugins/ml2/common/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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/neutron/plugins/ml2/common/exceptions.py b/neutron/plugins/ml2/common/exceptions.py new file mode 100644 index 0000000000..ed94b1e1f1 --- /dev/null +++ b/neutron/plugins/ml2/common/exceptions.py @@ -0,0 +1,23 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +"""Exceptions used by ML2.""" + +from neutron.common import exceptions + + +class MechanismDriverError(exceptions.NeutronException): + """Mechanism driver call failed.""" + message = _("%(method)s failed.") diff --git a/neutron/plugins/ml2/config.py b/neutron/plugins/ml2/config.py index 867fd8fcf9..43e752da76 100644 --- a/neutron/plugins/ml2/config.py +++ b/neutron/plugins/ml2/config.py @@ -29,9 +29,9 @@ ml2_opts = [ "networks.")), cfg.ListOpt('mechanism_drivers', default=[], - help=_("List of networking mechanism driver entrypoints to " - "be loaded from the neutron.ml2.mechanism_drivers " - "namespace.")), + help=_("An ordered list of networking mechanism driver " + "entrypoints to be loaded from the " + "neutron.ml2.mechanism_drivers namespace.")), ] diff --git a/neutron/plugins/ml2/driver_api.py b/neutron/plugins/ml2/driver_api.py index adc969b015..23e7e5d3ef 100644 --- a/neutron/plugins/ml2/driver_api.py +++ b/neutron/plugins/ml2/driver_api.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod, abstractproperty # The following keys are used in the segment dictionaries passed via # the driver API. These are defined separately from similar keys in @@ -128,15 +128,103 @@ class TypeDriver(object): pass +class NetworkContext(object): + """Context passed to MechanismDrivers for changes to network resources. + + A NetworkContext instance wraps a network resource. It provides + helper methods for accessing other relevant information. Results + from expensive operations are cached so that other + MechanismDrivers can freely access the same information. + """ + + __metaclass__ = ABCMeta + + @abstractproperty + def current(self): + """Return the current state of the network. + + Return the current state of the network, as defined by + NeutronPluginBaseV2.create_network and all extensions in the + ml2 plugin. + """ + pass + + @abstractproperty + def original(self): + """Return the original state of the network. + + Return the original state of the network, prior to a call to + update_network. Method is only valid within calls to + update_network_precommit and update_network_postcommit. + """ + pass + + @abstractproperty + def network_segments(self): + """Return the segments associated with this network resource.""" + pass + + +class PortContext(object): + """Context passed to MechanismDrivers for changes to port resources. + + A PortContext instance wraps a port resource. It provides helper + methods for accessing other relevant information. Results from + expensive operations are cached so that other MechanismDrivers can + freely access the same information. + """ + + __metaclass__ = ABCMeta + + @abstractproperty + def current(self): + """Return the current state of the port. + + Return the current state of the port, as defined by + NeutronPluginBaseV2.create_port and all extensions in the ml2 + plugin. + """ + pass + + @abstractproperty + def original(self): + """Return the original state of the port + + Return the original state of the port, prior to a call to + update_port. Method is only valid within calls to + update_port_precommit and update_port_postcommit. + """ + pass + + @abstractproperty + def network(self): + """Return the NetworkContext associated with this port.""" + pass + + class MechanismDriver(object): """Define stable abstract interface for ML2 mechanism drivers. - Note that this is currently a stub class, but it is expected to be - functional for the H-2 milestone. It currently serves mainly to - help solidify the architectural distinction between TypeDrivers - and MechanismDrivers. + A mechanism driver is called on the creation, update, and deletion + of networks and ports. For every event, there are two methods that + get called - one within the database transaction (method suffix of + _precommit), one right afterwards (method suffix of _postcommit). + + Exceptions raised by methods called inside the transaction can + rollback, but should not make any blocking calls (for example, + REST requests to an outside controller). Methods called after + transaction commits can make blocking external calls, though these + will block the entire process. Exceptions raised in calls after + the transaction commits may cause the associated resource to be + deleted. + + Because rollback outside of the transaction is not done in the + update network/port case, all data validation must be done within + methods that are part of the database transaction. """ + # TODO(apech): add calls for subnets + __metaclass__ = ABCMeta @abstractmethod @@ -149,10 +237,177 @@ class MechanismDriver(object): """ pass - # TODO(rkukura): Add methods called inside and after transaction - # for create_network, update_network, delete_network, create_port, - # update_port, delete_port, and maybe for port binding - # changes. Exceptions raised by methods called inside transactions - # can rollback, but shouldn't block. Methods called after - # transaction commits can block, and exceptions may cause deletion - # of resource. + def create_network_precommit(self, context): + """Allocate resources for a new network. + + :param context: NetworkContext instance describing the new + network. + + Create a new network, allocating resources as necessary in the + database. Called inside transaction context on session. Call + cannot block. Raising an exception will result in a rollback + of the current transaction. + """ + pass + + def create_network_postcommit(self, context): + """Create a network. + + :param context: NetworkContext instance describing the new + network. + + Called after the transaction commits. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Raising an exception will + cause the deletion of the resource. + """ + pass + + def update_network_precommit(self, context): + """Update resources of a network. + + :param context: NetworkContext instance describing the new + state of the network, as well as the original state prior + to the update_network call. + + Update values of a network, updating the associated resources + in the database. Called inside transaction context on session. + Raising an exception will result in rollback of the + transaction. + + update_network_precommit is called for all changes to the + network state. It is up to the mechanism driver to ignore + state or state changes that it does not know or care about. + """ + pass + + def update_network_postcommit(self, context): + """Update a network. + + :param context: NetworkContext instance describing the new + state of the network, as well as the original state prior + to the update_network call. + + Called after the transaction commits. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Raising an exception will + cause the deletion of the resource. + + update_network_postcommit is called for all changes to the + network state. It is up to the mechanism driver to ignore + state or state changes that it does not know or care about. + """ + pass + + def delete_network_precommit(self, context): + """Delete resources for a network. + + :param context: NetworkContext instance describing the current + state of the network, prior to the call to delete it. + + Delete network resources previously allocated by this + mechanism driver for a network. Called inside transaction + context on session. Runtime errors are not expected, but + raising an exception will result in rollback of the + transaction. + """ + pass + + def delete_network_postcommit(self, context): + """Delete a network. + + :param context: NetworkContext instance describing the current + state of the network, prior to the call to delete it. + + Called after the transaction commits. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Runtime errors are not + expected, and will not prevent the resource from being + deleted. + """ + pass + + def create_port_precommit(self, context): + """Allocate resources for a new port. + + :param context: PortContext instance describing the port. + + Create a new port, allocating resources as necessary in the + database. Called inside transaction context on session. Call + cannot block. Raising an exception will result in a rollback + of the current transaction. + """ + pass + + def create_port_postcommit(self, context): + """Create a port. + + :param context: PortContext instance describing the port. + + Called after the transaction completes. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Raising an exception will + result in the deletion of the resource. + """ + pass + + def update_port_precommit(self, context): + """Update resources of a port. + + :param context: PortContext instance describing the new + state of the port, as well as the original state prior + to the update_port call. + + Called inside transaction context on session to complete a + port update as defined by this mechanism driver. Raising an + exception will result in rollback of the transaction. + + update_port_precommit is called for all changes to the port + state. It is up to the mechanism driver to ignore state or + state changes that it does not know or care about. + """ + pass + + def update_port_postcommit(self, context): + """Update a port. + + :param context: PortContext instance describing the new + state of the port, as well as the original state prior + to the update_port call. + + Called after the transaction completes. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Raising an exception will + result in the deletion of the resource. + + update_port_postcommit is called for all changes to the port + state. It is up to the mechanism driver to ignore state or + state changes that it does not know or care about. + """ + pass + + def delete_port_precommit(self, context): + """Delete resources of a port. + + :param context: PortContext instance describing the current + state of the port, prior to the call to delete it. + + Called inside transaction context on session. Runtime errors + are not expected, but raising an exception will result in + rollback of the transaction. + """ + pass + + def delete_port_postcommit(self, context): + """Delete a port. + + :param context: PortContext instance describing the current + state of the port, prior to the call to delete it. + + Called after the transaction completes. Call can block, though + will block the entire process so care should be taken to not + drastically affect performance. Runtime errors are not + expected, and will not prevent the resource from being + deleted. + """ + pass diff --git a/neutron/plugins/ml2/driver_context.py b/neutron/plugins/ml2/driver_context.py new file mode 100644 index 0000000000..a5cf6b743d --- /dev/null +++ b/neutron/plugins/ml2/driver_context.py @@ -0,0 +1,74 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 neutron.plugins.ml2 import driver_api as api + + +class MechanismDriverContext(object): + """MechanismDriver context base class.""" + def __init__(self, plugin, plugin_context): + self._plugin = plugin + # This temporarily creates a reference loop, but the + # lifetime of PortContext is limited to a single + # method call of the plugin. + self._plugin_context = plugin_context + + +class NetworkContext(MechanismDriverContext, api.NetworkContext): + + def __init__(self, plugin, plugin_context, network, + segments=None, original_network=None): + super(NetworkContext, self).__init__(plugin, plugin_context) + self._network = network + self._original_network = original_network + self._segments = segments + + def current(self): + return self._network + + def original(self): + return self._original_network + + def network_segments(self): + if not self._segments: + self._segments = self._plugin.get_network_segments( + self._plugin_context, self._network['id']) + return self._segments + + +class PortContext(MechanismDriverContext, api.PortContext): + + def __init__(self, plugin, plugin_context, port, + original_port=None): + super(PortContext, self).__init__(plugin, plugin_context) + self._port = port + self._original_port = original_port + self._network_context = None + + def current(self): + return self._port + + def original(self): + return self._original_port + + def network(self): + """Return the NetworkContext associated with this port.""" + if not self._network_context: + network = self._plugin.get_network(self._plugin_context, + self._port["network_id"]) + self._network_context = NetworkContext(self._plugin, + self._plugin_context, + network) + return self._network_context diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index 5832aacd3b..e53bce7f8b 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -20,6 +20,7 @@ import stevedore from neutron.common import exceptions as exc from neutron.openstack.common import log +from neutron.plugins.ml2.common import exceptions as ml2_exc from neutron.plugins.ml2 import driver_api as api @@ -108,12 +109,18 @@ class TypeManager(stevedore.named.NamedExtensionManager): class MechanismManager(stevedore.named.NamedExtensionManager): """Manage networking mechanisms using drivers. - Note that this is currently a stub class, but it is expected to be - functional for the H-2 milestone. It currently serves mainly to - help solidify the architectural distinction between TypeDrivers - and MechanismDrivers. + Note that this is still a work in progress, and the interface + may change before the final release of Havana. """ + # TODO(apech): add calls for subnets + + # Registered mechanism drivers, keyed by name. + mech_drivers = {} + # Ordered list of mechanism drivers, defining + # the order in which the drivers are called. + ordered_mech_drivers = [] + def __init__(self): # REVISIT(rkukura): Need way to make stevedore use our logging # configuration. Currently, nothing is logged if loading a @@ -125,9 +132,226 @@ class MechanismManager(stevedore.named.NamedExtensionManager): cfg.CONF.ml2.mechanism_drivers, invoke_on_load=True) LOG.info(_("Loaded mechanism driver names: %s"), self.names()) - # TODO(rkukura): Register mechanisms. + self._register_mechanisms() + + def _register_mechanisms(self): + """Register all mechanism drivers. + + This method should only be called once in the MechanismManager + constructor. + """ + for ext in self: + if ext.name in self.mech_drivers: + LOG.error(_("Mechanism driver '%s' ignored because " + "driver is already registered"), + ext.name) + else: + self.mech_drivers[ext.name] = ext + self.ordered_mech_drivers.append(ext) + LOG.info(_("Registered mechanism drivers: %s"), + [driver.name for driver in self.ordered_mech_drivers]) def initialize(self): - pass + for driver in self.ordered_mech_drivers: + LOG.info(_("Initializing mechanism driver '%s'"), driver.name) + driver.obj.initialize() - # TODO(rkukura): Define mechanism dispatch methods + def _call_on_drivers(self, method_name, context, + continue_on_failure=False): + """Helper method for calling a method across all mechanism drivers. + + :param method_name: name of the method to call + :param context: context parameter to pass to each method call + :param continue_on_failure: whether or not to continue to call + all mechanism drivers once one has raised an exception + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver call fails. + """ + error = False + for driver in self.ordered_mech_drivers: + try: + getattr(driver.obj, method_name)(context) + except Exception: + LOG.exception( + _("Mechanism driver '%(name)s' failed in %(method)s"), + {'name': driver.name, 'method': method_name} + ) + error = True + if not continue_on_failure: + break + if error: + raise ml2_exc.MechanismDriverError( + method=method_name + ) + + def create_network_precommit(self, context): + """Notify all mechanism drivers of a network creation. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver create_network_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("create_network_precommit", context) + + def create_network_postcommit(self, context): + """Notify all mechanism drivers of network creation. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver create_network_postcommit call fails. + + Called after the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propagated + to the caller, where the network will be deleted, triggering + any required cleanup. There is no guarantee that all mechanism + drivers are called in this case. + """ + self._call_on_drivers("create_network_postcommit", context) + + def update_network_precommit(self, context): + """Notify all mechanism drivers of a network update. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver update_network_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("update_network_precommit", context) + + def update_network_postcommit(self, context): + """Notify all mechanism drivers of a network update. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver update_network_postcommit call fails. + + Called after the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propagated + to the caller, where an error is returned to the user. The + user is expected to take the appropriate action, whether by + retrying the call or deleting the network. There is no + guarantee that all mechanism drivers are called in this case. + """ + self._call_on_drivers("update_network_postcommit", context) + + def delete_network_precommit(self, context): + """Notify all mechanism drivers of a network deletion. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver delete_network_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("delete_network_precommit", context) + + def delete_network_postcommit(self, context): + """Notify all mechanism drivers of a network deletion. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver delete_network_postcommit call fails. + + Called after the database transaction. If any mechanism driver + raises an error, then the error is logged but we continue to + call every other mechanism driver. A MechanismDriverError is + then reraised at the end to notify the caller of a failure. In + general we expect the caller to ignore the error, as the + network resource has already been deleted from the database + and it doesn't make sense to undo the action by recreating the + network. + """ + self._call_on_drivers("delete_network_postcommit", context, + continue_on_failure=True) + + def create_port_precommit(self, context): + """Notify all mechanism drivers of a port creation. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver create_port_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("create_port_precommit", context) + + def create_port_postcommit(self, context): + """Notify all mechanism drivers of port creation. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver create_port_postcommit call fails. + + Called after the database transaction. Errors raised by + mechanism drivers are left to propogate to the caller, where + the port will be deleted, triggering any required + cleanup. There is no guarantee that all mechanism drivers are + called in this case. + """ + self._call_on_drivers("create_port_postcommit", context) + + def update_port_precommit(self, context): + """Notify all mechanism drivers of a port update. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver update_port_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("update_port_precommit", context) + + def update_port_postcommit(self, context): + """Notify all mechanism drivers of a port update. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver update_port_postcommit call fails. + + Called after the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propagated + to the caller, where an error is returned to the user. The + user is expected to take the appropriate action, whether by + retrying the call or deleting the port. There is no + guarantee that all mechanism drivers are called in this case. + """ + self._call_on_drivers("update_port_postcommit", context) + + def delete_port_precommit(self, context): + """Notify all mechanism drivers of a port deletion. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver delete_port_precommit call fails. + + Called within the database transaction. If a mechanism driver + raises an exception, then a MechanismDriverError is propogated + to the caller, triggering a rollback. There is no guarantee + that all mechanism drivers are called in this case. + """ + self._call_on_drivers("delete_port_precommit", context) + + def delete_port_postcommit(self, context): + """Notify all mechanism drivers of a port deletion. + + :raises: neutron.plugins.ml2.common.MechanismDriverError + if any mechanism driver delete_port_postcommit call fails. + + Called after the database transaction. If any mechanism driver + raises an error, then the error is logged but we continue to + call every other mechanism driver. A MechanismDriverError is + then reraised at the end to notify the caller of a failure. In + general we expect the caller to ignore the error, as the + port resource has already been deleted from the database + and it doesn't make sense to undo the action by recreating the + port. + """ + self._call_on_drivers("delete_port_postcommit", context, + continue_on_failure=True) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index c30dbff63f..7d10ae9734 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -30,12 +30,15 @@ from neutron.db import quota_db # noqa from neutron.db import securitygroups_rpc_base as sg_db_rpc from neutron.extensions import portbindings from neutron.extensions import providernet as provider +from neutron.openstack.common import excutils from neutron.openstack.common import importutils from neutron.openstack.common import log from neutron.openstack.common import rpc as c_rpc +from neutron.plugins.ml2.common import exceptions as ml2_exc from neutron.plugins.ml2 import config # noqa from neutron.plugins.ml2 import db from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import driver_context from neutron.plugins.ml2 import managers from neutron.plugins.ml2 import rpc @@ -138,7 +141,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, def _extend_network_dict_provider(self, context, network): id = network['id'] - segments = db.get_network_segments(context.session, id) + segments = self.get_network_segments(context, id) if not segments: LOG.error(_("Network %s has no segments"), id) network[provider.NETWORK_TYPE] = None @@ -170,7 +173,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, session = context.session with session.begin(subtransactions=True): network_id = port['network_id'] - segments = db.get_network_segments(session, network_id) + segments = self.get_network_segments(context, network_id) if not segments: LOG.warning(_("In _notify_port_updated() for port %(port_id), " "network %(network_id) has no segments"), @@ -184,6 +187,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, segment[api.SEGMENTATION_ID], segment[api.PHYSICAL_NETWORK]) + # TODO(apech): Need to override bulk operations + def create_network(self, context, network): attrs = network['network'] segment = self._process_provider_create(context, attrs) @@ -203,7 +208,19 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, # to TypeManager. db.add_network_segment(session, id, segment) self._extend_network_dict_provider(context, result) + mech_context = driver_context.NetworkContext(self, + context, + result, + segments=[segment]) + self.mechanism_manager.create_network_precommit(mech_context) + try: + self.mechanism_manager.create_network_postcommit(mech_context) + except ml2_exc.MechanismDriverError: + with excutils.save_and_reraise_exception(): + LOG.error(_("mechanism_manager.create_network failed, " + "deleting network '%s'"), result['id']) + self.delete_network(context, result['id']) return result def update_network(self, context, id, network): @@ -211,12 +228,24 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, session = context.session with session.begin(subtransactions=True): - result = super(Ml2Plugin, self).update_network(context, id, - network) - self._process_l3_update(context, result, network['network']) - self._extend_network_dict_provider(context, result) + original_network = super(Ml2Plugin, self).get_network(context, id) + updated_network = super(Ml2Plugin, self).update_network(context, + id, + network) + self._process_l3_update(context, updated_network, + network['network']) + self._extend_network_dict_provider(context, updated_network) + mech_context = driver_context.NetworkContext( + self, context, updated_network, + original_network=original_network) + self.mechanism_manager.update_network_precommit(mech_context) - return result + # TODO(apech) - handle errors raised by update_network, potentially + # by re-calling update_network with the previous attributes. For + # now the error is propogated to the caller, which is expected to + # either undo/retry the operation or delete the resource. + self.mechanism_manager.update_network_postcommit(mech_context) + return updated_network def get_network(self, context, id, fields=None): session = context.session @@ -241,16 +270,35 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, return [self._fields(net, fields) for net in nets] - def delete_network(self, context, id): + def get_network_segments(self, context, id): session = context.session with session.begin(subtransactions=True): segments = db.get_network_segments(session, id) + return segments + + def delete_network(self, context, id): + session = context.session + with session.begin(subtransactions=True): + network = self.get_network(context, id) + segments = self.get_network_segments(context, id) + mech_context = driver_context.NetworkContext(self, + context, + network, + segments=segments) + self.mechanism_manager.delete_network_precommit(mech_context) super(Ml2Plugin, self).delete_network(context, id) for segment in segments: self.type_manager.release_segment(session, segment) # The segment records are deleted via cascade from the # network record, so explicit removal is not necessary. + try: + self.mechanism_manager.delete_network_postcommit(mech_context) + except ml2_exc.MechanismDriverError: + # TODO(apech) - One or more mechanism driver failed to + # delete the network. Ideally we'd notify the caller of + # the fact that an error occurred. + pass self.notifier.network_delete(context, id) def create_port(self, context, port): @@ -266,7 +314,16 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, result) self._process_port_create_security_group(context, result, sgids) self._extend_port_dict_binding(context, result) + mech_context = driver_context.PortContext(self, context, result) + self.mechanism_manager.create_port_precommit(mech_context) + try: + self.mechanism_manager.create_port_postcommit(mech_context) + except ml2_exc.MechanismDriverError: + with excutils.save_and_reraise_exception(): + LOG.error(_("mechanism_manager.create_port failed, " + "deleting port '%s'"), result['id']) + self.delete_port(context, result['id']) self.notify_security_groups_member_updated(context, result) return result @@ -285,6 +342,16 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, attrs, updated_port) self._extend_port_dict_binding(context, updated_port) + mech_context = driver_context.PortContext( + self, context, updated_port, + original_port=original_port) + self.mechanism_manager.update_port_precommit(mech_context) + + # TODO(apech) - handle errors raised by update_port, potentially + # by re-calling update_port with the previous attributes. For + # now the error is propogated to the caller, which is expected to + # either undo/retry the operation or delete the resource. + self.mechanism_manager.update_port_postcommit(mech_context) need_port_update_notify |= self.is_security_group_member_updated( context, original_port, updated_port) @@ -326,7 +393,16 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, with session.begin(subtransactions=True): self.disassociate_floatingips(context, id) port = self.get_port(context, id) + mech_context = driver_context.PortContext(self, context, port) + self.mechanism_manager.delete_port_precommit(mech_context) self._delete_port_security_group_bindings(context, id) super(Ml2Plugin, self).delete_port(context, id) + try: + self.mechanism_manager.delete_port_postcommit(mech_context) + except ml2_exc.MechanismDriverError: + # TODO(apech) - One or more mechanism driver failed to + # delete the port. Ideally we'd notify the caller of the + # fact that an error occurred. + pass self.notify_security_groups_member_updated(context, port) diff --git a/neutron/tests/unit/ml2/drivers/__init__.py b/neutron/tests/unit/ml2/drivers/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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/neutron/tests/unit/ml2/drivers/mechanism_logger.py b/neutron/tests/unit/ml2/drivers/mechanism_logger.py new file mode 100644 index 0000000000..03b4316e6b --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/mechanism_logger.py @@ -0,0 +1,83 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 neutron.openstack.common import log +from neutron.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + + +class LoggerMechanismDriver(api.MechanismDriver): + """Mechanism driver that logs all calls and parameters made. + + Generally used for testing and debugging. + """ + + def initialize(self): + pass + + def _log_network_call(self, method_name, context): + LOG.info(_("%(method)s called with network settings %(current)s " + "(original settings %(original)s) and " + "network segments %(segments)s"), + {'method': method_name, + 'current': context.current(), + 'original': context.original(), + 'segments': context.network_segments()}) + + def create_network_precommit(self, context): + self._log_network_call("create_network_precommit", context) + + def create_network_postcommit(self, context): + self._log_network_call("create_network_postcommit", context) + + def update_network_precommit(self, context): + self._log_network_call("update_network_precommit", context) + + def update_network_postcommit(self, context): + self._log_network_call("update_network_postcommit", context) + + def delete_network_precommit(self, context): + self._log_network_call("delete_network_precommit", context) + + def delete_network_postcommit(self, context): + self._log_network_call("delete_network_postcommit", context) + + def _log_port_call(self, method_name, context): + network_context = context.network() + LOG.info(_("%(method)s called with port settings %(current)s " + "(original settings %(original)s) on network %(network)s"), + {'method': method_name, + 'current': context.current(), + 'original': context.original(), + 'network': network_context.current()}) + + def create_port_precommit(self, context): + self._log_port_call("create_port_precommit", context) + + def create_port_postcommit(self, context): + self._log_port_call("create_port_postcommit", context) + + def update_port_precommit(self, context): + self._log_port_call("update_port_precommit", context) + + def update_port_postcommit(self, context): + self._log_port_call("update_port_postcommit", context) + + def delete_port_precommit(self, context): + self._log_port_call("delete_port_precommit", context) + + def delete_port_postcommit(self, context): + self._log_port_call("delete_port_postcommit", context) diff --git a/neutron/tests/unit/ml2/drivers/mechanism_test.py b/neutron/tests/unit/ml2/drivers/mechanism_test.py new file mode 100644 index 0000000000..bd49672267 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/mechanism_test.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import driver_context + + +class TestMechanismDriver(api.MechanismDriver): + """Test mechanism driver for testing mechanism driver api.""" + + def initialize(self): + pass + + def _check_network_context(self, context, original_expected): + assert(isinstance(context, driver_context.NetworkContext)) + assert(context.current()) + if original_expected: + assert(context.original()) + else: + assert(not context.original()) + assert(context.network_segments()) + + def create_network_precommit(self, context): + self._check_network_context(context, False) + + def create_network_postcommit(self, context): + self._check_network_context(context, False) + + def update_network_precommit(self, context): + self._check_network_context(context, True) + + def update_network_postcommit(self, context): + self._check_network_context(context, True) + + def delete_network_precommit(self, context): + self._check_network_context(context, False) + + def delete_network_postcommit(self, context): + self._check_network_context(context, False) + + def _check_port_context(self, context, original_expected): + assert(isinstance(context, driver_context.PortContext)) + assert(context.current()) + if original_expected: + assert(context.original()) + else: + assert(not context.original()) + network_context = context.network() + assert(network_context) + self._check_network_context(network_context, False) + + def create_port_precommit(self, context): + self._check_port_context(context, False) + + def create_port_postcommit(self, context): + self._check_port_context(context, False) + + def update_port_precommit(self, context): + self._check_port_context(context, True) + + def update_port_postcommit(self, context): + self._check_port_context(context, True) + + def delete_port_precommit(self, context): + self._check_port_context(context, False) + + def delete_port_postcommit(self, context): + self._check_port_context(context, False) diff --git a/neutron/tests/unit/ml2/test_ml2_plugin.py b/neutron/tests/unit/ml2/test_ml2_plugin.py index 17987b43e2..c5ff52f80d 100644 --- a/neutron/tests/unit/ml2/test_ml2_plugin.py +++ b/neutron/tests/unit/ml2/test_ml2_plugin.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron.plugins.ml2 import config as config from neutron.tests.unit import _test_extension_portbindings as test_bindings from neutron.tests.unit import test_db_plugin as test_plugin @@ -25,6 +26,13 @@ class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): _plugin_name = PLUGIN_NAME def setUp(self): + # Enable the test mechanism driver to ensure that + # we can successfully call through to all mechanism + # driver apis. + config.cfg.CONF.set_override('mechanism_drivers', + ['logger', 'test'], + 'ml2') + self.addCleanup(config.cfg.CONF.reset) super(Ml2PluginV2TestCase, self).setUp(PLUGIN_NAME) self.port_create_status = 'DOWN' diff --git a/setup.cfg b/setup.cfg index 22acb1afe8..70d01d630e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -107,6 +107,9 @@ neutron.ml2.type_drivers = flat = neutron.plugins.ml2.drivers.type_flat:FlatTypeDriver local = neutron.plugins.ml2.drivers.type_local:LocalTypeDriver vlan = neutron.plugins.ml2.drivers.type_vlan:VlanTypeDriver +neutron.ml2.mechanism_drivers = + logger = neutron.tests.unit.ml2.drivers.mechanism_logger:LoggerMechanismDriver + test = neutron.tests.unit.ml2.drivers.mechanism_test:TestMechanismDriver [build_sphinx] all_files = 1