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 %}
+
+ {% 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"