From 5499e1b13a396a28ae3b69dd569543334411494d Mon Sep 17 00:00:00 2001 From: Drew Thorstensen Date: Tue, 2 Aug 2016 14:57:02 -0400 Subject: [PATCH] Pre-allocate trunk adapter on target host This change will allocate the trunk adapter on the target server as part of its live migration command when paired with OVS. This reduces the amount of down time when running a live migration with Open vSwitch as the VIF type. Closes-Bug: #1632366 Change-Id: I43f06ecd827bb1d08636c6e53efcad292d827b20 --- nova_powervm/objects/migrate_data.py | 4 +- .../tests/virt/powervm/test_driver.py | 4 + .../tests/virt/powervm/test_live_migration.py | 68 ++++-- nova_powervm/tests/virt/powervm/test_vif.py | 171 ++++++++------ nova_powervm/virt/powervm/driver.py | 7 + nova_powervm/virt/powervm/live_migration.py | 72 +++++- nova_powervm/virt/powervm/vif.py | 222 +++++++++++++----- 7 files changed, 382 insertions(+), 166 deletions(-) diff --git a/nova_powervm/objects/migrate_data.py b/nova_powervm/objects/migrate_data.py index e00de247..7b9851b3 100644 --- a/nova_powervm/objects/migrate_data.py +++ b/nova_powervm/objects/migrate_data.py @@ -23,7 +23,8 @@ from nova.objects import migrate_data @obj_base.NovaObjectRegistry.register class PowerVMLiveMigrateData(migrate_data.LiveMigrateData): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Added the Virtual Ethernet Adapter VLAN mappings. + VERSION = '1.1' fields = { 'host_mig_data': fields.DictOfNullableStringsField(), @@ -33,4 +34,5 @@ class PowerVMLiveMigrateData(migrate_data.LiveMigrateData): 'public_key': fields.StringField(), 'dest_proc_compat': fields.StringField(), 'vol_data': fields.DictOfNullableStringsField(), + 'vea_vlan_mappings': fields.DictOfNullableStringsField(), } diff --git a/nova_powervm/tests/virt/powervm/test_driver.py b/nova_powervm/tests/virt/powervm/test_driver.py index 16393de0..97349b08 100644 --- a/nova_powervm/tests/virt/powervm/test_driver.py +++ b/nova_powervm/tests/virt/powervm/test_driver.py @@ -1936,6 +1936,10 @@ class TestPowerVMDriver(test.TestCase): def test_rollbk_lpm_dest(self): self.drv.rollback_live_migration_at_destination( 'context', self.lpm_inst, 'network_info', 'block_device_info') + mock_rollback = self.lpm.rollback_live_migration_at_destination + mock_rollback.assert_called_once_with( + 'context', self.lpm_inst, 'network_info', 'block_device_info', + destroy_disks=True, migrate_data=None) self.assertRaises( KeyError, lambda: self.drv.live_migrations[self.lpm_inst.uuid]) diff --git a/nova_powervm/tests/virt/powervm/test_live_migration.py b/nova_powervm/tests/virt/powervm/test_live_migration.py index 7538da0b..64705b01 100644 --- a/nova_powervm/tests/virt/powervm/test_live_migration.py +++ b/nova_powervm/tests/virt/powervm/test_live_migration.py @@ -23,6 +23,7 @@ import mock from nova import exception from nova import objects from nova import test +from nova.tests.unit import fake_network from nova_powervm.objects import migrate_data as mig_obj from nova_powervm.tests.virt import powervm @@ -41,6 +42,10 @@ class TestLPM(test.TestCase): self.inst = objects.Instance(**powervm.TEST_INSTANCE) + self.network_infos = fake_network.fake_get_instance_nw_info(self, 1) + self.inst.info_cache = objects.InstanceInfoCache( + network_info=self.network_infos) + self.mig_data = mig_obj.PowerVMLiveMigrateData() self.mig_data.host_mig_data = {} self.mig_data.dest_ip = '1' @@ -49,6 +54,7 @@ class TestLPM(test.TestCase): self.mig_data.public_key = 'PublicKey' self.mig_data.dest_proc_compat = 'a,b,c' self.mig_data.vol_data = {} + self.mig_data.vea_vlan_mappings = {} self.lpmsrc = lpm.LiveMigrationSrc(self.drv, self.inst, self.mig_data) self.lpmdst = lpm.LiveMigrationDest(self.drv, self.inst) @@ -151,12 +157,30 @@ class TestLPM(test.TestCase): src_compute_info, dst_compute_info) @mock.patch('pypowervm.tasks.storage.ComprehensiveScrub') - def test_pre_live_mig(self, mock_scrub): + @mock.patch('nova_powervm.virt.powervm.vif.' + 'pre_live_migrate_at_destination') + def test_pre_live_mig(self, mock_vif_pre, mock_scrub): vol_drv = mock.MagicMock() + network_infos = [{'type': 'pvm_sea'}] + + def update_vea_mapping(adapter, host_uuid, instance, network_info, + vea_vlan_mappings): + # Make sure what comes in is None, but that we change it. + self.assertEqual(vea_vlan_mappings, {}) + vea_vlan_mappings['test'] = 'resp' + + mock_vif_pre.side_effect = update_vea_mapping + resp = self.lpmdst.pre_live_migration( - 'context', 'block_device_info', 'network_info', 'disk_info', + 'context', 'block_device_info', network_infos, 'disk_info', self.mig_data, [vol_drv]) + # Make sure the pre_live_migrate_at_destination was invoked for the vif + mock_vif_pre.assert_called_once_with( + self.drv.adapter, self.drv.host_uuid, self.inst, network_infos[0], + mock.ANY) + self.assertEqual({'test': 'resp'}, self.mig_data.vea_vlan_mappings) + # Make sure we get something back, and that the volume driver was # invoked. self.assertIsNotNone(resp) @@ -171,7 +195,7 @@ class TestLPM(test.TestCase): Exception('foo')) self.assertRaises( exception.MigrationPreCheckError, self.lpmdst.pre_live_migration, - 'context', 'block_device_info', 'network_info', 'disk_info', + 'context', 'block_device_info', network_infos, 'disk_info', self.mig_data, [vol_drv, raising_vol_drv]) vol_drv.pre_live_migration_on_destination.assert_called_once_with({}) (raising_vol_drv.pre_live_migration_on_destination. @@ -185,7 +209,14 @@ class TestLPM(test.TestCase): vol_drv.cleanup_volume_at_destination.assert_called_once_with({}) @mock.patch('pypowervm.tasks.migration.migrate_lpar') - def test_live_migration(self, mock_migr): + @mock.patch('nova_powervm.virt.powervm.live_migration.LiveMigrationSrc.' + '_convert_nl_io_mappings') + @mock.patch('nova_powervm.virt.powervm.vif.pre_live_migrate_at_source') + def test_live_migration(self, mock_vif_pre_lpm, mock_convert_mappings, + mock_migr): + mock_trunk = mock.MagicMock() + mock_vif_pre_lpm.return_value = [mock_trunk] + mock_convert_mappings.return_value = ['AABBCCDDEEFF/5'] self.lpmsrc.lpar_w = mock.Mock() self.lpmsrc.live_migration('context', self.mig_data) @@ -193,7 +224,12 @@ class TestLPM(test.TestCase): self.lpmsrc.lpar_w, 'a', sdn_override=True, tgt_mgmt_svr='1', tgt_mgmt_usr='neo', validate_only=False, virtual_fc_mappings=None, virtual_scsi_mappings=None, - vlan_check_override=True) + vlan_check_override=True, vlan_mappings=['AABBCCDDEEFF/5']) + + # Network assertions + mock_vif_pre_lpm.assert_called_once_with( + self.drv.adapter, self.drv.host_uuid, self.inst, mock.ANY) + mock_trunk.delete.assert_called_once() # Test that we raise errors received during migration mock_migr.side_effect = ValueError() @@ -203,20 +239,18 @@ class TestLPM(test.TestCase): self.lpmsrc.lpar_w, 'a', sdn_override=True, tgt_mgmt_svr='1', tgt_mgmt_usr='neo', validate_only=False, virtual_fc_mappings=None, virtual_scsi_mappings=None, - vlan_check_override=True) + vlan_mappings=['AABBCCDDEEFF/5'], vlan_check_override=True) - @mock.patch('nova_powervm.virt.powervm.vif.post_live_migrate_at_source') - def test_post_live_mig_src(self, mock_post_migrate_vif): - network_infos = [mock.Mock(), mock.Mock()] - self.lpmsrc.post_live_migration_at_source(network_infos) - self.assertEqual(2, mock_post_migrate_vif.call_count) + def test_convert_nl_io_mappings(self): + # Test simple None case + self.assertIsNone(self.lpmsrc._convert_nl_io_mappings(None)) - @mock.patch('nova_powervm.virt.powervm.vif.' - 'post_live_migrate_at_destination') - def test_post_live_mig_dest(self, mock_post_migrate_vif): - network_infos = [mock.Mock(), mock.Mock()] - self.lpmdst.post_live_migration_at_destination(network_infos, []) - self.assertEqual(2, mock_post_migrate_vif.call_count) + # Do some mappings + test_mappings = {'aa:bb:cc:dd:ee:ff': 5, 'aa:bb:cc:dd:ee:ee': 126} + expected = ['AABBCCDDEEFF/5', 'AABBCCDDEEEE/126'] + self.assertEqual( + set(expected), + set(self.lpmsrc._convert_nl_io_mappings(test_mappings))) @mock.patch('pypowervm.tasks.migration.migrate_recover') def test_rollback(self, mock_migr): diff --git a/nova_powervm/tests/virt/powervm/test_vif.py b/nova_powervm/tests/virt/powervm/test_vif.py index 6ef41aeb..ae1c095d 100644 --- a/nova_powervm/tests/virt/powervm/test_vif.py +++ b/nova_powervm/tests/virt/powervm/test_vif.py @@ -18,15 +18,17 @@ import mock from nova import exception from nova import test +from oslo_config import cfg from pypowervm import exceptions as pvm_ex from pypowervm.tests import test_fixtures as pvm_fx -from pypowervm import util as pvm_util from pypowervm.wrappers import logical_partition as pvm_lpar from pypowervm.wrappers import managed_system as pvm_ms from pypowervm.wrappers import network as pvm_net from nova_powervm.virt.powervm import vif +CONF = cfg.CONF + def cna(mac): """Builds a mock Client Network Adapter for unit tests.""" @@ -249,6 +251,29 @@ class TestVifFunctions(test.TestCase): vif._build_vif_driver, self.adpt, 'host_uuid', mock_inst, {'type': 'bad'}) + @mock.patch('nova_powervm.virt.powervm.vif._build_vif_driver') + def test_pre_live_migrate_at_source(self, mock_build_vif_drv): + mock_drv = mock.MagicMock() + mock_build_vif_drv.return_value = mock_drv + mock_vif = mock.MagicMock() + + vif.pre_live_migrate_at_source(self.adpt, 'host_uuid', mock.Mock(), + mock_vif) + + mock_drv.pre_live_migrate_at_source.assert_called_once_with(mock_vif) + + @mock.patch('nova_powervm.virt.powervm.vif._build_vif_driver') + def test_pre_live_migrate_at_destination(self, mock_build_vif_drv): + mock_drv = mock.MagicMock() + mock_build_vif_drv.return_value = mock_drv + mock_vif = mock.MagicMock() + + vif.pre_live_migrate_at_destination(self.adpt, 'host_uuid', + mock.Mock(), mock_vif, {}) + + mock_drv.pre_live_migrate_at_destination.assert_called_once_with( + mock_vif, {}) + class TestVifSriovDriver(test.TestCase): @@ -464,14 +489,6 @@ class TestVifSeaDriver(test.TestCase): self.assertEqual(0, cnas[1].delete.call_count) self.assertEqual(1, cnas[2].delete.call_count) - def test_post_live_migrate_at_destination(self): - # Make sure the no-op works properly - self.drv.post_live_migrate_at_destination(mock.Mock()) - - def test_post_live_migrate_at_source(self): - # Make sure the no-op works properly - self.drv.post_live_migrate_at_source(mock.Mock()) - class TestVifLBDriver(test.TestCase): @@ -691,72 +708,86 @@ class TestVifOvsDriver(test.TestCase): # Validate the OVS port delete call was made mock_del_ovs_port.assert_called_with('br-int', 'fake_dev') + @mock.patch('pypowervm.tasks.cna.find_trunks') + @mock.patch('pypowervm.wrappers.network.CNA.search') + @mock.patch('pypowervm.util.sanitize_mac_for_api') + @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') + def test_pre_live_migrate_at_source(self, mock_pvm_uuid, mock_sanitize, + mock_cna_search, mock_trunk_find): + # Set up the mocks + vif = {'address': 'aa:bb:cc:dd:ee:ff'} + mock_sanitize.return_value = 'AABBCCDDEEFF' + mock_trunk_find.return_value = 'trunk' + mock_pvm_uuid.return_value = 'pvm_uuid' + + resp = self.drv.pre_live_migrate_at_source(vif) + self.assertEqual(resp, 'trunk') + + # Make sure the APIs were called correctly + mock_sanitize.assert_called_once_with(vif['address']) + mock_cna_search.assert_called_once_with( + self.adpt, parent_type=pvm_lpar.LPAR.schema_type, + parent_uuid='pvm_uuid', one_result=True, mac='AABBCCDDEEFF') + mock_trunk_find.assert_called_once_with(self.adpt, mock.ANY) + @mock.patch('nova.network.linux_net.create_ovs_vif_port') @mock.patch('nova.utils.execute') - @mock.patch('nova_powervm.virt.powervm.vif.PvmOvsVifDriver.' - 'get_trunk_dev_name') - @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') + @mock.patch('pypowervm.tasks.cna.crt_trunk_with_free_vlan') @mock.patch('pypowervm.tasks.partition.get_this_partition') - @mock.patch('pypowervm.wrappers.network.CNA.bld') + def test_pre_live_migrate_at_destination( + self, mock_part_get, mock_trunk_crt, mock_execute, + mock_crt_ovs_port): + # Mock the vif + vif = {'devname': 'tap-dev', 'address': 'aa:bb:cc:dd:ee:ff', + 'network': {'bridge': 'br-int'}, 'id': 'vif_id'} + + # Mock out the management partition + mock_mgmt_wrap = mock.MagicMock() + mock_mgmt_wrap.uuid = 'mgmt_uuid' + mock_part_get.return_value = mock_mgmt_wrap + + mock_trunk_crt.return_value = [mock.Mock(pvid=2)] + + # Invoke and test the basic response + vea_vlan_mappings = {} + self.drv.pre_live_migrate_at_destination(vif, vea_vlan_mappings) + self.assertEqual(vea_vlan_mappings['aa:bb:cc:dd:ee:ff'], 2) + + # Now validate it called the things it needed to + mock_execute.assert_called_once_with('ip', 'link', 'set', 'tap-dev', + 'up', run_as_root=True) + mock_trunk_crt.assert_called_once_with( + self.adpt, 'host_uuid', ['mgmt_uuid'], + CONF.powervm.pvm_vswitch_for_novalink_io, dev_name='tap-dev') + mock_crt_ovs_port.assert_called_once_with( + 'br-int', 'tap-dev', 'vif_id', 'aa:bb:cc:dd:ee:ff', self.inst.uuid) + @mock.patch('pypowervm.wrappers.network.CNA.search') - @mock.patch('pypowervm.tasks.cna.assign_free_vlan') + @mock.patch('pypowervm.tasks.partition.get_this_partition') @mock.patch('pypowervm.wrappers.network.VSwitch.search') - def test_post_live_migrate_at_destination( - self, mock_vswitch_search, mock_assign_vlan, mock_cna_search, - mock_trunk_bld, mock_mgmt_lpar, mock_uuid, mock_dev_name, mock_exec, - mock_crt_ovs_vif_port): - # Mock the vif argument - def getitem(name): - return fake_vif[name] - - fake_vif = {'address': 'aa:bb:cc:dd:ee:ff', - 'network': {'bridge': 'br-int'}} - mock_vif = mock.MagicMock() - mock_vif.__getitem__.side_effect = getitem - - # Mock other values - mock_assign_vlan.return_value = mock.MagicMock(pvid=70, enabled=True) - mock_cna = mock.MagicMock(pvid=35, enabled=False) - mock_cna_search.return_value = mock_cna + def test_rollback_live_migrate_at_destination( + self, mock_vs_search, mock_get_part, mock_cna_search): + # All the fun mocking + mock_vs_search.return_value = mock.MagicMock(switch_id=5) + vea_vlan_mappings = {'aa:bb:cc:dd:ee:ff': 3, 'aa:bb:cc:dd:ee:ee': 4} + vif = {'address': 'aa:bb:cc:dd:ee:ee'} + mock_get_part.return_value = mock.MagicMock(schema_type='VIO', + uuid='uuid') mock_trunk = mock.MagicMock() - mock_trunk_bld.return_value = mock_trunk - mock_mgmt_lpar.return_value = mock.Mock(uuid='mgmt_uuid') - mock_vswitch = mock.MagicMock() - mock_vswitch_search.return_value = mock_vswitch - mock_uuid.return_value = 'lpar_uuid' - mock_dev_name.return_value = 'dev_name' - mac = pvm_util.sanitize_mac_for_api(fake_vif['address']) - self.drv.adapter = self.adpt + mock_cna_search.return_value = mock_trunk - # Execute and verify the results - self.drv.post_live_migrate_at_destination(mock_vif) - mock_assign_vlan.assert_called_once_with( - self.drv.adapter, self.drv.host_uuid, mock_vswitch, mock_cna) - mock_trunk_bld( - self.drv.adapter, pvid=70, vlan_ids=[], vswitch=mock_vswitch) - mock_trunk.create.assert_called_once_with( - parent=mock_mgmt_lpar.return_value) + # Invoke + self.drv.rollback_live_migrate_at_destination(vif, vea_vlan_mappings) + + # Make sure the trunk was deleted + mock_trunk.delete.assert_called_once() + + # Now make sure the calls were done correctly to actually produce a + # trunk adapter + mock_vs_search.assert_called_once_with( + self.drv.adapter, parent_type=pvm_ms.System, one_result=True, + name=CONF.powervm.pvm_vswitch_for_novalink_io) + mock_get_part.assert_called_once_with(self.drv.adapter) mock_cna_search.assert_called_once_with( - self.adpt, mac=mac, one_result=True, - parent_type=pvm_lpar.LPAR, parent_uuid='lpar_uuid') - - @mock.patch('pypowervm.tasks.cna.find_orphaned_trunks') - @mock.patch('nova.network.linux_net.delete_ovs_vif_port') - @mock.patch('nova_powervm.virt.powervm.vif.PvmOvsVifDriver.' - 'get_trunk_dev_name') - def test_post_live_migrate_at_source(self, mock_trunk_dev_name, - mock_del_ovs_port, - mock_find_orphan): - t1, t2 = mock.MagicMock(), mock.MagicMock() - mock_find_orphan.return_value = [t1, t2] - mock_trunk_dev_name.return_value = 'fake_dev1' - - mock_vif = {'network': {'bridge': 'br-int'}} - self.drv.post_live_migrate_at_source(mock_vif) - - # The orphans should have been deleted - self.assertTrue(t1.delete.called) - self.assertTrue(t2.delete.called) - - # Validate the OVS port delete call was made twice - mock_del_ovs_port.assert_called_once_with('br-int', 'fake_dev1') + self.drv.adapter, parent_type='VIO', parent_uuid='uuid', + vswitch_id=5, pvid=4, one_result=True) diff --git a/nova_powervm/virt/powervm/driver.py b/nova_powervm/virt/powervm/driver.py index 29a0f572..0dd4bf1a 100644 --- a/nova_powervm/virt/powervm/driver.py +++ b/nova_powervm/virt/powervm/driver.py @@ -1612,6 +1612,13 @@ class PowerVMDriver(driver.ComputeDriver): :param migrate_data: a LiveMigrateData object """ + # Run the rollback + mig = self.live_migrations[instance.uuid] + mig.rollback_live_migration_at_destination( + context, instance, network_info, block_device_info, + destroy_disks=destroy_disks, migrate_data=migrate_data) + + # Remove the active migration del self.live_migrations[instance.uuid] def check_instance_shared_storage_local(self, context, instance): diff --git a/nova_powervm/virt/powervm/live_migration.py b/nova_powervm/virt/powervm/live_migration.py index 758f4221..5eae18c1 100644 --- a/nova_powervm/virt/powervm/live_migration.py +++ b/nova_powervm/virt/powervm/live_migration.py @@ -23,6 +23,7 @@ from pypowervm.tasks import management_console as mgmt_task from pypowervm.tasks import migration as mig from pypowervm.tasks import storage as stor_task from pypowervm.tasks import vterm +from pypowervm import util import six from nova_powervm import conf as cfg @@ -129,14 +130,14 @@ class LiveMigrationDest(LiveMigration): return self.mig_data - def pre_live_migration(self, context, block_device_info, network_info, + def pre_live_migration(self, context, block_device_info, network_infos, disk_info, migrate_data, vol_drvs): """Prepare an instance for live migration :param context: security context :param block_device_info: instance block device information - :param network_info: instance network information + :param network_infos: instance network information :param disk_info: instance disk information :param migrate_data: a PowerVMLiveMigrateData object :param vol_drvs: volume drivers for the attached volumes @@ -149,6 +150,15 @@ class LiveMigrationDest(LiveMigration): mgmt_task.add_authorized_key(self.drvr.adapter, migrate_data.public_key) + # For each network info, run the pre-live migration. This tells the + # system what the target vlans will be. + vea_vlan_mappings = {} + for network_info in network_infos: + vif.pre_live_migrate_at_destination( + self.drvr.adapter, self.drvr.host_uuid, self.instance, + network_info, vea_vlan_mappings) + migrate_data.vea_vlan_mappings = vea_vlan_mappings + # For each volume, make sure it's ready to migrate for vol_drv in vol_drvs: LOG.info(_LI('Performing pre migration for volume %(volume)s'), @@ -182,12 +192,6 @@ class LiveMigrationDest(LiveMigration): LOG.debug("Post live migration at destination.", instance=self.instance) - # Run the post live migration steps at the destination - for network_info in network_infos: - vif.post_live_migrate_at_destination( - self.drvr.adapter, self.drvr.host_uuid, self.instance, - network_info) - # An unbounded dictionary that each volume adapter can use to persist # data from one call to the next. mig_vol_stor = {} @@ -205,6 +209,26 @@ class LiveMigrationDest(LiveMigration): host=self.drvr.host_wrapper.system_name, name=self.instance.name, volume=vol_drv.volume_id) + def rollback_live_migration_at_destination( + self, context, instance, network_infos, block_device_info, + destroy_disks=True, migrate_data=None): + """Clean up destination node after a failed live migration. + + :param context: security context + :param instance: instance object that was being migrated + :param network_infos: instance network infos + :param block_device_info: instance block device information + :param destroy_disks: + if true, destroy disks at destination during cleanup + :param migrate_data: a LiveMigrateData object + + """ + # Clean up any network infos + for network_info in network_infos: + vif.rollback_live_migration_at_destination( + self.drvr.adapter, self.drvr.host_uuid, self.instance, + network_info, migrate_data.vea_vlan_mappings) + def cleanup_volume(self, vol_drv): """Cleanup a volume after a failed migration. @@ -308,6 +332,7 @@ class LiveMigrationSrc(LiveMigration): # The passed in mig data has more info (dest data added), so replace self.mig_data = migrate_data + # Get the vFC and vSCSI live migration mappings vol_data = migrate_data.vol_data vfc_mappings = vol_data.get('vfc_lpm_mappings') @@ -317,6 +342,18 @@ class LiveMigrationSrc(LiveMigration): if vscsi_mappings is not None: vscsi_mappings = jsonutils.loads(vscsi_mappings) + # Run the pre-live migration on the network objects + network_infos = self.instance.info_cache.network_info + trunks_to_del = [] + for network_info in network_infos: + trunks_to_del.extend(vif.pre_live_migrate_at_source( + self.drvr.adapter, self.drvr.host_uuid, self.instance, + network_info)) + + # Convert the network mappings into something the API can understand. + vlan_mappings = self._convert_nl_io_mappings( + migrate_data.vea_vlan_mappings) + try: # Migrate the LPAR! mig.migrate_lpar( @@ -325,14 +362,27 @@ class LiveMigrationSrc(LiveMigration): tgt_mgmt_usr=self.mig_data.dest_user_id, virtual_fc_mappings=vfc_mappings, virtual_scsi_mappings=vscsi_mappings, - sdn_override=True, vlan_check_override=True) + vlan_mappings=vlan_mappings, sdn_override=True, + vlan_check_override=True) + # Delete the source side network trunk adapters + for trunk_to_del in trunks_to_del: + trunk_to_del.delete() except Exception: LOG.error(_LE("Live migration failed."), instance=self.instance) raise finally: LOG.debug("Finished migration.", instance=self.instance) + def _convert_nl_io_mappings(self, mappings): + if not mappings: + return None + + resp = [] + for mac, value in six.iteritems(mappings): + resp.append("%s/%s" % (util.sanitize_mac_for_api(mac), value)) + return resp + def post_live_migration(self, vol_drvs, migrate_data): """Post operation of live migration at source host. @@ -362,10 +412,6 @@ class LiveMigrationSrc(LiveMigration): :param network_infos: instance network information """ LOG.debug("Post live migration at source.", instance=self.instance) - for network_info in network_infos: - vif.post_live_migrate_at_source( - self.drvr.adapter, self.drvr.host_uuid, self.instance, - network_info) def rollback_live_migration(self, context): """Roll back a failed migration. diff --git a/nova_powervm/virt/powervm/vif.py b/nova_powervm/virt/powervm/vif.py index 5f0a7c9f..5336614d 100644 --- a/nova_powervm/virt/powervm/vif.py +++ b/nova_powervm/virt/powervm/vif.py @@ -15,15 +15,14 @@ # under the License. import abc -import logging import six from nova import exception from nova.network import linux_net from nova.network import model as network_model from nova import utils -from oslo_concurrency import lockutils from oslo_config import cfg +from oslo_log import log from oslo_serialization import jsonutils from oslo_utils import importutils from pypowervm import exceptions as pvm_ex @@ -43,7 +42,8 @@ from nova_powervm.virt.powervm.i18n import _LI from nova_powervm.virt.powervm.i18n import _LW from nova_powervm.virt.powervm import vm -LOG = logging.getLogger(__name__) + +LOG = log.getLogger(__name__) SECURE_RMC_VSWITCH = 'MGMTSWITCH' SECURE_RMC_VLAN = 4094 @@ -179,30 +179,57 @@ def unplug(adapter, host_uuid, instance, vif, slot_mgr, cna_w_list=None): slot_mgr.drop_vnet(vnet_w) -def post_live_migrate_at_destination(adapter, host_uuid, instance, vif): - """Performs live migrate cleanup on the destination host. +def pre_live_migrate_at_destination(adapter, host_uuid, instance, vif, + vea_vlan_mappings): + """Performs the pre live migrate on the destination host. :param adapter: The pypowervm adapter. :param host_uuid: The host UUID for the PowerVM API. :param instance: The nova instance object. - :param vif: The virtual interface that was migrated. This may be called - network_info in other portions of the code. + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif mac + address, value is the destination's target + hypervisor VLAN. """ vif_drv = _build_vif_driver(adapter, host_uuid, instance, vif) - vif_drv.post_live_migrate_at_destination(vif) + vif_drv.pre_live_migrate_at_destination(vif, vea_vlan_mappings) -def post_live_migrate_at_source(adapter, host_uuid, instance, vif): - """Performs live migrate cleanup on the source host. +def rollback_live_migration_at_destination(adapter, host_uuid, instance, vif, + vea_vlan_mappings): + """Performs the rollback of the live migrate on the destination host. :param adapter: The pypowervm adapter. :param host_uuid: The host UUID for the PowerVM API. :param instance: The nova instance object. - :param vif: The virtual interface that was migrated. This may be called - network_info in other portions of the code. + :param vif: The virtual interface that is being rolled back. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif mac + address, value is the destination's target + hypervisor VLAN. """ vif_drv = _build_vif_driver(adapter, host_uuid, instance, vif) - vif_drv.post_live_migrate_at_source(vif) + vif_drv.rollback_live_migration_at_destination(vif, vea_vlan_mappings) + + +def pre_live_migrate_at_source(adapter, host_uuid, instance, vif): + """Performs the pre live migrate on the source host. + + This is executed directly before the migration is started on the source + host. + + :param adapter: The pypowervm adapter. + :param host_uuid: The host UUID for the PowerVM API. + :param instance: The nova instance object. + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :return: The list of TrunkAdapter's on the source that are hosting the + VM's vif. Should only return data if those trunks should be + deleted after the migration. + """ + vif_drv = _build_vif_driver(adapter, host_uuid, instance, vif) + return vif_drv.pre_live_migrate_at_source(vif) def get_secure_rmc_vswitch(adapter, host_uuid): @@ -333,24 +360,45 @@ class PvmVifDriver(object): return cna_w return None - def post_live_migrate_at_destination(self, vif): - """Performs live migrate cleanup on the destination host. + def pre_live_migrate_at_destination(self, vif, vea_vlan_mappings): + """Performs the pre live migrate on the destination host. - This is optional, child classes do not need to implement this. + Pre live migrate at destination is invoked before + pre_live_migrate_at_source. - :param vif: The virtual interface that was migrated. + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif + mac address, value is the destination's + target hypervisor VLAN. """ pass - def post_live_migrate_at_source(self, vif): - """Performs live migrate cleanup on the source host. + def rollback_live_migrate_at_destination(self, vif, vea_vlan_mappings): + """Rolls back the pre live migrate on the destination host. - This is optional, child classes do not need to implement this. - - :param vif: The virtual interface that was migrated. + :param vif: The virtual interface that was being migrated. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif + mac address, value is the destination's + target hypervisor VLAN. """ pass + def pre_live_migrate_at_source(self, vif): + """Performs the pre live migrate on the source host. + + This is executed directly before the migration is started on the source + host. + + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :return: The list of TrunkAdapter's on the source that are hosting the + VM's vif. Should only return data if those trunks should be + deleted after the migration. + """ + return [] + class PvmSeaVifDriver(PvmVifDriver): """The PowerVM Shared Ethernet Adapter VIF Driver.""" @@ -661,58 +709,102 @@ class PvmOvsVifDriver(PvmLioVifDriver): # Now delete the client CNA return super(PvmOvsVifDriver, self).unplug(vif, cna_w_list=cna_w_list) - def post_live_migrate_at_destination(self, vif): - """Performs live migrate cleanup on the destination host. + def pre_live_migrate_at_destination(self, vif, vea_vlan_mappings): + """Performs the pre live migrate on the destination host. - This is optional, child classes do not need to implement this. + This method will create the trunk adapter on the destination host, + set its link state up, and attach it to the integration OVS switch. + It also updates the vea_vlan_mappings to indicate which unique + hypervisor VLAN should be used for this VIF for the migration operation + to complete properly. - :param vif: The virtual interface that was migrated. + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif + mac address, value is the destination's + target hypervisor VLAN. """ - # 1) Find a free vlan to use - # 2) Update the migrated CNA to use the new vlan that was found - # and ensure that the CNA is enabled - # 3) Create a trunk adapter on the destination of the migration - # using the same vlan as the CNA - mgmt_wrap = pvm_par.get_this_partition(self.adapter) - dev_name = self.get_trunk_dev_name(vif) - mac = pvm_util.sanitize_mac_for_api(vif['address']) + dev = self.get_trunk_dev_name(vif) - # Get vlan - vswitch_w = pvm_net.VSwitch.search( - self.adapter, parent_type=pvm_ms.System.schema_type, - one_result=True, parent_uuid=self.host_uuid, - name=CONF.powervm.pvm_vswitch_for_novalink_io) - cna = vm.get_cnas( - self.adapter, self.instance, mac=mac, one_result=True) + # Find a specific free VLAN and create the Trunk in a single atomic + # action. + cna_w = pvm_cna.crt_trunk_with_free_vlan( + self.adapter, self.host_uuid, [mgmt_wrap.uuid], + CONF.powervm.pvm_vswitch_for_novalink_io, dev_name=dev)[0] - # Assigns a free vlan (which is returned) to the cna_list - # also enable the cna - cna = pvm_cna.assign_free_vlan( - self.adapter, self.host_uuid, vswitch_w, cna) - # Create a trunk with the vlan_id - trunk_adpt = pvm_net.CNA.bld( - self.adapter, cna.pvid, vswitch_w.related_href, trunk_pri=1, - dev_name=dev_name) - trunk_adpt.create(parent=mgmt_wrap) - - utils.execute('ip', 'link', 'set', dev_name, 'up', run_as_root=True) - linux_net.create_ovs_vif_port(vif['network']['bridge'], dev_name, + # Bring the vif up. This signals to neutron that its ready for vif + # plugging + utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) + linux_net.create_ovs_vif_port(vif['network']['bridge'], dev, self.get_ovs_interfaceid(vif), vif['address'], self.instance.uuid) - @lockutils.synchronized("post_migration_pvm_ovs") - def post_live_migrate_at_source(self, vif): - """Performs live migrate cleanup on the source host. + # Save this data for the migration command. + vea_vlan_mappings[vif['address']] = cna_w.pvid + LOG.info(_LI("VIF with mac %(mac)s is going on trunk %(dev)s with " + "PVID %(pvid)s"), + {'mac': vif['address'], 'dev': dev, 'pvid': cna_w.pvid}, + instance=self.instance) - This is optional, child classes do not need to implement this. + def rollback_live_migrate_at_destination(self, vif, vea_vlan_mappings): + """Rolls back the pre live migrate on the destination host. - :param vif: The virtual interface that was migrated. + Will delete the TrunkAdapter that pre_live_migrate_at_destination + created with its unique hypervisor VLAN. This uses the + vea_vlan_mappings to provide the information as to what TrunkAdapter + it should remove. + + :param vif: The virtual interface that was being migrated. This may be + called network_info in other portions of the code. + :param vea_vlan_mappings: The VEA VLAN mappings. Key is the vif + mac address, value is the destination's + target hypervisor VLAN. """ - # Deletes orphaned trunks - orphaned_trunks = pvm_cna.find_orphaned_trunks( - self.adapter, CONF.powervm.pvm_vswitch_for_novalink_io) - dev = self.get_trunk_dev_name(vif) - linux_net.delete_ovs_vif_port(vif['network']['bridge'], dev) - for orphan in orphaned_trunks: - orphan.delete() + LOG.warning(_LW("Rolling back the live migrate of VIF with mac " + "%(mac)s."), {'mac': vif['address']}, + instance=self.instance) + + # We know that we just attached the VIF to the NovaLink VM. Search + # for a trunk adapter with the PVID and vSwitch that we specified + # above. This is guaranteed to be unique. + vlan = vea_vlan_mappings[vif['address']] + vswitch_id = pvm_net.VSwitch.search( + self.adapter, parent_type=pvm_ms.System, one_result=True, + name=CONF.powervm.pvm_vswitch_for_novalink_io).switch_id + + # Find the trunk + mgmt_wrap = pvm_par.get_this_partition(self.adapter) + trunk = pvm_net.CNA.search( + self.adapter, parent_type=mgmt_wrap.schema_type, + parent_uuid=mgmt_wrap.uuid, pvid=vlan, vswitch_id=vswitch_id, + one_result=True) + + if trunk: + # Delete the peer'd trunk adapter. + LOG.warning(_LW("Deleting target side trunk adapter %(dev)s for " + "rollback operation"), {'dev': trunk.dev_name}, + instance=self.instance) + trunk.delete() + + def pre_live_migrate_at_source(self, vif): + """Performs the pre live migrate on the source host. + + This is executed directly before the migration is started on the source + host. + + :param vif: The virtual interface that will be migrated. This may be + called network_info in other portions of the code. + :return: The list of TrunkAdapter's on the source that are hosting the + VM's vif. Should only return data if those trunks should be + deleted after the migration. + """ + # Right before the migration, we need to find the trunk on the source + # host. + mac = pvm_util.sanitize_mac_for_api(vif['address']) + cna_w = pvm_net.CNA.search( + self.adapter, parent_type=pvm_lpar.LPAR.schema_type, + parent_uuid=vm.get_pvm_uuid(self.instance), one_result=True, + mac=mac) + + return pvm_cna.find_trunks(self.adapter, cna_w)