1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 04:36:48 +00:00

fix(migration): remove the NVRAM file a live migration leaves on the source

libvirt undefines the source domain after a migration but keeps its NVRAM
file. A VM created later on that host with the same name reuses the file
and starts with the old VM's UEFI variables (boot entries, enrolled keys):
checked on libvirt 11.10, where only VIR_DOMAIN_START_RESET_NVRAM
recreates it from the template.

After a successful migration the file is now deleted on the source, as a
volume of the pool on libvirt's NVRAM directory or of a transient one.
Only a file in /var/lib/libvirt/qemu/nvram, and only when the destination
cannot create a marker file the source just created there (an exclusive
create also fails on shared storage whose listing an NFS client caches).
A failed removal is logged with the migration, which stands and is
recorded.
This commit is contained in:
catborise 2026-10-09 05:54:30 +03:00
parent e14a4cc10d
commit 975b7f0baf
6 changed files with 345 additions and 2 deletions

View file

@ -7,6 +7,7 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from libvirt import libvirtError
from logs.models import Logs
from rest_framework.test import APIClient
@ -22,11 +23,18 @@ class MigrationLogTests(TestCase):
self.dest = Compute.objects.create(name="ml-dst", hostname="198.51.100.9", login="root", password="", type=2)
self.vm = Instance.objects.create(compute=self.source, name="vm", uuid="u-ml")
def migrate(self, outcome, **request):
def migrate(self, outcome, nvram=None, remove_nvram=True, **request):
self.proxy = MagicMock()
self.proxy.get_autostart.return_value = False
self.proxy.get_nvram.return_value = nvram
if isinstance(remove_nvram, Exception):
self.proxy.remove_nvram.side_effect = remove_nvram
else:
self.proxy.remove_nvram.return_value = remove_nvram
with patch.object(Compute, "status", True), \
patch("instances.utils.wvmInstances") as instances, \
patch("instances.utils.wvmInstance"), \
patch.object(Instance, "proxy", MagicMock()), \
patch.object(Instance, "proxy", self.proxy), \
patch.object(Instance, "autostart", False):
if isinstance(outcome, Exception):
instances.return_value.moveto.side_effect = outcome
@ -34,6 +42,40 @@ class MigrationLogTests(TestCase):
instances.return_value.moveto.return_value = outcome
return migrate_instance(self.dest, self.vm, self.admin, **request)
def test_the_nvram_file_left_on_the_source_is_removed(self):
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd", live=True)
path, destination = self.proxy.remove_nvram.call_args[0]
self.assertEqual(path, "/var/lib/libvirt/qemu/nvram/vm_VARS.fd")
self.assertIsNotNone(destination)
self.assertEqual(Logs.objects.get().message, "Instance is migrated(live) to 198.51.100.9")
def test_a_vm_without_nvram_removes_nothing(self):
self.migrate("live", live=True)
self.assertFalse(self.proxy.remove_nvram.called)
def test_a_failed_nvram_removal_is_logged_but_the_migration_stands(self):
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd",
remove_nvram=libvirtError("cannot delete"), live=True)
self.assertEqual(
Logs.objects.get().message,
"Instance is migrated(live) to 198.51.100.9; removing its NVRAM file "
"/var/lib/libvirt/qemu/nvram/vm_VARS.fd on the source failed: cannot delete",
)
self.vm.refresh_from_db()
self.assertEqual(self.vm.compute, self.dest)
def test_any_nvram_removal_error_still_records_the_migration(self):
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd",
remove_nvram=AttributeError("unexpected"), live=True)
self.vm.refresh_from_db()
self.assertEqual(self.vm.compute, self.dest)
self.assertIn("on the source failed: unexpected", Logs.objects.get().message)
def test_a_failed_migration_removes_nothing(self):
with self.assertRaises(util.OperationError):
self.migrate(util.OperationError("no"), nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd", live=True)
self.assertFalse(self.proxy.remove_nvram.called)
def test_success_names_the_mode_used(self):
self.migrate("non-live, compressed", live=False, compress=True)
log = Logs.objects.get()

View file

@ -190,6 +190,7 @@ def migrate_instance(
)
autostart = instance.proxy.get_autostart()
nvram = instance.proxy.get_nvram()
mode = conn_migrate.moveto(
instance.proxy,
instance.name,
@ -236,6 +237,25 @@ def migrate_instance(
if conn_new is not None:
conn_new.close()
# libvirt keeps the NVRAM file on the source; a VM created there
# later with the same name would start with these variables.
if nvram:
conn_new = None
try:
conn_new = wvmInstances(
new_compute.hostname, new_compute.login, new_compute.password, new_compute.type
)
instance.proxy.remove_nvram(nvram, conn_new)
except Exception as error: # the migration stands and is recorded
msg = _("%(msg)s; removing its NVRAM file %(path)s on the source failed: %(error)s") % {
"msg": msg,
"path": nvram,
"error": error,
}
finally:
if conn_new is not None:
conn_new.close()
try:
record_migration(instance, new_compute)
except Exception as error: