]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 Cisco Nexus MD - not overwriting existing config
authorAbishek Subramanian <absubram@cisco.com>
Tue, 28 Oct 2014 18:36:32 +0000 (14:36 -0400)
committerAbishek Subramanian <absubram@cisco.com>
Tue, 28 Oct 2014 19:16:16 +0000 (15:16 -0400)
The Cisco Nexus ML2 MD overwrites any existing switchport
VLAN config such as management VLANs that are preconfigured
in the compute node ToR interfaces.
This bug addresses that issue and ensures the config is not
wiped out.

Closes-Bug: 1330597

Change-Id: Ie2969e5750a179a4940d12db4c9ee6e1ec53b79b

neutron/plugins/ml2/drivers/cisco/nexus/nexus_network_driver.py
neutron/tests/unit/ml2/drivers/cisco/nexus/test_cisco_mech.py

index 33ba85e980d989ad244c4ab274faddcce7495375..e1256f6091c2808c305c492c66b2d8fb295abfd6 100644 (file)
@@ -23,7 +23,6 @@ from neutron.openstack.common import log as logging
 from neutron.plugins.ml2.drivers.cisco.nexus import config as conf
 from neutron.plugins.ml2.drivers.cisco.nexus import constants as const
 from neutron.plugins.ml2.drivers.cisco.nexus import exceptions as cexc
-from neutron.plugins.ml2.drivers.cisco.nexus import nexus_db_v2
 from neutron.plugins.ml2.drivers.cisco.nexus import nexus_snippets as snipp
 
 LOG = logging.getLogger(__name__)
@@ -58,22 +57,27 @@ class CiscoNexusDriver(object):
                                  as a subset of their exception message
                                  (str(exception)) can be ignored
 
