Merge "Fix Cisco Nexus plugin failures for vlan IDs 1006-4094" into stable/grizzly

This commit is contained in:
Jenkins 2013-08-02 23:12:38 +00:00 committed by Gerrit Code Review
commit 1d3fee1149
4 changed files with 213 additions and 26 deletions

View File

@ -88,6 +88,11 @@ class CredentialAlreadyExists(exceptions.QuantumException):
"for tenant %(tenant_id)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")

View File

@ -26,6 +26,7 @@ import logging
from ncclient import manager
from quantum.plugins.cisco.common import cisco_exceptions
from quantum.plugins.cisco.db import network_db_v2 as cdb
from quantum.plugins.cisco.db import nexus_db_v2
from quantum.plugins.cisco.nexus import cisco_nexus_snippets as snipp
@ -41,6 +42,33 @@ class CiscoNEXUSDriver():
def __init__(self):
pass
def _edit_config(self, mgr, target='running', config='',
allowed_exc_strs=None):
"""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
:param allowed_exc_strs: Exceptions which have any of these strings
as a subset of their exception message
(str(exception)) can be ignored
:raises: NexusConfigFailed
"""
if not allowed_exc_strs:
allowed_exc_strs = []
try:
mgr.edit_config(target, config=config)
except Exception as e:
for exc_str in allowed_exc_strs:
if exc_str in str(e):
break
else:
# Raise a Quantum exception. Include a description of
# the original ncclient exception.
raise cisco_exceptions.NexusConfigFailed(config=config, exc=e)
def nxos_connect(self, nexus_host, nexus_ssh_port, nexus_user,
nexus_password):
"""
@ -58,12 +86,33 @@ class CiscoNEXUSDriver():
return conf_xml_snippet
def enable_vlan(self, mgr, vlanid, vlanname):
"""
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)
"""Create a VLAN on Nexus Switch given the VLAN ID and Name."""
confstr = self.create_xml_snippet(
snipp.CMD_VLAN_CONF_SNIPPET % (vlanid, vlanname))
self._edit_config(mgr, target='running', config=confstr)
# Enable VLAN active and no-shutdown states. Some versions of
# Nexus switch do not allow state changes for the extended VLAN
# range (1006-4094), but these errors can be ignored (default
# values are appropriate).
state_config = [snipp.CMD_VLAN_ACTIVE_SNIPPET,
snipp.CMD_VLAN_NO_SHUTDOWN_SNIPPET]
for snippet in state_config:
try:
confstr = self.create_xml_snippet(snippet % vlanid)
self._edit_config(
mgr,
target='running',
config=confstr,
allowed_exc_strs=["Can't modify state for extended",
"Command is only allowed on VLAN"])
except cisco_exceptions.NexusConfigFailed as e:
# Rollback VLAN creation
try:
self.disable_vlan(mgr, vlanid)
finally:
# Re-raise original exception
raise e
def disable_vlan(self, mgr, vlanid):
"""
@ -71,7 +120,7 @@ class CiscoNEXUSDriver():
"""
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):
"""
@ -80,7 +129,7 @@ class CiscoNEXUSDriver():
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):
"""
@ -89,7 +138,7 @@ class CiscoNEXUSDriver():
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, nexus_switch, interface, vlanid):
"""Enable vlan in trunk interface.
@ -104,7 +153,7 @@ class CiscoNEXUSDriver():
snippet = snipp.CMD_INT_VLAN_SNIPPET
confstr = self.create_xml_snippet(snippet % (interface, vlanid))
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):
"""
@ -114,7 +163,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

