cisco/nexus plugin doesn't create port for router interface

Fixes bug 1234826

This fix adds a "nexus_l3_enable" configuration boolean for the
Cisco Nexus plugin. When this config boolean is set to False (default),
then the Nexus switches are only used for L2 switching/segmentation, and
layer 3 functionality is deferred to the OVS subplugin / network
control node. If this config boolean is set to True, layer 3
functionality, e.g. switch virtual interfaces, are supported on
the Nexus switches. (Note that layer 3 functionality is not supported
on all versions/models Nexus switches.)

Some other things addressed with this fix:
- The l3_port_check keyword argument which is optionally passed to the
  Cisco plugin's delete_port method was not being forwarded on to the
  OVS (sub) plugin. This keyword argument needs to be forwarded to OVS
  e.g. when the delete_port is being done in the context of a
  router interface delete (whereby l3_port_check==False).
- UT test cases are added for new "nexus_l3_enable" config, which
  exercise router interface add/delete.
- The Cisco test_network_plugin.py module is refactored/reorganized
  in order to cleanly add a new router interface test class.
- The test_model_update_port_rollback test case was yielding a false
  positive result (device_owner was not being passed to self.port).

Change-Id: I994b2b82769ea5e10e50dbe3a223d1518e99f714
This commit is contained in:
Dane LeBlanc 2013-10-11 17:07:00 -04:00
parent d5c455c663
commit a178053854
4 changed files with 239 additions and 156 deletions

View File

@ -56,10 +56,15 @@
# With real hardware, use the CiscoNEXUSDriver class: # With real hardware, use the CiscoNEXUSDriver class:
# nexus_driver = neutron.plugins.cisco.nexus.cisco_nexus_network_driver_v2.CiscoNEXUSDriver # nexus_driver = neutron.plugins.cisco.nexus.cisco_nexus_network_driver_v2.CiscoNEXUSDriver
# (BoolOpt) A flag to enable Layer 3 support on the Nexus switches.
# Note: This feature is not supported on all models/versions of Cisco
# Nexus switches. To use this feature, all of the Nexus switches in the
# deployment must support it.
# nexus_l3_enable = False
# (BoolOpt) A flag to enable round robin scheduling of routers for SVI. # (BoolOpt) A flag to enable round robin scheduling of routers for SVI.
# svi_round_robin = False # svi_round_robin = False
# Cisco Nexus Switch configurations. # Cisco Nexus Switch configurations.
# Each switch to be managed by Openstack Neutron must be configured here. # Each switch to be managed by Openstack Neutron must be configured here.
# #

View File

