From 2b52cde565d542c03f004b48ee9c1a6a25f5b7cd Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 9 Mar 2018 15:50:36 -0500 Subject: [PATCH] libvirt: use dest host vif migrate details for live migration This takes the vif details from the destination host and uses them to overwrite the guest xml on the source host prior to starting the live migration. This allows live migrating between different vif types on different compute hosts. Co-Authored-By: Sean Mooney Part of blueprint neutron-new-port-binding-api Change-Id: I91627412744dad65122240f0aeb7a57ee85ba313 --- nova/tests/unit/virt/libvirt/test_driver.py | 37 ++++- .../tests/unit/virt/libvirt/test_migration.py | 127 +++++++++++++++++- nova/virt/libvirt/driver.py | 29 +++- nova/virt/libvirt/migration.py | 61 ++++++++- 4 files changed, 250 insertions(+), 4 deletions(-) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 630dc1d1744c..69fc1d44f513 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -9677,7 +9677,42 @@ class LibvirtConnTestCase(test.NoDBTestCase, migrate_data, guest, [], libvirt_driver.MIN_MIGRATION_SPEED_BW)) mupdate.assert_called_once_with( - guest, migrate_data, mock.ANY) + guest, migrate_data, mock.ANY, get_vif_config=None) + + def test_live_migration_update_vifs_xml(self): + """Tests that when migrate_data.vifs is populated, the destination + guest xml is updated with the migrate_data.vifs configuration. + """ + instance = objects.Instance(**self.test_instance) + migrate_data = objects.LibvirtLiveMigrateData( + serial_listen_addr='', + target_connect_addr=None, + bdms=[], + block_migration=False, + vifs=[objects.VIFMigrateData(port_id=uuids.port_id)]) + + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + guest = libvirt_guest.Guest(mock.MagicMock()) + fake_xml = '' + + def fake_get_updated_guest_xml(guest, migrate_data, get_volume_config, + get_vif_config=None): + self.assertIsNotNone(get_vif_config) + return fake_xml + + @mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml', + side_effect=fake_get_updated_guest_xml) + @mock.patch.object(drvr._host, 'has_min_version', return_value=True) + @mock.patch.object(guest, 'migrate') + def _test(migrate, has_min_version, get_updated_guest_xml): + drvr._live_migration_operation( + self.context, instance, 'dest.host', False, + migrate_data, guest, [], + CONF.libvirt.live_migration_bandwidth) + self.assertEqual(1, get_updated_guest_xml.call_count) + migrate.assert_called() + + _test() @mock.patch.object(host.Host, 'has_min_version', return_value=True) @mock.patch.object(fakelibvirt.virDomain, "migrateToURI3") diff --git a/nova/tests/unit/virt/libvirt/test_migration.py b/nova/tests/unit/virt/libvirt/test_migration.py index 02c515613c4b..a2a5729785ba 100644 --- a/nova/tests/unit/virt/libvirt/test_migration.py +++ b/nova/tests/unit/virt/libvirt/test_migration.py @@ -17,9 +17,11 @@ from collections import deque from lxml import etree import mock from oslo_utils import units - +import six from nova.compute import power_state +from nova import exception +from nova.network import model as network_model from nova import objects from nova import test from nova.tests.unit import matchers @@ -669,6 +671,129 @@ class UtilityMigrationTestCase(test.NoDBTestCase): """)) + def test_update_vif_xml(self): + """Simulates updating the guest xml for live migrating from a host + using OVS to a host using vhostuser as the networking backend. + """ + vif_ovs = network_model.VIF(id=uuids.vif, + address='DE:AD:BE:EF:CA:FE', + details={'port_filter': False}, + devname='tap-xxx-yyy-zzz', + ovs_interfaceid=uuids.ovs) + source_vif = vif_ovs + migrate_vifs = [ + objects.VIFMigrateData( + port_id=uuids.port_id, + vnic_type=network_model.VNIC_TYPE_NORMAL, + vif_type=network_model.VIF_TYPE_VHOSTUSER, + vif_details={ + 'vhostuser_socket': '/vhost-user/test.sock' + }, + profile={}, + host='dest.host', + source_vif=source_vif) + ] + data = objects.LibvirtLiveMigrateData(vifs=migrate_vifs) + + original_xml = """ + 3de6550a-8596-4937-8046-9d862036bca5 + + + + + + + + + +
+ + +""" % uuids.ovs + + conf = vconfig.LibvirtConfigGuestInterface() + conf.net_type = "vhostuser" + conf.vhostuser_type = "unix" + conf.vhostuser_mode = "server" + conf.mac_addr = "DE:AD:BE:EF:CA:FE" + conf.vhostuser_path = "/vhost-user/test.sock" + conf.model = "virtio" + + get_vif_config = mock.MagicMock(return_value=conf) + doc = etree.fromstring(original_xml) + updated_xml = etree.tostring( + migration._update_vif_xml(doc, data, get_vif_config), + encoding='unicode') + + # Note that and are dropped from the ovs source + # interface xml since they aren't applicable to the vhostuser + # destination interface xml. The type attribute value changes and the + # hardware address element is retained. + expected_xml = """ + 3de6550a-8596-4937-8046-9d862036bca5 + + + + + +
+ + +""" + self.assertThat(updated_xml, matchers.XMLMatches(expected_xml)) + + def test_update_vif_xml_no_mac_address_in_xml(self): + """Tests that the is not found in the XML + which results in an error. + """ + data = objects.LibvirtLiveMigrateData(vifs=[ + objects.VIFMigrateData(source_vif=network_model.VIF( + address="DE:AD:BE:EF:CA:FE"))]) + original_xml = """ + 3de6550a-8596-4937-8046-9d862036bca5 + + + + + + + + + """ + get_vif_config = mock.MagicMock(new_callable=mock.NonCallableMock) + doc = etree.fromstring(original_xml) + ex = self.assertRaises(exception.NovaException, + migration._update_vif_xml, + doc, data, get_vif_config) + self.assertIn('Unable to find MAC address in interface XML', + six.text_type(ex)) + + def test_update_vif_xml_no_matching_vif(self): + """Tests that the vif in the migrate data is not found in the existing + guest interfaces. + """ + data = objects.LibvirtLiveMigrateData(vifs=[ + objects.VIFMigrateData(source_vif=network_model.VIF( + address="DE:AD:BE:EF:CA:FE"))]) + original_xml = """ + 3de6550a-8596-4937-8046-9d862036bca5 + + + + + + + + + """ + get_vif_config = mock.MagicMock(new_callable=mock.NonCallableMock) + doc = etree.fromstring(original_xml) + ex = self.assertRaises(KeyError, migration._update_vif_xml, + doc, data, get_vif_config) + self.assertIn("CA:FE:DE:AD:BE:EF", six.text_type(ex)) + class MigrationMonitorTestCase(test.NoDBTestCase): def setUp(self): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 2f3c4c804743..ca9dd0bb50d6 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -7023,11 +7023,38 @@ class LibvirtDriver(driver.ComputeDriver): new_xml_str = None if CONF.libvirt.virt_type != "parallels": + # If the migrate_data has port binding information for the + # destination host, we need to prepare the guest vif config + # for the destination before we start migrating the guest. + get_vif_config = None + if 'vifs' in migrate_data and migrate_data.vifs: + # NOTE(mriedem): The vif kwarg must be built on the fly + # within get_updated_guest_xml based on migrate_data.vifs. + # We could stash the virt_type from the destination host + # into LibvirtLiveMigrateData but the host kwarg is a + # nova.virt.libvirt.host.Host object and is used to check + # information like libvirt version on the destination. + # If this becomes a problem, what we could do is get the + # VIF configs while on the destination host during + # pre_live_migration() and store those in the + # LibvirtLiveMigrateData object. For now we just use the + # source host information for virt_type and + # host (version) since the conductor live_migrate method + # _check_compatible_with_source_hypervisor() ensures that + # the hypervisor types and versions are compatible. + get_vif_config = functools.partial( + self.vif_driver.get_config, + instance=instance, + image_meta=instance.image_meta, + inst_type=instance.flavor, + virt_type=CONF.libvirt.virt_type, + host=self._host) new_xml_str = libvirt_migrate.get_updated_guest_xml( # TODO(sahid): It's not a really good idea to pass # the method _get_volume_config and we should to find # a way to avoid this in future. - guest, migrate_data, self._get_volume_config) + guest, migrate_data, self._get_volume_config, + get_vif_config=get_vif_config) params = { 'destination_xml': new_xml_str, 'migrate_disks': device_names, diff --git a/nova/virt/libvirt/migration.py b/nova/virt/libvirt/migration.py index a18f2ec820f9..ec22cdf6f57a 100644 --- a/nova/virt/libvirt/migration.py +++ b/nova/virt/libvirt/migration.py @@ -24,6 +24,7 @@ from oslo_log import log as logging from nova.compute import power_state import nova.conf +from nova import exception from nova.virt.libvirt import config as vconfig LOG = logging.getLogger(__name__) @@ -77,13 +78,16 @@ def serial_listen_ports(migrate_data): return ports -def get_updated_guest_xml(guest, migrate_data, get_volume_config): +def get_updated_guest_xml(guest, migrate_data, get_volume_config, + get_vif_config=None): xml_doc = etree.fromstring(guest.get_xml_desc(dump_migratable=True)) xml_doc = _update_graphics_xml(xml_doc, migrate_data) xml_doc = _update_serial_xml(xml_doc, migrate_data) xml_doc = _update_volume_xml(xml_doc, migrate_data, get_volume_config) xml_doc = _update_perf_events_xml(xml_doc, migrate_data) xml_doc = _update_memory_backing_xml(xml_doc, migrate_data) + if get_vif_config is not None: + xml_doc = _update_vif_xml(xml_doc, migrate_data, get_vif_config) return etree.tostring(xml_doc, encoding='unicode') @@ -276,6 +280,61 @@ def _update_memory_backing_xml(xml_doc, migrate_data): return xml_doc +def _update_vif_xml(xml_doc, migrate_data, get_vif_config): + # Loop over each interface element in the original xml and find the + # corresponding vif based on mac and then overwrite the xml with the new + # attributes but maintain the order of the interfaces and maintain the + # guest pci address. + instance_uuid = xml_doc.findtext('uuid') + parser = etree.XMLParser(remove_blank_text=True) + interface_nodes = xml_doc.findall('./devices/interface') + migrate_vif_by_mac = {vif.source_vif['address']: vif + for vif in migrate_data.vifs} + for interface_dev in interface_nodes: + mac = interface_dev.find('mac') + mac = mac if mac is not None else {} + mac_addr = mac.get('address') + if mac_addr: + migrate_vif = migrate_vif_by_mac[mac_addr] + vif = migrate_vif.get_dest_vif() + # get_vif_config is a partial function of + # nova.virt.libvirt.vif.LibvirtGenericVIFDriver.get_config + # with all but the 'vif' kwarg set already and returns a + # LibvirtConfigGuestInterface object. + vif_config = get_vif_config(vif=vif) + else: + # This shouldn't happen but if it does, we need to abort the + # migration. + raise exception.NovaException( + 'Unable to find MAC address in interface XML for ' + 'instance %s: %s' % ( + instance_uuid, + etree.tostring(interface_dev, encoding='unicode'))) + + # At this point we want to replace the interface elements with the + # destination vif config xml *except* for the guest PCI address. + conf_xml = vif_config.to_xml() + LOG.debug('Updating guest XML with vif config: %s', conf_xml, + instance_uuid=instance_uuid) + dest_interface_elem = etree.XML(conf_xml, parser) + # Save off the hw address presented to the guest since that can't + # change during live migration. + address = interface_dev.find('address') + # Now clear the interface's current elements and insert everything + # from the destination vif config xml. + interface_dev.clear() + # Insert attributes. + for attr_name, attr_value in dest_interface_elem.items(): + interface_dev.set(attr_name, attr_value) + # Insert sub-elements. + for index, dest_interface_subelem in enumerate(dest_interface_elem): + interface_dev.insert(index, dest_interface_subelem) + # And finally re-insert the hw address. + interface_dev.insert(index + 1, address) + + return xml_doc + + def find_job_type(guest, instance): """Determine the (likely) current migration job type