@ -45,9 +45,29 @@ CMD_VLAN_CONF_SNIPPET = """
<name>
<vlan-name>%s</vlan-name>
</name>
</__XML__MODE_vlan>
</vlan-id-create-delete>
</vlan>
"""
CMD_VLAN_ACTIVE_SNIPPET = """
<vlan>
<vlan-id-create-delete>
<__XML__PARAM_value>%s</__XML__PARAM_value>
<__XML__MODE_vlan>
<state>
<vstate>active</vstate>
</state>
</__XML__MODE_vlan>
</vlan-id-create-delete>
</vlan>
"""
CMD_VLAN_NO_SHUTDOWN_SNIPPET = """
<vlan>
<vlan-id-create-delete>
<__XML__PARAM_value>%s</__XML__PARAM_value>
<__XML__MODE_vlan>
<no>
<shutdown/>
</no>

View File

@ -17,12 +17,16 @@ import contextlib
import logging
import mock
from quantum.common import config
import webob.exc as wexc
from quantum.api.v2 import base
from quantum.common import exceptions as q_exc
from quantum import context
from quantum.db import api as db
from quantum.manager import QuantumManager
from quantum.plugins.cisco.common import cisco_constants as const
from quantum.plugins.cisco.common import cisco_credentials_v2
from quantum.plugins.cisco.common import cisco_exceptions
from quantum.plugins.cisco.db import network_db_v2
from quantum.plugins.cisco.db import network_models_v2
from quantum.plugins.cisco import l2network_plugin_configuration
@ -137,6 +141,26 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
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, name='myname', cidr='1.0.0.0/24',
device_id=DEVICE_ID_1, do_delete=True):
@ -152,8 +176,9 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
end of testing
"""
with self.network(name=name) as network:
with self.subnet(network=network, cidr=cidr) as subnet:
with self.network(name=name, do_delete=do_delete) as network:
with self.subnet(network=network, cidr=cidr,
do_delete=do_delete) as subnet:
net_id = subnet['subnet']['network_id']
res = self._create_port(self.fmt, net_id, device_id=device_id)
port = self.deserialize(self.fmt, res)
@ -163,6 +188,23 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
if do_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 = wexc.HTTPInternalServerError.code
self.assertEqual(status, expected_http)
def _is_in_last_nexus_cfg(self, words):
last_cfg = (CiscoNetworkPluginV2TestCase.mock_ncclient.manager.
connect.return_value.edit_config.
@ -195,8 +237,11 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
net['network']['id'],
'test',
True)
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'ports', 500)
# Expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'ports',
wexc.HTTPInternalServerError.code)
def test_create_ports_bulk_native_plugin_failure(self):
if self._skip_native_bulk:
@ -215,8 +260,11 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
patched_plugin.side_effect = side_effect
res = self._create_port_bulk(self.fmt, 2, net['network']['id'],
'test', True, context=ctx)
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'ports', 500)
# We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'ports',
wexc.HTTPInternalServerError.code)
def test_nexus_enable_vlan_cmd(self):
"""Verify the syntax of the command to enable a vlan on an intf."""
@ -231,6 +279,59 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
self.assertTrue(
self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add']))
def test_nexus_extended_vlan_range_failure(self):
"""Test that extended VLAN range config errors are ignored.
Some versions of Nexus switch do not allow state changes for
the extended VLAN range (1006-4094), but these errors can be
ignored (default values are appropriate). Test that such errors
are ignored by the Nexus plugin.
"""
def mock_edit_config_a(target, config):
if all(word in config for word in ['state', 'active']):
raise Exception("Can't modify state for extended")
with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect',
mock_edit_config_a):
with self._create_port_res() as res:
self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def mock_edit_config_b(target, config):
if all(word in config for word in ['no', 'shutdown']):
raise Exception("Command is only allowed on VLAN")
with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect',
mock_edit_config_b):
with self._create_port_res() as res:
self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def test_nexus_vlan_config_rollback(self):
"""Test rollback following Nexus VLAN state config failure.
Test that the Cisco Nexus plugin correctly deletes the VLAN
on the Nexus switch when the 'state active' command fails (for
a reason other than state configuration change is rejected
for the extended VLAN range).
"""
def mock_edit_config(target, config):
if all(word in config for word in ['state', 'active']):
raise ValueError
with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect',
mock_edit_config):
with self._create_port_res(do_delete=False) as res:
# Confirm that the last configuration sent to the Nexus
# switch was deletion of the VLAN.
self.assertTrue(
self._is_in_last_nexus_cfg(['<no>', '<vlan>'])
)
self._assertExpectedHTTP(res.status_int,
cisco_exceptions.NexusConfigFailed)
class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestNetworksV2):
@ -256,8 +357,11 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
patched_plugin.side_effect = side_effect
res = self._create_network_bulk(self.fmt, 2, 'test', True)
LOG.debug("response is %s" % res)
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'networks', 500)
# We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'networks',
wexc.HTTPInternalServerError.code)
def test_create_networks_bulk_native_plugin_failure(self):
if self._skip_native_bulk:
@ -273,8 +377,11 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
patched_plugin.side_effect = side_effect
res = self._create_network_bulk(self.fmt, 2, 'test', True)
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'networks', 500)
# We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'networks',
wexc.HTTPInternalServerError.code)
class TestCiscoSubnetsV2(CiscoNetworkPluginV2TestCase,
@ -305,8 +412,11 @@ class TestCiscoSubnetsV2(CiscoNetworkPluginV2TestCase,
res = self._create_subnet_bulk(self.fmt, 2,
net['network']['id'],
'test')
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'subnets', 500)
# We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'subnets',
wexc.HTTPInternalServerError.code)
def test_create_subnets_bulk_native_plugin_failure(self):
if self._skip_native_bulk:
@ -325,8 +435,11 @@ class TestCiscoSubnetsV2(CiscoNetworkPluginV2TestCase,
net['network']['id'],
'test')
# We expect a 500 as we injected a fault in the plugin
self._validate_behavior_on_bulk_failure(res, 'subnets', 500)
# We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure(
res,
'subnets',
wexc.HTTPInternalServerError.code)
class TestCiscoPortsV2XML(TestCiscoPortsV2):