-        :raises: NexusConfigFailed
+        :returns None: if config was edited successfully
+                 Exception object: if _edit_config() encountered an exception
+                                   containing one of allowed_exc_strs
+
+        :raises: NexusConfigFailed: if _edit_config() encountered an exception
+                                    not containing one of allowed_exc_strs
 
         """
         if not allowed_exc_strs:
             allowed_exc_strs = []
         mgr = self.nxos_connect(nexus_host)
         try:
+            LOG.debug("NexusDriver config: %s", config)
             mgr.edit_config(target=target, config=config)
         except Exception as e:
             for exc_str in allowed_exc_strs:
                 if exc_str in str(e):
-                    break
-            else:
+                    return e
                 # Raise a Neutron exception. Include a description of
                 # the original ncclient exception.
-                raise cexc.NexusConfigFailed(config=config, exc=e)
+            raise cexc.NexusConfigFailed(config=config, exc=e)
 
     def nxos_connect(self, nexus_host):
         """Make SSH connection to the Nexus Switch."""
@@ -120,7 +124,6 @@ class CiscoNexusDriver(object):
         """Create a VLAN on Nexus Switch given the VLAN ID and Name."""
         confstr = self.create_xml_snippet(
             snipp.CMD_VLAN_CONF_SNIPPET % (vlanid, vlanname))
-        LOG.debug(_("NexusDriver: %s"), confstr)
         self._edit_config(nexus_host, target='running', config=confstr)
 
         # Enable VLAN active and no-shutdown states. Some versions of
@@ -147,20 +150,39 @@ class CiscoNexusDriver(object):
         confstr = self.create_xml_snippet(confstr)
         self._edit_config(nexus_host, target='running', config=confstr)
 
-    def enable_vlan_on_trunk_int(self, nexus_host, vlanid, intf_type,
-                                 interface):
-        """Enable a VLAN on a trunk interface."""
-        # If more than one VLAN is configured on this interface then
-        # include the 'add' keyword.
-        if len(nexus_db_v2.get_port_switch_bindings(
-                '%s:%s' % (intf_type, interface), nexus_host)) == 1:
-            snippet = snipp.CMD_INT_VLAN_SNIPPET
-        else:
-            snippet = snipp.CMD_INT_VLAN_ADD_SNIPPET
+    def build_intf_confstr(self, snippet, intf_type, interface, vlanid):
+        """Build the VLAN config string xml snippet to be used."""
         confstr = snippet % (intf_type, interface, vlanid, intf_type)
         confstr = self.create_xml_snippet(confstr)
         LOG.debug(_("NexusDriver: %s"), confstr)
-        self._edit_config(nexus_host, target='running', config=confstr)
+        return confstr
+
+    def enable_vlan_on_trunk_int(self, nexus_host, vlanid, intf_type,
+                                 interface):
+        """Enable a VLAN on a trunk interface."""
+        # Configure a new VLAN into the interface using 'ADD' keyword
+        confstr = self.build_intf_confstr(
+            snippet=snipp.CMD_INT_VLAN_ADD_SNIPPET,
+            intf_type=intf_type,
+            interface=interface,
+            vlanid=vlanid
+        )
+        exc_str = ["switchport trunk allowed vlan list is empty"]
+        ret_exc = self._edit_config(nexus_host, target='running',
+                                    config=confstr,
+                                    allowed_exc_strs=exc_str)
+        if ret_exc:
+            # If no switchports have been configured on the switch
+            # before the new 'ADD', configure the VLAN into the
+            # interface without the keyword so as to create a vlan list
+            confstr = self.build_intf_confstr(
+                snippet=snipp.CMD_INT_VLAN_SNIPPET,
+                intf_type=intf_type,
+                interface=interface,
+                vlanid=vlanid
+            )
+            self._edit_config(nexus_host, target='running',
+                              config=confstr)
 
     def disable_vlan_on_trunk_int(self, nexus_host, vlanid, intf_type,
                                   interface):
@@ -168,7 +190,6 @@ class CiscoNexusDriver(object):
         confstr = (snipp.CMD_NO_VLAN_INT_SNIPPET %
                    (intf_type, interface, vlanid, intf_type))
         confstr = self.create_xml_snippet(confstr)
-        LOG.debug(_("NexusDriver: %s"), confstr)
         self._edit_config(nexus_host, target='running', config=confstr)
 
     def create_and_trunk_vlan(self, nexus_host, vlan_id, vlan_name,
index 38b60b8ef9d8d9f19912e274dfbd494bf611699d..efd3c7188c4c0e83de05ca6a31150801406743ff 100644 (file)
@@ -208,9 +208,14 @@ class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
         return all(word in last_cfg for word in words)
 
     def _is_vlan_configured(self, vlan_creation_expected=True,
-                            add_keyword_expected=False):
+                            first_vlan_addition=False):
+        """Confirm if VLAN was configured or not."""
         vlan_created = self._is_in_nexus_cfg(['vlan', 'vlan-name'])
         add_appears = self._is_in_last_nexus_cfg(['add'])
+        # The first VLAN being configured should be done without the
+        # ADD keyword. Thereafter additional VLANs to be configured
+        # should be done with the ADD keyword.
+        add_keyword_expected = not first_vlan_addition
         return (self._is_in_last_nexus_cfg(['allowed', 'vlan']) and
                 vlan_created == vlan_creation_expected and
                 add_appears == add_keyword_expected)
@@ -288,6 +293,24 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
             expected_http = wexc.HTTPInternalServerError.code
         self.assertEqual(status, expected_http)
 
+    def _mock_config_first_trunk(self):
+        """Mock the behavior for the first VLAN addition.
+
+        When the first VLAN is being added to the interface the usage of
+        the ADD keyword should raise an exception specifying that the ADD
+        keyword cannot be used till a VLAN list is created and to create
+        a VLAN list the configuration should not contain the ADD keyword.
+
+        """
+        config = "switchport trunk allowed vlan add"
+        exc = Exception("switchport trunk allowed vlan list is empty, "
+                        "please specify a list using "
+                        "'switchport trunk allowed vlan X' "
+                        "before using the add option")
+        return (self._patch_ncclient(
+                'connect.return_value.edit_config.side_effect',
+                self._config_dependent_side_effect(config, exc)))
+
     def test_create_ports_bulk_emulated_plugin_failure(self):
         real_has_attr = hasattr
 
@@ -351,44 +374,90 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                     'ports',
                     wexc.HTTPInternalServerError.code)
 
-    def test_nexus_enable_vlan_cmd(self):
+    def test_nexus_enable_vlan_cmd_on_same_host(self):
         """Verify the syntax of the command to enable a vlan on an intf.
 
         Test of the following ml2_conf_cisco_ini config:
         [ml2_mech_cisco_nexus:1.1.1.1]
