]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 Cisco Nexus MD: VM migration support
authorRich Curran <rcurran@cisco.com>
Thu, 6 Mar 2014 21:26:27 +0000 (16:26 -0500)
committerRich Curran <rcurran@cisco.com>
Thu, 6 Mar 2014 21:26:27 +0000 (16:26 -0500)
Add VM (live) migration support to the ML2 cisco nexus mechanism driver.

Change-Id: I8be2fc1f020ef1fc6c19daba0a9e278629046016
Closes-Bug: #1247976

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

index a5153f7aa350f23d1d5763d68dedd40275712582..d36f0d3f04f188a58c1d1a0c8855f23f2a0be402 100644 (file)
@@ -56,11 +56,10 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
                 cfg.CONF.ml2_cisco.managed_physical_network ==
                 segment[api.PHYSICAL_NETWORK])
 
-    def _get_vlanid(self, context):
-        segment = context.bound_segment
+    def _get_vlanid(self, segment):
         if (segment and segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN and
             self._valid_network_segment(segment)):
-            return context.bound_segment.get(api.SEGMENTATION_ID)
+            return segment.get(api.SEGMENTATION_ID)
 
     def _is_deviceowner_compute(self, port):
         return port['device_owner'].startswith('compute')
@@ -76,24 +75,22 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
         else:
             raise excep.NexusComputeHostNotConfigured(host=host_id)
 
-    def _configure_nxos_db(self, context, vlan_id, device_id, host_id):
+    def _configure_nxos_db(self, vlan_id, device_id, host_id):
         """Create the nexus database entry.
 
         Called during update precommit port event.
-
         """
         port_id, switch_ip = self._get_switch_info(host_id)
         nxos_db.add_nexusport_binding(port_id, str(vlan_id), switch_ip,
                                       device_id)
 
-    def _configure_switch_entry(self, context, vlan_id, device_id, host_id):
+    def _configure_switch_entry(self, vlan_id, device_id, host_id):
         """Create a nexus switch entry.
 
         if needed, create a VLAN in the appropriate switch/port and
         configure the appropriate interfaces for this VLAN.
 
         Called during update postcommit port event.
-
         """
         port_id, switch_ip = self._get_switch_info(host_id)
         vlan_name = cfg.CONF.ml2_cisco.vlan_name_prefix + str(vlan_id)
@@ -109,11 +106,10 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
             LOG.debug(_("Nexus: trunk vlan %s"), vlan_name)
             self.driver.enable_vlan_on_trunk_int(switch_ip, vlan_id, port_id)
 
-    def _delete_nxos_db(self, context, vlan_id, device_id, host_id):
+    def _delete_nxos_db(self, vlan_id, device_id, host_id):
         """Delete the nexus database entry.
 
         Called during delete precommit port event.
-
         """
         try:
             row = nxos_db.get_nexusvm_binding(vlan_id, device_id)
@@ -122,14 +118,13 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
         except excep.NexusPortBindingNotFound:
             return
 
-    def _delete_switch_entry(self, context, vlan_id, device_id, host_id):
+    def _delete_switch_entry(self, vlan_id, device_id, host_id):
         """Delete the nexus switch entry.
 
         By accessing the current db entries determine if switch
         configuration can be removed.
 
         Called during update postcommit port event.
-
         """
         port_id, switch_ip = self._get_switch_info(host_id)
 
@@ -147,20 +142,25 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
             except excep.NexusPortBindingNotFound:
                 self.driver.delete_vlan(switch_ip, vlan_id)
 
-    def _port_action(self, context, func):
+    def _is_vm_migration(self, context):
+        if not context.bound_segment and context.original_bound_segment:
+            return (context.current.get(portbindings.HOST_ID) !=
+                    context.original.get(portbindings.HOST_ID))
+
+    def _port_action(self, port, segment, func):
         """Verify configuration and then process event."""
-        device_id = context.current.get('device_id')
-        host_id = context.current.get(portbindings.HOST_ID)
+        device_id = port.get('device_id')
+        host_id = port.get(portbindings.HOST_ID)
 
         # Workaround until vlan can be retrieved during delete_port_postcommit
         # (or prehaps unbind_port) event.
         if func == self._delete_switch_entry:
             vlan_id = self._delete_port_postcommit_vlan
         else:
-            vlan_id = self._get_vlanid(context)
+            vlan_id = self._get_vlanid(segment)
 
         if vlan_id and device_id and host_id:
-            func(context, vlan_id, device_id, host_id)
+            func(vlan_id, device_id, host_id)
         else:
             fields = "vlan_id " if not vlan_id else ""
             fields += "device_id " if not device_id else ""
@@ -176,22 +176,46 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
 
     def update_port_precommit(self, context):
         """Update port pre-database transaction commit event."""
-        port = context.current
-        if self._is_deviceowner_compute(port) and self._is_status_active(port):
-            self._port_action(context, self._configure_nxos_db)
+
+        # if VM migration is occurring then remove previous database entry
+        # else process update event.
+        if self._is_vm_migration(context):
+            self._port_action(context.original,
+                              context.original_bound_segment,
+                              self._delete_nxos_db)
+        else:
+            if (self._is_deviceowner_compute(context.current) and
+                self._is_status_active(context.current)):
+                self._port_action(context.current,
+                                  context.bound_segment,
+                                  self._configure_nxos_db)
 
     def update_port_postcommit(self, context):
         """Update port non-database commit event."""
-        port = context.current
-        if self._is_deviceowner_compute(port) and self._is_status_active(port):
-            self._port_action(context, self._configure_switch_entry)
+
+        # if VM migration is occurring then remove previous nexus switch entry
+        # else process update event.
+        if self._is_vm_migration(context):
+            self._port_action(context.original,
+                              context.original_bound_segment,
+                              self._delete_switch_entry)
+        else:
+            if (self._is_deviceowner_compute(context.current) and
+                self._is_status_active(context.current)):
+                self._port_action(context.current,
+                                  context.bound_segment,
+                                  self._configure_switch_entry)
 
     def delete_port_precommit(self, context):
         """Delete port pre-database commit event."""
         if self._is_deviceowner_compute(context.current):
-            self._port_action(context, self._delete_nxos_db)
+            self._port_action(context.current,
+                              context.bound_segment,
+                              self._delete_nxos_db)
 
     def delete_port_postcommit(self, context):
         """Delete port non-database commit event."""
         if self._is_deviceowner_compute(context.current):
-            self._port_action(context, self._delete_switch_entry)
+            self._port_action(context.current,
+                              context.bound_segment,
+                              self._delete_switch_entry)
index e90333634aad783b83ac1bc1296470e78f485015..8ebfd28e84a5fd58296809cdd9fef850345e9319 100644 (file)
@@ -24,14 +24,19 @@ from neutron import context
 from neutron.extensions import portbindings
 from neutron.manager import NeutronManager
 from neutron.openstack.common import log as logging
+from neutron.plugins.common import constants as p_const
 from neutron.plugins.ml2 import config as ml2_config
+from neutron.plugins.ml2 import driver_api as api
+from neutron.plugins.ml2 import driver_context
 from neutron.plugins.ml2.drivers.cisco.nexus import config as cisco_config
 from neutron.plugins.ml2.drivers.cisco.nexus import exceptions as c_exc
 from neutron.plugins.ml2.drivers.cisco.nexus import mech_cisco_nexus
+from neutron.plugins.ml2.drivers.cisco.nexus import nexus_db_v2
 from neutron.plugins.ml2.drivers.cisco.nexus import nexus_network_driver
 from neutron.plugins.ml2.drivers import type_vlan as vlan_config
 from neutron.tests.unit import test_db_plugin
 
+
 LOG = logging.getLogger(__name__)
 ML2_PLUGIN = 'neutron.plugins.ml2.plugin.Ml2Plugin'
 PHYS_NET = 'physnet1'
@@ -49,6 +54,12 @@ 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'
+BOUND_SEGMENT1 = {api.NETWORK_TYPE: p_const.TYPE_VLAN,
+                  api.PHYSICAL_NETWORK: PHYS_NET,
+                  api.SEGMENTATION_ID: VLAN_START}
+BOUND_SEGMENT2 = {api.NETWORK_TYPE: p_const.TYPE_VLAN,
+                  api.PHYSICAL_NETWORK: PHYS_NET,
+                  api.SEGMENTATION_ID: VLAN_START + 1}
 
 
 class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
@@ -102,24 +113,24 @@ class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
                           '_import_ncclient',
                           return_value=self.mock_ncclient).start()
 
-        # Mock port values for 'status' and 'binding:segmentation_id'
+        # Mock port context values for bound_segments and 'status'.
+        self.mock_bound_segment = mock.patch.object(
+            driver_context.PortContext,
+            'bound_segment',
+            new_callable=mock.PropertyMock).start()
+        self.mock_bound_segment.return_value = BOUND_SEGMENT1
+
+        self.mock_original_bound_segment = mock.patch.object(
+            driver_context.PortContext,
+            'original_bound_segment',
+            new_callable=mock.PropertyMock).start()
+        self.mock_original_bound_segment.return_value = None
+
         mock_status = mock.patch.object(
             mech_cisco_nexus.CiscoNexusMechanismDriver,
             '_is_status_active').start()
         mock_status.return_value = n_const.PORT_STATUS_ACTIVE
 
-        def _mock_get_vlanid(context):
-            network = context.network.current
-            if network['name'] == NETWORK_NAME:
-                return VLAN_START
-            else:
-                return VLAN_START + 1
-
-        mock_vlanid = mock.patch.object(
-            mech_cisco_nexus.CiscoNexusMechanismDriver,
-            '_get_vlanid').start()
-        mock_vlanid.side_effect = _mock_get_vlanid
-
         super(CiscoML2MechanismTestCase, self).setUp(ML2_PLUGIN)
 
         self.port_create_status = 'DOWN'
@@ -213,8 +224,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                                      'admin_state_up': True}}
                     req = self.new_update_request('ports', data,
                                                   port['port']['id'])
-                    res = req.get_response(self.api)
-                    yield res.status_int
+                    yield req.get_response(self.api)
 
     def _assertExpectedHTTP(self, status, exc):
         """Confirm that an HTTP status corresponds to an expected exception.
@@ -313,6 +323,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                 vlan_creation_expected=True,
                 add_keyword_expected=False))
             self.mock_ncclient.reset_mock()
+            self.mock_bound_segment.return_value = BOUND_SEGMENT2
 
             # Second vlan should be configured with 'add' keyword
             with self._create_resources(name=NETWORK_NAME_2,
@@ -322,6 +333,9 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                     vlan_creation_expected=True,
                     add_keyword_expected=True))
 
+            # Return to first segment for delete port calls.
+            self.mock_bound_segment.return_value = BOUND_SEGMENT1
+
     def test_nexus_connect_fail(self):
         """Test failure to connect to a Nexus switch.
 
@@ -332,8 +346,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         """
         with self._patch_ncclient('connect.side_effect',
                                   AttributeError):
-            with self._create_resources() as result_status:
-                self._assertExpectedHTTP(result_status,
+            with self._create_resources() as result:
+                self._assertExpectedHTTP(result.status_int,
                                          c_exc.NexusConnectFailed)
 
     def test_nexus_vlan_config_two_hosts(self):
@@ -381,6 +395,62 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                 self.assertTrue(self._is_vlan_unconfigured(
                     vlan_deletion_expected=True))
 
+    def test_nexus_vm_migration(self):
+        """Verify VM (live) migration.
+
+        Simulate the following:
+        Nova informs neutron of live-migration with port-update(new host).
+        This should trigger two update_port_pre/postcommit() calls.
+
+        The first one should only change the current host_id and remove the
+        binding resulting in the mechanism drivers receiving:
+          PortContext.original['binding:host_id']: previous value
+          PortContext.original_bound_segment: previous value
+          PortContext.current['binding:host_id']: current (new) value
+          PortContext.bound_segment: None
+
+        The second one binds the new host resulting in the mechanism
+        drivers receiving:
+          PortContext.original['binding:host_id']: previous value
+          PortContext.original_bound_segment: None
+          PortContext.current['binding:host_id']: previous value
+          PortContext.bound_segment: new value
+        """
+
+        # Create network, subnet and port.
+        with self._create_resources() as result:
+            # Verify initial database entry.
+            # Use port_id to verify that 1st host name was used.
+            binding = nexus_db_v2.get_nexusvm_binding(VLAN_START, DEVICE_ID_1)
+            self.assertEqual(binding.port_id, NEXUS_INTERFACE)
+
+            port = self.deserialize(self.fmt, result)
+            port_id = port['port']['id']
+
+            # Trigger update event to unbind segment.
+            # Results in port being deleted from nexus DB and switch.
+            data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME_2}}
+            self.mock_bound_segment.return_value = None
+            self.mock_original_bound_segment.return_value = BOUND_SEGMENT1
+            self.new_update_request('ports', data,
+                                    port_id).get_response(self.api)
+
+            # Verify that port entry has been deleted.
+            self.assertRaises(c_exc.NexusPortBindingNotFound,
+                              nexus_db_v2.get_nexusvm_binding,
+                              VLAN_START, DEVICE_ID_1)
+
+            # Trigger update event to bind segment with new host.
+            self.mock_bound_segment.return_value = BOUND_SEGMENT1
+            self.mock_original_bound_segment.return_value = None
+            self.new_update_request('ports', data,
+                                    port_id).get_response(self.api)
+
+            # Verify that port entry has been added using new host name.
+            # Use port_id to verify that 2nd host name was used.
+            binding = nexus_db_v2.get_nexusvm_binding(VLAN_START, DEVICE_ID_1)
+            self.assertEqual(binding.port_id, NEXUS_INTERFACE_2)
+
     def test_nexus_config_fail(self):
         """Test a Nexus switch configuration failure.
 
@@ -392,8 +462,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             AttributeError):
-            with self._create_resources() as result_status:
-                self._assertExpectedHTTP(result_status,
+            with self._create_resources() as result:
+                self._assertExpectedHTTP(result.status_int,
                                          c_exc.NexusConfigFailed)
 
     def test_nexus_extended_vlan_range_failure(self):
@@ -412,8 +482,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config_a):
-            with self._create_resources() as result_status:
-                self.assertEqual(result_status, wexc.HTTPOk.code)
+            with self._create_resources() as result:
+                self.assertEqual(result.status_int, wexc.HTTPOk.code)
 
         def mock_edit_config_b(target, config):
             if all(word in config for word in ['no', 'shutdown']):
@@ -422,8 +492,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config_b):
-            with self._create_resources() as result_status:
-                self.assertEqual(result_status, wexc.HTTPOk.code)
+            with self._create_resources() as result:
+                self.assertEqual(result.status_int, wexc.HTTPOk.code)
 
     def test_nexus_vlan_config_rollback(self):
         """Test rollback following Nexus VLAN state config failure.
@@ -440,11 +510,11 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config):
-            with self._create_resources() as result_status:
+            with self._create_resources() as result:
                 # Confirm that the last configuration sent to the Nexus
                 # switch was deletion of the VLAN.
                 self.assertTrue(self._is_in_last_nexus_cfg(['<no>', '<vlan>']))
-                self._assertExpectedHTTP(result_status,
+                self._assertExpectedHTTP(result.status_int,
                                          c_exc.NexusConfigFailed)
 
     def test_nexus_host_not_configured(self):
@@ -454,8 +524,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         a fictitious host name during port creation.
 
         """
-        with self._create_resources(host_id='fake_host') as result_status:
-            self._assertExpectedHTTP(result_status,
+        with self._create_resources(host_id='fake_host') as result:
+            self._assertExpectedHTTP(result.status_int,
                                      c_exc.NexusComputeHostNotConfigured)
 
     def test_nexus_missing_fields(self):
@@ -465,8 +535,8 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         empty host_id and device_id values during port creation.
 
         """
-        with self._create_resources(device_id='', host_id='') as result_status:
-            self._assertExpectedHTTP(result_status,
+        with self._create_resources(device_id='', host_id='') as result:
+            self._assertExpectedHTTP(result.status_int,
                                      c_exc.NexusMissingRequiredFields)