mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
feat(instances): a shut-off VM with a TPM migrates with its TPM state
A shut-off VM with TPM state, as every Windows 11 VM, was refused for migration: the state is a file of the source host, and an offline move would start it there with a new TPM. Such a VM now migrates as a shut-off VM with disks to copy does: its UEFI variables are copied, it is started paused, its CPUs never run, migrates live, which carries the TPM state, and is shut off on the destination. A shut-off VM with a saved state is refused before any of that, for every way it moves: started paused, it would resume and then lose the saved memory. The clone tab says that a clone gets a new, empty TPM, and the destroy page that the TPM state goes with the VM.
This commit is contained in:
parent
e35342bff1
commit
dbc7e688ac
11 changed files with 168 additions and 105 deletions
|
|
@ -158,8 +158,9 @@ class wvmInstances(wvmConnect):
|
|||
# not carry it to the destination.
|
||||
if dom.snapshotNum(0):
|
||||
raise util.OperationError("A VM with snapshots cannot be migrated")
|
||||
if state == 5:
|
||||
self.refuse_local_state(dom.XMLDesc(0))
|
||||
# a shut-off VM moves as its definition or started paused: either loses the saved memory
|
||||
if state == 5 and dom.hasManagedSaveImage(0):
|
||||
raise util.OperationError("A VM with a saved state cannot be migrated")
|
||||
# the definition the migration carries first: it has the disks a migration copies
|
||||
definitions = [dom.XMLDesc(0)]
|
||||
if state != 5:
|
||||
|
|
@ -185,8 +186,10 @@ class wvmInstances(wvmConnect):
|
|||
"Destination host emulator is different. Cannot be migrated"
|
||||
)
|
||||
|
||||
if copies:
|
||||
note = self.migrate_copying(conn, state, flags, uri, copies)
|
||||
# the TPM state is a file of the source host: a live migration carries it
|
||||
tpm_state = etree.fromstring(definitions[0]).xpath("devices/tpm/backend[@type='emulator']")
|
||||
if copies or (state == 5 and tpm_state):
|
||||
note = self.migrate_running(conn, state, flags, uri, copies)
|
||||
if note:
|
||||
mode.append(note)
|
||||
elif state == 5:
|
||||
|
|
@ -199,22 +202,24 @@ class wvmInstances(wvmConnect):
|
|||
dom.migrate(self.wvm, flags, None, uri, 0)
|
||||
return ", ".join(mode), [copy["path"] for copy in copies]
|
||||
|
||||
def migrate_copying(self, conn, state, flags, uri, copies):
|
||||
"""Migrates the VM of conn while libvirt copies the disks of copies
|
||||
into their pools here (NON_SHARED_DISK), the definitions naming the
|
||||
new paths. libvirt copies the disks of a running or paused VM only: a
|
||||
shut-off VM is started paused for it, its CPUs never run, and is shut
|
||||
off here after it; its UEFI variables are copied first, as this host
|
||||
would start from the firmware's template. Returns a note for the log
|
||||
when the VM could not be shut off here."""
|
||||
def migrate_running(self, conn, state, flags, uri, copies):
|
||||
"""Migrates the VM of conn as a running VM, which carries its TPM
|
||||
state, while libvirt copies the disks of copies into their pools here
|
||||
(NON_SHARED_DISK), the definitions naming the new paths. A shut-off
|
||||
VM is started paused for it, its CPUs never run, and is shut off here
|
||||
after it; its UEFI variables are copied first, as this host would
|
||||
start from the firmware's template. Returns a note for the log when
|
||||
the VM could not be shut off here."""
|
||||
dom = conn.instance
|
||||
nvram = None
|
||||
xml_flags = VIR_DOMAIN_XML_SECURE | VIR_DOMAIN_XML_MIGRATABLE
|
||||
params = {
|
||||
VIR_MIGRATE_PARAM_MIGRATE_DISKS: [copy["target"] for copy in copies],
|
||||
VIR_MIGRATE_PARAM_DEST_XML: self.moved_disks(dom.XMLDesc(xml_flags), copies),
|
||||
VIR_MIGRATE_PARAM_PERSIST_XML: self.moved_disks(dom.XMLDesc(xml_flags | VIR_DOMAIN_XML_INACTIVE), copies),
|
||||
}
|
||||
if copies:
|
||||
params[VIR_MIGRATE_PARAM_MIGRATE_DISKS] = [copy["target"] for copy in copies]
|
||||
flags |= VIR_MIGRATE_NON_SHARED_DISK
|
||||
if uri:
|
||||
params[VIR_MIGRATE_PARAM_URI] = uri
|
||||
try:
|
||||
|
|
@ -222,7 +227,7 @@ class wvmInstances(wvmConnect):
|
|||
flags &= ~VIR_MIGRATE_OFFLINE
|
||||
nvram = self.copy_nvram(conn)
|
||||
dom.createWithFlags(VIR_DOMAIN_START_PAUSED)
|
||||
dom.migrate3(self.wvm, params, flags | VIR_MIGRATE_NON_SHARED_DISK)
|
||||
dom.migrate3(self.wvm, params, flags)
|
||||
except libvirtError as error:
|
||||
if state == 5:
|
||||
with contextlib.suppress(libvirtError):
|
||||
|
|
@ -390,18 +395,6 @@ class wvmInstances(wvmConnect):
|
|||
f"{error}; remove the partial disk copies from the destination host: {', '.join(left)}"
|
||||
) from error
|
||||
|
||||
@staticmethod
|
||||
def refuse_local_state(xml):
|
||||
"""A shut-off VM moves without its TPM state, a file of the source
|
||||
host: on the destination it would get a new TPM that no longer
|
||||
unlocks its disks. A live migration carries it. (The UEFI variables
|
||||
are copied: copy_nvram.)"""
|
||||
if ElementTree.fromstring(xml).find("devices/tpm/backend[@type='emulator']") is not None:
|
||||
raise util.OperationError(
|
||||
"A shut-off VM with TPM state cannot be migrated: the state stays on the source host. "
|
||||
"Start the VM and migrate it live."
|
||||
)
|
||||
|
||||
def copy_nvram(self, conn):
|
||||
"""Copies the UEFI variables file of the shut-off VM of conn (on the
|
||||
source) from libvirt's NVRAM directory there to the same path here,
|
||||
|
|
@ -506,7 +499,7 @@ class wvmInstances(wvmConnect):
|
|||
def move_definition(self, dom, conn):
|
||||
"""Migrates a shut-off VM: that is only its definition, as the disks
|
||||
are on storage both hosts share, and its UEFI variables file
|
||||
(copy_nvram; moveto refuses a VM with TPM state). The destination's
|
||||
(copy_nvram; a VM with TPM state migrates paused: migrate_running). The destination's
|
||||
qemu migration hook does not run for it.
|
||||
|
||||
Workaround, to be removed in a later version: libvirt's own offline
|
||||
|
|
@ -517,8 +510,6 @@ class wvmInstances(wvmConnect):
|
|||
first (RHEL-145770) and tracks the fix as RHEL-156800; RHEL/Rocky 9.8
|
||||
with libvirt 11.10.0-12.el9_8 is affected. When supported hosts all
|
||||
have the fix, offline migration can go back to dom.migrate."""
|
||||
if dom.hasManagedSaveImage(0):
|
||||
raise util.OperationError("A VM with a saved state cannot be migrated")
|
||||
for lookup, key in ((self.wvm.lookupByUUIDString, dom.UUIDString()), (self.wvm.lookupByName, dom.name())):
|
||||
try:
|
||||
lookup(key)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ the disks a pool is chosen for (NON_SHARED_DISK, only those targets), the
|
|||
definitions name the new paths; every other file disk must be on storage
|
||||
both hosts share. A shut-off VM is started paused for the copy and shut off
|
||||
on the destination. Disks that cannot be copied are refused before anything
|
||||
changes; the copies of a failed migration are removed again."""
|
||||
changes; the copies of a failed migration are removed again. A shut-off VM
|
||||
with TPM state migrates paused too, copies or not: a live migration carries
|
||||
that state, a file of the source host."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
|
@ -70,6 +72,7 @@ class Source:
|
|||
self.instance = mock.Mock()
|
||||
self.instance.XMLDesc.side_effect = lambda flags: inactive if inactive and flags & 2 else xml
|
||||
self.instance.snapshotNum.return_value = 0
|
||||
self.instance.hasManagedSaveImage.return_value = 0
|
||||
self.instance.UUIDString.return_value = "u-1"
|
||||
self.wvm = mock.Mock()
|
||||
volume = self.wvm.storageVolLookupByPath.return_value
|
||||
|
|
@ -158,6 +161,26 @@ class DiskCopyTestCase(unittest.TestCase):
|
|||
self.assertFalse(flags & (libvirt.VIR_MIGRATE_OFFLINE | libvirt.VIR_MIGRATE_LIVE))
|
||||
dest.wvm.lookupByUUIDString.return_value.destroy.assert_called_once_with()
|
||||
|
||||
def test_a_shut_off_vm_with_tpm_state_runs_paused_for_the_migration(self):
|
||||
tpm = "<tpm model='tpm-crb'><backend type='emulator' version='2.0'/></tpm>"
|
||||
source = Source(5, domain(disk(f"{IMAGES}/vm.qcow2"), tpm))
|
||||
dest = destination(Pool("default", IMAGES), shared=True, vm_here=True)
|
||||
self.assertEqual(migrate(dest, source, {}), ("offline", []))
|
||||
source.instance.createWithFlags.assert_called_once_with(libvirt.VIR_DOMAIN_START_PAUSED)
|
||||
params, flags = source.instance.migrate3.call_args.args[1:]
|
||||
# no disk is copied: they are on storage both hosts share
|
||||
self.assertNotIn(libvirt.VIR_MIGRATE_PARAM_MIGRATE_DISKS, params)
|
||||
self.assertFalse(flags & (libvirt.VIR_MIGRATE_NON_SHARED_DISK | libvirt.VIR_MIGRATE_OFFLINE))
|
||||
dest.wvm.lookupByUUIDString.return_value.destroy.assert_called_once_with()
|
||||
|
||||
def test_a_shut_off_vm_with_a_saved_state_is_refused_before_it_starts(self):
|
||||
# started paused, it would resume the saved memory, then lose it
|
||||
source = Source(5, domain(disk(f"{IMAGES}/vm.qcow2")))
|
||||
source.instance.hasManagedSaveImage.return_value = 1
|
||||
with self.assertRaisesRegex(util.OperationError, "saved state"):
|
||||
migrate(destination(Pool("default", IMAGES)), source, {"vda": "default"})
|
||||
source.instance.createWithFlags.assert_not_called()
|
||||
|
||||
def test_a_shut_off_vm_whose_migration_fails_is_shut_off_on_the_source_again(self):
|
||||
source = Source(5, domain(disk(f"{IMAGES}/vm.qcow2")))
|
||||
source.instance.migrate3.side_effect = libvirt.libvirtError("no route")
|
||||
|
|
|
|||
|
|
@ -225,17 +225,6 @@ class ShutOffMigrationTestCase(unittest.TestCase):
|
|||
self.assertEqual(source.instance.calls, [])
|
||||
self.assertEqual(source.instance.undefined, libvirt.VIR_DOMAIN_UNDEFINE_KEEP_NVRAM)
|
||||
|
||||
def test_a_vm_with_tpm_state_is_refused(self):
|
||||
"""That state stays on the source host: the VM would start on the
|
||||
destination with a new TPM."""
|
||||
host = FakeHost()
|
||||
xml = "<domain><devices><tpm model='tpm-crb'><backend type='emulator' version='2.0'/></tpm></devices></domain>"
|
||||
source = FakeSource(5, xml=xml)
|
||||
with self.assertRaisesRegex(util.OperationError, "migrate it live"):
|
||||
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
|
||||
self.assertEqual(host.defined, [])
|
||||
self.assertIsNone(source.instance.undefined)
|
||||
|
||||
def test_a_tpm_without_local_state_moves(self):
|
||||
xml = "<domain><devices><tpm model='tpm-tis'><backend type='passthrough'/></tpm></devices></domain>"
|
||||
host = FakeHost()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue