1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(jobs): a recovered migration restores autostart and removes the source NVRAM file

migrate_instance sets autostart on the destination (it is per host and
the migration does not carry it) and deletes the NVRAM file libvirt
keeps on the source. A worker that stopped after libvirt moved the VM
skipped both: recovery only recorded the move, so an autostart VM no
longer started after the destination rebooted, and a later VM of the
same name on the source started with these UEFI variables. The job now
records both before the move, and recovery does them after a proven
move; a step that fails is a warning of the job's result.
This commit is contained in:
catborise 2026-11-06 09:00:20 +03:00
parent c3dfab287d
commit 65345bd980
6 changed files with 96 additions and 27 deletions

View file

@ -930,11 +930,15 @@ def migration_target(instance, target_pk):
return target
def _connect(compute_pk):
c = Compute.objects.get(pk=compute_pk)
return wvmConnect(c.hostname, c.login, c.password, c.type)
def _domain(compute_pk, uuid):
"""The VM on the compute, or None when it has none there."""
c = Compute.objects.get(pk=compute_pk)
try:
return wvmConnect(c.hostname, c.login, c.password, c.type).wvm.lookupByUUIDString(uuid)
return _connect(compute_pk).wvm.lookupByUUIDString(uuid)
except libvirtError as err:
if err.get_error_code() != VIR_ERR_NO_DOMAIN:
raise # unreachable: recovery waits and retries
@ -947,6 +951,25 @@ def _shut_off_again(op, dom):
dom.destroy()
def _moved(op, dest):
"""What migrate_instance does once libvirt moved the VM, for a job that
stopped before it: autostart is per host, and the source keeps the
NVRAM file. The job's result, with a warning for a step that failed."""
notes = []
if op.intent.get("autostart"):
try:
dest.setAutostart(1)
except libvirtError as err:
notes += say(_("The VM migrated, but autostart could not be set on the destination: %(error)s"),
error=err)
if nvram := op.intent.get("nvram"):
try:
_connect(op.intent["compute"]).remove_nvram(nvram, _connect(op.params["target"]))
except libvirtError as err:
notes += say(_("The VM migrated, but its NVRAM file stays on the source: %(error)s"), error=err)
return {"warning": notes} if notes else {}
@register
class MigrateInstance(Kind):
name = "instance.migrate"
@ -993,8 +1016,10 @@ class MigrateInstance(Kind):
def run(self, op, ctx):
p = op.params
instance = _instance(p["instance"])
# a shut-off VM may migrate started paused (migrate_running): recovery shuts it off again
ctx.record_intent(shut_off=instance.proxy.get_status() == 5)
# a shut-off VM may migrate started paused (migrate_running): recovery shuts it off again;
# it also does what migrate_instance does after the move (_moved)
proxy = instance.proxy
ctx.record_intent(shut_off=proxy.get_status() == 5, autostart=proxy.get_autostart(), nvram=proxy.get_nvram())
ctx.set_phase(gettext_noop("Migrating the VM"))
try:
utils.migrate_instance(Compute.objects.get(pk=p["target"]), instance, op.user,
@ -1048,7 +1073,7 @@ class MigrateInstance(Kind):
if instance is not None: # the job stopped before it moved the record
utils.record_migration(instance, Compute.objects.get(pk=op.params["target"]))
_shut_off_again(op, dest)
return Outcome(Operation.SUCCEEDED)
return Outcome(Operation.SUCCEEDED, result=_moved(op, dest))
if source is not None and dest is None:
_shut_off_again(op, source)
return Outcome(Operation.FAILED, error=say(_("The VM stayed on the source host")))

View file

@ -100,6 +100,27 @@ class MigrateJobTestCase(TransactionTestCase):
MigrateInstance().reconcile(op)
dom.destroy.assert_not_called()
def test_recovery_of_a_moved_vm_sets_its_autostart_and_removes_the_source_nvram(self):
self.vm.proxy.get_autostart.return_value = True
self.vm.proxy.get_nvram.return_value = "/var/lib/libvirt/qemu/nvram/mig-vm_VARS.fd"
op = self.queue()
ctx = self.start(op)
self.dest_conn.moveto.side_effect = OSError("the worker stops here")
with self.assertRaises(OSError):
get_kind(op.kind).run(op, ctx)
dom = MagicMock()
with patch("instances.jobs._domain", side_effect=[None, dom]), patch("instances.jobs._connect") as connect:
outcome = MigrateInstance().reconcile(op)
self.assertEqual((outcome.state, outcome.result), (Operation.SUCCEEDED, {}))
dom.setAutostart.assert_called_once_with(1)
connect.return_value.remove_nvram.assert_called_once_with(
"/var/lib/libvirt/qemu/nvram/mig-vm_VARS.fd", connect.return_value)
dom.setAutostart.side_effect = libvirtError("refused")
with patch("instances.jobs._domain", side_effect=[None, dom]), patch("instances.jobs._connect"):
outcome = MigrateInstance().reconcile(op)
self.assertEqual(outcome.state, Operation.SUCCEEDED)
self.assertIn("autostart could not be set", outcome.result["warning"][0]["text"])
def test_a_copy_claims_its_volume_on_both_computes(self):
Instance.proxy.get_disk_devices.return_value = [
{"dev": "vda", "storage": "default", "image": "mig-vm.qcow2", "path": "/i/mig-vm.qcow2"},

Binary file not shown.

View file

@ -3018,53 +3018,65 @@ msgstr "%(compute)s sunucusuna ulaşılamıyor"
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s, sanal makinenin kendi sunucusunun başka bir adı"
#: instances/jobs.py:947
#: instances/jobs.py:963
#, python-format
msgid ""
"The VM migrated, but autostart could not be set on the destination: %(error)s"
msgstr ""
"Sanal makine taşındı, ancak hedefte otomatik başlatma ayarlanamadı: %(error)s"
#: instances/jobs.py:969
#, python-format
msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s"
msgstr "Sanal makine taşındı, ancak NVRAM dosyası kaynakta kaldı: %(error)s"
#: instances/jobs.py:976
msgid "Migrate VM"
msgstr "Sanal makineyi taşı"
#: instances/jobs.py:991
#: instances/jobs.py:1023
msgid "Migrating the VM"
msgstr "Sanal makine taşınıyor"
#: instances/jobs.py:1038
#: instances/jobs.py:1070
msgid "libvirt is still migrating the VM"
msgstr "libvirt sanal makineyi hâlâ taşıyor"
#: instances/jobs.py:1045
#: instances/jobs.py:1079
msgid "The VM stayed on the source host"
msgstr "Sanal makine kaynak sunucuda kaldı"
#: instances/jobs.py:1047
#: instances/jobs.py:1081
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr ""
"Sanal makine ya iki sunucuda da var ya da hiçbirinde yok; kontrol edin, sonra"
" bu işi çözün."
#: instances/jobs.py:1095
#: instances/jobs.py:1129
msgid "Move disk"
msgstr "Diski taşı"
#: instances/jobs.py:1102
#: instances/jobs.py:1136
msgid "Choose a disk of the VM"
msgstr "Sanal makinenin bir diskini seçin"
#: instances/jobs.py:1126
#: instances/jobs.py:1160
msgid "Copying the disk"
msgstr "Disk kopyalanıyor"
#: instances/jobs.py:1169
#: instances/jobs.py:1203
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr ""
"%(dev)s üzerinde başka bir blok işi çalışıyor; sanal makineyi kontrol edin"
#: instances/jobs.py:1175
#: instances/jobs.py:1209
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "Disk %(path)s içinde kaldı; varsa %(dest)s dosyasını silin"
#: instances/jobs.py:1177
#: instances/jobs.py:1211
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s tanımları %(paths)s gösteriyor; sanal makineyi kontrol edin"

Binary file not shown.

View file

@ -2898,50 +2898,61 @@ msgstr "无法连接 %(compute)s"
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s 是该虚拟机所在主机的另一个名称"
#: instances/jobs.py:947
#: instances/jobs.py:963
#, python-format
msgid ""
"The VM migrated, but autostart could not be set on the destination: %(error)s"
msgstr "虚拟机已迁移,但无法在目标主机上设置自动启动:%(error)s"
#: instances/jobs.py:969
#, python-format
msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s"
msgstr "虚拟机已迁移,但其 NVRAM 文件仍留在源主机上:%(error)s"
#: instances/jobs.py:976
msgid "Migrate VM"
msgstr "迁移虚拟机"
#: instances/jobs.py:991
#: instances/jobs.py:1023
msgid "Migrating the VM"
msgstr "正在迁移虚拟机"
#: instances/jobs.py:1038
#: instances/jobs.py:1070
msgid "libvirt is still migrating the VM"
msgstr "libvirt 仍在迁移虚拟机"
#: instances/jobs.py:1045
#: instances/jobs.py:1079
msgid "The VM stayed on the source host"
msgstr "虚拟机留在了源主机上"
#: instances/jobs.py:1047
#: instances/jobs.py:1081
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。"
#: instances/jobs.py:1095
#: instances/jobs.py:1129
msgid "Move disk"
msgstr "移动磁盘"
#: instances/jobs.py:1102
#: instances/jobs.py:1136
msgid "Choose a disk of the VM"
msgstr "请选择虚拟机的一个磁盘"
#: instances/jobs.py:1126
#: instances/jobs.py:1160
msgid "Copying the disk"
msgstr "正在复制磁盘"
#: instances/jobs.py:1169
#: instances/jobs.py:1203
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机"
#: instances/jobs.py:1175
#: instances/jobs.py:1209
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除"
#: instances/jobs.py:1177
#: instances/jobs.py:1211
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机"