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
This commit is contained in:
Dane LeBlanc 2013-04-10 18:23:37 -04:00
parent f66035c131
commit 8ac8222eaa
8 changed files with 548 additions and 217 deletions

View File

@ -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 NexusComputeHostNotConfigured(exceptions.QuantumException):
"""Connection to compute host is not configured."""
message = _("Connection to %(host)s is not configured.")
class NexusConnectFailed(exceptions.QuantumException):
"""Failed to connect to Nexus switch."""
message = _("Unable to connect to Nexus %(nexus_host)s. Reason: %(exc)s.")
class NexusConfigFailed(exceptions.QuantumException):
"""Failed to configure Nexus switch."""
message = _("Failed to configure Nexus: %(config)s. Reason: %(exc)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 UcsmBindingNotFound(exceptions.QuantumException):
"""Ucsm Binding is not present."""
message = _("Ucsm Binding with ip %(ucsm_ip)s is not present")
class UcsmBindingAlreadyExists(exceptions.QuantumException):
"""Ucsm Binding already exists."""
message = _("Ucsm Binding with ip %(ucsm_ip)s already exists")
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")
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.")

View 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 = {}

View 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
)

View 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."""

View 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

View 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,

View 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']

View File

@ -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('<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):