From dbc7e688acf7c73c2b68c30d7014cf9d0097ea01 Mon Sep 17 00:00:00 2001 From: catborise Date: Sun, 1 Nov 2026 17:27:38 +0300 Subject: [PATCH] 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. --- README.md | 4 +- .../instances/destroy_instance_form.html | 3 + .../templates/instances/settings_tab.html | 3 + instances/tests/test_destroy.py | 8 + instances/tests/test_live_jobs.py | 27 ++++ instances/tests/test_pending_edits.py | 5 + locale/tr/LC_MESSAGES/django.mo | Bin 157093 -> 157510 bytes locale/tr/LC_MESSAGES/django.po | 138 ++++++++++-------- vrtManager/instance.py | 49 +++---- vrtManager/tests/test_disk_copy_migration.py | 25 +++- vrtManager/tests/test_migration.py | 11 -- 11 files changed, 168 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index e53cd5c2..e7c62e22 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ A compute node is a KVM host the panel manages over libvirt. Firewall: the compute's VNC ports (`5900`–`65535`) must accept connections **only** from the panel's IP, never from public networks. -For VM migration between computes, define shared storage as a storage pool on every host (a VM migrates only when its disk, backing and ISO files and block devices are volumes of the destination's pools), and where the hosts cannot resolve each other's names, set each compute's **Migration address** on its edit page. A VM's disks can instead be copied into storage pools of the destination during the migration: after choosing the destination, the **Disks** list shows each disk file and the destination pools that can take it (directory, filesystem and NFS pools; one with a file of the same name refuses the copy), and a disk on storage both hosts share can also stay there. Plain raw or qcow2 disks without a backing file are copied; a shut-off VM is started paused for the copy, its CPUs never run, and is shut off on the destination. With **Delete the source files of copied disks** the source files go after the migration, unless another VM or volume uses them. The copy goes through the destination's migration port range (49152-49215 by default), like the memory. +For VM migration between computes, define shared storage as a storage pool on every host (a VM migrates only when its disk, backing and ISO files and block devices are volumes of the destination's pools), and where the hosts cannot resolve each other's names, set each compute's **Migration address** on its edit page. A VM's disks can instead be copied into storage pools of the destination during the migration: after choosing the destination, the **Disks** list shows each disk file and the destination pools that can take it (directory, filesystem and NFS pools; one with a file of the same name refuses the copy), and a disk on storage both hosts share can also stay there. Plain raw or qcow2 disks without a backing file are copied; a shut-off VM is started paused for the copy, its CPUs never run, and is shut off on the destination. A shut-off VM with a TPM (Windows 11) migrates the same way, also without copies: only a running VM's migration carries its TPM state. With **Delete the source files of copied disks** the source files go after the migration, unless another VM or volume uses them. The copy goes through the destination's migration port range (49152-49215 by default), like the memory. If libvirt warns `Host SMBIOS information is not available`, install `dmidecode` (`apt-get`, `dnf` or `zypper install -y dmidecode`) and restart `libvirtd`. @@ -638,7 +638,7 @@ A version from before v3.0.0 has no worker: `systemctl disable --now webvirt-wor - Migrating a VM runs as a job: the page returns at once, the job panel shows its progress, and a running migration can be cancelled there (the VM keeps running on its source host). The 540 s limit is gone. The API's migrate answers 202 with the job and needs a new `submission_key` per request. - Uploading an ISO is tracked as a job: the storage page's job panel shows it and can cancel it, and the page no longer reloads when it ends. The upload dialog closes once the upload starts; the browser still sends the file, so stay on the page until it ends. An upload left idle for an hour is ended and its partial file deleted. - A running or paused VM can be cloned: libvirt copies its disks from one instant while it keeps running (file systems are frozen for a moment when the guest agent is ready). It needs libvirt 11.10 or later on the host, local file disks and no TPM; the VM cannot be changed from the panel until the copy ends, and the copy can be cancelled. Cancelling a clone of a shut-off VM has no effect: it shows as cancelling until the copy ends. A target pool that fills up during the copy can pause the source VM; a clone that ends uncertain keeps the VM locked until a superuser resolves the job. -- A shut-off UEFI VM can be migrated: its NVRAM file is copied to the destination host (unless both hosts share the NVRAM directory). A shut-off VM with TPM state still has to be started and migrated live. +- A shut-off UEFI VM can be migrated: its NVRAM file is copied to the destination host (unless both hosts share the NVRAM directory). A shut-off VM with TPM state is started paused for the migration, which carries the state, and shut off on the destination. - Manual installs that keep their own `settings.py` copy the SQLite `OPTIONS` and `TEST` entries from `settings.py.template` and add `"operations"` to `INSTALLED_APPS`. - A host's Devices page lists its PCI devices by IOMMU group and its USB devices, with what the host itself has on each (network interfaces, disks, its graphics card) and the VMs that name it. Manual installs that keep their own `settings.py` add `"nodedevs"` to `INSTALLED_APPS`. - A user's quota counts, for each VM, the larger of its running and its next-start vCPUs, memory and disks, and a queued clone as one VM from the moment it is accepted. A user can be over the quota after the update; nothing is stopped, but new VMs, clones and raises are refused until it fits. diff --git a/instances/templates/instances/destroy_instance_form.html b/instances/templates/instances/destroy_instance_form.html index 806603c0..e495faef 100644 --- a/instances/templates/instances/destroy_instance_form.html +++ b/instances/templates/instances/destroy_instance_form.html @@ -43,6 +43,9 @@ {% endif %} + {% if instance.config_tpm == "emulator" %} +

{% trans "Its TPM state is deleted with it, and the keys in it, such as BitLocker's." %}

+ {% endif %}
{% trans "This cannot be undone." %} {% trans "Cancel" %} diff --git a/instances/templates/instances/settings_tab.html b/instances/templates/instances/settings_tab.html index 2e8cf4af..4fb425ee 100644 --- a/instances/templates/instances/settings_tab.html +++ b/instances/templates/instances/settings_tab.html @@ -756,6 +756,9 @@

{% trans "Create a clone" %}

{% blocktrans with name=instance.name %}Source VM: {{ name }}{% endblocktrans %}

+ {% if instance.config_tpm == "emulator" %} +

{% trans "The clone gets a new, empty TPM: keys in this VM's TPM, such as BitLocker's, do not come along." %}

+ {% endif %}
{% if request.user.is_superuser %} diff --git a/instances/tests/test_destroy.py b/instances/tests/test_destroy.py index b8ac8b5a..a10430ab 100644 --- a/instances/tests/test_destroy.py +++ b/instances/tests/test_destroy.py @@ -175,6 +175,14 @@ class DestroyOrderTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(calls, []) + def test_the_confirmation_page_says_the_tpm_state_goes(self): + for tpm, shown in (("emulator", True), (None, False), ("custom", False)): + with self.subTest(tpm=tpm), patch("instances.models.wvmInstance") as wvm: + wvm.return_value.get_status.return_value = 5 + wvm.return_value.get_tpm.return_value = tpm + response = self.client.get(reverse("instances:destroy", args=[self.instance.id])) + self.assertEqual(b"Its TPM state is deleted" in response.content, shown) + def test_the_confirmation_page_of_a_paused_vm_offers_the_form(self): with patch("instances.models.wvmInstance") as wvm: wvm.return_value.get_status.return_value = 3 # paused diff --git a/instances/tests/test_live_jobs.py b/instances/tests/test_live_jobs.py index dc4016d8..ae143da3 100644 --- a/instances/tests/test_live_jobs.py +++ b/instances/tests/test_live_jobs.py @@ -6,6 +6,7 @@ migration address. The ISO upload runs on both hosts: TCP writes through a libvirt stream, SSH through SFTP.""" import os +import subprocess import time import unittest @@ -258,6 +259,32 @@ class LiveJobsTestCase(TransactionTestCase): self.assertEqual(nvram(target), content) self.assertIsNone(nvram(source)) + def test_a_shut_off_vm_with_tpm_state_migrates_with_it_there_and_back(self): + dest, dest_conn = self.second_host() + tpm = "" + # UEFI with a TPM, as a Windows 11 VM: its NVRAM file is copied, its TPM state migrated + dom = livetest.define_vm(self.conn, P + "tpm", [], uefi=True, extra_devices=tpm) + dom.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # swtpm makes the state + dom.destroy() + uuid = dom.UUIDString() + + def state_on_dest(): + """The checksum of the TPM state file on the destination (an SSH host, as root).""" + path = f"/var/lib/libvirt/swtpm/{uuid}/tpm2/tpm2-00.permall" + return subprocess.run(["ssh", "-o", "BatchMode=yes", "-l", dest.login, dest.hostname, + f"sha256sum {path} 2>/dev/null"], capture_output=True, text=True).stdout.split()[:1] + + instance = Instance.objects.create(compute=self.compute, name=dom.name(), uuid=uuid) + sums = [] + for n, target in enumerate((dest, self.compute, dest)): + offline = {"instance": instance.pk, "target": target.pk, "offline": True} + self.run_job(jobs.submit("instance.migrate", self.admin, offline, f"key-tpm{n:05d}")) + sums.append(state_on_dest()) + self.assertFalse(dest_conn.lookupByUUIDString(uuid).isActive()) # shut off again after the move + # the same state went there, back and there again; none stays behind + self.assertTrue(sums[0]) + self.assertEqual((sums[1], sums[2]), ([], sums[0])) + def upload(self, compute, key, chunks): """Post chunks of a two-chunk ISO upload to the storage view; returns the job.""" self.client.force_login(self.admin) diff --git a/instances/tests/test_pending_edits.py b/instances/tests/test_pending_edits.py index 5e19f30e..b4db1111 100644 --- a/instances/tests/test_pending_edits.py +++ b/instances/tests/test_pending_edits.py @@ -235,6 +235,11 @@ class PendingEditsTestCase(TestCase): doc = self.page(get_tpm=("emulator", "emulator"), tpm_model=None) self.assertEqual(doc.xpath(f"{tpm}/@disabled"), []) + def test_the_clone_tab_says_a_clone_gets_a_new_tpm(self): + self.assertIn("new, empty TPM", self.page(get_tpm=("emulator", "emulator")).xpath("string(//p[@id='clone-tpm'])")) + for tpm in (None, "custom"): # a passthrough TPM or one with its own state path comes along + self.assertEqual(self.page(get_tpm=(tpm, tpm)).xpath("//p[@id='clone-tpm']"), []) + def test_the_users_tab_shows_each_owner_and_offers_only_new_owners(self): from accounts.models import UserInstance diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index a4fbf3209966559018aad6e4a5ff349572deed76..fdab93881d9e95774bab1b18dabbda8f748c64a0 100644 GIT binary patch delta 37081 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 fb)VWCdLBhK!8=nFFx(*Uv<4*@a2}~e! zAUFT~)tN(y9HhU)kjOQ|lDTDR<`kvs;(qScEnSU>?aJFHca)wpEg}(l$)8%m1kbU*ZV8w22KcDGg<%qdTOB z({M}F^+^qK^|qZJCNC@ZGU}uzuQuU@3_w>V;)}Qk+p@ldH_>Jq?gpf#quk->jrq1m z;XX95g^V#|gppB>!ZB$qJ>f3o{r8F_J&1c7iTf#^hPw@Q|0JzCVO?9O^PRL7k?BUH z@_H6oc!ZzZi2UuOtujTNdbAy%@em(QkF?mL$Cy@i~-VNSZFKr>@@QN4IG? zZGVl`H|_98B4-ckYLCr>KZT#tNDne=;Uvakw!zf?(CZXKi zdCA#mOBSca8iWVh9=c;+?)0=!h4jLB%$AL1d%Z*cB*N`!>nQnSO-Igj(h_s4OzK^w;y9ZZLHc+48AQ2Sw6T@DuzjLVq&?++ zLfKBF4YGac&riC1ZMpK12S)oAEVL`KqG%sv8+vNN16{6u2T539aK!h8%T>w-c8b8 zQ9c9VU)L4Vj&Rqv39%UDAnpT{DTC`Ne}?>Hizcd~f3Qx#w|r zu^kV_tmF-)TsP9)kNn3$5^j)j+jjDm1_qO+>zjSrSfp*@jzf7}6-kdmydL5C+@DGR zjkvDd+({YuQCqeXWh)RqW!oA@-hIMvy!fYid&uZVq&Ah?+X@+om#2`f{iNMuaBXc~ z6gx=e72qyu(_2$N3*kWSh17p-(?{EOj#4%WKil}0eMoDmY^qxMje8iSUy+`bnz}X; z|454`iH{+ki*OM3#bYu!T}#MmvDql#NGzNAfS@9NTsY@+Xp3#I`Y&yzZo}bRsxBG~SSe zZZ=cx{$?ADO9QvKt8kCzzDt=NbfPN_b#&E>JTX31$Ud8@ly~H;qg7o|XsMLV%R=}m zc?}|cCM0u@(AEW8>NnbMN=|9?yrZEHc>^hPp0sn^bqN>c9>N`;Th|U+UW1jX`xIhpVy%EhD1W_-pyjl439VnY;uT{WJ_Tq^IUpsu|XyiM9I+rR?SpHu0F$#zbV zKR)vIgjD(CkTcfSYDNiNsff>}=D#ND`OlN26=P^k?Q~|2%sDYtx)-+Q6ml*Q?qd7< zRk3Yk$BD_3oTj%CrVYnv_e;zyEQzldzaV1*!Dk>jY_D z6x6jFr`SgNlHQmOXOsSnx^1afk+eX{6eDddX)6e4wCyb+ub~b9M&1$+qEUFc||?W_Yfp}mVXo{w-2!b_;v z`B$6%w&DUBjBN`lX$J*zkk)up-O1gnZ%X#4Tllu88N41X+m`i=SN1B~E~WAAUiN, 2020\n" "Language-Team: Turkish (https://www.transifex.com/catborise/teams/110663/" @@ -277,7 +277,7 @@ msgstr "Parola Değiştir" #: instances/templates/add_instance_volume.html:108 #: instances/templates/create_instance_w1.html:39 #: instances/templates/create_instance_w2.html:352 -#: instances/templates/instances/destroy_instance_form.html:48 +#: instances/templates/instances/destroy_instance_form.html:51 #: instances/templates/instances/edit_instance_volume.html:111 #: instances/templates/instances/move_disk.html:27 #: instances/templates/instances/settings_tab.html:415 @@ -710,10 +710,10 @@ msgstr "Üyeler" #: instances/templates/instances/resize_tab.html:114 #: instances/templates/instances/settings_tab.html:416 #: instances/templates/instances/settings_tab.html:647 -#: instances/templates/instances/settings_tab.html:873 -#: instances/templates/instances/settings_tab.html:915 -#: instances/templates/instances/settings_tab.html:975 -#: instances/templates/instances/settings_tab.html:1005 +#: instances/templates/instances/settings_tab.html:876 +#: instances/templates/instances/settings_tab.html:918 +#: instances/templates/instances/settings_tab.html:978 +#: instances/templates/instances/settings_tab.html:1008 #: networks/templates/add_network_qos.html:36 #: networks/templates/network.html:335 networks/templates/network.html:375 #: networks/templates/network.html:402 networks/templates/network.html:441 @@ -877,7 +877,7 @@ msgid "Search removed VMs" msgstr "Kaldırılan sanal makinelerde ara" #: admin/templates/admin/tombstone_list.html:28 -#: instances/templates/instances/settings_tab.html:881 +#: instances/templates/instances/settings_tab.html:884 msgid "Template" msgstr "Şablon" @@ -1727,8 +1727,8 @@ msgid "Stored, leave empty to keep" msgstr "Kayıtlı, korumak için boş bırakın" #: computes/forms.py:28 computes/templates/computes/instances.html:45 -#: instances/templates/instances/settings_tab.html:838 -#: instances/templates/instances/settings_tab.html:870 +#: instances/templates/instances/settings_tab.html:841 +#: instances/templates/instances/settings_tab.html:873 #: instances/templates/instances/snapshots_tab.html:23 #: instances/templates/instances/snapshots_tab.html:67 msgid "Description" @@ -1992,7 +1992,7 @@ msgstr "Sanal Makineler" #: computes/templates/computes/host_nav.html:16 #: computes/templates/overview.html:74 #: instances/templates/instances/settings_tab.html:554 -#: instances/templates/instances/settings_tab.html:812 +#: instances/templates/instances/settings_tab.html:815 #: storages/templates/storage.html:6 storages/templates/storage.html:8 msgid "Storage" msgstr "Depolama" @@ -2132,14 +2132,14 @@ msgstr "vCPU" #: console/templates/console-vnc.html:13 #: console/templates/console-xterm.html:16 #: instances/templates/allinstances_index_grouped.html:36 -#: instances/templates/instances/settings_tab.html:992 +#: instances/templates/instances/settings_tab.html:995 #: networks/templates/network.html:260 msgid "Connected" msgstr "Bağlı" #: computes/templates/computes/list.html:73 computes/templates/overview.html:21 #: instances/templates/allinstances_index_grouped.html:38 -#: instances/templates/instances/settings_tab.html:993 +#: instances/templates/instances/settings_tab.html:996 msgid "Not connected" msgstr "Bağlı değil" @@ -3401,7 +3401,7 @@ msgid "Firmware" msgstr "Üretici yazılımı" #: instances/templates/create_instance_w2.html:83 -#: instances/templates/instances/settings_tab.html:949 +#: instances/templates/instances/settings_tab.html:952 msgid "TPM 2.0" msgstr "TPM 2.0" @@ -3686,8 +3686,8 @@ msgstr "Konuk ajanı yanıt vermedi" #: instances/templates/instance_actions.html:13 #: instances/templates/instances/settings_tab.html:53 -#: instances/templates/instances/settings_tab.html:848 -#: instances/templates/instances/settings_tab.html:850 +#: instances/templates/instances/settings_tab.html:851 +#: instances/templates/instances/settings_tab.html:853 #: nwfilters/templates/nwfilters.html:57 nwfilters/templates/nwfilters.html:94 #: storages/templates/storage.html:143 storages/templates/storage.html:203 msgid "Clone" @@ -3907,10 +3907,15 @@ msgstr "" "NVRAM dosyasını (ürün yazılımı ayarları) sil. Korumak için işareti kaldırın." #: instances/templates/instances/destroy_instance_form.html:47 +msgid "" +"Its TPM state is deleted with it, and the keys in it, such as BitLocker's." +msgstr "TPM durumu da onunla birlikte silinir, içindeki anahtarlar da, örneğin BitLocker'ınkiler." + +#: instances/templates/instances/destroy_instance_form.html:50 msgid "This cannot be undone." msgstr "Bu işlem geri alınamaz." -#: instances/templates/instances/destroy_instance_form.html:49 +#: instances/templates/instances/destroy_instance_form.html:52 msgid "Destroy VM" msgstr "Sanal makineyi sil" @@ -4403,17 +4408,17 @@ msgstr "" "sağlar." #: instances/templates/instances/resize_tab.html:102 -#: instances/templates/instances/settings_tab.html:893 +#: instances/templates/instances/settings_tab.html:896 msgid "Shut the VM down to change this." msgstr "Bunu değiştirmek için sanal makineyi kapatın." #: instances/templates/instances/resize_tab.html:109 -#: instances/templates/instances/settings_tab.html:1000 +#: instances/templates/instances/settings_tab.html:1003 msgid "Enabled" msgstr "Etkin" #: instances/templates/instances/resize_tab.html:110 -#: instances/templates/instances/settings_tab.html:1001 +#: instances/templates/instances/settings_tab.html:1004 msgid "Disabled" msgstr "Devredışı" @@ -4488,7 +4493,7 @@ msgid "Boot" msgstr "Ön Yükleme" #: instances/templates/instances/settings_tab.html:39 -#: instances/templates/instances/settings_tab.html:790 +#: instances/templates/instances/settings_tab.html:793 #: instances/templates/instances/stats_tab.html:42 #: networks/templates/network.html:6 networks/templates/network.html:8 #: networks/templates/network.html:48 @@ -5100,56 +5105,62 @@ msgstr "Bir klon oluştur" msgid "Source VM: %(name)s" msgstr "Kaynak sanal makine: %(name)s" -#: instances/templates/instances/settings_tab.html:762 -#: instances/templates/instances/settings_tab.html:773 +#: instances/templates/instances/settings_tab.html:760 +msgid "" +"The clone gets a new, empty TPM: keys in this VM's TPM, such as BitLocker's, " +"do not come along." +msgstr "Klon yeni ve boş bir TPM alır: bu sanal makinenin TPM'indeki anahtarlar, örneğin BitLocker'ınkiler, klona geçmez." + +#: instances/templates/instances/settings_tab.html:765 #: instances/templates/instances/settings_tab.html:776 +#: instances/templates/instances/settings_tab.html:779 msgid "Clone name" msgstr "Klon adı" -#: instances/templates/instances/settings_tab.html:768 -#: instances/templates/instances/settings_tab.html:800 +#: instances/templates/instances/settings_tab.html:771 +#: instances/templates/instances/settings_tab.html:803 msgid "Guess" msgstr "Tahmin" -#: instances/templates/instances/settings_tab.html:774 +#: instances/templates/instances/settings_tab.html:777 msgid "Automatic" msgstr "Otomatik" -#: instances/templates/instances/settings_tab.html:794 +#: instances/templates/instances/settings_tab.html:797 msgid "New MAC address" msgstr "Yeni MAC adresi" -#: instances/templates/instances/settings_tab.html:797 +#: instances/templates/instances/settings_tab.html:800 msgid "Random" msgstr "Rastgele" -#: instances/templates/instances/settings_tab.html:815 +#: instances/templates/instances/settings_tab.html:818 #, python-format msgid "Pool: %(pool)s" msgstr "Havuz: %(pool)s" -#: instances/templates/instances/settings_tab.html:816 +#: instances/templates/instances/settings_tab.html:819 msgid "New volume name" msgstr "Yeni birim adı" -#: instances/templates/instances/settings_tab.html:822 +#: instances/templates/instances/settings_tab.html:825 msgid "Preallocate qcow2 metadata" msgstr "qcow2 meta verisini önceden ayır" -#: instances/templates/instances/settings_tab.html:827 +#: instances/templates/instances/settings_tab.html:830 msgid "The VM has no disk to copy." msgstr "Sanal makinenin kopyalanacak diski yok." -#: instances/templates/instances/settings_tab.html:835 +#: instances/templates/instances/settings_tab.html:838 msgid "Optional details" msgstr "İsteğe bağlı ayrıntılar" -#: instances/templates/instances/settings_tab.html:836 -#: instances/templates/instances/settings_tab.html:868 +#: instances/templates/instances/settings_tab.html:839 +#: instances/templates/instances/settings_tab.html:871 msgid "Title" msgstr "Başlık" -#: instances/templates/instances/settings_tab.html:843 +#: instances/templates/instances/settings_tab.html:846 msgid "" "The clone gets none of the VM's host devices: one VM holds a device of the " "host." @@ -5157,46 +5168,46 @@ msgstr "" "Klon, sanal makinenin sunucu aygıtlarını almaz: bir sunucu aygıtını tek bir " "sanal makine tutar." -#: instances/templates/instances/settings_tab.html:846 +#: instances/templates/instances/settings_tab.html:849 msgid "Cloning runs as a background job." msgstr "Klonlama arka planda bir iş olarak çalışır." -#: instances/templates/instances/settings_tab.html:864 +#: instances/templates/instances/settings_tab.html:867 msgid "VM details" msgstr "Sanal makine ayrıntıları" -#: instances/templates/instances/settings_tab.html:865 +#: instances/templates/instances/settings_tab.html:868 msgid "A title and description to identify the VM." msgstr "Sanal makineyi tanımlamak için bir başlık ve açıklama." -#: instances/templates/instances/settings_tab.html:882 +#: instances/templates/instances/settings_tab.html:885 msgid "" "A template is a VM to clone: it is not counted in quotas and cannot be " "started." msgstr "" "Şablon, klonlanacak bir sanal makinedir: kotalarda sayılmaz ve başlatılamaz." -#: instances/templates/instances/settings_tab.html:889 +#: instances/templates/instances/settings_tab.html:892 msgid "Use as a template" msgstr "Şablon olarak kullan" -#: instances/templates/instances/settings_tab.html:892 +#: instances/templates/instances/settings_tab.html:895 msgid "Only administrators and staff can change this." msgstr "Bunu yalnızca yöneticiler ve personel değiştirebilir." -#: instances/templates/instances/settings_tab.html:902 +#: instances/templates/instances/settings_tab.html:905 msgid "Primary video model" msgstr "Birincil video modeli" -#: instances/templates/instances/settings_tab.html:903 +#: instances/templates/instances/settings_tab.html:906 msgid "The graphics device the guest sees." msgstr "Konuk sistemin gördüğü grafik aygıtı." -#: instances/templates/instances/settings_tab.html:924 +#: instances/templates/instances/settings_tab.html:927 msgid "Random number generator" msgstr "Rastgele sayı üreteci" -#: instances/templates/instances/settings_tab.html:925 +#: instances/templates/instances/settings_tab.html:928 msgid "" "A virtio device that feeds the guest's entropy from the host: a new VM does " "not wait for it at boot. From the next start." @@ -5204,38 +5215,41 @@ msgstr "" "Konuk sistemin entropisini sunucudan besleyen bir virtio aygıtı: yeni bir " "sanal makine açılışta onu beklemez. Sonraki başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:931 +#: instances/templates/instances/settings_tab.html:934 msgid "Virtio RNG" msgstr "Virtio RNG" -#: instances/templates/instances/settings_tab.html:934 +#: instances/templates/instances/settings_tab.html:937 msgid "The VM has its own RNG device: change it in the XML editor." msgstr "Sanal makinenin kendi RNG aygıtı var: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:942 +#: instances/templates/instances/settings_tab.html:945 msgid "TPM" msgstr "TPM" -#: instances/templates/instances/settings_tab.html:943 +#: instances/templates/instances/settings_tab.html:946 msgid "" "An emulated TPM 2.0, as Windows 11 needs with UEFI Secure Boot. Its state " "stays on the host until the VM is destroyed: a TPM added again has it back. " "From the next start." -msgstr "Windows 11'in UEFI Secure Boot ile birlikte istediği emüle edilmiş bir TPM 2.0. Durumu sanal makine yok edilene kadar sunucuda kalır: yeniden eklenen TPM onu geri alır. Bir sonraki başlatmadan itibaren." +msgstr "" +"Windows 11'in UEFI Secure Boot ile birlikte istediği emüle edilmiş bir TPM " +"2.0. Durumu sanal makine yok edilene kadar sunucuda kalır: yeniden eklenen " +"TPM onu geri alır. Bir sonraki başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:952 +#: instances/templates/instances/settings_tab.html:955 msgid "The VM has its own TPM: change it in the XML editor." msgstr "Sanal makinenin kendi TPM'i var: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:954 +#: instances/templates/instances/settings_tab.html:957 msgid "This host offers no TPM 2.0: install swtpm on it." msgstr "Bu sunucu TPM 2.0 sunmuyor: üzerine swtpm kurun." -#: instances/templates/instances/settings_tab.html:962 +#: instances/templates/instances/settings_tab.html:965 msgid "When the guest crashes" msgstr "Konuk sistem çöktüğünde" -#: instances/templates/instances/settings_tab.html:963 +#: instances/templates/instances/settings_tab.html:966 msgid "" "For a crash the guest reports, not a hang. Stop keeps a VM that crashes " "again and again down for a look. From the next start." @@ -5244,39 +5258,39 @@ msgstr "" "tekrar tekrar çöken bir sanal makineyi incelemek için kapalı tutar. Sonraki " "başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:970 +#: instances/templates/instances/settings_tab.html:973 msgid "Restart" msgstr "Yeniden başlat" -#: instances/templates/instances/settings_tab.html:971 +#: instances/templates/instances/settings_tab.html:974 #: interfaces/templates/interface.html:16 networks/templates/network.html:16 #: storages/templates/storage.html:18 msgid "Stop" msgstr "Durdur" -#: instances/templates/instances/settings_tab.html:979 +#: instances/templates/instances/settings_tab.html:982 #, python-format msgid "%(action)s: change it in the XML editor." msgstr "%(action)s: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:987 +#: instances/templates/instances/settings_tab.html:990 msgid "QEMU guest agent" msgstr "QEMU konuk ajanı" -#: instances/templates/instances/settings_tab.html:988 +#: instances/templates/instances/settings_tab.html:991 msgid "The channel to the agent in the guest: a running VM gets it at once." msgstr "" "Konuk sistemdeki ajana giden kanal: çalışan bir sanal makine onu hemen alır." -#: instances/templates/instances/settings_tab.html:989 +#: instances/templates/instances/settings_tab.html:992 msgid "Agent" msgstr "Ajan" -#: instances/templates/instances/settings_tab.html:990 +#: instances/templates/instances/settings_tab.html:993 msgid "VM not running" msgstr "Sanal makine çalışmıyor" -#: instances/templates/instances/settings_tab.html:991 +#: instances/templates/instances/settings_tab.html:994 msgid "No channel" msgstr "Kanal yok" diff --git a/vrtManager/instance.py b/vrtManager/instance.py index d85bee2e..71752ba3 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -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) diff --git a/vrtManager/tests/test_disk_copy_migration.py b/vrtManager/tests/test_disk_copy_migration.py index a368b1ec..7abed515 100644 --- a/vrtManager/tests/test_disk_copy_migration.py +++ b/vrtManager/tests/test_disk_copy_migration.py @@ -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 = "" + 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") diff --git a/vrtManager/tests/test_migration.py b/vrtManager/tests/test_migration.py index d2acee55..ba875edd 100644 --- a/vrtManager/tests/test_migration.py +++ b/vrtManager/tests/test_migration.py @@ -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 = "" - 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 = "" host = FakeHost()