@ -41,6 +41,8 @@ cisco_opts = [
cfg.BoolOpt('provider_vlan_auto_trunk', default=True, cfg.BoolOpt('provider_vlan_auto_trunk', default=True,
help=_('Provider VLANs are automatically trunked as needed ' help=_('Provider VLANs are automatically trunked as needed '
'on the ports of the Nexus switch')), 'on the ports of the Nexus switch')),
cfg.BoolOpt('nexus_l3_enable', default=False,
help=_("Enable L3 support on the Nexus switches")),
cfg.BoolOpt('svi_round_robin', default=False, cfg.BoolOpt('svi_round_robin', default=False,
help=_("Distribute SVI interfaces over all switches")), help=_("Distribute SVI interfaces over all switches")),
cfg.StrOpt('model_class', cfg.StrOpt('model_class',

View File

@ -23,8 +23,6 @@ import inspect
import logging import logging
import sys import sys
from oslo.config import cfg
from neutron.api.v2 import attributes from neutron.api.v2 import attributes
from neutron.db import api as db_api from neutron.db import api as db_api
from neutron.extensions import portbindings from neutron.extensions import portbindings
@ -96,10 +94,10 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
'name': self.__class__.__name__}) 'name': self.__class__.__name__})
# Check whether we have a valid Nexus driver loaded # Check whether we have a valid Nexus driver loaded
self.config_nexus = False self.is_nexus_plugin = False
nexus_driver = cfg.CONF.CISCO.nexus_driver nexus_driver = conf.CISCO.nexus_driver
if nexus_driver.endswith('CiscoNEXUSDriver'): if nexus_driver.endswith('CiscoNEXUSDriver'):
self.config_nexus = True self.is_nexus_plugin = True
def __getattribute__(self, name): def __getattribute__(self, name):
"""Delegate calls to OVS sub-plugin. """Delegate calls to OVS sub-plugin.
@ -130,7 +128,8 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
func_name = frame_record[3] func_name = frame_record[3]
return func_name return func_name
def _invoke_plugin_per_device(self, plugin_key, function_name, args): def _invoke_plugin_per_device(self, plugin_key, function_name,
args, **kwargs):
"""Invoke plugin per device. """Invoke plugin per device.
Invokes a device plugin's relevant functions (based on the Invokes a device plugin's relevant functions (based on the
@ -143,10 +142,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
{'plugin_key': plugin_key, 'function_name': function_name, {'plugin_key': plugin_key, 'function_name': function_name,
'args': args}) 'args': args})
return return
return [self._invoke_plugin(plugin_key, function_name, args, kwargs)]
device_params = {const.DEVICE_IP: []}
return [self._invoke_plugin(plugin_key, function_name, args,
device_params)]
def _invoke_plugin(self, plugin_key, function_name, args, kwargs): def _invoke_plugin(self, plugin_key, function_name, args, kwargs):
"""Invoke plugin. """Invoke plugin.
@ -156,7 +152,6 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
""" """
func = getattr(self._plugins[plugin_key], function_name) func = getattr(self._plugins[plugin_key], function_name)
func_args_len = int(inspect.getargspec(func).args.__len__()) - 1 func_args_len = int(inspect.getargspec(func).args.__len__()) - 1
fargs, varargs, varkw, defaults = inspect.getargspec(func)
if args.__len__() > func_args_len: if args.__len__() > func_args_len:
func_args = args[:func_args_len] func_args = args[:func_args_len]
extra_args = args[func_args_len:] extra_args = args[func_args_len:]
@ -165,10 +160,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
kwargs[k] = v kwargs[k] = v
return func(*func_args, **kwargs) return func(*func_args, **kwargs)
else: else:
if (varkw == 'kwargs'): return func(*args, **kwargs)
return func(*args, **kwargs)
else:
return func(*args)
def _get_segmentation_id(self, network_id): def _get_segmentation_id(self, network_id):
binding_seg_id = odb.get_network_binding(None, network_id) binding_seg_id = odb.get_network_binding(None, network_id)
@ -261,7 +253,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
def _invoke_nexus_for_net_create(self, context, tenant_id, net_id, def _invoke_nexus_for_net_create(self, context, tenant_id, net_id,
instance_id, host_id): instance_id, host_id):
if not self.config_nexus: if not self.is_nexus_plugin:
return False return False
network = self.get_network(context, net_id) network = self.get_network(context, net_id)
@ -387,7 +379,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
# Re-raise the original exception # Re-raise the original exception
raise exc_info[0], exc_info[1], exc_info[2] raise exc_info[0], exc_info[1], exc_info[2]
def delete_port(self, context, id): def delete_port(self, context, id, l3_port_check=True):
"""Delete port. """Delete port.
Perform this operation in the context of the configured device Perform this operation in the context of the configured device
@ -398,7 +390,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
host_id = self._get_port_host_id_from_bindings(port) host_id = self._get_port_host_id_from_bindings(port)
if (self.config_nexus and host_id and if (self.is_nexus_plugin and host_id and
self._check_valid_port_device_owner(port)): self._check_valid_port_device_owner(port)):
vlan_id = self._get_segmentation_id(port['network_id']) vlan_id = self._get_segmentation_id(port['network_id'])
n_args = [port['device_id'], vlan_id] n_args = [port['device_id'], vlan_id]
@ -407,9 +399,9 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
n_args) n_args)
try: try:
args = [context, id] args = [context, id]
ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, ovs_output = self._invoke_plugin_per_device(
self._func_name(), const.VSWITCH_PLUGIN, self._func_name(),
args) args, l3_port_check=l3_port_check)
except Exception: except Exception:
exc_info = sys.exc_info() exc_info = sys.exc_info()
# Roll back the delete port on the Nexus plugin # Roll back the delete port on the Nexus plugin
@ -429,12 +421,12 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
def add_router_interface(self, context, router_id, interface_info): def add_router_interface(self, context, router_id, interface_info):
"""Add a router interface on a subnet. """Add a router interface on a subnet.
Only invoke the Nexus plugin to create SVI if a Nexus Only invoke the Nexus plugin to create SVI if L3 support on
plugin is loaded, otherwise send it to the vswitch plugin the Nexus switches is enabled and a Nexus plugin is loaded,
otherwise send it to the vswitch plugin
""" """
nexus_driver = cfg.CONF.CISCO.nexus_driver if (conf.CISCO.nexus_l3_enable and self.is_nexus_plugin):
if nexus_driver.endswith('CiscoNEXUSDriver'): LOG.debug(_("L3 enabled on Nexus plugin, create SVI on switch"))
LOG.debug(_("Nexus plugin loaded, creating SVI on switch"))
if 'subnet_id' not in interface_info: if 'subnet_id' not in interface_info:
raise cexc.SubnetNotSpecified() raise cexc.SubnetNotSpecified()
if 'port_id' in interface_info: if 'port_id' in interface_info:
@ -454,7 +446,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
self._func_name(), self._func_name(),
n_args) n_args)
else: else:
LOG.debug(_("No Nexus plugin, sending to vswitch")) LOG.debug(_("L3 disabled or not Nexus plugin, send to vswitch"))
n_args = [context, router_id, interface_info] n_args = [context, router_id, interface_info]
return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
self._func_name(), self._func_name(),
@ -463,12 +455,12 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
def remove_router_interface(self, context, router_id, interface_info): def remove_router_interface(self, context, router_id, interface_info):
"""Remove a router interface. """Remove a router interface.
Only invoke the Nexus plugin to delete SVI if a Nexus Only invoke the Nexus plugin to delete SVI if L3 support on
plugin is loaded, otherwise send it to the vswitch plugin the Nexus switches is enabled and a Nexus plugin is loaded,
otherwise send it to the vswitch plugin
""" """
nexus_driver = cfg.CONF.CISCO.nexus_driver if (conf.CISCO.nexus_l3_enable and self.is_nexus_plugin):
if nexus_driver.endswith('CiscoNEXUSDriver'): LOG.debug(_("L3 enabled on Nexus plugin, delete SVI from switch"))
LOG.debug(_("Nexus plugin loaded, deleting SVI from switch"))
subnet = self.get_subnet(context, interface_info['subnet_id']) subnet = self.get_subnet(context, interface_info['subnet_id'])
network_id = subnet['network_id'] network_id = subnet['network_id']
@ -479,7 +471,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
self._func_name(), self._func_name(),
n_args) n_args)
else: else:
LOG.debug(_("No Nexus plugin, sending to vswitch")) LOG.debug(_("L3 disabled or not Nexus plugin, send to vswitch"))
n_args = [context, router_id, interface_info] n_args = [context, router_id, interface_info]
return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
self._func_name(), self._func_name(),