-        hostA=1/1
-        hostB=1/2 (second pass only)
-        where vlan_id = 100
+        Resource A on host=COMP_HOST_NAME with vlan_id = 1000
+        Resource B on host=COMP_HOST_NAME with vlan_id = 1001
 
-        Confirm that for the first VLAN configured on a Nexus interface,
-        the command string sent to the switch does not contain the
-        keyword 'add'.
+        Confirm that when configuring the first VLAN on a Nexus interface,
+        the final command string sent to the switch does not contain the
+        keyword 'add'. The initial attempt will contain 'add' but when
+        the switch rejects it, the re-attempt shouldn't contain the 'add'.
 
         Confirm that for the second VLAN configured on a Nexus interface,
         the command string sent to the switch contains the keyword 'add'
-        if it is on the same host. Confirm it does not contain the
-        keyword 'add' when on different hosts.
+        since it is on the same host.
+
+        """
+        # First vlan should be configured without 'add' keyword and an
+        # exception should be raised when it is done with the 'add'
+        # thereby triggering a re-attempt without the 'add'.
+        with self._mock_config_first_trunk():
+            with self._create_resources():
+                self.assertTrue(self._is_vlan_configured(
+                        vlan_creation_expected=True,
+                        first_vlan_addition=True))
+                self.mock_ncclient.reset_mock()
+                self.mock_bound_segment.return_value = BOUND_SEGMENT2
+
+                # Second vlan should be configured with the 'add' keyword
+                # when on first host.
+                with(self._patch_ncclient(
+                        'connect.return_value.edit_config.side_effect',
+                        None)):
+                    with self._create_resources(name=NETWORK_NAME_2,
+                                                device_id=DEVICE_ID_2,
+                                                cidr=CIDR_2,
+                                                host_id=COMP_HOST_NAME):
+                        self.assertTrue(self._is_vlan_configured(
+                                vlan_creation_expected=True,
+                                first_vlan_addition=False
+                        ))
+
+                    # Return to first segment for delete port calls.
+                    self.mock_bound_segment.return_value = BOUND_SEGMENT1
+
+    def test_nexus_enable_vlan_cmd_on_different_hosts(self):
+        """Verify the syntax of the command to enable a vlan on an intf.
+
+        Test of the following ml2_conf_cisco_ini config:
+        [ml2_mech_cisco_nexus:1.1.1.1]
+        Resource A on host=COMP_HOST_NAME with vlan_id = 1000
+        Resource B on host=COMP_HOST_NAME_2 with vlan_id = 1001
+
+        Confirm that when configuring the first VLAN on a Nexus interface,
+        the final command string sent to the switch does not contain the
+        keyword 'add'. The initial attempt will contain 'add' but when
+        the switch rejects it, the re-attempt shouldn't contain the 'add'.
+
+        Confirm that for the second VLAN configured on a Nexus interface,
+        the command string sent to the switch does not contain the
+        keyword 'add' since it is on a different host.
 
         """
-        hosts = [COMP_HOST_NAME, COMP_HOST_NAME_2]
-        for host in hosts:
-            # First vlan should be configured without 'add' keyword
+        # First vlan should be configured without 'add' keyword and an
+        # exception should be raised when it is done with the 'add'
+        # thereby triggering a re-attempt without the 'add'.
+        with self._mock_config_first_trunk():
             with self._create_resources():
                 self.assertTrue(self._is_vlan_configured(
-                    vlan_creation_expected=True,
-                    add_keyword_expected=False))
+                        vlan_creation_expected=True,
+                        first_vlan_addition=True))
                 self.mock_ncclient.reset_mock()
                 self.mock_bound_segment.return_value = BOUND_SEGMENT2
 
-                # Second vlan should be configured with 'add' keyword
-                # when on a single host.
+                # Second vlan should be configured without the 'add' keyword
+                # when on second host.
                 with self._create_resources(name=NETWORK_NAME_2,
-                    device_id=DEVICE_ID_2,
-                    cidr=CIDR_2,
-                    host_id=host):
+                                            device_id=DEVICE_ID_2,
+                                            cidr=CIDR_2,
+                                            host_id=COMP_HOST_NAME_2):
                     self.assertTrue(self._is_vlan_configured(
-                        vlan_creation_expected=True,
-                        add_keyword_expected=(host == COMP_HOST_NAME)
+                            vlan_creation_expected=True,
+                            first_vlan_addition=True
                     ))
 
                 # Return to first segment for delete port calls.
@@ -463,35 +532,38 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                 req.get_response(self.api)
                 self.assertTrue(self._is_vlan_configured(
                     vlan_creation_expected=vlan_creation_expected,
-                    add_keyword_expected=False))
+                    first_vlan_addition=True))
                 self.mock_ncclient.reset_mock()
                 yield
             self._delete('ports', port['port']['id'])
 
         # Create network and subnet
-        with self.network(name=NETWORK_NAME) as network:
-            with self.subnet(network=network, cidr=CIDR_1) as subnet:
-
-                # Create an instance on first compute host
-                with _create_port_check_vlan(COMP_HOST_NAME, DEVICE_ID_1,
-                                             vlan_creation_expected=True):
-                    # Create an instance on second compute host
-                    with _create_port_check_vlan(COMP_HOST_NAME_2, DEVICE_ID_2,
-                                                 vlan_creation_expected=False):
-                        pass
-
-                    # Instance on second host is now terminated.
-                    # Vlan should be untrunked from port, but vlan should
-                    # still exist on the switch.
+        with self._mock_config_first_trunk():
+            with self.network(name=NETWORK_NAME) as network:
+                with self.subnet(network=network, cidr=CIDR_1) as subnet:
+
+                    # Create an instance on first compute host
+                    with _create_port_check_vlan(COMP_HOST_NAME, DEVICE_ID_1,
+                                                 vlan_creation_expected=True):
+                        # Create an instance on second compute host
+                        with _create_port_check_vlan(
+                            COMP_HOST_NAME_2,
+                            DEVICE_ID_2,
+                            vlan_creation_expected=False):
+                            pass
+
+                        # Instance on second host is now terminated.
+                        # Vlan should be untrunked from port, but vlan should
+                        # still exist on the switch.
+                        self.assertTrue(self._is_vlan_unconfigured(
+                                vlan_deletion_expected=False))
+                        self.mock_ncclient.reset_mock()
+
+                    # Instance on first host is now terminated.
+                    # Vlan should be untrunked from port and vlan should have
+                    # been deleted from the switch.
                     self.assertTrue(self._is_vlan_unconfigured(
-                        vlan_deletion_expected=False))
-                    self.mock_ncclient.reset_mock()
-
-                # Instance on first host is now terminated.
-                # Vlan should be untrunked from port and vlan should have
-                # been deleted from the switch.
-                self.assertTrue(self._is_vlan_unconfigured(
-                    vlan_deletion_expected=True))
+                            vlan_deletion_expected=True))
 
     def test_nexus_vm_migration(self):
         """Verify VM (live) migration.