]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
cisco/nexus plugin doesn't create port for router interface
authorDane LeBlanc <leblancd@cisco.com>
Fri, 11 Oct 2013 21:07:00 +0000 (17:07 -0400)
committerDane LeBlanc <leblancd@cisco.com>
Sat, 12 Oct 2013 00:23:55 +0000 (20:23 -0400)
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
neutron/plugins/cisco/common/config.py
neutron/plugins/cisco/models/virt_phy_sw_v2.py
neutron/tests/unit/cisco/test_network_plugin.py

index 50e6fc52ed42617580e5e1eda452503bd983bfad..e065e73a41bfd737d5ad692493a71a0795cb416f 100644 (file)
 # 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.
 #
index 04d05d186771e9c0f3d859c00190b5b40abb3842..86a55426fc29530e0a252fbf7cd7af061314a02c 100644 (file)
@@ -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',
index 3e8fa2d6471135f028fe7d14a6ccc7d38393d545..48346d37294ac153dcfc0fb8e20ec68b30ca4888 100644 (file)
@@ -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(),
index b336d40f6e8d75c5423a0a5c04f18e34197756c7..09ccd56b95c041a003165ea151f2534b9f6dde48 100644 (file)
@@ -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'