View File

@ -18,9 +18,9 @@ import inspect
import logging import logging
import mock import mock
from oslo.config import cfg
import webob.exc as wexc import webob.exc as wexc
from neutron.api import extensions
from neutron.api.v2 import base from neutron.api.v2 import base
from neutron.common import exceptions as q_exc from neutron.common import exceptions as q_exc
from neutron import context from neutron import context
@ -38,29 +38,88 @@ from neutron.plugins.openvswitch.common import config as ovs_config
from neutron.plugins.openvswitch import ovs_db_v2 from neutron.plugins.openvswitch import ovs_db_v2
from neutron.tests.unit import _test_extension_portbindings as test_bindings from neutron.tests.unit import _test_extension_portbindings as test_bindings
from neutron.tests.unit import test_db_plugin from neutron.tests.unit import test_db_plugin
from neutron.tests.unit import test_extensions
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CORE_PLUGIN = 'neutron.plugins.cisco.network_plugin.PluginV2'
NEXUS_PLUGIN = 'neutron.plugins.cisco.nexus.cisco_nexus_plugin_v2.NexusPlugin' NEXUS_PLUGIN = 'neutron.plugins.cisco.nexus.cisco_nexus_plugin_v2.NexusPlugin'
NEXUS_DRIVER = ('neutron.plugins.cisco.nexus.'
'cisco_nexus_network_driver_v2.CiscoNEXUSDriver')
PHYS_NET = 'physnet1'
BRIDGE_NAME = 'br-eth1'
VLAN_START = 1000
VLAN_END = 1100
COMP_HOST_NAME = 'testhost'
NEXUS_IP_ADDR = '1.1.1.1'
NEXUS_DEV_ID = 'NEXUS_SWITCH'
NEXUS_USERNAME = 'admin'
NEXUS_PASSWORD = 'mySecretPassword'
NEXUS_SSH_PORT = 22
NEXUS_INTERFACE = '1/1'
NETWORK_NAME = 'test_network'
CIDR_1 = '10.0.0.0/24'
CIDR_2 = '10.0.1.0/24'
DEVICE_ID_1 = '11111111-1111-1111-1111-111111111111'
DEVICE_ID_2 = '22222222-2222-2222-2222-222222222222'
DEVICE_OWNER = 'compute:None'
class CiscoNetworkPluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase): class CiscoNetworkPluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase):
_plugin_name = 'neutron.plugins.cisco.network_plugin.PluginV2'
def setUp(self): def setUp(self):
"""Configure for end-to-end neutron testing using a mock ncclient.
This setup includes:
- Configure the OVS plugin to use VLANs in the range of
VLAN_START-VLAN_END.
- Configure the Cisco plugin model to use the Nexus driver.
- Configure the Nexus driver to use an imaginary switch
at NEXUS_IP_ADDR.
"""
# Configure the OVS and Cisco plugins
phys_bridge = ':'.join([PHYS_NET, BRIDGE_NAME])
phys_vlan_range = ':'.join([PHYS_NET, str(VLAN_START), str(VLAN_END)])
config = {
ovs_config: {
'OVS': {'bridge_mappings': phys_bridge,
'network_vlan_ranges': [phys_vlan_range],
'tenant_network_type': 'vlan'}
},
cisco_config: {
'CISCO': {'nexus_driver': NEXUS_DRIVER},
'CISCO_PLUGINS': {'nexus_plugin': NEXUS_PLUGIN},
}
}
for module in config:
for group in config[module]:
for opt, val in config[module][group].items():
module.cfg.CONF.set_override(opt, val, group)
self.addCleanup(module.cfg.CONF.reset)
# Configure the Nexus switch dictionary
# TODO(Henry): add tests for other devices
nexus_config = {
(NEXUS_DEV_ID, NEXUS_IP_ADDR, 'username'): NEXUS_USERNAME,
(NEXUS_DEV_ID, NEXUS_IP_ADDR, 'password'): NEXUS_PASSWORD,
(NEXUS_DEV_ID, NEXUS_IP_ADDR, 'ssh_port'): NEXUS_SSH_PORT,
(NEXUS_DEV_ID, NEXUS_IP_ADDR, COMP_HOST_NAME): NEXUS_INTERFACE,
}
nexus_patch = mock.patch.dict(cisco_config.device_dictionary,
nexus_config)
nexus_patch.start()
self.addCleanup(nexus_patch.stop)
# Use a mock netconf client # Use a mock netconf client
self.mock_ncclient = mock.Mock() self.mock_ncclient = mock.Mock()
self.patch_obj = mock.patch.dict('sys.modules', ncclient_patch = mock.patch.dict('sys.modules',
{'ncclient': self.mock_ncclient}) {'ncclient': self.mock_ncclient})
self.patch_obj.start() ncclient_patch.start()
self.addCleanup(ncclient_patch.stop)
cisco_config.cfg.CONF.set_override('nexus_plugin', NEXUS_PLUGIN, # Call the parent setUp, start the core plugin
'CISCO_PLUGINS') super(CiscoNetworkPluginV2TestCase, self).setUp(CORE_PLUGIN)
self.addCleanup(cisco_config.cfg.CONF.reset)
super(CiscoNetworkPluginV2TestCase, self).setUp(self._plugin_name)
self.port_create_status = 'DOWN' self.port_create_status = 'DOWN'
self.addCleanup(self.patch_obj.stop)
def _get_plugin_ref(self): def _get_plugin_ref(self):
plugin_obj = NeutronManager.get_plugin() plugin_obj = NeutronManager.get_plugin()
@ -72,74 +131,6 @@ class CiscoNetworkPluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase):
return plugin_ref return plugin_ref
class TestCiscoBasicGet(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestBasicGet):
pass
class TestCiscoV2HTTPResponse(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestV2HTTPResponse):
pass
class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestPortsV2,
test_bindings.PortBindingsHostTestCaseMixin):
def setUp(self):
"""Configure for end-to-end neutron 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 = ('neutron.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},
'CISCO_PLUGINS': {'nexus_plugin': NEXUS_PLUGIN},
}
}
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)
# TODO(Henry): add tests for other devices
self.dev_id = 'NEXUS_SWITCH'
self.switch_ip = '1.1.1.1'
nexus_config = {
(self.dev_id, self.switch_ip, 'username'): 'admin',
(self.dev_id, self.switch_ip, 'password'): 'mySecretPassword',
(self.dev_id, self.switch_ip, 'ssh_port'): 22,
(self.dev_id, self.switch_ip, 'testhost'): '1/1',
}
mock.patch.dict(cisco_config.device_dictionary, nexus_config).start()
super(TestCiscoPortsV2, self).setUp()
@contextlib.contextmanager @contextlib.contextmanager
def _patch_ncclient(self, attr, value): def _patch_ncclient(self, attr, value):
"""Configure an attribute on the mock ncclient module. """Configure an attribute on the mock ncclient module.
@ -160,9 +151,38 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
config = {attr: None} config = {attr: None}
self.mock_ncclient.configure_mock(**config) self.mock_ncclient.configure_mock(**config)
def _is_in_nexus_cfg(self, words):
"""Check if any config sent to Nexus contains all words in a list."""
for call in (self.mock_ncclient.manager.connect.return_value.
edit_config.mock_calls):
configlet = call[2]['config']
if all(word in configlet for word in words):
return True
def _is_in_last_nexus_cfg(self, words):
"""Check if last config sent to Nexus contains all words in a list."""
last_cfg = (self.mock_ncclient.manager.connect.return_value.
edit_config.mock_calls[-1][2]['config'])
return all(word in last_cfg for word in words)
class TestCiscoBasicGet(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestBasicGet):
pass
class TestCiscoV2HTTPResponse(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestV2HTTPResponse):
pass
class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestPortsV2,
test_bindings.PortBindingsHostTestCaseMixin):
@contextlib.contextmanager @contextlib.contextmanager
def _create_port_res(self, name='myname', cidr='1.0.0.0/24', def _create_port_res(self, name=NETWORK_NAME, cidr=CIDR_1,
do_delete=True, host_id='testhost'): do_delete=True, host_id=COMP_HOST_NAME):
"""Create a network, subnet, and port and yield the result. """Create a network, subnet, and port and yield the result.
Create a network, subnet, and port, yield the result, Create a network, subnet, and port, yield the result,
@ -172,6 +192,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
:param cidr: cidr address of subnetwork to be created :param cidr: cidr address of subnetwork to be created
:param do_delete: If set to True, delete the port at the :param do_delete: If set to True, delete the port at the
end of testing end of testing
:param host_id: Name of compute host to use for testing
""" """
ctx = context.get_admin_context() ctx = context.get_admin_context()
@ -180,8 +201,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
net_id = subnet['subnet']['network_id'] net_id = subnet['subnet']['network_id']
args = (portbindings.HOST_ID, 'device_id', 'device_owner') args = (portbindings.HOST_ID, 'device_id', 'device_owner')
port_dict = {portbindings.HOST_ID: host_id, port_dict = {portbindings.HOST_ID: host_id,
'device_id': 'testdev', 'device_id': DEVICE_ID_1,
'device_owner': 'compute:None'} 'device_owner': DEVICE_OWNER}
res = self._create_port(self.fmt, net_id, arg_list=args, res = self._create_port(self.fmt, net_id, arg_list=args,
context=ctx, **port_dict) context=ctx, **port_dict)
port = self.deserialize(self.fmt, res) port = self.deserialize(self.fmt, res)
@ -208,11 +229,6 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
expected_http = wexc.HTTPInternalServerError.code expected_http = wexc.HTTPInternalServerError.code
self.assertEqual(status, expected_http) self.assertEqual(status, expected_http)
def _is_in_last_nexus_cfg(self, words):
last_cfg = (self.mock_ncclient.manager.connect().
edit_config.mock_calls[-1][2]['config'])
return all(word in last_cfg for word in words)
def test_create_ports_bulk_emulated_plugin_failure(self): def test_create_ports_bulk_emulated_plugin_failure(self):
real_has_attr = hasattr real_has_attr = hasattr
@ -268,7 +284,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
*args, **kwargs) *args, **kwargs)
patched_plugin.side_effect = side_effect patched_plugin.side_effect = side_effect
res = self._create_port_bulk(self.fmt, 2, net['network']['id'], res = self._create_port_bulk(self.fmt, 2,
net['network']['id'],
'test', True, context=ctx) 'test', True, context=ctx)
# We expect an internal server error as we injected a fault # We expect an internal server error as we injected a fault
self._validate_behavior_on_bulk_failure( self._validate_behavior_on_bulk_failure(
@ -279,11 +296,11 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
def test_nexus_enable_vlan_cmd(self): def test_nexus_enable_vlan_cmd(self):
"""Verify the syntax of the command to enable a vlan on an intf.""" """Verify the syntax of the command to enable a vlan on an intf."""
# First vlan should be configured without 'add' keyword # First vlan should be configured without 'add' keyword
with self._create_port_res(name='net1', cidr='1.0.0.0/24'): with self._create_port_res(name='net1', cidr=CIDR_1):
self.assertTrue(self._is_in_last_nexus_cfg(['allowed', 'vlan'])) self.assertTrue(self._is_in_last_nexus_cfg(['allowed', 'vlan']))
self.assertFalse(self._is_in_last_nexus_cfg(['add'])) self.assertFalse(self._is_in_last_nexus_cfg(['add']))
# Second vlan should be configured with 'add' keyword # Second vlan should be configured with 'add' keyword
with self._create_port_res(name='net2', cidr='1.0.1.0/24'): with self._create_port_res(name='net2', cidr=CIDR_2):
self.assertTrue( self.assertTrue(
self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add'])) self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add']))
@ -332,7 +349,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
with self._patch_ncclient( with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect', 'manager.connect.return_value.edit_config.side_effect',
mock_edit_config_a): mock_edit_config_a):
with self._create_port_res(name='myname') as res: with self._create_port_res() as res:
self.assertEqual(res.status_int, wexc.HTTPCreated.code) self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def mock_edit_config_b(target, config): def mock_edit_config_b(target, config):
@ -342,7 +359,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
with self._patch_ncclient( with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect', 'manager.connect.return_value.edit_config.side_effect',
mock_edit_config_b): mock_edit_config_b):
with self._create_port_res(name='myname') as res: with self._create_port_res() as res:
self.assertEqual(res.status_int, wexc.HTTPCreated.code) self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def test_nexus_vlan_config_rollback(self): def test_nexus_vlan_config_rollback(self):
@ -360,7 +377,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
with self._patch_ncclient( with self._patch_ncclient(
'manager.connect.return_value.edit_config.side_effect', 'manager.connect.return_value.edit_config.side_effect',
mock_edit_config): mock_edit_config):
with self._create_port_res(name='myname', do_delete=False) as res: with self._create_port_res(do_delete=False) as res:
# Confirm that the last configuration sent to the Nexus # Confirm that the last configuration sent to the Nexus
# switch was deletion of the VLAN. # switch was deletion of the VLAN.
self.assertTrue( self.assertTrue(
@ -402,7 +419,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
a fictitious host name during port creation. a fictitious host name during port creation.
""" """
with self._create_port_res(do_delete=False, host_id='fakehost') as res: with self._create_port_res(do_delete=False,
host_id='fakehost') as res:
self._assertExpectedHTTP(res.status_int, self._assertExpectedHTTP(res.status_int,
c_exc.NexusComputeHostNotConfigured) c_exc.NexusComputeHostNotConfigured)
@ -431,8 +449,14 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
(restored) by the Cisco plugin model layer when there is a (restored) by the Cisco plugin model layer when there is a
failure in the Nexus sub-plugin for an update port operation. failure in the Nexus sub-plugin for an update port operation.
The update port operation simulates a port attachment scenario:
first a port is created with no instance (null device_id),
and then a port update is requested with a non-null device_id
to simulate the port attachment.
""" """
with self.port(fmt=self.fmt) as orig_port: with self.port(fmt=self.fmt, device_id='',
device_owner=DEVICE_OWNER) as orig_port:
inserted_exc = ValueError inserted_exc = ValueError
with mock.patch.object( with mock.patch.object(
@ -440,12 +464,10 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
'_invoke_nexus_for_net_create', '_invoke_nexus_for_net_create',
side_effect=inserted_exc): side_effect=inserted_exc):
# Send an update port request with a new device ID # Send an update port request including a non-null device ID
device_id = "00fff4d0-e4a8-4a3a-8906-4c4cdafb59f1" data = {'port': {'device_id': DEVICE_ID_2,
if orig_port['port']['device_id'] == device_id: 'device_owner': DEVICE_OWNER,
device_id = "600df00d-e4a8-4a3a-8906-feed600df00d" portbindings.HOST_ID: COMP_HOST_NAME}}
data = {'port': {'device_id': device_id,
portbindings.HOST_ID: 'testhost'}}
port_id = orig_port['port']['id'] port_id = orig_port['port']['id']
req = self.new_update_request('ports', data, port_id) req = self.new_update_request('ports', data, port_id)
res = req.get_response(self.api) res = req.get_response(self.api)
@ -473,8 +495,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
# After port is created, we should have one binding for this # After port is created, we should have one binding for this
# vlan/nexus switch. # vlan/nexus switch.
port = self.deserialize(self.fmt, res) port = self.deserialize(self.fmt, res)
start_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start, start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,
self.switch_ip) NEXUS_IP_ADDR)
self.assertEqual(len(start_rows), 1) self.assertEqual(len(start_rows), 1)
# Inject an exception in the OVS plugin delete_port # Inject an exception in the OVS plugin delete_port
@ -489,8 +511,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
# Confirm that the Cisco model plugin has restored # Confirm that the Cisco model plugin has restored
# the nexus configuration for this port after deletion failure. # the nexus configuration for this port after deletion failure.
end_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start, end_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,
self.switch_ip) NEXUS_IP_ADDR)
self.assertEqual(start_rows, end_rows) self.assertEqual(start_rows, end_rows)
def test_nexus_delete_port_rollback(self): def test_nexus_delete_port_rollback(self):
@ -507,8 +529,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
# Check that there is only one binding in the nexus database # Check that there is only one binding in the nexus database
# for this VLAN/nexus switch. # for this VLAN/nexus switch.
start_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start, start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,
self.switch_ip) NEXUS_IP_ADDR)
self.assertEqual(len(start_rows), 1) self.assertEqual(len(start_rows), 1)
# Simulate a Nexus switch configuration error during # Simulate a Nexus switch configuration error during
@ -520,24 +542,14 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
base.FAULT_MAP[c_exc.NexusConfigFailed].code) base.FAULT_MAP[c_exc.NexusConfigFailed].code)
# Confirm that the binding has been restored (rolled back). # Confirm that the binding has been restored (rolled back).
end_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start, end_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,
self.switch_ip) NEXUS_IP_ADDR)
self.assertEqual(start_rows, end_rows) self.assertEqual(start_rows, end_rows)
class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase, class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
test_db_plugin.TestNetworksV2): test_db_plugin.TestNetworksV2):
def setUp(self):
self.physnet = 'testphys1'
self.vlan_range = '100:199'
phys_vrange = ':'.join([self.physnet, self.vlan_range])
cfg.CONF.set_override('tenant_network_type', 'vlan', 'OVS')
cfg.CONF.set_override('network_vlan_ranges', [phys_vrange], 'OVS')
self.addCleanup(cfg.CONF.reset)
super(TestCiscoNetworksV2, self).setUp()
def test_create_networks_bulk_emulated_plugin_failure(self): def test_create_networks_bulk_emulated_plugin_failure(self):
real_has_attr = hasattr real_has_attr = hasattr
@ -587,7 +599,7 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
def test_create_provider_vlan_network(self): def test_create_provider_vlan_network(self):
provider_attrs = {provider.NETWORK_TYPE: 'vlan', provider_attrs = {provider.NETWORK_TYPE: 'vlan',
provider.PHYSICAL_NETWORK: self.physnet, provider.PHYSICAL_NETWORK: PHYS_NET,
provider.SEGMENTATION_ID: '1234'} provider.SEGMENTATION_ID: '1234'}
arg_list = tuple(provider_attrs.keys()) arg_list = tuple(provider_attrs.keys())
res = self._create_network(self.fmt, 'pvnet1', True, res = self._create_network(self.fmt, 'pvnet1', True,
@ -598,7 +610,7 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
('status', 'ACTIVE'), ('status', 'ACTIVE'),
('shared', False), ('shared', False),
(provider.NETWORK_TYPE, 'vlan'), (provider.NETWORK_TYPE, 'vlan'),
(provider.PHYSICAL_NETWORK, self.physnet), (provider.PHYSICAL_NETWORK, PHYS_NET),
(provider.SEGMENTATION_ID, 1234)] (provider.SEGMENTATION_ID, 1234)]
for k, v in expected: for k, v in expected:
self.assertEqual(net['network'][k], v) self.assertEqual(net['network'][k], v)
@ -662,6 +674,74 @@ class TestCiscoSubnetsV2(CiscoNetworkPluginV2TestCase,
wexc.HTTPInternalServerError.code) wexc.HTTPInternalServerError.code)
class TestCiscoRouterInterfacesV2(CiscoNetworkPluginV2TestCase):
def setUp(self):
"""Configure an API extension manager."""
super(TestCiscoRouterInterfacesV2, self).setUp()
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr)
@contextlib.contextmanager
def _router(self, subnet):
"""Create a virtual router, yield it for testing, then delete it."""
data = {'router': {'tenant_id': 'test_tenant_id'}}
router_req = self.new_create_request('routers', data, self.fmt)
res = router_req.get_response(self.ext_api)
router = self.deserialize(self.fmt, res)
try:
yield router
finally:
self._delete('routers', router['router']['id'])
@contextlib.contextmanager
def _router_interface(self, router, subnet):
"""Create a router interface, yield for testing, then delete it."""
interface_data = {'subnet_id': subnet['subnet']['id']}
req = self.new_action_request('routers', interface_data,
router['router']['id'],
'add_router_interface')
req.get_response(self.ext_api)
try:
yield
finally:
req = self.new_action_request('routers', interface_data,
router['router']['id'],
'remove_router_interface')
req.get_response(self.ext_api)
def test_nexus_l3_enable_config(self):
"""Verify proper operation of the Nexus L3 enable configuration."""
self.addCleanup(cisco_config.CONF.reset)
with self.network() as network:
with self.subnet(network=network) as subnet:
with self._router(subnet) as router:
# With 'nexus_l3_enable' configured to True, confirm that
# a switched virtual interface (SVI) is created/deleted
# on the Nexus switch when a virtual router interface is
# created/deleted.
cisco_config.CONF.set_override('nexus_l3_enable',
True, 'CISCO')
with self._router_interface(router, subnet):
self.assertTrue(self._is_in_last_nexus_cfg(
['interface', 'vlan', 'ip', 'address']))
self.assertTrue(self._is_in_nexus_cfg(
['no', 'interface', 'vlan']))
self.assertTrue(self._is_in_last_nexus_cfg(
['no', 'vlan']))
# With 'nexus_l3_enable' configured to False, confirm
# that no changes are made to the Nexus switch running
# configuration when a virtual router interface is
# created and then deleted.
cisco_config.CONF.set_override('nexus_l3_enable',
False, 'CISCO')
self.mock_ncclient.reset_mock()
self._router_interface(router, subnet)
self.assertFalse(self.mock_ncclient.manager.connect.
return_value.edit_config.called)
class TestCiscoPortsV2XML(TestCiscoPortsV2): class TestCiscoPortsV2XML(TestCiscoPortsV2):
fmt = 'xml' fmt = 'xml'
@ -672,3 +752,7 @@ class TestCiscoNetworksV2XML(TestCiscoNetworksV2):
class TestCiscoSubnetsV2XML(TestCiscoSubnetsV2): class TestCiscoSubnetsV2XML(TestCiscoSubnetsV2):
fmt = 'xml' fmt = 'xml'
class TestCiscoRouterInterfacesV2XML(TestCiscoRouterInterfacesV2):
fmt = 'xml'