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 a4fbf320..fdab9388 100644
Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ
diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po
index 75afe0a4..8102ee44 100644
--- a/locale/tr/LC_MESSAGES/django.po
+++ b/locale/tr/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-01 13:33+0000\n"
+"POT-Creation-Date: 2026-11-01 14:22+0000\n"
"PO-Revision-Date: 2020-11-04 12:00+0000\n"
"Last-Translator: catborise , 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()