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 <sean.k.mooney@intel.com>

Part of blueprint neutron-new-port-binding-api

Change-Id: I91627412744dad65122240f0aeb7a57ee85ba313
This commit is contained in:
Matt Riedemann 2018-03-09 15:50:36 -05:00
parent a81811e111
commit 2b52cde565
4 changed files with 250 additions and 4 deletions

View File

@ -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 = '<domain type="qemu"/>'
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")

View File

@ -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):
</memoryBacking>
</domain>"""))
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 = """<domain>
<uuid>3de6550a-8596-4937-8046-9d862036bca5</uuid>
<devices>
<interface type="bridge">
<mac address="DE:AD:BE:EF:CA:FE"/>
<model type="virtio"/>
<source bridge="qbra188171c-ea"/>
<target dev="tapa188171c-ea"/>
<virtualport type="openvswitch">
<parameters interfaceid="%s"/>
</virtualport>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04'
function='0x0'/>
</interface>
</devices>
</domain>""" % 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 <target> and <virtualport> 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 = """<domain>
<uuid>3de6550a-8596-4937-8046-9d862036bca5</uuid>
<devices>
<interface type="vhostuser">
<mac address="DE:AD:BE:EF:CA:FE"/>
<model type="virtio"/>
<source mode="server" path="/vhost-user/test.sock" type="unix"/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04'
function='0x0'/>
</interface>
</devices>
</domain>"""
self.assertThat(updated_xml, matchers.XMLMatches(expected_xml))
def test_update_vif_xml_no_mac_address_in_xml(self):
"""Tests that the <mac address> is not found in the <interface> 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 = """<domain>
<uuid>3de6550a-8596-4937-8046-9d862036bca5</uuid>
<devices>
<interface type='direct'>
<source dev='eth0' mode='private'/>
<virtualport type='802.1Qbh'>
<parameters profileid='finance'/>
</virtualport>
</interface>
</devices>
</domain>"""
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 = """<domain>
<uuid>3de6550a-8596-4937-8046-9d862036bca5</uuid>
<devices>
<interface type="bridge">
<mac address="CA:FE:DE:AD:BE:EF"/>
<model type="virtio"/>
<source bridge="qbra188171c-ea"/>
<target dev="tapa188171c-ea"/>
</interface>
</devices>
</domain>"""
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):

View File

@ -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,

View File

@ -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