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:
parent
a81811e111
commit
2b52cde565
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user