From a178053854a57170084492f8378675ccf46200df Mon Sep 17 00:00:00 2001 From: Dane LeBlanc Date: Fri, 11 Oct 2013 17:07:00 -0400 Subject: [PATCH] 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 --- etc/neutron/plugins/cisco/cisco_plugins.ini | 7 +- neutron/plugins/cisco/common/config.py | 2 + .../plugins/cisco/models/virt_phy_sw_v2.py | 58 ++-- .../tests/unit/cisco/test_network_plugin.py | 316 +++++++++++------- 4 files changed, 233 insertions(+), 150 deletions(-) diff --git a/etc/neutron/plugins/cisco/cisco_plugins.ini b/etc/neutron/plugins/cisco/cisco_plugins.ini index 50e6fc52e..e065e73a4 100644 --- a/etc/neutron/plugins/cisco/cisco_plugins.ini +++ b/etc/neutron/plugins/cisco/cisco_plugins.ini @@ -56,10 +56,15 @@ # With real hardware, use the CiscoNEXUSDriver class: # 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. # svi_round_robin = False - # Cisco Nexus Switch configurations. # Each switch to be managed by Openstack Neutron must be configured here. # diff --git a/neutron/plugins/cisco/common/config.py b/neutron/plugins/cisco/common/config.py index 04d05d186..86a55426f 100644 --- a/neutron/plugins/cisco/common/config.py +++ b/neutron/plugins/cisco/common/config.py @@ -41,6 +41,8 @@ cisco_opts = [ cfg.BoolOpt('provider_vlan_auto_trunk', default=True, help=_('Provider VLANs are automatically trunked as needed ' '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, help=_("Distribute SVI interfaces over all switches")), cfg.StrOpt('model_class', diff --git a/neutron/plugins/cisco/models/virt_phy_sw_v2.py b/neutron/plugins/cisco/models/virt_phy_sw_v2.py index 3e8fa2d64..48346d372 100644 --- a/neutron/plugins/cisco/models/virt_phy_sw_v2.py +++ b/neutron/plugins/cisco/models/virt_phy_sw_v2.py @@ -23,8 +23,6 @@ import inspect import logging import sys -from oslo.config import cfg - from neutron.api.v2 import attributes from neutron.db import api as db_api from neutron.extensions import portbindings @@ -96,10 +94,10 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): 'name': self.__class__.__name__}) # Check whether we have a valid Nexus driver loaded - self.config_nexus = False - nexus_driver = cfg.CONF.CISCO.nexus_driver + self.is_nexus_plugin = False + nexus_driver = conf.CISCO.nexus_driver if nexus_driver.endswith('CiscoNEXUSDriver'): - self.config_nexus = True + self.is_nexus_plugin = True def __getattribute__(self, name): """Delegate calls to OVS sub-plugin. @@ -130,7 +128,8 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): func_name = frame_record[3] 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. 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, 'args': args}) return - - device_params = {const.DEVICE_IP: []} - return [self._invoke_plugin(plugin_key, function_name, args, - device_params)] + return [self._invoke_plugin(plugin_key, function_name, args, kwargs)] def _invoke_plugin(self, plugin_key, function_name, args, kwargs): """Invoke plugin. @@ -156,7 +152,6 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): """ func = getattr(self._plugins[plugin_key], function_name) func_args_len = int(inspect.getargspec(func).args.__len__()) - 1 - fargs, varargs, varkw, defaults = inspect.getargspec(func) if args.__len__() > func_args_len: func_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 return func(*func_args, **kwargs) else: - if (varkw == 'kwargs'): - return func(*args, **kwargs) - else: - return func(*args) + return func(*args, **kwargs) def _get_segmentation_id(self, 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, instance_id, host_id): - if not self.config_nexus: + if not self.is_nexus_plugin: return False network = self.get_network(context, net_id) @@ -387,7 +379,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): # Re-raise the original exception 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. 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) - if (self.config_nexus and host_id and + if (self.is_nexus_plugin and host_id and self._check_valid_port_device_owner(port)): vlan_id = self._get_segmentation_id(port['network_id']) n_args = [port['device_id'], vlan_id] @@ -407,9 +399,9 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): n_args) try: args = [context, id] - ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, - self._func_name(), - args) + ovs_output = self._invoke_plugin_per_device( + const.VSWITCH_PLUGIN, self._func_name(), + args, l3_port_check=l3_port_check) except Exception: exc_info = sys.exc_info() # 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): """Add a router interface on a subnet. - Only invoke the Nexus plugin to create SVI if a Nexus - plugin is loaded, otherwise send it to the vswitch plugin + Only invoke the Nexus plugin to create SVI if L3 support on + 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 nexus_driver.endswith('CiscoNEXUSDriver'): - LOG.debug(_("Nexus plugin loaded, creating SVI on switch")) + if (conf.CISCO.nexus_l3_enable and self.is_nexus_plugin): + LOG.debug(_("L3 enabled on Nexus plugin, create SVI on switch")) if 'subnet_id' not in interface_info: raise cexc.SubnetNotSpecified() if 'port_id' in interface_info: @@ -454,7 +446,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): self._func_name(), n_args) 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] return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, self._func_name(), @@ -463,12 +455,12 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): def remove_router_interface(self, context, router_id, interface_info): """Remove a router interface. - Only invoke the Nexus plugin to delete SVI if a Nexus - plugin is loaded, otherwise send it to the vswitch plugin + Only invoke the Nexus plugin to delete SVI if L3 support on + 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 nexus_driver.endswith('CiscoNEXUSDriver'): - LOG.debug(_("Nexus plugin loaded, deleting SVI from switch")) + if (conf.CISCO.nexus_l3_enable and self.is_nexus_plugin): + LOG.debug(_("L3 enabled on Nexus plugin, delete SVI from switch")) subnet = self.get_subnet(context, interface_info['subnet_id']) network_id = subnet['network_id'] @@ -479,7 +471,7 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2): self._func_name(), n_args) 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] return self._invoke_plugin_per_device(const.VSWITCH_PLUGIN, self._func_name(), diff --git a/neutron/tests/unit/cisco/test_network_plugin.py b/neutron/tests/unit/cisco/test_network_plugin.py index b336d40f6..09ccd56b9 100644 --- a/neutron/tests/unit/cisco/test_network_plugin.py +++ b/neutron/tests/unit/cisco/test_network_plugin.py @@ -18,9 +18,9 @@ import inspect import logging import mock -from oslo.config import cfg import webob.exc as wexc +from neutron.api import extensions from neutron.api.v2 import base from neutron.common import exceptions as q_exc from neutron import context @@ -38,107 +38,98 @@ from neutron.plugins.openvswitch.common import config as ovs_config 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_db_plugin +from neutron.tests.unit import test_extensions LOG = logging.getLogger(__name__) +CORE_PLUGIN = 'neutron.plugins.cisco.network_plugin.PluginV2' 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): - _plugin_name = 'neutron.plugins.cisco.network_plugin.PluginV2' - - def setUp(self): - # Use a mock netconf client - self.mock_ncclient = mock.Mock() - self.patch_obj = mock.patch.dict('sys.modules', - {'ncclient': self.mock_ncclient}) - self.patch_obj.start() - - cisco_config.cfg.CONF.set_override('nexus_plugin', NEXUS_PLUGIN, - 'CISCO_PLUGINS') - self.addCleanup(cisco_config.cfg.CONF.reset) - - super(CiscoNetworkPluginV2TestCase, self).setUp(self._plugin_name) - self.port_create_status = 'DOWN' - self.addCleanup(self.patch_obj.stop) - - def _get_plugin_ref(self): - plugin_obj = NeutronManager.get_plugin() - if getattr(plugin_obj, "_master"): - plugin_ref = plugin_obj - else: - plugin_ref = getattr(plugin_obj, "_model").\ - _plugins[const.VSWITCH_PLUGIN] - - 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. + - 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. """ - 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') - + # 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': 'physnet1:br-eth1', - 'network_vlan_ranges': [range_str], + 'OVS': {'bridge_mappings': phys_bridge, + 'network_vlan_ranges': [phys_vlan_range], 'tenant_network_type': 'vlan'} }, cisco_config: { - 'CISCO': {'nexus_driver': nexus_driver}, + '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) + 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 - 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', + (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, } - mock.patch.dict(cisco_config.device_dictionary, nexus_config).start() + nexus_patch = mock.patch.dict(cisco_config.device_dictionary, + nexus_config) + nexus_patch.start() + self.addCleanup(nexus_patch.stop) - super(TestCiscoPortsV2, self).setUp() + # Use a mock netconf client + self.mock_ncclient = mock.Mock() + ncclient_patch = mock.patch.dict('sys.modules', + {'ncclient': self.mock_ncclient}) + ncclient_patch.start() + self.addCleanup(ncclient_patch.stop) + + # Call the parent setUp, start the core plugin + super(CiscoNetworkPluginV2TestCase, self).setUp(CORE_PLUGIN) + self.port_create_status = 'DOWN' + + def _get_plugin_ref(self): + plugin_obj = NeutronManager.get_plugin() + if getattr(plugin_obj, "_master"): + plugin_ref = plugin_obj + else: + plugin_ref = getattr(plugin_obj, "_model").\ + _plugins[const.VSWITCH_PLUGIN] + + return plugin_ref @contextlib.contextmanager def _patch_ncclient(self, attr, value): @@ -160,9 +151,38 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, config = {attr: None} 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 - def _create_port_res(self, name='myname', cidr='1.0.0.0/24', - do_delete=True, host_id='testhost'): + def _create_port_res(self, name=NETWORK_NAME, cidr=CIDR_1, + do_delete=True, host_id=COMP_HOST_NAME): """Create a network, subnet, and port and 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 do_delete: If set to True, delete the port at the end of testing + :param host_id: Name of compute host to use for testing """ ctx = context.get_admin_context() @@ -180,8 +201,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, net_id = subnet['subnet']['network_id'] args = (portbindings.HOST_ID, 'device_id', 'device_owner') port_dict = {portbindings.HOST_ID: host_id, - 'device_id': 'testdev', - 'device_owner': 'compute:None'} + 'device_id': DEVICE_ID_1, + 'device_owner': DEVICE_OWNER} res = self._create_port(self.fmt, net_id, arg_list=args, context=ctx, **port_dict) port = self.deserialize(self.fmt, res) @@ -208,11 +229,6 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, expected_http = wexc.HTTPInternalServerError.code 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): real_has_attr = hasattr @@ -268,7 +284,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, *args, **kwargs) 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) # We expect an internal server error as we injected a fault self._validate_behavior_on_bulk_failure( @@ -279,11 +296,11 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, def test_nexus_enable_vlan_cmd(self): """Verify the syntax of the command to enable a vlan on an intf.""" # 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.assertFalse(self._is_in_last_nexus_cfg(['add'])) # 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._is_in_last_nexus_cfg(['allowed', 'vlan', 'add'])) @@ -332,7 +349,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, with self._patch_ncclient( 'manager.connect.return_value.edit_config.side_effect', 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) def mock_edit_config_b(target, config): @@ -342,7 +359,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, with self._patch_ncclient( 'manager.connect.return_value.edit_config.side_effect', 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) def test_nexus_vlan_config_rollback(self): @@ -360,7 +377,7 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, with self._patch_ncclient( 'manager.connect.return_value.edit_config.side_effect', 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 # switch was deletion of the VLAN. self.assertTrue( @@ -402,7 +419,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, 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, c_exc.NexusComputeHostNotConfigured) @@ -431,8 +449,14 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, (restored) by the Cisco plugin model layer when there is a 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 with mock.patch.object( @@ -440,12 +464,10 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, '_invoke_nexus_for_net_create', side_effect=inserted_exc): - # Send an update port request with a new device ID - device_id = "00fff4d0-e4a8-4a3a-8906-4c4cdafb59f1" - if orig_port['port']['device_id'] == device_id: - device_id = "600df00d-e4a8-4a3a-8906-feed600df00d" - data = {'port': {'device_id': device_id, - portbindings.HOST_ID: 'testhost'}} + # Send an update port request including a non-null device ID + data = {'port': {'device_id': DEVICE_ID_2, + 'device_owner': DEVICE_OWNER, + portbindings.HOST_ID: COMP_HOST_NAME}} port_id = orig_port['port']['id'] req = self.new_update_request('ports', data, port_id) res = req.get_response(self.api) @@ -473,8 +495,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, # 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) + start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, + NEXUS_IP_ADDR) self.assertEqual(len(start_rows), 1) # Inject an exception in the OVS plugin delete_port @@ -489,8 +511,8 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, # 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) + end_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, + NEXUS_IP_ADDR) self.assertEqual(start_rows, end_rows) 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 # for this VLAN/nexus switch. - start_rows = nexus_db_v2.get_nexusvlan_binding(self.vlan_start, - self.switch_ip) + start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, + NEXUS_IP_ADDR) self.assertEqual(len(start_rows), 1) # Simulate a Nexus switch configuration error during @@ -520,24 +542,14 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase, 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) + end_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, + NEXUS_IP_ADDR) self.assertEqual(start_rows, end_rows) class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase, 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): real_has_attr = hasattr @@ -587,7 +599,7 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase, def test_create_provider_vlan_network(self): provider_attrs = {provider.NETWORK_TYPE: 'vlan', - provider.PHYSICAL_NETWORK: self.physnet, + provider.PHYSICAL_NETWORK: PHYS_NET, provider.SEGMENTATION_ID: '1234'} arg_list = tuple(provider_attrs.keys()) res = self._create_network(self.fmt, 'pvnet1', True, @@ -598,7 +610,7 @@ class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase, ('status', 'ACTIVE'), ('shared', False), (provider.NETWORK_TYPE, 'vlan'), - (provider.PHYSICAL_NETWORK, self.physnet), + (provider.PHYSICAL_NETWORK, PHYS_NET), (provider.SEGMENTATION_ID, 1234)] for k, v in expected: self.assertEqual(net['network'][k], v) @@ -662,6 +674,74 @@ class TestCiscoSubnetsV2(CiscoNetworkPluginV2TestCase, 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): fmt = 'xml' @@ -672,3 +752,7 @@ class TestCiscoNetworksV2XML(TestCiscoNetworksV2): class TestCiscoSubnetsV2XML(TestCiscoSubnetsV2): fmt = 'xml' + + +class TestCiscoRouterInterfacesV2XML(TestCiscoRouterInterfacesV2): + fmt = 'xml' -- 2.45.2