Support live migration with vpmem

1. Check if the cluster supports live migration with vpmem
2. On source host we generate new dest xml with vpmem info stored in
   migration_context.new_resources.
3. If there are vpmems, cleanup them on host/destination when live
   migration succeeds/fails

Change-Id: I5c346e690148678a2f0dc63f4f516a944c3db8cd
Implements: blueprint support-live-migration-with-virtual-persistent-memory
This commit is contained in:
LuyaoZhong
2020-03-27 08:07:17 +00:00
parent 990a26ef1f
commit 4bd5af66b5
10 changed files with 268 additions and 38 deletions
+24 -7
View File
@@ -7664,7 +7664,7 @@ class ComputeManager(manager.Manager):
LOG.debug('destination check data is %s', dest_check_data) LOG.debug('destination check data is %s', dest_check_data)
try: try:
allocs = self.reportclient.get_allocations_for_consumer( allocs = self.reportclient.get_allocations_for_consumer(
ctxt, instance.uuid) ctxt, instance.uuid)
migrate_data = self.compute_rpcapi.check_can_live_migrate_source( migrate_data = self.compute_rpcapi.check_can_live_migrate_source(
ctxt, instance, dest_check_data) ctxt, instance, dest_check_data)
if ('src_supports_numa_live_migration' in migrate_data and if ('src_supports_numa_live_migration' in migrate_data and
@@ -8249,9 +8249,10 @@ class ComputeManager(manager.Manager):
self.driver.live_migration_abort(instance) self.driver.live_migration_abort(instance)
self._notify_live_migrate_abort_end(context, instance) self._notify_live_migrate_abort_end(context, instance)
def _live_migration_cleanup_flags(self, migrate_data): def _live_migration_cleanup_flags(self, migrate_data, migr_ctxt=None):
"""Determine whether disks or instance path need to be cleaned up after """Determine whether disks, instance path or other resources
live migration (at source on success, at destination on rollback) need to be cleaned up after live migration (at source on success,
at destination on rollback)
Block migration needs empty image at destination host before migration Block migration needs empty image at destination host before migration
starts, so if any failure occurs, any empty images has to be deleted. starts, so if any failure occurs, any empty images has to be deleted.
@@ -8260,7 +8261,11 @@ class ComputeManager(manager.Manager):
newly created instance-xxx dir on the destination as a part of its newly created instance-xxx dir on the destination as a part of its
rollback process rollback process
There may be other resources which need cleanup; currently this is
limited to vPMEM devices with the libvirt driver.
:param migrate_data: implementation specific data :param migrate_data: implementation specific data
:param migr_ctxt: specific resources stored in migration_context
:returns: (bool, bool) -- do_cleanup, destroy_disks :returns: (bool, bool) -- do_cleanup, destroy_disks
""" """
# NOTE(pkoniszewski): block migration specific params are set inside # NOTE(pkoniszewski): block migration specific params are set inside
@@ -8270,11 +8275,20 @@ class ComputeManager(manager.Manager):
do_cleanup = False do_cleanup = False
destroy_disks = False destroy_disks = False
if isinstance(migrate_data, migrate_data_obj.LibvirtLiveMigrateData): if isinstance(migrate_data, migrate_data_obj.LibvirtLiveMigrateData):
has_vpmem = False
if migr_ctxt and migr_ctxt.old_resources:
for resource in migr_ctxt.old_resources:
if ('metadata' in resource and
isinstance(resource.metadata,
objects.LibvirtVPMEMDevice)):
has_vpmem = True
break
# No instance booting at source host, but instance dir # No instance booting at source host, but instance dir
# must be deleted for preparing next block migration # must be deleted for preparing next block migration
# must be deleted for preparing next live migration w/o shared # must be deleted for preparing next live migration w/o shared
# storage # storage
do_cleanup = not migrate_data.is_shared_instance_path # vpmem must be cleanped
do_cleanup = not migrate_data.is_shared_instance_path or has_vpmem
destroy_disks = not migrate_data.is_shared_block_storage destroy_disks = not migrate_data.is_shared_block_storage
elif isinstance(migrate_data, migrate_data_obj.XenapiLiveMigrateData): elif isinstance(migrate_data, migrate_data_obj.XenapiLiveMigrateData):
do_cleanup = migrate_data.block_migration do_cleanup = migrate_data.block_migration
@@ -8427,7 +8441,7 @@ class ComputeManager(manager.Manager):
source_node = instance.node source_node = instance.node
do_cleanup, destroy_disks = self._live_migration_cleanup_flags( do_cleanup, destroy_disks = self._live_migration_cleanup_flags(
migrate_data) migrate_data, migr_ctxt=instance.migration_context)
if do_cleanup: if do_cleanup:
LOG.debug('Calling driver.cleanup from _post_live_migration', LOG.debug('Calling driver.cleanup from _post_live_migration',
@@ -8727,7 +8741,7 @@ class ComputeManager(manager.Manager):
bdms=bdms) bdms=bdms)
do_cleanup, destroy_disks = self._live_migration_cleanup_flags( do_cleanup, destroy_disks = self._live_migration_cleanup_flags(
migrate_data) migrate_data, migr_ctxt=instance.migration_context)
if do_cleanup: if do_cleanup:
self.compute_rpcapi.rollback_live_migration_at_destination( self.compute_rpcapi.rollback_live_migration_at_destination(
@@ -8867,6 +8881,9 @@ class ComputeManager(manager.Manager):
# check_can_live_migrate_destination() # check_can_live_migrate_destination()
self.rt.free_pci_device_claims_for_instance(context, instance) self.rt.free_pci_device_claims_for_instance(context, instance)
# NOTE(luyao): Apply migration_context temporarily since it's
# on destination host, we rely on instance object to cleanup
# specific resources like vpmem
with instance.mutated_migration_context(): with instance.mutated_migration_context():
self.driver.rollback_live_migration_at_destination( self.driver.rollback_live_migration_at_destination(
context, instance, network_info, block_device_info, context, instance, network_info, block_device_info,
+19 -3
View File
@@ -46,6 +46,17 @@ def supports_vif_related_pci_allocations(context, host):
return svc.version >= 36 return svc.version >= 36
def supports_vpmem_live_migration(context):
"""Checks if the commpute host service is new enough to support
instance live migration with virtual persistent memory.
:param context: The user request context.
:returns: True if the compute hosts are new enough to support live
migration with vpmem
"""
return objects.Service.get_minimum_version(context, 'nova-compute') >= 51
class LiveMigrationTask(base.TaskBase): class LiveMigrationTask(base.TaskBase):
def __init__(self, context, instance, destination, def __init__(self, context, instance, destination,
block_migration, disk_over_commit, migration, compute_rpcapi, block_migration, disk_over_commit, migration, compute_rpcapi,
@@ -261,11 +272,16 @@ class LiveMigrationTask(base.TaskBase):
if not self.instance.resources: if not self.instance.resources:
return return
has_vpmem = False
for resource in self.instance.resources: for resource in self.instance.resources:
if resource.resource_class.startswith("CUSTOM_PMEM_NAMESPACE_"): if resource.resource_class.startswith("CUSTOM_PMEM_NAMESPACE_"):
raise exception.MigrationPreCheckError( has_vpmem = True
reason="Cannot live migration with virtual persistent " break
"memory, the operation is not supported.")
if has_vpmem and not supports_vpmem_live_migration(self.context):
raise exception.MigrationPreCheckError(
reason="Cannot live migrate with virtual persistent memory, "
"the operation is not supported.")
def _check_host_is_up(self, host): def _check_host_is_up(self, host):
service = objects.Service.get_by_compute_host(self.context, host) service = objects.Service.get_by_compute_host(self.context, host)
+3 -1
View File
@@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter # NOTE(danms): This is the global service version counter
SERVICE_VERSION = 50 SERVICE_VERSION = 51
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any # NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@@ -183,6 +183,8 @@ SERVICE_VERSION_HISTORY = (
# Version 50: Compute RPC v5.11: # Version 50: Compute RPC v5.11:
# Add accel_uuids (accelerator requests) param to build_and_run_instance # Add accel_uuids (accelerator requests) param to build_and_run_instance
{'compute_rpc': '5.11'}, {'compute_rpc': '5.11'},
# Version 51: Add support for live migration with vpmem
{'compute_rpc': '5.11'},
) )
@@ -10137,6 +10137,25 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase,
instance, instance,
migration.id) migration.id)
def test_live_migration_cleanup_flags_shared_path_and_vpmem_libvirt(self):
migrate_data = objects.LibvirtLiveMigrateData(
is_shared_block_storage=False,
is_shared_instance_path=True)
migr_ctxt = objects.MigrationContext()
vpmem_resource = objects.Resource(
provider_uuid=uuids.rp_uuid,
resource_class="CUSTOM_PMEM_NAMESPACE_4GB",
identifier='ns_0', metadata=objects.LibvirtVPMEMDevice(
label='4GB',
name='ns_0', devpath='/dev/dax0.0',
size=4292870144, align=2097152))
migr_ctxt.old_resources = objects.ResourceList(
objects=[vpmem_resource])
do_cleanup, destroy_disks = self.compute._live_migration_cleanup_flags(
migrate_data, migr_ctxt)
self.assertTrue(do_cleanup)
self.assertTrue(destroy_disks)
def test_live_migration_cleanup_flags_block_migrate_libvirt(self): def test_live_migration_cleanup_flags_block_migrate_libvirt(self):
migrate_data = objects.LibvirtLiveMigrateData( migrate_data = objects.LibvirtLiveMigrateData(
is_shared_block_storage=False, is_shared_block_storage=False,
@@ -833,6 +833,15 @@ class LiveMigrationTaskTestCase(test.NoDBTestCase):
_test, pci_requests, True, True) _test, pci_requests, True, True)
def test_check_can_migrate_specific_resources(self): def test_check_can_migrate_specific_resources(self):
"""Test _check_can_migrate_specific_resources allows live migration
with vpmem.
"""
@mock.patch.object(live_migrate, 'supports_vpmem_live_migration')
def _test(resources, supp_lm_vpmem_retval, mock_support_lm_vpmem):
self.instance.resources = resources
mock_support_lm_vpmem.return_value = supp_lm_vpmem_retval
self.task._check_can_migrate_specific_resources()
vpmem_0 = objects.LibvirtVPMEMDevice( vpmem_0 = objects.LibvirtVPMEMDevice(
label='4GB', name='ns_0', devpath='/dev/dax0.0', label='4GB', name='ns_0', devpath='/dev/dax0.0',
size=4292870144, align=2097152) size=4292870144, align=2097152)
@@ -840,7 +849,11 @@ class LiveMigrationTaskTestCase(test.NoDBTestCase):
provider_uuid=uuids.rp, provider_uuid=uuids.rp,
resource_class="CUSTOM_PMEM_NAMESPACE_4GB", resource_class="CUSTOM_PMEM_NAMESPACE_4GB",
identifier='ns_0', metadata=vpmem_0) identifier='ns_0', metadata=vpmem_0)
self.instance.resources = objects.ResourceList( resources = objects.ResourceList(
objects=[resource_0]) objects=[resource_0])
_test(None, False)
_test(None, True)
_test(resources, True)
self.assertRaises(exception.MigrationPreCheckError, self.assertRaises(exception.MigrationPreCheckError,
self.task._check_can_migrate_specific_resources) _test, resources, False)
+27 -2
View File
@@ -11218,7 +11218,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
drvr._live_migration_uri(target_connection), drvr._live_migration_uri(target_connection),
params=params, flags=0) params=params, flags=0)
mock_updated_guest_xml.assert_called_once_with( mock_updated_guest_xml.assert_called_once_with(
guest, migrate_data, mock.ANY, get_vif_config=None) guest, migrate_data, mock.ANY, get_vif_config=None,
new_resources=None)
def test_live_migration_update_vifs_xml(self): def test_live_migration_update_vifs_xml(self):
"""Tests that when migrate_data.vifs is populated, the destination """Tests that when migrate_data.vifs is populated, the destination
@@ -11245,7 +11246,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
fake_xml = '<domain type="qemu"/>' fake_xml = '<domain type="qemu"/>'
def fake_get_updated_guest_xml(guest, migrate_data, get_volume_config, def fake_get_updated_guest_xml(guest, migrate_data, get_volume_config,
get_vif_config=None): get_vif_config=None,
new_resources=None):
self.assertIsNotNone(get_vif_config) self.assertIsNotNone(get_vif_config)
return fake_xml return fake_xml
@@ -25894,6 +25896,29 @@ class LibvirtPMEMNamespaceTests(test.NoDBTestCase):
self.assertEqual('SMALL', vpmems[1].label) self.assertEqual('SMALL', vpmems[1].label)
self.assertEqual('SMALL', vpmems[2].label) self.assertEqual('SMALL', vpmems[2].label)
@mock.patch('nova.virt.hardware.get_vpmems')
def test_sorted_migrating_vpmem_resources(self, mock_labels):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
instance = fake_instance.fake_instance_obj(self.context)
instance.flavor = objects.Flavor(
name='m1.small', memory_mb=2048, vcpus=2, root_gb=10,
ephemeral_gb=20, swap=0, extra_specs={
'hw:pmem': 'SMALL,4GB,SMALL'})
mock_labels.return_value = ['SMALL', '4GB', 'SMALL']
migr_context = objects.MigrationContext()
# original order is '4GB' 'SMALL' 'SMALL'
migr_context.new_resources = objects.ResourceList(objects=[
self.resource_0, self.resource_1, self.resource_2])
instance.migration_context = migr_context
new_resources = drvr._sorted_migrating_resources(
instance, instance.flavor)
# ordered vpmems are 'SMAL' '4GB' 'SMALL'
expected_new_resources = objects.ResourceList(objects=[
self.resource_1, self.resource_0, self.resource_2])
for i in range(3):
self.assertEqual(expected_new_resources[i], new_resources[i])
@mock.patch('nova.privsep.libvirt.cleanup_vpmem') @mock.patch('nova.privsep.libvirt.cleanup_vpmem')
def test_cleanup_vpmems(self, mock_cleanup_vpmem): def test_cleanup_vpmems(self, mock_cleanup_vpmem):
vpmems = [self.vpmem_0, self.vpmem_1, self.vpmem_2] vpmems = [self.vpmem_0, self.vpmem_1, self.vpmem_2]
@@ -116,6 +116,70 @@ class UtilityMigrationTestCase(test.NoDBTestCase):
mock_memory_backing.assert_called_once_with(mock.ANY, data) mock_memory_backing.assert_called_once_with(mock.ANY, data)
self.assertEqual(1, mock_tostring.called) self.assertEqual(1, mock_tostring.called)
def test_update_device_resources_xml_vpmem(self):
# original xml for vpmems, /dev/dax0.1 and /dev/dax0.2 here
# are vpmem device path on source host
old_xml = textwrap.dedent("""
<domain>
<devices>
<memory model='nvdimm'>
<source>
<path>/dev/dax0.1</path>
<alignsize>2048</alignsize>
<pmem>on</pmem>
</source>
<target>
<size>4192256</size>
<label>
<size>2048</size>
</label>
<node>0</node>
</target>
</memory>
<memory model='nvdimm'>
<source>
<path>/dev/dax0.2</path>
<alignsize>2048</alignsize>
<pmem>on</pmem>
</source>
<target>
<size>4192256</size>
<label>
<size>2048</size>
</label>
<node>0</node>
</target>
</memory>
</devices>
</domain>""")
doc = etree.fromstring(old_xml)
vpmem_resource_0 = objects.Resource(
provider_uuid=uuids.rp_uuid,
resource_class="CUSTOM_PMEM_NAMESPACE_4GB",
identifier='ns_0',
metadata= objects.LibvirtVPMEMDevice(
label='4GB', name='ns_0', devpath='/dev/dax1.0',
size=4292870144, align=2097152))
vpmem_resource_1 = objects.Resource(
provider_uuid=uuids.rp_uuid,
resource_class="CUSTOM_PMEM_NAMESPACE_4GB",
identifier='ns_0',
metadata= objects.LibvirtVPMEMDevice(
label='4GB', name='ns_1', devpath='/dev/dax2.0',
size=4292870144, align=2097152))
# new_resources contains vpmems claimed on destination,
# /dev/dax1.0 and /dev/dax2.0 are where vpmem data is migrated to
new_resources = objects.ResourceList(
objects=[vpmem_resource_0, vpmem_resource_1])
res = etree.tostring(migration._update_device_resources_xml(
copy.deepcopy(doc), new_resources),
encoding='unicode')
# we expect vpmem info will be updated in xml after invoking
# _update_device_resources_xml
new_xml = old_xml.replace("/dev/dax0.1", "/dev/dax1.0")
new_xml = new_xml.replace("/dev/dax0.2", "/dev/dax2.0")
self.assertXmlEqual(res, new_xml)
def test_update_numa_xml(self): def test_update_numa_xml(self):
xml = textwrap.dedent(""" xml = textwrap.dedent("""
<domain> <domain>
+63 -22
View File
@@ -5858,30 +5858,18 @@ class LibvirtDriver(driver.ComputeDriver):
return guest return guest
def _get_ordered_vpmems(self, instance, flavor): def _get_ordered_vpmems(self, instance, flavor):
ordered_vpmems = [] resources = self._get_resources(instance)
vpmems = self._get_vpmems(instance) ordered_vpmem_resources = self._get_ordered_vpmem_resources(
labels = hardware.get_vpmems(flavor) resources, flavor)
for label in labels: ordered_vpmems = [self._vpmems_by_name[resource.identifier]
for vpmem in vpmems: for resource in ordered_vpmem_resources]
if vpmem.label == label:
ordered_vpmems.append(vpmem)
vpmems.remove(vpmem)
break
return ordered_vpmems return ordered_vpmems
def _get_vpmems(self, instance, prefix=None): def _get_vpmems(self, instance, prefix=None):
vpmems = [] resources = self._get_resources(instance, prefix=prefix)
resources = instance.resources vpmem_resources = self._get_vpmem_resources(resources)
if prefix == 'old' and instance.migration_context: vpmems = [self._vpmems_by_name[resource.identifier]
if 'old_resources' in instance.migration_context: for resource in vpmem_resources]
resources = instance.migration_context.old_resources
if not resources:
return vpmems
for resource in resources:
rc = resource.resource_class
if rc.startswith("CUSTOM_PMEM_NAMESPACE_"):
vpmem = self._vpmems_by_name[resource.identifier]
vpmems.append(vpmem)
return vpmems return vpmems
def _guest_add_vpmems(self, guest, vpmems): def _guest_add_vpmems(self, guest, vpmems):
@@ -8143,6 +8131,53 @@ class LibvirtDriver(driver.ComputeDriver):
claim.image_meta) claim.image_meta)
return migrate_data return migrate_data
def _get_resources(self, instance, prefix=None):
resources = []
if prefix:
migr_context = instance.migration_context
attr_name = prefix + 'resources'
if migr_context and attr_name in migr_context:
resources = getattr(migr_context, attr_name) or []
else:
resources = instance.resources or []
return resources
def _get_vpmem_resources(self, resources):
vpmem_resources = []
for resource in resources:
if 'metadata' in resource and \
isinstance(resource.metadata, objects.LibvirtVPMEMDevice):
vpmem_resources.append(resource)
return vpmem_resources
def _get_ordered_vpmem_resources(self, resources, flavor):
vpmem_resources = self._get_vpmem_resources(resources)
ordered_vpmem_resources = []
labels = hardware.get_vpmems(flavor)
for label in labels:
for vpmem_resource in vpmem_resources:
if vpmem_resource.metadata.label == label:
ordered_vpmem_resources.append(vpmem_resource)
vpmem_resources.remove(vpmem_resource)
break
return ordered_vpmem_resources
def _sorted_migrating_resources(self, instance, flavor):
"""This method is used to sort instance.migration_context.new_resources
claimed on dest host, then the ordered new resources will be used to
update resources info (e.g. vpmems) in the new xml which is used for
live migration.
"""
resources = self._get_resources(instance, prefix='new_')
if not resources:
return
ordered_resources = []
ordered_vpmem_resources = self._get_ordered_vpmem_resources(
resources, flavor)
ordered_resources.extend(ordered_vpmem_resources)
ordered_resources_obj = objects.ResourceList(objects=ordered_resources)
return ordered_resources_obj
def _get_live_migrate_numa_info(self, instance_numa_topology, flavor, def _get_live_migrate_numa_info(self, instance_numa_topology, flavor,
image_meta): image_meta):
"""Builds a LibvirtLiveMigrateNUMAInfo object to send to the source of """Builds a LibvirtLiveMigrateNUMAInfo object to send to the source of
@@ -8614,12 +8649,16 @@ class LibvirtDriver(driver.ComputeDriver):
host=self._host) host=self._host)
self._detach_direct_passthrough_vifs(context, self._detach_direct_passthrough_vifs(context,
migrate_data, instance) migrate_data, instance)
new_resources = None
if isinstance(instance, objects.Instance):
new_resources = self._sorted_migrating_resources(
instance, instance.flavor)
new_xml_str = libvirt_migrate.get_updated_guest_xml( new_xml_str = libvirt_migrate.get_updated_guest_xml(
# TODO(sahid): It's not a really good idea to pass # TODO(sahid): It's not a really good idea to pass
# the method _get_volume_config and we should to find # the method _get_volume_config and we should to find
# a way to avoid this in future. # 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) get_vif_config=get_vif_config, new_resources=new_resources)
# NOTE(pkoniszewski): Because of precheck which blocks # NOTE(pkoniszewski): Because of precheck which blocks
# tunnelled block live migration with mapped volumes we # tunnelled block live migration with mapped volumes we
@@ -8803,6 +8842,8 @@ class LibvirtDriver(driver.ComputeDriver):
n = 0 n = 0
start = time.time() start = time.time()
is_post_copy_enabled = self._is_post_copy_enabled(migration_flags) is_post_copy_enabled = self._is_post_copy_enabled(migration_flags)
# vpmem does not support post copy
is_post_copy_enabled &= not bool(self._get_vpmems(instance))
while True: while True:
info = guest.get_job_info() info = guest.get_job_info()
+26 -1
View File
@@ -25,6 +25,7 @@ from oslo_log import log as logging
from nova.compute import power_state from nova.compute import power_state
import nova.conf import nova.conf
from nova import exception from nova import exception
from nova import objects
from nova.virt import hardware from nova.virt import hardware
from nova.virt.libvirt import config as vconfig from nova.virt.libvirt import config as vconfig
@@ -80,7 +81,7 @@ def serial_listen_ports(migrate_data):
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): get_vif_config=None, new_resources=None):
xml_doc = etree.fromstring(guest.get_xml_desc(dump_migratable=True)) xml_doc = etree.fromstring(guest.get_xml_desc(dump_migratable=True))
xml_doc = _update_graphics_xml(xml_doc, migrate_data) xml_doc = _update_graphics_xml(xml_doc, migrate_data)
xml_doc = _update_serial_xml(xml_doc, migrate_data) xml_doc = _update_serial_xml(xml_doc, migrate_data)
@@ -91,9 +92,33 @@ def get_updated_guest_xml(guest, migrate_data, get_volume_config,
xml_doc = _update_vif_xml(xml_doc, migrate_data, get_vif_config) xml_doc = _update_vif_xml(xml_doc, migrate_data, get_vif_config)
if 'dst_numa_info' in migrate_data: if 'dst_numa_info' in migrate_data:
xml_doc = _update_numa_xml(xml_doc, migrate_data) xml_doc = _update_numa_xml(xml_doc, migrate_data)
if new_resources:
xml_doc = _update_device_resources_xml(xml_doc, new_resources)
return etree.tostring(xml_doc, encoding='unicode') return etree.tostring(xml_doc, encoding='unicode')
def _update_device_resources_xml(xml_doc, new_resources):
vpmems = []
for resource in new_resources:
if 'metadata' in resource:
res_meta = resource.metadata
if isinstance(res_meta, objects.LibvirtVPMEMDevice):
vpmems.append(res_meta)
# If there are other resources in the future, the xml should
# be updated here like vpmems
xml_doc = _update_vpmems_xml(xml_doc, vpmems)
return xml_doc
def _update_vpmems_xml(xml_doc, vpmems):
memory_devices = xml_doc.findall("./devices/memory")
for pos, memory_dev in enumerate(memory_devices):
if memory_dev.get('model') == 'nvdimm':
devpath = memory_dev.find('./source/path')
devpath.text = vpmems[pos].devpath
return xml_doc
def _update_numa_xml(xml_doc, migrate_data): def _update_numa_xml(xml_doc, migrate_data):
LOG.debug('_update_numa_xml input xml=%s', LOG.debug('_update_numa_xml input xml=%s',
etree.tostring(xml_doc, encoding='unicode', pretty_print=True)) etree.tostring(xml_doc, encoding='unicode', pretty_print=True))
@@ -0,0 +1,8 @@
---
features:
- |
The libvirt driver now supports live migration with virtual persistent
memory (vPMEM), which requires QEMU as hypervisor. In virtualization layer,
QEMU will copy vpmem over the network like volatile memory, due to the
typical large capacity of vPMEM, it may takes longer time for live
migration.