1
0
Fork 0
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:
catborise 2026-11-01 17:27:38 +03:00
parent e35342bff1
commit dbc7e688ac
11 changed files with 168 additions and 105 deletions

View file

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

View file

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