]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
blueprint cisco-plugin-exception-handling
authorDane LeBlanc <leblancd@cisco.com>
Wed, 10 Apr 2013 22:23:37 +0000 (18:23 -0400)
committerDane LeBlanc <leblancd@cisco.com>
Mon, 6 May 2013 15:59:33 +0000 (11:59 -0400)
Improvements to exception handling in the Cisco plugins. Changes
include:

- Added mapping of Cisco exceptions to HTTP codes
  (extension to FAULT_MAP).
- Removed several unused Cisco exception definitions.
- Added several new Cisco exceptions for fault conditions.
- Added several rollbacks for various sequential operations, e.g.:
  * Create port: Nexus sub-plugin fails after OVS sub-plugin success
  * Create port: Nexus switch conig fails after adding binding to
                 Nexus binding database
  * Delete port: OVS sub-plugin fails after Nexus sub-plugin success
- Delete Port: Reversed order of OVS/Nexus sub-plugin calls so that
  it is done in the reverse order as is done for create port.
- Removed several empty except/raise blocks
- Delete network: Removed call to Nexus sub-plugin delete_network,
  since that is a no-op.
- Removed a block of unused code in model's create_network method.
- Added several unit testcases, including patching of OVS, Cisco
  and Nexus config.
- Remove CISCO_TEST configuration from cisco plugin
  config .ini file.

Change-Id: Iabdf4842aa2f0b090a90e2c565848290832b5197

quantum/plugins/cisco/common/cisco_exceptions.py
quantum/plugins/cisco/common/config.py
quantum/plugins/cisco/db/nexus_models_v2.py
quantum/plugins/cisco/models/virt_phy_sw_v2.py
quantum/plugins/cisco/network_plugin.py
quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py
quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py
quantum/tests/unit/cisco/test_network_plugin.py

index 99504fcf066f3825424490dcaa42aadc5b07bcc8..2b27cc23dc1e3aedf23e814a4ee1e69e22a1f166 100644 (file)
 from quantum.common import exceptions
 
 
+class NetworkSegmentIDNotFound(exceptions.QuantumException):
+    """Segmentation ID for network is not found."""
+    message = _("Segmentation ID for network %(net_id)s is not found.")
+
+
 class NoMoreNics(exceptions.QuantumException):
     """No more dynamic nics are available in the system."""
     message = _("Unable to complete operation. No more dynamic nics are "
                 "available in the system.")
 
 
-class NetworksLimit(exceptions.QuantumException):
-    """Total number of network objects limit has been hit."""
-    message = _("Unable to create new network. Number of networks"
-                "for the system has exceeded the limit")
-
-
 class NetworkVlanBindingAlreadyExists(exceptions.QuantumException):
     """Binding cannot be created, since it already exists."""
     message = _("NetworkVlanBinding for %(vlan_id)s and network "
@@ -56,12 +55,6 @@ class QosNotFound(exceptions.QuantumException):
                 "for tenant %(tenant_id)s")
 
 
-class QoSLevelInvalidDelete(exceptions.QuantumException):
-    """QoS is associated with a port profile, hence cannot be deleted."""
-    message = _("QoS level %(qos_id)s could not be deleted "
-                "for tenant %(tenant_id)s since association exists")
-
-
 class QosNameAlreadyExists(exceptions.QuantumException):
     """QoS Name already exists."""
     message = _("QoS level with name %(qos_name)s already exists "
@@ -86,44 +79,24 @@ class CredentialAlreadyExists(exceptions.QuantumException):
                 "for tenant %(tenant_id)s")
 
 
-class NexusPortBindingNotFound(exceptions.QuantumException):
-    """NexusPort Binding is not present."""
-    message = _("Nexus Port Binding %(port_id)s is not present")
-
-
-class NexusPortBindingAlreadyExists(exceptions.QuantumException):
-    """NexusPort Binding alredy exists."""
-    message = _("Nexus Port Binding %(port_id)s already exists")
-
+class NexusComputeHostNotConfigured(exceptions.QuantumException):
+    """Connection to compute host is not configured."""
+    message = _("Connection to %(host)s is not configured.")
 
-class UcsmBindingNotFound(exceptions.QuantumException):
-    """Ucsm Binding is not present."""
-    message = _("Ucsm Binding with ip %(ucsm_ip)s is not present")
 
+class NexusConnectFailed(exceptions.QuantumException):
+    """Failed to connect to Nexus switch."""
+    message = _("Unable to connect to Nexus %(nexus_host)s. Reason: %(exc)s.")
 
-class UcsmBindingAlreadyExists(exceptions.QuantumException):
-    """Ucsm Binding already exists."""
-    message = _("Ucsm Binding with ip %(ucsm_ip)s already exists")
 
+class NexusConfigFailed(exceptions.QuantumException):
+    """Failed to configure Nexus switch."""
+    message = _("Failed to configure Nexus: %(config)s. Reason: %(exc)s.")
 
-class DynamicVnicNotFound(exceptions.QuantumException):
-    """Ucsm Binding is not present."""
-    message = _("Dyanmic Vnic %(vnic_id)s is not present")
 
-
-class DynamicVnicAlreadyExists(exceptions.QuantumException):
-    """Ucsm Binding already exists."""
-    message = _("Dynamic Vnic with name %(device_name)s already exists")
-
-
-class BladeNotFound(exceptions.QuantumException):
-    """Blade is not present."""
-    message = _("Blade %(blade_id)s is not present")
-
-
-class BladeAlreadyExists(exceptions.QuantumException):
-    """Blade already exists."""
-    message = _("Blade with mgmt_ip %(mgmt_ip)s already exists")
+class NexusPortBindingNotFound(exceptions.QuantumException):
+    """NexusPort Binding is not present."""
+    message = _("Nexus Port Binding %(port_id)s is not present.")
 
 
 class PortVnicBindingAlreadyExists(exceptions.QuantumException):
@@ -134,17 +107,3 @@ class PortVnicBindingAlreadyExists(exceptions.QuantumException):
 class PortVnicNotFound(exceptions.QuantumException):
     """PortVnic Binding is not present."""
     message = _("PortVnic Binding %(port_id)s is not present")
-
-
-class InvalidAttach(exceptions.QuantumException):
-    message = _("Unable to plug the attachment %(att_id)s into port "
-                "%(port_id)s for network %(net_id)s. Association of "
-                "attachment ID with port ID happens implicitly when "
-                "VM is instantiated; attach operation can be "
-                "performed subsequently.")
-
-
-class InvalidDetach(exceptions.QuantumException):
-    message = _("Unable to unplug the attachment %(att_id)s from port "
-                "%(port_id)s for network %(net_id)s. The attachment "
-                "%(att_id)s does not exist.")
index a2feccb028e91d9583219c49ee70b017572032d5..af285a5ab727a8b1e0ccd9824b277f064aa119ed 100644 (file)
@@ -19,12 +19,6 @@ from oslo.config import cfg
 from quantum.agent.common import config
 
 
-cisco_test_opts = [
-    cfg.StrOpt('host',
-               default=None,
-               help=_("Cisco test host option.")),
-]
-
 cisco_plugins_opts = [
     cfg.StrOpt('vswitch_plugin',
                default='quantum.plugins.openvswitch.ovs_quantum_plugin.'
@@ -66,13 +60,12 @@ cisco_opts = [
 
 cfg.CONF.register_opts(cisco_opts, "CISCO")
 cfg.CONF.register_opts(cisco_plugins_opts, "CISCO_PLUGINS")
-cfg.CONF.register_opts(cisco_test_opts, "CISCO_TEST")
 config.register_root_helper(cfg.CONF)
 
 # shortcuts
+CONF = cfg.CONF
 CISCO = cfg.CONF.CISCO
 CISCO_PLUGINS = cfg.CONF.CISCO_PLUGINS
-CISCO_TEST = cfg.CONF.CISCO_TEST
 
 #
 # When populated the nexus_dictionary format is:
@@ -81,6 +74,7 @@ CISCO_TEST = cfg.CONF.CISCO_TEST
 # Example:
 # {('1.1.1.1', 'username'): 'admin',
 #  ('1.1.1.1', 'password'): 'mySecretPassword',
+#  ('1.1.1.1', 'ssh_port'): 22,
 #  ('1.1.1.1', 'compute1'): '1/1', ...}
 #
 nexus_dictionary = {}
index 72da1445eaf5afb6cd50ce252f5d7e43e3818565..ca3710ccecafc5003828813faede31c307404a2b 100644 (file)
@@ -41,3 +41,11 @@ class NexusPortBinding(model_base.BASEV2, L2NetworkBase):
     def __repr__(self):
         return "<NexusPortBinding (%s,%d, %s, %s)>" % \
             (self.port_id, self.vlan_id, self.switch_ip, self.instance_id)
+
+    def __eq__(self, other):
+        return (
+            self.port_id == other.port_id and
+            self.vlan_id == other.vlan_id and
+            self.switch_ip == other.switch_ip and
+            self.instance_id == other.instance_id
+        )
index 2938608eadbdc0149b3c5e457cf1382f8702b0c5..835d72961820fdfbdf54bac5f6558d46ddbc44a4 100644 (file)
@@ -26,10 +26,10 @@ from novaclient.v1_1 import client as nova_client
 from oslo.config import cfg
 
 from quantum.db import api as db_api
-from quantum.manager import QuantumManager
 from quantum.openstack.common import importutils
 from quantum.plugins.cisco.common import cisco_constants as const
 from quantum.plugins.cisco.common import cisco_credentials_v2 as cred
+from quantum.plugins.cisco.common import cisco_exceptions as cexc
 from quantum.plugins.cisco.common import config as conf
 from quantum.plugins.cisco.db import network_db_v2 as cdb
 from quantum.plugins.openvswitch import ovs_db_v2 as odb
@@ -69,7 +69,8 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         for key in conf.CISCO_PLUGINS.keys():
             plugin_obj = conf.CISCO_PLUGINS[key]
             self._plugins[key] = importutils.import_object(plugin_obj)
-            LOG.debug(_("Loaded device plugin %s\n"), conf.CISCO_PLUGINS[key])
+            LOG.debug(_("Loaded device plugin %s\n"),
+                      conf.CISCO_PLUGINS[key])
 
         if ((const.VSWITCH_PLUGIN in self._plugins) and
             hasattr(self._plugins[const.VSWITCH_PLUGIN],
@@ -161,6 +162,8 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
 
     def _get_segmentation_id(self, network_id):
         binding_seg_id = odb.get_network_binding(None, network_id)
+        if not binding_seg_id:
+            raise cexc.NetworkSegmentIDNotFound(net_id=network_id)
         return binding_seg_id.segmentation_id
 
     def _get_all_segmentation_ids(self):
@@ -199,23 +202,11 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         plugins.
         """
         LOG.debug(_("create_network() called"))
-        try:
-            args = [context, network]
-            ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
-                                                        self._func_name(),
-                                                        args)
-            vlan_id = self._get_segmentation_id(ovs_output[0]['id'])
-            if not self._validate_vlan_id(vlan_id):
-                return ovs_output[0]
-            vlan_name = conf.CISCO.vlan_name_prefix + str(vlan_id)
-            vlanids = self._get_all_segmentation_ids()
-            args = [ovs_output[0]['tenant_id'], ovs_output[0]['name'],
-                    ovs_output[0]['id'], vlan_name, vlan_id,
-                    {'vlan_ids': vlanids}]
-            return ovs_output[0]
-        except Exception:
-            # TODO(Sumit): Check if we need to perform any rollback here
-            raise
+        args = [context, network]
+        ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
+                                                    self._func_name(),
+                                                    args)
+        return ovs_output[0]
 
     def update_network(self, context, id, network):
         """Update network.
@@ -228,16 +219,25 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
                                                     self._func_name(),
                                                     args)
-        vlan_id = self._get_segmentation_id(ovs_output[0]['id'])
-        if not self._validate_vlan_id(vlan_id):
-            return ovs_output[0]
-        vlanids = self._get_all_segmentation_ids()
-        args = [ovs_output[0]['tenant_id'], id, {'vlan_id': vlan_id},
-                {'net_admin_state': ovs_output[0]['admin_state_up']},
-                {'vlan_ids': vlanids}]
-        self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
-                                       self._func_name(),
-                                       args)
+        try:
+            vlan_id = self._get_segmentation_id(ovs_output[0]['id'])
+            if not self._validate_vlan_id(vlan_id):
+                return ovs_output[0]
+            vlan_ids = self._get_all_segmentation_ids()
+            args = [ovs_output[0]['tenant_id'], id, {'vlan_id': vlan_id},
+                    {'net_admin_state': ovs_output[0]['admin_state_up']},
+                    {'vlan_ids': vlan_ids}]
+            self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
+                                           self._func_name(),
+                                           args)
+        except Exception:
+            # TODO(dane): The call to the nexus plugin update network
+            # failed, so the OVS plugin should be rolled back, that is,
+            # "re-updated" back to the original network config.
+            LOG.exception(_("Unable to update network '%s' on Nexus switch"),
+                          network['network']['name'])
+            raise
+
         return ovs_output[0]
 
     def delete_network(self, context, id):
@@ -246,24 +246,11 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         Perform this operation in the context of the configured device
         plugins.
         """
-        try:
-            base_plugin_ref = QuantumManager.get_plugin()
-            n = base_plugin_ref.get_network(context, id)
-            tenant_id = n['tenant_id']
-            vlan_id = self._get_segmentation_id(id)
-            args = [context, id]
-            ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
-                                                        self._func_name(),
-                                                        args)
-            args = [tenant_id, id, {const.VLANID: vlan_id},
-                    {const.CONTEXT: context},
-                    {const.BASE_PLUGIN_REF: base_plugin_ref}]
-            if self._validate_vlan_id(vlan_id):
-                self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
-                                               self._func_name(), args)
-            return ovs_output[0]
-        except Exception:
-            raise
+        args = [context, id]
+        ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
+                                                    self._func_name(),
+                                                    args)
+        return ovs_output[0]
 
     def get_network(self, context, id, fields=None):
         """For this model this method will be delegated to vswitch plugin."""
@@ -292,6 +279,10 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
 
         return nexus_output
 
+    @staticmethod
+    def _should_call_create_net(device_owner, instance_id):
+        return (instance_id and device_owner != 'network:dhcp')
+
     def create_port(self, context, port):
         """Create port.
 
@@ -299,28 +290,34 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         plugins.
         """
         LOG.debug(_("create_port() called"))
+        args = [context, port]
+        ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
+                                                    self._func_name(),
+                                                    args)
         try:
-            args = [context, port]
-            ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
-                                                        self._func_name(),
-                                                        args)
-
             instance_id = port['port']['device_id']
             device_owner = port['port']['device_owner']
 
-            create_net = (conf.CISCO_TEST.host is None and
-                          device_owner != 'network:dhcp' and
-                          instance_id)
-            if create_net:
+            if self._should_call_create_net(device_owner, instance_id):
                 net_id = port['port']['network_id']
                 tenant_id = port['port']['tenant_id']
                 self._invoke_nexus_for_net_create(
                     context, tenant_id, net_id, instance_id)
 
-            return ovs_output[0]
-        except Exception:
-            # TODO(asomya): Check if we need to perform any rollback here
-            raise
+        except Exception as e:
+            # Create network on the Nexus plugin has failed, so we need
+            # to rollback the port creation on the VSwitch plugin.
+            try:
+                id = ovs_output[0]['id']
+                args = [context, id]
+                ovs_output = self._invoke_plugin_per_device(
+                    const.VSWITCH_PLUGIN,
+                    'delete_port',
+                    args)
+            finally:
+                # Re-raise the original exception
+                raise e
+        return ovs_output[0]
 
     def get_port(self, context, id, fields=None):
         """For this model this method will be delegated to vswitch plugin."""
@@ -337,16 +334,13 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         plugins.
         """
         LOG.debug(_("update_port() called"))
+        old_port = self.get_port(context, id)
+        old_device = old_port['device_id']
+        args = [context, id, port]
+        ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
+                                                    self._func_name(),
+                                                    args)
         try:
-            # Get port
-            old_port = self.get_port(context, id)
-            # Check old port device_id
-            old_device = old_port['device_id']
-            # Update port with vswitch plugin
-            args = [context, id, port]
-            ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
-                                                        self._func_name(),
-                                                        args)
             net_id = old_port['network_id']
             instance_id = ''
             if 'device_id' in port['port']:
@@ -360,6 +354,11 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
 
             return ovs_output[0]
         except Exception:
+            # TODO(dane): The call to the nexus plugin create network
+            # failed, so the OVS plugin should be rolled back, that is,
+            # "re-updated" back to the original port config.
+            LOG.exception(_("Unable to update port '%s' on Nexus switch"),
+                          port['port']['name'])
             raise
 
     def delete_port(self, context, id):
@@ -369,21 +368,30 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         plugins.
         """
         LOG.debug(_("delete_port() called"))
+        port = self.get_port(context, id)
+        vlan_id = self._get_segmentation_id(port['network_id'])
+        n_args = [port['device_id'], vlan_id]
+        self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
+                                       self._func_name(),
+                                       n_args)
         try:
             args = [context, id]
-            port = self.get_port(context, id)
-            vlan_id = self._get_segmentation_id(port['network_id'])
-            n_args = [port['device_id'], vlan_id]
             ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
                                                         self._func_name(),
                                                         args)
-            self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
-                                           self._func_name(),
-                                           n_args)
-            return ovs_output[0]
-        except Exception:
-            # TODO(asomya): Check if we need to perform any rollback here
-            raise
+        except Exception as e:
+            # Roll back the delete port on the Nexus plugin
+            try:
+                tenant_id = port['tenant_id']
+                net_id = port['network_id']
+                instance_id = port['device_id']
+                self._invoke_nexus_for_net_create(context, tenant_id,
+                                                  net_id, instance_id)
+            finally:
+                # Raise the original exception.
+                raise e
+
+        return ovs_output[0]
 
     def create_subnet(self, context, subnet):
         """For this model this method will be delegated to vswitch plugin."""
index 18750554028a6429da2a64e82256c3121e3eb08c..2985be448cae246ec0778d0c908117bc27177a23 100644 (file)
@@ -20,7 +20,9 @@ import inspect
 import logging
 
 from sqlalchemy import orm
+import webob.exc as wexc
 
+from quantum.api.v2 import base
 from quantum.common import exceptions as exc
 from quantum.db import db_base_plugin_v2
 from quantum.db import models_v2
@@ -47,6 +49,24 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
                             'get_subnet', 'get_subnets', ]
     _master = True
 
+    CISCO_FAULT_MAP = {
+        cexc.NetworkSegmentIDNotFound: wexc.HTTPNotFound,
+        cexc.NoMoreNics: wexc.HTTPBadRequest,
+        cexc.NetworkVlanBindingAlreadyExists: wexc.HTTPBadRequest,
+        cexc.VlanIDNotFound: wexc.HTTPNotFound,
+        cexc.VlanIDNotAvailable: wexc.HTTPNotFound,
+        cexc.QosNotFound: wexc.HTTPNotFound,
+        cexc.QosNameAlreadyExists: wexc.HTTPBadRequest,
+        cexc.CredentialNotFound: wexc.HTTPNotFound,
+        cexc.CredentialNameNotFound: wexc.HTTPNotFound,
+        cexc.CredentialAlreadyExists: wexc.HTTPBadRequest,
+        cexc.NexusComputeHostNotConfigured: wexc.HTTPNotFound,
+        cexc.NexusConnectFailed: wexc.HTTPServiceUnavailable,
+        cexc.NexusConfigFailed: wexc.HTTPBadRequest,
+        cexc.NexusPortBindingNotFound: wexc.HTTPNotFound,
+        cexc.PortVnicBindingAlreadyExists: wexc.HTTPBadRequest,
+        cexc.PortVnicNotFound: wexc.HTTPNotFound}
+
     def __init__(self):
         """Load the model class."""
         self._model = importutils.import_object(config.CISCO.model_class)
@@ -62,6 +82,9 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
             self.supported_extension_aliases.extend(
                 self._model.supported_extension_aliases)
 
+        # Extend the fault map
+        self._extend_fault_map()
+
         LOG.debug(_("Plugin initialization complete"))
 
     def __getattribute__(self, name):
@@ -94,6 +117,15 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
             raise AttributeError("'%s' object has no attribute '%s'" %
                                  (self._model, name))
 
+    def _extend_fault_map(self):
+        """Extend the Quantum Fault Map for Cisco exceptions.
+
+        Map exceptions which are specific to the Cisco Plugin
+        to standard HTTP exceptions.
+
+        """
+        base.FAULT_MAP.update(self.CISCO_FAULT_MAP)
+
     """
     Core API implementation
     """
@@ -143,15 +175,12 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
                 raise exc.NetworkInUse(net_id=id)
         context.session.close()
         #Network does not have any ports, we can proceed to delete
-        try:
-            network = self._get_network(context, id)
-            kwargs = {const.NETWORK: network,
-                      const.BASE_PLUGIN_REF: self}
-            self._invoke_device_plugins(self._func_name(), [context, id,
-                                                            kwargs])
-            return super(PluginV2, self).delete_network(context, id)
-        except Exception:
-            raise
+        network = self._get_network(context, id)
+        kwargs = {const.NETWORK: network,
+                  const.BASE_PLUGIN_REF: self}
+        self._invoke_device_plugins(self._func_name(), [context, id,
+                                                        kwargs])
+        return super(PluginV2, self).delete_network(context, id)
 
     def get_network(self, context, id, fields=None):
         """Get a particular network."""
@@ -186,24 +215,18 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
         #    raise exc.PortInUse(port_id=id, net_id=port['network_id'],
         #                        att_id=port['device_id'])
         """
-        try:
-            kwargs = {const.PORT: port}
-            # TODO(Sumit): Might first need to check here if port is active
-            self._invoke_device_plugins(self._func_name(), [context, id,
-                                                            kwargs])
-            return super(PluginV2, self).delete_port(context, id)
-        except Exception:
-            raise
+        kwargs = {const.PORT: port}
+        # TODO(Sumit): Might first need to check here if port is active
+        self._invoke_device_plugins(self._func_name(), [context, id,
+                                                        kwargs])
+        return super(PluginV2, self).delete_port(context, id)
 
     def update_port(self, context, id, port):
         """Update the state of a port and return the updated port."""
         LOG.debug(_("update_port() called"))
-        try:
-            self._invoke_device_plugins(self._func_name(), [context, id,
-                                                            port])
-            return super(PluginV2, self).update_port(context, id, port)
-        except Exception:
-            raise
+        self._invoke_device_plugins(self._func_name(), [context, id,
+                                                        port])
+        return super(PluginV2, self).update_port(context, id, port)
 
     def create_subnet(self, context, subnet):
         """Create subnet.
@@ -224,12 +247,9 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
     def update_subnet(self, context, id, subnet):
         """Updates the state of a subnet and returns the updated subnet."""
         LOG.debug(_("update_subnet() called"))
-        try:
-            self._invoke_device_plugins(self._func_name(), [context, id,
-                                                            subnet])
-            return super(PluginV2, self).update_subnet(context, id, subnet)
-        except Exception:
-            raise
+        self._invoke_device_plugins(self._func_name(), [context, id,
+                                                        subnet])
+        return super(PluginV2, self).update_subnet(context, id, subnet)
 
     def delete_subnet(self, context, id):
         LOG.debug(_("delete_subnet() called"))
@@ -245,13 +265,10 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2):
                        for a in allocated):
                 raise exc.SubnetInUse(subnet_id=id)
         context.session.close()
-        try:
-            kwargs = {const.SUBNET: subnet}
-            self._invoke_device_plugins(self._func_name(), [context, id,
-                                                            kwargs])
-            return super(PluginV2, self).delete_subnet(context, id)
-        except Exception:
-            raise
+        kwargs = {const.SUBNET: subnet}
+        self._invoke_device_plugins(self._func_name(), [context, id,
+                                                        kwargs])
+        return super(PluginV2, self).delete_subnet(context, id)
 
     """
     Extension API implementation
