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 7ab1ed1f..259f3dd5 100644 Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index 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"