From 005fa59bf0060a16c772835f51941048a2f3ca5b Mon Sep 17 00:00:00 2001 From: catborise Date: Sun, 1 Nov 2026 14:05:21 +0300 Subject: [PATCH] feat(instances): a new VM can have UEFI Secure Boot and a TPM 2.0 Windows 11 does not install without Secure Boot and a TPM 2.0, and the new VM form offered neither: its firmware list was BIOS and one UEFI loader guessed from file names, which on RHEL never matched the Secure Boot one. The firmware choices are now BIOS, UEFI and UEFI Secure Boot, as the host's libvirt reports them (its firmware features from libvirt 12.1, before that its loaders), and libvirt picks the firmware itself: UEFI without Secure Boot, or with Microsoft's keys enrolled. A TPM 2.0 switch, shown when the host has swtpm, adds an emulated TPM. The create job refuses a firmware or a TPM the host does not offer, and the API takes both. The path-guessing helpers are gone. --- README.md | 3 + instances/api/serializers.py | 4 + instances/jobs.py | 22 +- instances/models.py | 2 + instances/templates/create_instance_w2.html | 7 + instances/tests/test_cloud_init_create.py | 1 + instances/tests/test_cpu_mode.py | 2 +- instances/tests/test_create_form_input.py | 4 +- instances/tests/test_create_instance.py | 39 +++- instances/tests/test_create_job.py | 14 ++ instances/tests/test_create_rollback.py | 1 + instances/tests/test_firmware_tpm.py | 126 ++++++++++++ instances/tests/test_live_create.py | 41 ++-- instances/tests/test_pci_slots.py | 2 +- instances/views.py | 13 +- locale/tr/LC_MESSAGES/django.mo | Bin 155114 -> 155411 bytes locale/tr/LC_MESSAGES/django.po | 211 +++++++++++--------- vrtManager/connection.py | 73 ++++--- vrtManager/create.py | 37 ++-- vrtManager/util.py | 25 --- 20 files changed, 399 insertions(+), 228 deletions(-) create mode 100644 instances/tests/test_firmware_tpm.py diff --git a/README.md b/README.md index 51de284d..e53cd5c2 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ WebVirtCloud and `virt-manager`/`virsh` can manage the same libvirt host **concu * Storage pool and volume management * Network (with DHCP leases), interface, network filter and secret management * PCI and USB devices of the host given to VMs +* UEFI Secure Boot and TPM 2.0 for new VMs, as Windows 11 needs * Browser-based VNC console (noVNC) and serial console * Role-based access control: users only see the VMs assigned to them, within their quota * 2FA (OTP / TOTP) and failed-login lockout @@ -238,6 +239,8 @@ A compute node is a KVM host the panel manages over libvirt. # or, from a cloned repository: sudo sh ./dev/libvirt-bootstrap.sh ``` + The new VM form offers UEFI, UEFI Secure Boot (with Microsoft's keys, for Windows 11) and a TPM 2.0 only when the host's libvirt finds the UEFI firmware and swtpm: install `ovmf swtpm-tools` (Debian / Ubuntu) or `edk2-ovmf swtpm swtpm-tools` (RHEL family) if they are missing. + 2. For SSH connections, give the user the panel runs as an SSH key and copy it to the compute node. **Use only the block for your installation type:** a key set up with the host commands is not seen by the panel in Docker. `ssh-copy-id` asks for the compute node's root password once. `accept-new` accepts a new host's key on first connect and refuses a changed one. **Installer or manual installation**, on the panel host: diff --git a/instances/api/serializers.py b/instances/api/serializers.py index fbcf6d99..1a4b72c0 100644 --- a/instances/api/serializers.py +++ b/instances/api/serializers.py @@ -72,6 +72,8 @@ class DestroyOptionsSerializer(serializers.Serializer): class CreateInstanceSerializer(serializers.ModelSerializer): + # BIOS, UEFI or UEFI Secure Boot, as the host offers; left out: the machine's default + firmware = serializers.CharField(required=False) video = serializers.CharField(initial="vga") storage = serializers.CharField(initial="default") cache_mode = serializers.CharField(initial="none") @@ -90,6 +92,8 @@ class CreateInstanceSerializer(serializers.ModelSerializer): model = CreateInstance fields = [ "name", + "firmware", + "tpm", "vcpu", "vcpu_mode", "memory", diff --git a/instances/jobs.py b/instances/jobs.py index 547fdce0..242e3d72 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -665,11 +665,11 @@ class CreateInstance(Kind): def normalize(self, raw): # what the API leaves out means what create_instance did by default params = {"template": "", "images": "", "firmware": "", "net_model": "virtio", "add_cdrom": "sata", - "add_input": "default", "meta_prealloc": False, "hdd_size": 0, "cloud_init": False} + "add_input": "default", "meta_prealloc": False, "hdd_size": 0, "cloud_init": False, "tpm": False} params.update({k: v for k, v in raw.items() if k not in ("submission_key", "create", "csrfmiddlewaretoken")}) params.update(compute=int(raw["compute"]), devices=[list(d) for d in raw.get("devices", [])]) - if "UEFI" in params["firmware"] and "secboot" in params["firmware"].partition(":")[2]: - params["machine"] = "q35" # a secure boot loader needs SMM, on q35 only + if params["firmware"] == "UEFI Secure Boot" and "q35" not in params["machine"]: + params["machine"] = "q35" # Secure Boot needs SMM, on q35 only return params def validate(self, user, params): @@ -708,8 +708,10 @@ class CreateInstance(Kind): utils.nic_macs(data["mac"], data["networks"]) except ValueError as err: raise util.OperationError(str(err)) - if "UEFI" in data["firmware"] and ":" not in data["firmware"]: - raise util.OperationError(_("Invalid firmware")) + if data["firmware"] and data["firmware"] not in conn.get_firmwares(data["arch"], data["machine"]): + raise util.OperationError(_("This host offers no firmware %(firmware)s") % {"firmware": data["firmware"]}) + if data["tpm"] and not conn.get_tpm_model(data["arch"], data["machine"]): + raise util.OperationError(_("This host offers no TPM 2.0: install swtpm on it")) def plan(self, user, params): compute = Compute.objects.get(pk=params["compute"]) @@ -727,13 +729,6 @@ class CreateInstance(Kind): default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID) default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID) - firmware = dict() - if "UEFI" in data["firmware"]: - firmware["loader"] = data["firmware"].split(":")[1].strip() - firmware["secure"] = "yes" if "secboot" in firmware["loader"] else "no" - firmware["readonly"] = "yes" - firmware["type"] = "pflash" - def volume(path, device="disk", bus=default_bus): return { "path": path, @@ -783,7 +778,7 @@ class CreateInstance(Kind): uuid=op.intent["uuid"], arch=data["arch"], machine=data["machine"], - firmware=firmware, + firmware=data["firmware"], volumes=volume_list, networks=data["networks"], virtio=data["virtio"], @@ -796,6 +791,7 @@ class CreateInstance(Kind): qemu_ga=data["qemu_ga"], add_cdrom=data["add_cdrom"], add_input=data["add_input"], + tpm_model=conn.get_tpm_model(data["arch"], data["machine"]) if data["tpm"] else None, ) except Exception as err: # the VM is not defined: its new volumes go diff --git a/instances/models.py b/instances/models.py index a840a3df..64fd6c61 100644 --- a/instances/models.py +++ b/instances/models.py @@ -402,6 +402,8 @@ class CreateInstance(models.Model): error_messages={"required": _("No Virtual Machine name has been entered")}, ) firmware = models.CharField(max_length=64) + # an emulated TPM 2.0, as Windows 11 needs + tpm = models.BooleanField(default=False) vcpu = models.IntegerField( error_messages={"required": _("No VCPU has been entered")}, validators=[MinValueValidator(1)] ) diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html index 62ea9694..62350a4a 100644 --- a/instances/templates/create_instance_w2.html +++ b/instances/templates/create_instance_w2.html @@ -77,6 +77,13 @@ {% endfor %} {% field_error vm_form.firmware "vm_firmware" %} + {% if tpm_support %} +
+ + +
{% trans "Windows 11 needs it, with UEFI Secure Boot" %}
+
+ {% endif %} {% endif %} {% if dom_caps.cpu_modes %} diff --git a/instances/tests/test_cloud_init_create.py b/instances/tests/test_cloud_init_create.py index 5af7f222..33999f77 100644 --- a/instances/tests/test_cloud_init_create.py +++ b/instances/tests/test_cloud_init_create.py @@ -32,6 +32,7 @@ class CloudInitCreateTestCase(TransactionTestCase): self.addCleanup(patcher.stop) self.conn.get_instances.return_value = [] self.conn.get_cache_modes.return_value = {"none": "Disable cache"} + self.conn.get_firmwares.return_value = ["BIOS", "UEFI", "UEFI Secure Boot"] self.conn.get_dom_capabilities.return_value = {"cpu_modes": ["host-model"]} self.conn.get_disk_bus_types.return_value = ["virtio", "sata"] self.conn.create_volume.return_value = "/srv/default/api-vm.qcow2" diff --git a/instances/tests/test_cpu_mode.py b/instances/tests/test_cpu_mode.py index c69e8073..d8a2c9fb 100644 --- a/instances/tests/test_cpu_mode.py +++ b/instances/tests/test_cpu_mode.py @@ -22,7 +22,7 @@ def defined_cpu(vcpu_mode): patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS): conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode=vcpu_mode, uuid="00000000-0000-4000-8000-0000000000c1", arch="x86_64", machine="q35", - firmware={}, volumes=[], networks="", nwfilter="", virtio=True, listener_addr="0.0.0.0", + firmware="BIOS", volumes=[], networks="", nwfilter="", virtio=True, listener_addr="0.0.0.0", add_cdrom="None", add_input="None") return etree.fromstring(conn.wvm.defineXML.call_args.args[0]).find("cpu") diff --git a/instances/tests/test_create_form_input.py b/instances/tests/test_create_form_input.py index 058dea62..3ad699b9 100644 --- a/instances/tests/test_create_form_input.py +++ b/instances/tests/test_create_form_input.py @@ -182,8 +182,8 @@ class CreateFormValuesTests(TestCase): self.assertIn("Networks: This field is required.", self.summary(page)) def test_a_firmware_error_without_a_firmware_choice_is_in_the_summary(self): - # an architecture with no firmware label renders no firmware select - with patch("vrtManager.connection.wvmConnect.label_for_firmware_path", return_value=None): + # an architecture with no firmware choice renders no firmware select + with patch("vrtManager.connection.wvmConnect.get_firmwares", return_value=[]): fields = valid() del fields["firmware"] _, response = render_create_page(self, fields) diff --git a/instances/tests/test_create_instance.py b/instances/tests/test_create_instance.py index 1d24e090..5fb029aa 100644 --- a/instances/tests/test_create_instance.py +++ b/instances/tests/test_create_instance.py @@ -47,11 +47,24 @@ class CreateInstanceViewTests(TestCase): @patch("instances.views.wvmCreate", new=wvm_create) @patch("instances.views.NewVMForm") - def test_secure_boot_loader_on_q35_requests_secure_boot(self, form_class): - _, conn = self.create(form_class, firmware="UEFI x86_64: /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd") + def test_secure_boot_and_a_tpm_reach_the_definition(self, form_class): + _, conn = self.create(form_class, firmware="UEFI Secure Boot", tpm=True) run(Operation.objects.get()) - firmware = conn.create_instance.call_args.kwargs["firmware"] - self.assertEqual(firmware["secure"], "yes") + kwargs = conn.create_instance.call_args.kwargs + self.assertEqual((kwargs["firmware"], kwargs["machine"], kwargs["tpm_model"]), + ("UEFI Secure Boot", "q35", "tpm-crb")) + + @patch("instances.views.wvmCreate", new=wvm_create) + @patch("instances.views.NewVMForm") + def test_a_firmware_or_a_tpm_the_host_does_not_offer_is_refused(self, form_class): + for data, host in (({"firmware": "UEFI Secure Boot"}, {"get_firmwares": ["BIOS", "UEFI"]}), + ({"tpm": True}, {"get_tpm_model": None})): + with self.subTest(data): + Instance.objects.all().delete() + response, conn = self.create(form_class, host=host, **data) + self.assertEqual(response.status_code, 200) + self.assertFalse(Operation.objects.exists()) + conn.create_instance.assert_not_called() @patch("instances.views.wvmCreate", new=wvm_create) @patch("instances.views.NewVMForm") @@ -71,7 +84,9 @@ class CreateInstanceViewTests(TestCase): conn.create_volume.assert_not_called() conn.create_instance.assert_not_called() - def create(self, form_class, **data): + def create(self, form_class, host=None, **data): + """POST the create form with data over the form's defaults; host: what + the mocked host answers, over its defaults.""" user = get_user_model().objects.filter(username="create-test-admin").first() or ( get_user_model().objects.create_superuser( username="create-test-admin", password="password", email="create@example.com" @@ -95,8 +110,10 @@ class CreateInstanceViewTests(TestCase): conn.get_storages.return_value = ["default"] conn.get_dom_capabilities.return_value = {"loader_enums": [], "loaders": [], "cpu_modes": ["host-model"]} conn.get_capabilities.return_value = {} - conn.label_for_firmware_path.return_value = None - conn.find_uefi_path_for_arch.return_value = None + conn.get_firmwares.return_value = ["BIOS", "UEFI", "UEFI Secure Boot"] + conn.get_tpm_model.return_value = "tpm-crb" + for method, value in (host or {}).items(): + getattr(conn, method).return_value = value conn.get_volume_path.return_value = "/var/lib/libvirt/images/base.qcow2" conn.get_volume_format_type.return_value = "qcow2" @@ -110,7 +127,7 @@ class CreateInstanceViewTests(TestCase): "virtio": True, "listener_addr": "0.0.0.0", "nwfilter": "", "graphics": "vnc", "video": "vga", "mac": "52:54:00:00:00:01", "qemu_ga": False, - "add_cdrom": "sata", "add_input": "default", + "add_cdrom": "sata", "add_input": "default", "tpm": False, **data, } @@ -138,13 +155,13 @@ class CreateInstanceViewTests(TestCase): # A host without loader support has no "loaders"/"loader_enums" keys conn.get_dom_capabilities.return_value = {"loader_support": "no", "cpu_modes": ["host-model"]} conn.get_capabilities.return_value = {} - conn.label_for_firmware_path.return_value = None - conn.find_uefi_path_for_arch.return_value = None + conn.get_firmwares.return_value = ["BIOS"] + conn.get_tpm_model.return_value = None response = self.client.get(reverse("instances:create_instance", args=[compute.id, "x86_64", "q35"])) self.assertEqual(response.status_code, 200) - conn.find_uefi_path_for_arch.assert_called_once_with("x86_64", "q35") + conn.get_firmwares.assert_called_once_with("x86_64", "q35") class NicMacsTestCase(TestCase): diff --git a/instances/tests/test_create_job.py b/instances/tests/test_create_job.py index 5601f20c..99b6d00c 100644 --- a/instances/tests/test_create_job.py +++ b/instances/tests/test_create_job.py @@ -31,6 +31,7 @@ class CreateJobTestCase(TransactionTestCase): self.addCleanup(patcher.stop) self.conn.get_instances.return_value = [] self.conn.get_cache_modes.return_value = {"none": "Disable cache"} + self.conn.get_firmwares.return_value = ["BIOS", "UEFI", "UEFI Secure Boot"] self.conn.get_disk_bus_types.return_value = ["virtio", "sata"] self.conn.get_dom_capabilities.return_value = {"cpu_modes": ["host-model", "custom"], "cpu_custom_models": ["Skylake-Server-noTSX-IBRS"]} @@ -51,6 +52,19 @@ class CreateJobTestCase(TransactionTestCase): self.assertEqual(self.conn.create_instance.call_args.kwargs["uuid"], op.intent["uuid"]) self.assertTrue(Instance.objects.filter(uuid=op.intent["uuid"], name="api-vm").exists()) + def test_the_api_takes_secure_boot_and_a_tpm(self): + url = f"/api/v1/computes/{self.compute.pk}/instances/create/x86_64/pc/" + body = {**BODY, "firmware": "UEFI Secure Boot", "tpm": True} + self.assertEqual(self.client.post(url, body, content_type="application/json").status_code, 202) + params = Operation.objects.get().params + self.assertEqual((params["firmware"], params["tpm"], params["machine"]), ("UEFI Secure Boot", True, "q35")) + + def test_secure_boot_keeps_a_versioned_q35_machine(self): + url = f"/api/v1/computes/{self.compute.pk}/instances/create/x86_64/pc-q35-9.2/" + body = {**BODY, "firmware": "UEFI Secure Boot"} + self.assertEqual(self.client.post(url, body, content_type="application/json").status_code, 202) + self.assertEqual(Operation.objects.get().params["machine"], "pc-q35-9.2") + def test_a_refused_create_queues_nothing(self): url = f"/api/v1/computes/{self.compute.pk}/instances/create/x86_64/q35/" response = self.client.post(url, {**BODY, "cache_mode": "writeback"}, content_type="application/json") diff --git a/instances/tests/test_create_rollback.py b/instances/tests/test_create_rollback.py index f6475429..0c5d4b5a 100644 --- a/instances/tests/test_create_rollback.py +++ b/instances/tests/test_create_rollback.py @@ -33,6 +33,7 @@ class CreateRollbackTestCase(TestCase): with patch("instances.jobs.wvmCreate") as wvm: conn = wvm.return_value conn.get_cache_modes.return_value = {"none": "Disable cache"} + conn.get_firmwares.return_value = ["BIOS", "UEFI", "UEFI Secure Boot"] conn.get_dom_capabilities.return_value = {"cpu_modes": ["host-model"]} conn.get_instances.return_value = [] conn.create_volume.return_value = "/pool/rb-vm.qcow2" diff --git a/instances/tests/test_firmware_tpm.py b/instances/tests/test_firmware_tpm.py new file mode 100644 index 00000000..0dcafba3 --- /dev/null +++ b/instances/tests/test_firmware_tpm.py @@ -0,0 +1,126 @@ +"""A new VM's firmware and TPM: the choices come from the host's domain +capabilities, UEFI is libvirt's firmware autoselection (Secure Boot with +Microsoft's keys enrolled, as Windows 11 needs) and a TPM is an emulated +TPM 2.0 (swtpm).""" + +from unittest.mock import MagicMock, patch + +from django.test import SimpleTestCase +from lxml import etree +from vrtManager.connection import wvmConnect +from vrtManager.create import wvmCreate + +FULL = """ + efi + + yesno + yesno + + + tpm-tistpm-crb + emulatorexternal + 2.0 +""" + +# Secure Boot firmware without Microsoft's keys, and no swtpm +BARE = """ + efi + + yesno + no + +""" + +# only Secure Boot firmware, and a TPM of another machine +SECURE_ONLY = """ + efi + + yes + yes + + + tpm-spapr + emulator + 2.0 +""" + +# libvirt before 12.1: no firmwareFeatures, the loaders say whether they do Secure Boot +EARLIER = """ + efi + yesno +""" +EARLIER_PLAIN = """ + efi + +""" + +DOM_CAPS = {"domain": "kvm", "os_support": "yes", "cpu_modes": ["host-model"]} + + +def host(dom_cap_xml): + conn = wvmConnect.__new__(wvmConnect) + conn.get_dom_cap_xml = lambda arch, machine: dom_cap_xml + return conn + + +def defined(firmware, tpm_model=None): + conn = wvmCreate.__new__(wvmCreate) + conn.wvm = MagicMock() + with patch.object(wvmCreate, "get_capabilities", return_value={"features": ["acpi"], "os_type": "hvm"}), \ + patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS): + conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model", + uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine="q35", + firmware=firmware, volumes=[], networks="", nwfilter="", virtio=True, + listener_addr="0.0.0.0", add_cdrom="None", add_input="None", tpm_model=tpm_model) + return etree.fromstring(conn.wvm.defineXML.call_args.args[0]) + + +class HostOffersTests(SimpleTestCase): + def test_a_host_with_enrolled_keys_and_swtpm_offers_everything(self): + self.assertEqual(host(FULL).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI", "UEFI Secure Boot"]) + self.assertEqual(host(FULL).get_tpm_model("x86_64", "q35"), "tpm-crb") + + def test_secure_boot_without_microsofts_keys_and_no_swtpm_are_not_offered(self): + self.assertEqual(host(BARE).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI"]) + self.assertIsNone(host(BARE).get_tpm_model("x86_64", "q35")) + + def test_bios_is_x86_only_and_secure_boot_x86_64_only(self): + self.assertEqual(host(FULL).get_firmwares("aarch64", "virt"), ["UEFI"]) + + def test_uefi_is_offered_only_where_secure_boot_can_be_off(self): + self.assertEqual(host(SECURE_ONLY).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI Secure Boot"]) + + def test_an_earlier_libvirt_offers_what_its_loaders_do(self): + self.assertEqual(host(EARLIER).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI", "UEFI Secure Boot"]) + self.assertEqual(host(EARLIER_PLAIN).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI"]) + + def test_a_machine_without_crb_or_tis_takes_its_own_tpm_model(self): + self.assertEqual(host(SECURE_ONLY).get_tpm_model("ppc64le", "pseries"), "tpm-spapr") + + +class DefinitionTests(SimpleTestCase): + def features(self, tree): + return {f.get("name"): f.get("enabled") for f in tree.findall("os/firmware/feature")} + + def test_bios_leaves_the_firmware_to_the_machine(self): + tree = defined("BIOS") + self.assertIsNone(tree.find("os").get("firmware")) + self.assertEqual((self.features(tree), tree.find("features/smm")), ({}, None)) + + def test_uefi_is_picked_by_libvirt_without_secure_boot(self): + tree = defined("UEFI") + self.assertEqual(tree.find("os").get("firmware"), "efi") + self.assertEqual(self.features(tree), {"secure-boot": "no"}) + self.assertIsNone(tree.find("features/smm")) + + def test_secure_boot_has_microsofts_keys_and_smm(self): + tree = defined("UEFI Secure Boot") + self.assertEqual(tree.find("os").get("firmware"), "efi") + self.assertEqual(self.features(tree), {"secure-boot": "yes", "enrolled-keys": "yes"}) + self.assertEqual(tree.find("features/smm").get("state"), "on") + + def test_a_tpm_is_an_emulated_tpm_2(self): + self.assertEqual(defined("UEFI").findall("devices/tpm"), []) + tpm = defined("UEFI Secure Boot", tpm_model="tpm-crb").find("devices/tpm") + self.assertEqual((tpm.get("model"), tpm.find("backend").get("type"), tpm.find("backend").get("version")), + ("tpm-crb", "emulator", "2.0")) diff --git a/instances/tests/test_live_create.py b/instances/tests/test_live_create.py index 0d94b8b3..8281dcf6 100644 --- a/instances/tests/test_live_create.py +++ b/instances/tests/test_live_create.py @@ -117,8 +117,8 @@ class LiveCreateTestCase(TransactionTestCase): def test_invalid_cache_mode_allocates_nothing(self): self.assert_nothing_left("cr-cache", self.create("cr-cache", cache_mode="bogus")) - def test_malformed_uefi_firmware_allocates_nothing(self): - self.assert_nothing_left("cr-uefi", self.create("cr-uefi", firmware="UEFI")) + def test_a_firmware_the_host_does_not_offer_allocates_nothing(self): + self.assert_nothing_left("cr-uefi", self.create("cr-uefi", firmware="Coreboot")) def test_rejected_definition_removes_the_new_disk(self): self.assert_nothing_left("cr-define", self.create("cr-define", video="no-such-model")) @@ -129,36 +129,37 @@ class LiveCreateTestCase(TransactionTestCase): self.assertTrue(self.domain_exists(P + "cr-ok")) self.assertEqual(len(self.volumes()), 1) - def test_offered_uefi_firmware_boots_on_q35(self): + def offered(self): page = self.client.get(reverse("instances:create_instance", args=[self.compute.id, "x86_64", "q35"])) - uefi = [f for f in page.context["firmwares"] if f.startswith("UEFI")] - self.assertTrue(uefi, "no UEFI firmware offered for q35") + return page.context["firmwares"], page.context["tpm_support"] - self.create("cr-uefi-ok", firmware=uefi[0]) + def test_uefi_boots_without_secure_boot(self): + self.assertIn("UEFI", self.offered()[0], "no UEFI offered for q35") + + self.create("cr-uefi-ok", firmware="UEFI") dom = self.conn.lookupByName(P + "cr-uefi-ok") - loader = etree.fromstring(dom.XMLDesc(0)).findtext("os/loader") - self.assertEqual(loader, uefi[0].split(":", 1)[1].strip()) - # Listed by libvirt for q35, so the domain starts with it - caps = etree.fromstring(self.conn.getDomainCapabilities(None, "x86_64", "q35", "kvm")) - self.assertIn(loader, caps.xpath("os/loader/value/text()")) dom.create() - self.assertTrue(dom.isActive()) + tree = etree.fromstring(dom.XMLDesc(0)) + self.assertEqual(tree.find("os").get("firmware"), "efi") + self.assertNotEqual(tree.find("os/loader").get("secure"), "yes") + self.assertTrue(tree.findtext("os/nvram")) - def test_secure_boot_loader_on_q35_gets_secure_boot(self): - caps = etree.fromstring(self.conn.getDomainCapabilities(None, "x86_64", "q35", "kvm")) - secboot = [p for p in caps.xpath("os/loader/value/text()") if "secboot" in p and "OVMF_CODE" in p] - if not secboot: - self.skipTest("the host has no secure boot loader for q35") + def test_secure_boot_with_a_tpm_boots_as_windows_11_needs(self): + firmwares, tpm_support = self.offered() + if "UEFI Secure Boot" not in firmwares or not tpm_support: + self.skipTest("the host offers no Secure Boot with Microsoft's keys, or no swtpm") - self.create("cr-secboot", firmware=f"UEFI x86_64: {secboot[0]}") + self.create("cr-secboot", firmware="UEFI Secure Boot", tpm="true") dom = self.conn.lookupByName(P + "cr-secboot") + dom.create() tree = etree.fromstring(dom.XMLDesc(0)) self.assertEqual(tree.find("os/loader").get("secure"), "yes") self.assertEqual(tree.find("features/smm").get("state"), "on") - dom.create() - self.assertTrue(dom.isActive()) + # the variables libvirt chose come with Microsoft's keys enrolled + self.assertEqual(tree.xpath("os/firmware/feature[@name='enrolled-keys']/@enabled"), ["yes"]) + self.assertEqual(tree.xpath("devices/tpm/backend/@version"), ["2.0"]) def test_every_offered_cdrom_bus_creates_a_vm_that_starts(self): page = self.client.get(reverse("instances:create_instance", args=[self.compute.id, "x86_64", "q35"])) diff --git a/instances/tests/test_pci_slots.py b/instances/tests/test_pci_slots.py index 32a30739..4a1f21bf 100644 --- a/instances/tests/test_pci_slots.py +++ b/instances/tests/test_pci_slots.py @@ -28,7 +28,7 @@ def defined_controllers(machine): patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS): conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model", uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine=machine, - firmware={}, volumes=[], networks="default", nwfilter="", virtio=True, + firmware="BIOS", volumes=[], networks="default", nwfilter="", virtio=True, listener_addr="0.0.0.0", add_cdrom="None", add_input="None") domain = etree.fromstring(conn.wvm.defineXML.call_args.args[0]) return [c.get("model") for c in domain.findall("devices/controller[@type='pci']")] diff --git a/instances/views.py b/instances/views.py index 3e45c706..397633ec 100755 --- a/instances/views.py +++ b/instances/views.py @@ -2459,7 +2459,6 @@ def create_instance(request, compute_id, arch, machine): storages = list() networks = list() hypervisors = list() - firmwares = list() meta_prealloc = False compute = get_object_or_404(Compute, pk=compute_id) flavors = Flavor.objects.filter().order_by("id") @@ -2512,16 +2511,8 @@ def create_instance(request, compute_id, arch, machine): caps = conn.get_capabilities(arch) virtio_support = conn.is_supports_virtio(arch, machine) - # Add BIOS - label = conn.label_for_firmware_path(arch, None) - if label: - firmwares.append(label) - # Add UEFI - loader_path = conn.find_uefi_path_for_arch(arch, machine) - label = conn.label_for_firmware_path(arch, loader_path) - if label: - firmwares.append(label) - firmwares = list(set(firmwares)) + firmwares = conn.get_firmwares(arch, machine) + tpm_support = bool(conn.get_tpm_model(arch, machine)) if not storages: raise util.OperationError(_("You haven't defined any storage pools")) diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index 7ab1ed1fdb3b2df0e92d39c79071a5659a75e39f..259f3dd5f1bdb98c457036be438d1eebc6cf3ffe 100644 GIT binary patch delta 35686 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 diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index 0122ec10..d535f790 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-10-31 18:13+0000\n" +"POT-Creation-Date: 2026-11-01 10:56+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/" @@ -275,7 +275,7 @@ msgstr "Parola Değiştir" #: instances/templates/add_instance_owner_block.html:29 #: instances/templates/add_instance_volume.html:108 #: instances/templates/create_instance_w1.html:39 -#: instances/templates/create_instance_w2.html:345 +#: instances/templates/create_instance_w2.html:352 #: instances/templates/instances/destroy_instance_form.html:48 #: instances/templates/instances/edit_instance_volume.html:111 #: instances/templates/instances/move_disk.html:27 @@ -1395,7 +1395,9 @@ msgstr "Oturum açma hata sınırı" #: appsettings/migrations/0014_login_lockout_settings.py:8 msgid "Failed logins of a user from one address before it is locked" -msgstr "Bir kullanıcının bir adresten kilitlenmeden önceki başarısız oturum açma sayısı" +msgstr "" +"Bir kullanıcının bir adresten kilitlenmeden önceki başarısız oturum açma " +"sayısı" #: appsettings/migrations/0014_login_lockout_settings.py:9 msgid "Login Lockout Minutes" @@ -1413,7 +1415,9 @@ msgstr "Anlık görüntü boş alan yüzdesi" msgid "" "Free space a storage pool keeps for a snapshot, in percent of the VM's disks " "in it (0: none)" -msgstr "Bir depolama havuzunun anlık görüntü için ayırdığı boş alan; sanal makinenin bu havuzdaki disklerinin yüzdesi olarak (0: yok)" +msgstr "" +"Bir depolama havuzunun anlık görüntü için ayırdığı boş alan; sanal makinenin " +"bu havuzdaki disklerinin yüzdesi olarak (0: yok)" #: appsettings/migrations/0017_snapshot_free_space_settings.py:9 msgid "Snapshot Free Min GiB" @@ -1422,7 +1426,9 @@ msgstr "Anlık görüntü en az boş alan (GiB)" #: appsettings/migrations/0017_snapshot_free_space_settings.py:10 msgid "" "Free space a storage pool keeps for a snapshot at least, in GiB (0: none)" -msgstr "Bir depolama havuzunun anlık görüntü için en az ayırdığı boş alan, GiB olarak (0: yok)" +msgstr "" +"Bir depolama havuzunun anlık görüntü için en az ayırdığı boş alan, GiB " +"olarak (0: yok)" #: appsettings/migrations/0018_worker_settings.py:7 msgid "Worker Concurrency" @@ -1459,7 +1465,9 @@ msgstr "Yeni bir sanal makine için önce seçilen CPU modu" #: appsettings/migrations/0021_setting_descriptions.py:10 msgid "" "Network card model chosen first for a new VM (default: libvirt's choice)" -msgstr "Yeni bir sanal makine için önce seçilen ağ kartı modeli (default: libvirt'in seçimi)" +msgstr "" +"Yeni bir sanal makine için önce seçilen ağ kartı modeli (default: libvirt'in " +"seçimi)" #: appsettings/migrations/0021_setting_descriptions.py:11 msgid "Video device chosen first for a new VM" @@ -1467,7 +1475,8 @@ msgstr "Yeni bir sanal makine için önce seçilen video aygıtı" #: appsettings/migrations/0021_setting_descriptions.py:12 msgid "Input device chosen first for a new VM (None: no input device)" -msgstr "Yeni bir sanal makine için önce seçilen girdi aygıtı (None: girdi aygıtı yok)" +msgstr "" +"Yeni bir sanal makine için önce seçilen girdi aygıtı (None: girdi aygıtı yok)" #: appsettings/migrations/0021_setting_descriptions.py:13 msgid "Bus of the CD-ROM a new VM gets (None: no CD-ROM)" @@ -1487,16 +1496,21 @@ msgstr "Yeni disklerin önbellek modu" #: appsettings/migrations/0021_setting_descriptions.py:17 msgid "I/O mode of new disks (native needs cache none or directsync)" -msgstr "Yeni disklerin G/Ç modu (native, önbellek modu none veya directsync gerektirir)" +msgstr "" +"Yeni disklerin G/Ç modu (native, önbellek modu none veya directsync " +"gerektirir)" #: appsettings/migrations/0021_setting_descriptions.py:18 msgid "Discard of new disks (unmap: the guest's TRIM frees space in the image)" -msgstr "Yeni disklerin discard ayarı (unmap: konuğun TRIM komutu imajda yer açar)" +msgstr "" +"Yeni disklerin discard ayarı (unmap: konuğun TRIM komutu imajda yer açar)" #: appsettings/migrations/0021_setting_descriptions.py:19 msgid "" "Detect zeroes of new disks: how blocks of zeros the guest writes are stored" -msgstr "Yeni disklerin sıfır algılama ayarı: konuğun yazdığı sıfır bloklarının nasıl saklanacağı" +msgstr "" +"Yeni disklerin sıfır algılama ayarı: konuğun yazdığı sıfır bloklarının nasıl " +"saklanacağı" #: appsettings/migrations/0021_setting_descriptions.py:20 msgid "Controller model for disks on the SCSI bus" @@ -1549,15 +1563,21 @@ msgstr "Bir sanal makine birden fazla kullanıcıya verilebilir" #: appsettings/migrations/0021_setting_descriptions.py:32 msgid "" "A quota refusal shows the numbers: what would be used and what is allowed" -msgstr "Kota reddi sayıları gösterir: ne kadar kullanılacağını ve ne kadarına izin verildiğini" +msgstr "" +"Kota reddi sayıları gösterir: ne kadar kullanılacağını ve ne kadarına izin " +"verildiğini" #: appsettings/migrations/0021_setting_descriptions.py:33 msgid "Instances page for administrators: VMs under their host, or one list" -msgstr "Yöneticiler için Sanal Makineler sayfası: sanal makineler sunucularının altında ya da tek bir listede" +msgstr "" +"Yöneticiler için Sanal Makineler sayfası: sanal makineler sunucularının " +"altında ya da tek bir listede" #: appsettings/migrations/0021_setting_descriptions.py:34 msgid "A VM's page shows a bar with its host's links at the bottom" -msgstr "Sanal makinenin sayfası, altta sunucusunun bağlantılarını içeren bir çubuk gösterir" +msgstr "" +"Sanal makinenin sayfası, altta sunucusunun bağlantılarını içeren bir çubuk " +"gösterir" #: appsettings/migrations/0021_setting_descriptions.py:35 msgid "The Instances page shows each VM's DRBD state" @@ -1921,7 +1941,7 @@ msgid "Connection" msgstr "Bağlantı" #: computes/templates/computes/form.html:31 -#: instances/templates/create_instance_w2.html:248 +#: instances/templates/create_instance_w2.html:255 msgid "Advanced" msgstr "Gelişmiş" @@ -1978,8 +1998,8 @@ msgstr "Depolama" #: computes/templates/computes/host_nav.html:19 #: computes/templates/computes/host_nav.html:20 #: instances/templates/add_instance_network_block.html:18 -#: instances/templates/create_instance_w2.html:220 -#: instances/templates/create_instance_w2.html:222 +#: instances/templates/create_instance_w2.html:227 +#: instances/templates/create_instance_w2.html:229 #: instances/templates/instances/settings_tab.html:379 #: networks/templates/networks.html:5 networks/templates/networks.html:7 msgid "Networks" @@ -2376,9 +2396,9 @@ msgstr "Ölçekleme" #: console/templates/console-vnc.html:29 #: instances/templates/add_instance_network_block.html:41 -#: instances/templates/create_instance_w2.html:275 -#: instances/templates/create_instance_w2.html:304 -#: instances/templates/create_instance_w2.html:314 +#: instances/templates/create_instance_w2.html:282 +#: instances/templates/create_instance_w2.html:311 +#: instances/templates/create_instance_w2.html:321 #: instances/templates/instance.html:55 #: instances/templates/instances/settings_tab.html:186 #: instances/templates/instances/settings_tab.html:342 @@ -2451,7 +2471,7 @@ msgstr "Bağlan" #: instances/templates/allinstances.html:32 #: instances/templates/allinstances.html:88 #: instances/templates/create_inst_block.html:12 -#: instances/templates/create_instance_w2.html:357 +#: instances/templates/create_instance_w2.html:364 #: instances/templates/instances/edit_instance_volume.html:13 #: instances/templates/instances/move_disk.html:13 #: instances/templates/instances/settings_tab.html:366 @@ -2695,7 +2715,7 @@ msgstr "libvirt sanal makineyi hâlâ geri döndürüyor" msgid "Clone VM" msgstr "Sanal makineyi klonla" -#: instances/jobs.py:495 instances/jobs.py:503 instances/jobs.py:751 +#: instances/jobs.py:495 instances/jobs.py:503 instances/jobs.py:746 msgid "Copying disks" msgstr "Diskler kopyalanıyor" @@ -2704,7 +2724,7 @@ msgid "libvirt is still copying the disks" msgstr "libvirt diskleri hâlâ kopyalıyor" #: instances/jobs.py:662 instances/templates/create_instance_w1.html:58 -#: instances/templates/create_instance_w2.html:346 +#: instances/templates/create_instance_w2.html:353 msgid "Create VM" msgstr "Sanal makine oluştur" @@ -2754,49 +2774,54 @@ msgid "The cloud-init seed needs a directory storage pool for the disk" msgstr "cloud-init tohumu, disk için bir dizin depolama havuzu gerektirir" #: instances/jobs.py:712 -msgid "Invalid firmware" -msgstr "Geçersiz donanım yazılımı" +#, python-format +msgid "This host offers no firmware %(firmware)s" +msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor" -#: instances/jobs.py:770 +#: instances/jobs.py:714 +msgid "This host offers no TPM 2.0: install swtpm on it" +msgstr "Bu sunucu TPM 2.0 sunmuyor: üzerine swtpm kurun" + +#: instances/jobs.py:765 msgid "Writing the cloud-init seed" msgstr "cloud-init tohumu yazılıyor" -#: instances/jobs.py:777 +#: instances/jobs.py:772 msgid "Defining the VM" msgstr "Sanal makine tanımlanıyor" -#: instances/jobs.py:833 +#: instances/jobs.py:829 #, python-format msgid "The instance is already on %(compute)s" msgstr "Sanal makine zaten %(compute)s üzerinde" -#: instances/jobs.py:835 +#: instances/jobs.py:831 #, python-format msgid "%(compute)s is not reachable" msgstr "%(compute)s sunucusuna ulaşılamıyor" -#: instances/jobs.py:837 +#: instances/jobs.py:833 #, python-format msgid "%(compute)s is the VM's own host under another name" msgstr "%(compute)s, sanal makinenin kendi sunucusunun başka bir adı" -#: instances/jobs.py:855 +#: instances/jobs.py:851 msgid "Migrate VM" msgstr "Sanal makineyi taşı" -#: instances/jobs.py:899 +#: instances/jobs.py:895 msgid "Migrating the VM" msgstr "Sanal makine taşınıyor" -#: instances/jobs.py:931 +#: instances/jobs.py:927 msgid "libvirt is still migrating the VM" msgstr "libvirt sanal makineyi hâlâ taşıyor" -#: instances/jobs.py:972 +#: instances/jobs.py:968 msgid "Move disk" msgstr "Diski taşı" -#: instances/jobs.py:1003 +#: instances/jobs.py:999 msgid "Copying the disk" msgstr "Disk kopyalanıyor" @@ -2832,14 +2857,14 @@ msgstr "oluşturuldu" msgid "drbd" msgstr "drbd" -#: instances/models.py:221 instances/templates/create_instance_w2.html:108 +#: instances/models.py:221 instances/templates/create_instance_w2.html:115 #: instances/templates/instances/resize_tab.html:20 #: instances/templates/instances/settings_tab.html:25 #: instances/templates/instances/stats_tab.html:49 msgid "Disk" msgstr "Disk" -#: instances/models.py:223 instances/templates/create_instance_w2.html:302 +#: instances/models.py:223 instances/templates/create_instance_w2.html:309 #: instances/templates/instances/settings_tab.html:167 msgid "CD-ROM" msgstr "CD-ROM" @@ -2872,27 +2897,27 @@ msgstr "Güvenli olmayan" msgid "No Virtual Machine name has been entered" msgstr "Sanal makine adı girilmedi" -#: instances/models.py:406 +#: instances/models.py:408 msgid "No VCPU has been entered" msgstr "VCPU belirtilmedi" -#: instances/models.py:412 +#: instances/models.py:414 msgid "No RAM size has been entered" msgstr "RAM boyutu belirtilmedi" -#: instances/models.py:416 +#: instances/models.py:418 msgid "No Network pool has been choosen" msgstr "Ağ havuzu seçilmedi" -#: instances/models.py:424 +#: instances/models.py:426 msgid "Please select HDD cache mode" msgstr "Lütfen HDD ön bellek modunu seçin" -#: instances/models.py:436 +#: instances/models.py:438 msgid "Please select a video driver" msgstr "Lütfen bir video sürücüsü seçin" -#: instances/models.py:473 +#: instances/models.py:475 msgid "Can access console without password" msgstr "Parolasız konsol erişimi yapabilir" @@ -2986,12 +3011,12 @@ msgid "Add disk form" msgstr "Disk ekleme formu" #: instances/templates/add_instance_volume.html:19 -#: instances/templates/create_instance_w2.html:110 +#: instances/templates/create_instance_w2.html:117 msgid "Disk source" msgstr "Disk kaynağı" #: instances/templates/add_instance_volume.html:21 -#: instances/templates/create_instance_w2.html:124 instances/views.py:2427 +#: instances/templates/create_instance_w2.html:131 instances/views.py:2427 msgid "New disk" msgstr "Yeni disk" @@ -3009,8 +3034,8 @@ msgstr "Sunucunun bir havuzunda zaten bulunan bir birim." #: instances/templates/add_instance_volume.html:31 #: instances/templates/add_instance_volume.html:70 -#: instances/templates/create_instance_w2.html:127 -#: instances/templates/create_instance_w2.html:155 +#: instances/templates/create_instance_w2.html:134 +#: instances/templates/create_instance_w2.html:162 #: instances/templates/instances/move_disk.html:16 #: instances/templates/instances/settings_tab.html:229 msgid "Storage pool" @@ -3033,8 +3058,8 @@ msgid "Format" msgstr "Biçem" #: instances/templates/add_instance_volume.html:61 -#: instances/templates/create_instance_w2.html:144 -#: instances/templates/create_instance_w2.html:210 storages/forms.py:95 +#: instances/templates/create_instance_w2.html:151 +#: instances/templates/create_instance_w2.html:217 storages/forms.py:95 #: storages/forms.py:115 storages/templates/storage.html:198 msgid "Preallocate metadata (qcow2)" msgstr "Meta veriyi önceden ayır (qcow2)" @@ -3052,7 +3077,7 @@ msgid "Choose a pool first" msgstr "Önce bir havuz seçin" #: instances/templates/add_instance_volume.html:90 -#: instances/templates/create_instance_w2.html:174 +#: instances/templates/create_instance_w2.html:181 #: instances/templates/instances/edit_instance_volume.html:82 #: instances/templates/instances/edit_instance_volume.html:87 msgid "Bus" @@ -3335,43 +3360,51 @@ msgstr "Hazır boyutları yönet" msgid "Firmware" msgstr "Üretici yazılımı" +#: instances/templates/create_instance_w2.html:83 +msgid "TPM 2.0" +msgstr "TPM 2.0" + #: instances/templates/create_instance_w2.html:84 +msgid "Windows 11 needs it, with UEFI Secure Boot" +msgstr "Windows 11, UEFI Secure Boot ile birlikte bunu ister" + +#: instances/templates/create_instance_w2.html:91 msgid "CPU mode" msgstr "CPU modu" -#: instances/templates/create_instance_w2.html:86 +#: instances/templates/create_instance_w2.html:93 #: storages/templates/create_stg_block.html:118 msgid "Choose…" msgstr "Seçin…" -#: instances/templates/create_instance_w2.html:89 +#: instances/templates/create_instance_w2.html:96 msgid "Custom CPU Models" msgstr "Özel CPU modelleri" -#: instances/templates/create_instance_w2.html:136 storages/forms.py:94 +#: instances/templates/create_instance_w2.html:143 storages/forms.py:94 msgid "Size (GiB)" msgstr "Boyut (GiB)" -#: instances/templates/create_instance_w2.html:151 instances/views.py:2428 +#: instances/templates/create_instance_w2.html:158 instances/views.py:2428 msgid "Existing volumes" msgstr "Mevcut birimler" -#: instances/templates/create_instance_w2.html:157 -#: instances/templates/create_instance_w2.html:189 +#: instances/templates/create_instance_w2.html:164 +#: instances/templates/create_instance_w2.html:196 msgid "Select pool" msgstr "Havuz seçin" -#: instances/templates/create_instance_w2.html:162 -#: instances/templates/create_instance_w2.html:163 +#: instances/templates/create_instance_w2.html:169 +#: instances/templates/create_instance_w2.html:170 #: storages/templates/storage.html:116 storages/templates/storages.html:47 msgid "Volumes" msgstr "Birimler" -#: instances/templates/create_instance_w2.html:167 +#: instances/templates/create_instance_w2.html:174 msgid "Chosen volumes" msgstr "Seçilen birimler" -#: instances/templates/create_instance_w2.html:171 +#: instances/templates/create_instance_w2.html:178 #: instances/templates/instances/edit_instance_volume.html:81 #: interfaces/templates/create_iface_block.html:78 #: interfaces/templates/interface.html:113 networks/templates/networks.html:39 @@ -3380,91 +3413,91 @@ msgstr "Seçilen birimler" msgid "Device" msgstr "Aygıt" -#: instances/templates/create_instance_w2.html:178 -#: instances/templates/create_instance_w2.html:236 +#: instances/templates/create_instance_w2.html:185 +#: instances/templates/create_instance_w2.html:243 #: instances/templates/instances/settings_tab.html:678 #: networks/templates/network.html:303 msgid "Remove" msgstr "Kaldır" -#: instances/templates/create_instance_w2.html:184 instances/views.py:2429 +#: instances/templates/create_instance_w2.html:191 instances/views.py:2429 msgid "Clone from template" msgstr "Şablondan klonla" -#: instances/templates/create_instance_w2.html:187 +#: instances/templates/create_instance_w2.html:194 msgid "Template pool" msgstr "Şablon havuzu" -#: instances/templates/create_instance_w2.html:194 +#: instances/templates/create_instance_w2.html:201 msgid "Template volume" msgstr "Şablon birimi" -#: instances/templates/create_instance_w2.html:199 +#: instances/templates/create_instance_w2.html:206 msgid "Copy into pool" msgstr "Havuza kopyala" -#: instances/templates/create_instance_w2.html:221 +#: instances/templates/create_instance_w2.html:228 msgid "Add networks" msgstr "Ağ ekle" -#: instances/templates/create_instance_w2.html:228 +#: instances/templates/create_instance_w2.html:235 msgid "Network interfaces" msgstr "Ağ arabirimleri" -#: instances/templates/create_instance_w2.html:235 +#: instances/templates/create_instance_w2.html:242 msgid "MAC address" msgstr "MAC adresi" -#: instances/templates/create_instance_w2.html:255 +#: instances/templates/create_instance_w2.html:262 msgid "Disk cache mode" msgstr "Disk önbellek modu" -#: instances/templates/create_instance_w2.html:264 +#: instances/templates/create_instance_w2.html:271 msgid "NIC type" msgstr "NIC tipi" -#: instances/templates/create_instance_w2.html:273 +#: instances/templates/create_instance_w2.html:280 #: nwfilters/templates/nwfilter.html:6 nwfilters/templates/nwfilter.html:8 msgid "Network filter" msgstr "Ağ filtresi" -#: instances/templates/create_instance_w2.html:283 vrtManager/nodedevs.py:29 +#: instances/templates/create_instance_w2.html:290 vrtManager/nodedevs.py:29 msgid "Video" msgstr "Görüntü" -#: instances/templates/create_instance_w2.html:293 +#: instances/templates/create_instance_w2.html:300 msgid "Console access" msgstr "Konsol erişimi" -#: instances/templates/create_instance_w2.html:312 +#: instances/templates/create_instance_w2.html:319 msgid "Input device" msgstr "Girdi aygıtı" -#: instances/templates/create_instance_w2.html:324 +#: instances/templates/create_instance_w2.html:331 msgid "Guest agent" msgstr "Misafir aracı" -#: instances/templates/create_instance_w2.html:328 +#: instances/templates/create_instance_w2.html:335 msgid "Cloud-init seed" msgstr "Cloud-init tohumu" -#: instances/templates/create_instance_w2.html:329 +#: instances/templates/create_instance_w2.html:336 msgid "The hostname and your SSH keys on the CD-ROM, for cloud images" msgstr "Bulut imajları için CD-ROM'da sunucuadı ve SSH anahtarlarınız" -#: instances/templates/create_instance_w2.html:334 +#: instances/templates/create_instance_w2.html:341 msgid "VirtIO devices" msgstr "VirtIO aygıtları" -#: instances/templates/create_instance_w2.html:356 +#: instances/templates/create_instance_w2.html:363 msgid "Flavors" msgstr "Hazır boyutlar" -#: instances/templates/create_instance_w2.html:375 +#: instances/templates/create_instance_w2.html:382 msgid "Add volume" msgstr "Birim ekle" -#: instances/templates/create_instance_w2.html:382 +#: instances/templates/create_instance_w2.html:389 msgid "Add network" msgstr "Ağ ekle" @@ -5960,35 +5993,35 @@ msgstr "Bir havuzda zaten bulunan birimler" msgid "A copy of a template volume" msgstr "Bir şablon biriminin kopyası" -#: instances/views.py:2527 +#: instances/views.py:2518 msgid "You haven't defined any storage pools" msgstr "Hiç bir depolama havuzu tanımlamadınız" -#: instances/views.py:2529 +#: instances/views.py:2520 msgid "You haven't defined any network pools" msgstr "Hiç bir ağ havuzu tanımlamadınız" -#: instances/views.py:2592 +#: instances/views.py:2583 msgid "Flavor Created" msgstr "Flavor Oluşturuldu" -#: instances/views.py:2600 +#: instances/views.py:2591 msgid "Create Flavor" msgstr "Kullanıcı Oluştur" -#: instances/views.py:2615 +#: instances/views.py:2606 msgid "Flavor Updated" msgstr "Flavor Güncellendi" -#: instances/views.py:2621 +#: instances/views.py:2612 msgid "Update Flavor" msgstr "Flavor Güncelle" -#: instances/views.py:2633 +#: instances/views.py:2624 msgid "Flavor Deleted" msgstr "Flavor Silindi" -#: instances/views.py:2639 +#: instances/views.py:2630 msgid "Only the size template goes; VMs made from it keep their size." msgstr "" "Yalnızca boyut şablonu silinir; ondan oluşturulan sanal makineler " diff --git a/vrtManager/connection.py b/vrtManager/connection.py index 2766e608..57bb7feb 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -1189,48 +1189,47 @@ class wvmConnect(object): # self.wvm.close() pass - def find_uefi_path_for_arch(self, arch, machine): - """ - Search the loader paths for one that matches the passed arch - """ - if not self.arch_can_uefi(arch): - return + def get_firmwares(self, arch, machine): + """The firmware a new VM can have: BIOS on x86, UEFI without Secure Boot + when libvirt picks the firmware itself, and on x86_64 UEFI Secure Boot + with Microsoft's keys enrolled, as Windows 11 needs.""" - loaders = self.get_os_loaders(arch, machine) - patterns = util.UEFI_ARCH_PATTERNS.get(arch) - for pattern in patterns: - for path in loaders: - if re.match(pattern, path): - return path + def firmwares(ctx): + os_caps = "/domainCapabilities/os[@supported='yes']" + features = f"{os_caps}/firmwareFeatures[@supported='yes']" + if ctx.xpath(features): # libvirt 12.1 and later: what its firmware offers + secure_boot = ctx.xpath(f"{features}/enum[@name='secureBoot']/value/text()") + enrolled_keys = ctx.xpath(f"{features}/enum[@name='enrolledKeys']/value/text()") + else: # earlier: the loaders; libvirt refuses the VM if no firmware has the keys + secure_boot = ctx.xpath(f"{os_caps}/loader[@supported='yes']/enum[@name='secure']/value/text()") or ["no"] + enrolled_keys = secure_boot + choices = ["BIOS"] if arch in ("i686", "x86_64") else [] + if "efi" in ctx.xpath(f"{os_caps}/enum[@name='firmware']/value/text()"): + if "no" in secure_boot: + choices.append("UEFI") + if arch == "x86_64" and "yes" in secure_boot and "yes" in enrolled_keys: + choices.append("UEFI Secure Boot") + return choices - def label_for_firmware_path(self, arch, path): - """ - Return a pretty label for passed path, based on if we know - about it or not - """ - if not path: - if arch in ["i686", "x86_64"]: - return "BIOS" - return + return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=firmwares) - for arch, patterns in util.UEFI_ARCH_PATTERNS.items(): - for pattern in patterns: - if re.match(pattern, path): - return "UEFI %(arch)s: %(path)s" % {"arch": arch, "path": path} + def get_tpm_model(self, arch, machine): + """The model of an emulated TPM 2.0 for a new VM; None when the host has + none (no swtpm).""" - return "Custom: %(path)s" % {"path": path} + def tpm_model(ctx): + tpm = "/domainCapabilities/devices/tpm[@supported='yes']" + if ("emulator" not in ctx.xpath(f"{tpm}/enum[@name='backendModel']/value/text()") + or "2.0" not in ctx.xpath(f"{tpm}/enum[@name='backendVersion']/value/text()")): + return None + models = ctx.xpath(f"{tpm}/enum[@name='model']/value/text()") + # x86: CRB, which Windows takes, or TIS; another machine: its own + for model in ("tpm-crb", "tpm-tis", *models): + if model in models: + return model + return None - def arch_can_uefi(self, arch): - """ - Return True if we know how to setup UEFI for the passed arch - """ - return arch in list(util.UEFI_ARCH_PATTERNS.keys()) - - def supports_uefi_xml(self, loader_enums): - """ - Return True if libvirt advertises support for proper UEFI setup - """ - return "readonly" in loader_enums and "yes" in loader_enums.get("readonly") + return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=tpm_model) def is_supports_virtio(self, arch, machine): if not self.is_qemu(): diff --git a/vrtManager/create.py b/vrtManager/create.py index f60cfe0a..f0f4f267 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -203,10 +203,12 @@ class wvmCreate(wvmConnect): mac=None, qemu_ga=True, add_cdrom="sata", - add_input="default" + add_input="default", + tpm_model=None, ): """ - Create VM function + Create VM function. firmware: BIOS, UEFI or UEFI Secure Boot, of + get_firmwares; tpm_model: an emulated TPM 2.0 of get_tpm_model, or none. """ caps = self.get_capabilities(arch) dom_caps = self.get_dom_capabilities(arch, machine) @@ -221,33 +223,30 @@ class wvmCreate(wvmConnect): {memory} {vcpu}""" + secure_boot = firmware == "UEFI Secure Boot" if dom_caps["os_support"] == "yes": - xml += f""" + # UEFI: libvirt picks the firmware and its variables, for Secure + # Boot one with Microsoft's keys enrolled + efi = " firmware='efi'" if firmware in ("UEFI", "UEFI Secure Boot") else "" + xml += f""" {caps["os_type"]}""" + if secure_boot: + xml += """ + + + """ + elif efi: + xml += """""" xml += """ """ - if firmware: - if firmware["secure"] == "yes": - xml += """%s""" % ( - firmware["readonly"], - firmware["type"], - firmware["secure"], - firmware["loader"], - ) - if firmware["secure"] == "no": - xml += """%s""" % ( - firmware["readonly"], - firmware["type"], - firmware["loader"], - ) xml += """""" if caps["features"]: xml += """""" for feat in [x for x in ("acpi", "apic", "pae",) if x in caps["features"]]: xml += f"""<{feat}/>""" - if firmware.get("secure", "no") == "yes": + if secure_boot: xml += """""" xml += """""" @@ -382,6 +381,8 @@ class wvmCreate(wvmConnect): xml += f""" """ + if tpm_model: + xml += f"""""" # q35 plugs each PCIe device (a virtio disk, a NIC) into a root port of # its own and libvirt makes only as many as the VM is defined with: as # virt-install, 14 ports, so a running VM can take more devices diff --git a/vrtManager/util.py b/vrtManager/util.py index 89716bf4..15aaf55e 100644 --- a/vrtManager/util.py +++ b/vrtManager/util.py @@ -195,31 +195,6 @@ def validate_macaddr(val): raise ValueError("MAC address must be of the format AA:BB:CC:DD:EE:FF, was {}".format(val)) -# Mapping of UEFI binary names to their associated architectures. We -# only use this info to do things automagically for the user, it shouldn't -# validate anything the user explicitly enters. -UEFI_ARCH_PATTERNS = { - "i686": [ - r".*ovmf-ia32.*", # fedora, gerd's firmware repo - ], - "x86_64": [ - r".*OVMF_CODE\.fd", # RHEL - r".*ovmf-x64/OVMF.*\.fd", # gerd's firmware repo - r".*ovmf-x86_64-.*", # SUSE - r".*ovmf.*", - ".*OVMF.*", # generic attempt at a catchall - ], - "aarch64": [ - r".*AAVMF_CODE\.fd", # RHEL - r".*aarch64/QEMU_EFI.*", # gerd's firmware repo - r".*aarch64.*", # generic attempt at a catchall - ], - "armv7l": [ - r".*arm/QEMU_EFI.*", # fedora, gerd's firmware repo - ], -} - - def vol_dev_type(vol_bus): if vol_bus == "ide": return "hd"