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:
parent
e35342bff1
commit
dbc7e688ac
11 changed files with 168 additions and 105 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue