1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +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

@ -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.

View file

@ -43,6 +43,9 @@
<label class="form-check-label" for="delete_nvram">{% trans "Delete the NVRAM file (firmware settings). Untick to keep it." %}</label>
</div>
{% endif %}
{% if instance.config_tpm == "emulator" %}
<p class="small text-body-secondary" id="destroy-tpm">{% trans "Its TPM state is deleted with it, and the keys in it, such as BitLocker's." %}</p>
{% endif %}
<div class="border-top pt-3 d-flex flex-wrap align-items-center gap-2">
<span class="small text-danger me-auto">{% trans "This cannot be undone." %}</span>
<a class="btn btn-secondary" href="{% url 'instances:instance' instance.id %}">{% trans "Cancel" %}</a>

View file

@ -756,6 +756,9 @@
<header class="mb-4">
<h2 class="h6 mb-1" id="clone-heading">{% trans "Create a clone" %}</h2>
<p class="small text-body-secondary mb-0">{% blocktrans with name=instance.name %}Source VM: {{ name }}{% endblocktrans %}</p>
{% if instance.config_tpm == "emulator" %}
<p class="small text-body-secondary mb-0" id="clone-tpm">{% trans "The clone gets a new, empty TPM: keys in this VM's TPM, such as BitLocker's, do not come along." %}</p>
{% endif %}
</header>
<div class="mb-4">
{% if request.user.is_superuser %}

View file

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

View file

@ -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 = "<tpm model='tpm-crb'><backend type='emulator' version='2.0'/></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)

View file

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

Binary file not shown.

View file

@ -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 <muhammetalisag@gmail.com>, 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"

View file

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

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