From 5e8043e8a2675b4485a52d688ea86d1165d81a29 Mon Sep 17 00:00:00 2001 From: Rich Curran Date: Thu, 6 Mar 2014 16:26:27 -0500 Subject: [PATCH] ML2 Cisco Nexus MD: VM migration support Add VM (live) migration support to the ML2 cisco nexus mechanism driver. Change-Id: I8be2fc1f020ef1fc6c19daba0a9e278629046016 Closes-Bug: #1247976 --- .../drivers/cisco/nexus/mech_cisco_nexus.py | 72 ++++++---- .../drivers/cisco/nexus/test_cisco_mech.py | 128 ++++++++++++++---- 2 files changed, 147 insertions(+), 53 deletions(-) diff --git a/neutron/plugins/ml2/drivers/cisco/nexus/mech_cisco_nexus.py b/neutron/plugins/ml2/drivers/cisco/nexus/mech_cisco_nexus.py index a5153f7aa..d36f0d3f0 100644 --- a/neutron/plugins/ml2/drivers/cisco/nexus/mech_cisco_nexus.py +++ b/neutron/plugins/ml2/drivers/cisco/nexus/mech_cisco_nexus.py @@ -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) diff --git a/neutron/tests/unit/ml2/drivers/cisco/nexus/test_cisco_mech.py b/neutron/tests/unit/ml2/drivers/cisco/nexus/test_cisco_mech.py index e90333634..8ebfd28e8 100644 --- a/neutron/tests/unit/ml2/drivers/cisco/nexus/test_cisco_mech.py +++ b/neutron/tests/unit/ml2/drivers/cisco/nexus/test_cisco_mech.py @@ -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(['', ''])) - 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) -- 2.45.2