index 82452742c70b14bb805f3ccaca99e4a194eb4a9d..b0748f1ef7ba6390c6f39b4dc441be378417f7be 100644 (file)
@@ -25,10 +25,10 @@ import logging
 
 from ncclient import manager
 
+from quantum.plugins.cisco.common import cisco_exceptions as cexc
 from quantum.plugins.cisco.db import network_db_v2 as cdb
 from quantum.plugins.cisco.nexus import cisco_nexus_snippets as snipp
 
-
 LOG = logging.getLogger(__name__)
 
 
@@ -37,11 +37,35 @@ class CiscoNEXUSDriver():
     def __init__(self):
         pass
 
+    def _edit_config(self, mgr, target='running', config=''):
+        """Modify switch config for a target config type.
+
+        :param mgr: NetConf client manager
+        :param target: Target config type
+        :param config: Configuration string in XML format
+
+        :raises: NexusConfigFailed
+
+        """
+        try:
+            mgr.edit_config(target, config=config)
+        except Exception as e:
+            # Raise a Quantum exception. Include a description of
+            # the original ncclient exception.
+            raise cexc.NexusConfigFailed(config=config, exc=e)
+
     def nxos_connect(self, nexus_host, nexus_ssh_port, nexus_user,
                      nexus_password):
         """Make SSH connection to the Nexus Switch."""
-        man = manager.connect(host=nexus_host, port=nexus_ssh_port,
-                              username=nexus_user, password=nexus_password)
+        try:
+            man = manager.connect(host=nexus_host, port=nexus_ssh_port,
+                                  username=nexus_user,
+                                  password=nexus_password)
+        except Exception as e:
+            # Raise a Quantum exception. Include a description of
+            # the original ncclient exception.
+            raise cexc.NexusConnectFailed(nexus_host=nexus_host, exc=e)
+
         return man
 
     def create_xml_snippet(self, cutomized_config):
