From 3ba9b9873cec0571c4657af48fed9365050c49cc Mon Sep 17 00:00:00 2001 From: Dane LeBlanc Date: Wed, 10 Apr 2013 18:23:37 -0400 Subject: [PATCH] blueprint cisco-plugin-exception-handling 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 --- .../plugins/cisco/common/cisco_exceptions.py | 75 +---- quantum/plugins/cisco/common/config.py | 10 +- quantum/plugins/cisco/db/nexus_models_v2.py | 8 + .../plugins/cisco/models/virt_phy_sw_v2.py | 166 +++++----- quantum/plugins/cisco/network_plugin.py | 89 +++--- .../nexus/cisco_nexus_network_driver_v2.py | 42 ++- .../cisco/nexus/cisco_nexus_plugin_v2.py | 76 +++-- .../tests/unit/cisco/test_network_plugin.py | 291 +++++++++++++++++- 8 files changed, 544 insertions(+), 213 deletions(-) diff --git a/quantum/plugins/cisco/common/cisco_exceptions.py b/quantum/plugins/cisco/common/cisco_exceptions.py index 99504fcf0..2b27cc23d 100644 --- a/quantum/plugins/cisco/common/cisco_exceptions.py +++ b/quantum/plugins/cisco/common/cisco_exceptions.py @@ -22,18 +22,17 @@ 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.") diff --git a/quantum/plugins/cisco/common/config.py b/quantum/plugins/cisco/common/config.py index a2feccb02..af285a5ab 100644 --- a/quantum/plugins/cisco/common/config.py +++ b/quantum/plugins/cisco/common/config.py @@ -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 = {} diff --git a/quantum/plugins/cisco/db/nexus_models_v2.py b/quantum/plugins/cisco/db/nexus_models_v2.py index 72da1445e..ca3710cce 100644 --- a/quantum/plugins/cisco/db/nexus_models_v2.py +++ b/quantum/plugins/cisco/db/nexus_models_v2.py @@ -41,3 +41,11 @@ class NexusPortBinding(model_base.BASEV2, L2NetworkBase): def __repr__(self): return "" % \ (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 + ) diff --git a/quantum/plugins/cisco/models/virt_phy_sw_v2.py b/quantum/plugins/cisco/models/virt_phy_sw_v2.py index 2938608ea..835d72961 100644 --- a/quantum/plugins/cisco/models/virt_phy_sw_v2.py +++ b/quantum/plugins/cisco/models/virt_phy_sw_v2.py @@ -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.""" diff --git a/quantum/plugins/cisco/network_plugin.py b/quantum/plugins/cisco/network_plugin.py index 187505540..2985be448 100644 --- a/quantum/plugins/cisco/network_plugin.py +++ b/quantum/plugins/cisco/network_plugin.py @@ -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 diff --git a/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py b/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py index 82452742c..b0748f1ef 100644 --- a/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py +++ b/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py @@ -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, diff --git a/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py b/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py index 0cb0ae010..a5873965a 100644 --- a/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py +++ b/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py @@ -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'] diff --git a/quantum/tests/unit/cisco/test_network_plugin.py b/quantum/tests/unit/cisco/test_network_plugin.py index 2491ea7bc..bab52177f 100644 --- a/quantum/tests/unit/cisco/test_network_plugin.py +++ b/quantum/tests/unit/cisco/test_network_plugin.py @@ -13,13 +13,23 @@ # 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('' in last_nexus_cfg) + self.assertTrue('' 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): -- 2.45.2