@@ -56,27 +80,27 @@ class CiscoNEXUSDriver():
         """Creates a VLAN on Nexus Switch given the VLAN ID and Name."""
         confstr = snipp.CMD_VLAN_CONF_SNIPPET % (vlanid, vlanname)
         confstr = self.create_xml_snippet(confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def disable_vlan(self, mgr, vlanid):
         """Delete a VLAN on Nexus Switch given the VLAN ID."""
         confstr = snipp.CMD_NO_VLAN_CONF_SNIPPET % vlanid
         confstr = self.create_xml_snippet(confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def enable_port_trunk(self, mgr, interface):
         """Enable trunk mode an interface on Nexus Switch."""
         confstr = snipp.CMD_PORT_TRUNK % (interface)
         confstr = self.create_xml_snippet(confstr)
         LOG.debug(_("NexusDriver: %s"), confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def disable_switch_port(self, mgr, interface):
         """Disable trunk mode an interface on Nexus Switch."""
         confstr = snipp.CMD_NO_SWITCHPORT % (interface)
         confstr = self.create_xml_snippet(confstr)
         LOG.debug(_("NexusDriver: %s"), confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def enable_vlan_on_trunk_int(self, mgr, interface, vlanid):
         """Enable vlan in trunk interface.
@@ -87,7 +111,7 @@ class CiscoNEXUSDriver():
         confstr = snipp.CMD_VLAN_INT_SNIPPET % (interface, vlanid)
         confstr = self.create_xml_snippet(confstr)
         LOG.debug(_("NexusDriver: %s"), confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def disable_vlan_on_trunk_int(self, mgr, interface, vlanid):
         """Disable VLAN.
@@ -98,7 +122,7 @@ class CiscoNEXUSDriver():
         confstr = snipp.CMD_NO_VLAN_INT_SNIPPET % (interface, vlanid)
         confstr = self.create_xml_snippet(confstr)
         LOG.debug(_("NexusDriver: %s"), confstr)
-        mgr.edit_config(target='running', config=confstr)
+        self._edit_config(mgr, target='running', config=confstr)
 
     def create_vlan(self, vlan_name, vlan_id, nexus_host, nexus_user,
                     nexus_password, nexus_ports,
index 0cb0ae010bfec7ba3ea0e2052487528e9300dc29..a5873965aa2aba1e6b171f378c40ad49284e8294 100644 (file)
@@ -30,6 +30,7 @@ from quantum.common import exceptions as exc
 from quantum.openstack.common import importutils
 from quantum.plugins.cisco.common import cisco_constants as const
 from quantum.plugins.cisco.common import cisco_credentials_v2 as cred
+from quantum.plugins.cisco.common import cisco_exceptions as cisco_exc
 from quantum.plugins.cisco.common import config as conf
 from quantum.plugins.cisco.db import network_db_v2 as cdb
 from quantum.plugins.cisco.db import nexus_db_v2 as nxos_db
@@ -78,16 +79,18 @@ class NexusPlugin(L2DevicePluginBase):
         """
         LOG.debug(_("NexusPlugin:create_network() called"))
         # Grab the switch IP and port for this host
-        switch_ip = ''
-        port_id = ''
-        for keys in self._nexus_switches.keys():
-            if str(keys[1]) == str(host):
-                switch_ip = keys[0]
-                port_id = self._nexus_switches[keys[0], keys[1]]
+        for switch_ip, attr in self._nexus_switches:
+            if str(attr) == str(host):
+                port_id = self._nexus_switches[switch_ip, attr]
+                break
+        else:
+            raise cisco_exc.NexusComputeHostNotConfigured(host=host)
 
         # Check if this network is already in the DB
         binding = nxos_db.get_port_vlan_switch_binding(
             port_id, vlan_id, switch_ip)
+        vlan_created = False
+        vlan_enabled = False
         if not binding:
             _nexus_ip = switch_ip
             _nexus_ports = (port_id,)
@@ -104,6 +107,7 @@ class NexusPlugin(L2DevicePluginBase):
                     vlan_name, str(vlan_id), _nexus_ip,
                     _nexus_username, _nexus_password,
                     _nexus_ports, _nexus_ssh_port, vlan_id)
+                vlan_created = True
             else:
                 # Only trunk vlan on the port
                 man = self._client.nxos_connect(_nexus_ip,
@@ -113,9 +117,27 @@ class NexusPlugin(L2DevicePluginBase):
                 self._client.enable_vlan_on_trunk_int(man,
                                                       port_id,
                                                       vlan_id)
+                vlan_enabled = True
+
+        try:
+            nxos_db.add_nexusport_binding(port_id, str(vlan_id),
+                                          switch_ip, instance)
+        except Exception as e:
+            try:
+                # Add binding failed, roll back any vlan creation/enabling
+                if vlan_created:
+                    self._client.delete_vlan(
+                        str(vlan_id), _nexus_ip,
+                        _nexus_username, _nexus_password,
+                        _nexus_ports, _nexus_ssh_port)
+                if vlan_enabled:
+                    self._client.disable_vlan_on_trunk_int(man,
+                                                           port_id,
+                                                           vlan_id)
+            finally:
+                # Raise the original exception
+                raise e
 
-        nxos_db.add_nexusport_binding(port_id, str(vlan_id),
-                                      switch_ip, instance)
         new_net_dict = {const.NET_ID: net_id,
                         const.NET_NAME: net_name,
                         const.NET_PORTS: {},
@@ -176,18 +198,32 @@ class NexusPlugin(L2DevicePluginBase):
                 row['vlan_id'], row['switch_ip'])
 
             if not bindings:
-                # Delete this vlan from this switch
-                _nexus_ip = row['switch_ip']
-                _nexus_ports = (row['port_id'],)
-                _nexus_ssh_port = \
-                    self._nexus_switches[_nexus_ip, 'ssh_port']
-                _nexus_creds = self.get_credential(_nexus_ip)
-                _nexus_username = _nexus_creds['username']
-                _nexus_password = _nexus_creds['password']
-                self._client.delete_vlan(
-                    str(row['vlan_id']), _nexus_ip,
-                    _nexus_username, _nexus_password,
-                    _nexus_ports, _nexus_ssh_port)
+                try:
+                    # Delete this vlan from this switch
+                    _nexus_ip = row['switch_ip']
+                    _nexus_ports = (row['port_id'],)
+                    _nexus_ssh_port = (self._nexus_switches[_nexus_ip,
+                                                            'ssh_port'])
+                    _nexus_creds = self.get_credential(_nexus_ip)
+                    _nexus_username = _nexus_creds['username']
+                    _nexus_password = _nexus_creds['password']
+                    self._client.delete_vlan(
+                        str(row['vlan_id']), _nexus_ip,
+                        _nexus_username, _nexus_password,
+                        _nexus_ports, _nexus_ssh_port)
+                except Exception as e:
+                    # The delete vlan operation on the Nexus failed,
+                    # so this delete_port request has failed. For
+                    # consistency, roll back the Nexus database to what
+                    # it was before this request.
+                    try:
+                        nxos_db.add_nexusport_binding(row['port_id'],
+                                                      row['vlan_id'],
+                                                      row['switch_ip'],
+                                                      row['instance_id'])
+                    finally:
+                        # Raise the original exception
+                        raise e
 
             return row['instance_id']
 
index 2491ea7bcfd3288e339929abae8449fedc320343..bab52177f5c524e249e11ff8a2ecdf1f961ca8a4 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import contextlib
+import inspect
 import logging
 import mock
 
+from quantum.api.v2 import base
+from quantum.common import exceptions as q_exc
 from quantum import context
+from quantum.db import l3_db
 from quantum.manager import QuantumManager
 from quantum.plugins.cisco.common import cisco_constants as const
-from quantum.plugins.cisco.db import network_db_v2  # noqa
+from quantum.plugins.cisco.common import cisco_exceptions as c_exc
+from quantum.plugins.cisco.common import config as cisco_config
+from quantum.plugins.cisco.db import nexus_db_v2
+from quantum.plugins.cisco.models import virt_phy_sw_v2
+from quantum.plugins.openvswitch.common import config as ovs_config
+from quantum.plugins.openvswitch import ovs_db_v2
 from quantum.tests.unit import test_db_plugin
 
 LOG = logging.getLogger(__name__)
@@ -31,9 +41,9 @@ class CiscoNetworkPluginV2TestCase(test_db_plugin.QuantumDbPluginV2TestCase):
 
     def setUp(self):
         # Use a mock netconf client
-        mock_ncclient = mock.Mock()
+        self.mock_ncclient = mock.Mock()
         self.patch_obj = mock.patch.dict('sys.modules',
-                                         {'ncclient': mock_ncclient})
+                                         {'ncclient': self.mock_ncclient})
         self.patch_obj.start()
 
         super(CiscoNetworkPluginV2TestCase, self).setUp(self._plugin_name)
@@ -65,6 +75,124 @@ class TestCiscoV2HTTPResponse(CiscoNetworkPluginV2TestCase,
 class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
                        test_db_plugin.TestPortsV2):
 
+    def setUp(self):
+        """Configure for end-to-end quantum testing using a mock ncclient.
+
+        This setup includes:
+        - Configure the OVS plugin to use VLANs in the range of 1000-1100.
+        - Configure the Cisco plugin model to use the real Nexus driver.
+        - Configure the Nexus sub-plugin to use an imaginary switch
+          at 1.1.1.1.
+
+        """
+        self.addCleanup(mock.patch.stopall)
+
+        self.vlan_start = 1000
+        self.vlan_end = 1100
+        range_str = 'physnet1:%d:%d' % (self.vlan_start,
+                                        self.vlan_end)
+        nexus_driver = ('quantum.plugins.cisco.nexus.'
+                        'cisco_nexus_network_driver_v2.CiscoNEXUSDriver')
+
+        config = {
+            ovs_config: {
+                'OVS': {'bridge_mappings': 'physnet1:br-eth1',
+                        'network_vlan_ranges': [range_str],
+                        'tenant_network_type': 'vlan'}
+            },
+            cisco_config: {
+                'CISCO': {'nexus_driver': nexus_driver},
+            }
+        }
+
+        for module in config:
+            for group in config[module]:
+                for opt in config[module][group]:
+                    module.cfg.CONF.set_override(opt,
+                                                 config[module][group][opt],
+                                                 group)
+            self.addCleanup(module.cfg.CONF.reset)
+
+        self.switch_ip = '1.1.1.1'
+        nexus_config = {(self.switch_ip, 'username'): 'admin',
+                        (self.switch_ip, 'password'): 'mySecretPassword',
+                        (self.switch_ip, 'ssh_port'): 22,
+                        (self.switch_ip, 'testhost'): '1/1'}
+        mock.patch.dict(cisco_config.nexus_dictionary, nexus_config).start()
+
+        patches = {
+            '_should_call_create_net': True,
+            '_get_instance_host': 'testhost'
+        }
+        for func in patches:
+            mock_sw = mock.patch.object(
+                virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
+                func).start()
+            mock_sw.return_value = patches[func]
+
+        super(TestCiscoPortsV2, self).setUp()
+
+    @contextlib.contextmanager
+    def _patch_ncclient(self, attr, value):
+        """Configure an attribute on the mock ncclient module.
+
+        This method can be used to inject errors by setting a side effect
+        or a return value for an ncclient method.
+
+        :param attr: ncclient attribute (typically method) to be configured.
+        :param value: Value to be configured on the attribute.
+
+        """
+        # Configure attribute.
+        config = {attr: value}
+        self.mock_ncclient.configure_mock(**config)
+        # Continue testing
+        yield
+        # Unconfigure attribute
+        config = {attr: None}
+        self.mock_ncclient.configure_mock(**config)
+
+    @contextlib.contextmanager
+    def _create_port_res(self, fmt=None, no_delete=False,
+                         **kwargs):
+        """Create a network, subnet, and port and yield the result.
+
+        Create a network, subnet, and port, yield the result,
+        then delete the port, subnet, and network.
+
+        :param fmt: Format to be used for API requests.
+        :param no_delete: If set to True, don't delete the port at the
+                          end of testing.
+        :param kwargs: Keyword args to be passed to self._create_port.
+
+        """
+        with self.subnet() as subnet:
+            net_id = subnet['subnet']['network_id']
+            res = self._create_port(fmt, net_id, **kwargs)
+            port = self.deserialize(fmt, res)
+            try:
+                yield res
+            finally:
+                if not no_delete:
+                    self._delete('ports', port['port']['id'])
+
+    def _assertExpectedHTTP(self, status, exc):
+        """Confirm that an HTTP status corresponds to an expected exception.
+
+        Confirm that an HTTP status which has been returned for an
+        quantum API request matches the HTTP status corresponding
+        to an expected exception.
+
+        :param status: HTTP status
+        :param exc: Expected exception
+
+        """
+        if exc in base.FAULT_MAP:
+            expected_http = base.FAULT_MAP[exc].code
+        else:
+            expected_http = 500
+        self.assertEqual(status, expected_http)
+
     def test_create_ports_bulk_emulated_plugin_failure(self):
         real_has_attr = hasattr
 
@@ -114,6 +242,163 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
                 # We expect a 500 as we injected a fault in the plugin
                 self._validate_behavior_on_bulk_failure(res, 'ports', 500)
 
+    def test_nexus_connect_fail(self):
+        """Test failure to connect to a Nexus switch.
+
+        While creating a network, subnet, and port, simulate a connection
+        failure to a nexus switch. Confirm that the expected HTTP code
+        is returned for the create port operation.
+
+        """
+        with self._patch_ncclient('manager.connect.side_effect',
+                                  AttributeError):
+            with self._create_port_res(self.fmt, no_delete=True,
+                                       name='myname') as res:
+                self._assertExpectedHTTP(res.status_int,
+                                         c_exc.NexusConnectFailed)
+
+    def test_nexus_config_fail(self):
+        """Test a Nexus switch configuration failure.
+
+        While creating a network, subnet, and port, simulate a nexus
+        switch configuration error. Confirm that the expected HTTP code
+        is returned for the create port operation.
+
+        """
+        with self._patch_ncclient(
+            'manager.connect.return_value.edit_config.side_effect',
+            AttributeError):
+            with self._create_port_res(self.fmt, no_delete=True,
+                                       name='myname') as res:
+                self._assertExpectedHTTP(res.status_int,
+                                         c_exc.NexusConfigFailed)
+
+    def test_get_seg_id_fail(self):
+        """Test handling of a NetworkSegmentIDNotFound exception.
+
+        Test the Cisco NetworkSegmentIDNotFound exception by simulating
+        a return of None by the OVS DB get_network_binding method
+        during port creation.
+
+        """
+        orig = ovs_db_v2.get_network_binding
+
+        def _return_none_if_nexus_caller(self, *args, **kwargs):
+            def _calling_func_name(offset=0):
+                """Get name of the calling function 'offset' frames back."""
+                return inspect.stack()[1 + offset][3]
+            if (_calling_func_name(1) == '_get_segmentation_id' and
+                _calling_func_name(2) == '_invoke_nexus_for_net_create'):
+                return None
+            else:
+                return orig(self, *args, **kwargs)
+
+        with mock.patch.object(ovs_db_v2, 'get_network_binding',
+                               new=_return_none_if_nexus_caller):
+            with self._create_port_res(self.fmt, no_delete=True,
+                                       name='myname') as res:
+                self._assertExpectedHTTP(res.status_int,
+                                         c_exc.NetworkSegmentIDNotFound)
+
+    def test_nexus_host_non_configured(self):
+        """Test handling of a NexusComputeHostNotConfigured exception.
+
+        Test the Cisco NexusComputeHostNotConfigured exception by using
+        a fictitious host name during port creation.
+
+        """
+        with mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
+                               '_get_instance_host') as mock_get_instance:
+            mock_get_instance.return_value = 'fictitious_host'
+            with self._create_port_res(self.fmt, no_delete=True,
+                                       name='myname') as res:
+                self._assertExpectedHTTP(res.status_int,
+                                         c_exc.NexusComputeHostNotConfigured)
+
+    def test_nexus_bind_fail_rollback(self):
+        """Test for proper rollback following add Nexus DB binding failure.
+
+        Test that the Cisco Nexus plugin correctly rolls back the vlan
+        configuration on the Nexus switch when add_nexusport_binding fails
+        within the plugin's create_port() method.
+
+        """
+        with mock.patch.object(nexus_db_v2, 'add_nexusport_binding',
+                               side_effect=KeyError):
+            with self._create_port_res(self.fmt, no_delete=True,
+                                       name='myname') as res:
+                # Confirm that the last configuration sent to the Nexus
+                # switch was a removal of vlan from the test interface.
+                last_nexus_cfg = (self.mock_ncclient.manager.connect().
+                                  edit_config.mock_calls[-1][2]['config'])
+                self.assertTrue('<vlan>' in last_nexus_cfg)
+                self.assertTrue('<remove>' in last_nexus_cfg)
+                self._assertExpectedHTTP(res.status_int, KeyError)
+
+    def test_model_delete_port_rollback(self):
+        """Test for proper rollback for OVS plugin delete port failure.
+
+        Test that the nexus port configuration is rolled back (restored)
+        by the Cisco model plugin when there is a failure in the OVS
+        plugin for a delete port operation.
+
+        """
+        with self._create_port_res(self.fmt, name='myname') as res:
+
+            # After port is created, we should have one binding for this
+            # vlan/nexus switch.
+            port = self.deserialize(self.fmt, res)
+            start_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start,
+                                                           self.switch_ip)
+            self.assertEqual(len(start_rows), 1)
+
+            # Inject an exception in the OVS plugin delete_port
+            # processing, and attempt a port deletion.
+            inserted_exc = q_exc.Conflict
+            expected_http = base.FAULT_MAP[inserted_exc].code
+            with mock.patch.object(l3_db.L3_NAT_db_mixin,
+                                   'disassociate_floatingips',
+                                   side_effect=inserted_exc):
+                self._delete('ports', port['port']['id'],
+                             expected_code=expected_http)
+
+            # Confirm that the Cisco model plugin has restored
+            # the nexus configuration for this port after deletion failure.
+            end_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start,
+                                                         self.switch_ip)
+            self.assertEqual(start_rows, end_rows)
+
+    def test_nexus_delete_port_rollback(self):
+        """Test for proper rollback for nexus plugin delete port failure.
+
+        Test for rollback (i.e. restoration) of a VLAN entry in the
+        nexus database whenever the nexus plugin fails to reconfigure the
+        nexus switch during a delete_port operation.
+
+        """
+        with self._create_port_res(self.fmt, name='myname') as res:
+
+            port = self.deserialize(self.fmt, res)
+
+            # Check that there is only one binding in the nexus database
+            # for this VLAN/nexus switch.
+            start_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start,
+                                                           self.switch_ip)
+            self.assertEqual(len(start_rows), 1)
+
+            # Simulate a Nexus switch configuration error during
+            # port deletion.
+            with self._patch_ncclient(
+                'manager.connect.return_value.edit_config.side_effect',
+                AttributeError):
+                self._delete('ports', port['port']['id'],
+                             base.FAULT_MAP[c_exc.NexusConfigFailed].code)
+
+            # Confirm that the binding has been restored (rolled back).
+            end_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start,
+                                                         self.switch_ip)
+            self.assertEqual(start_rows, end_rows)
+
 
 class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
                           test_db_plugin.TestNetworksV2):