1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

feat(instances): a new VM can have UEFI Secure Boot and a TPM 2.0

Windows 11 does not install without Secure Boot and a TPM 2.0, and the new
VM form offered neither: its firmware list was BIOS and one UEFI loader
guessed from file names, which on RHEL never matched the Secure Boot one.

The firmware choices are now BIOS, UEFI and UEFI Secure Boot, as the
host's libvirt reports them (its firmware features from libvirt 12.1,
before that its loaders), and libvirt picks the firmware itself: UEFI
without Secure Boot, or with Microsoft's keys enrolled. A TPM 2.0 switch,
shown when the host has swtpm, adds an emulated TPM. The create job
refuses a firmware or a TPM the host does not offer, and the API takes
both. The path-guessing helpers are gone.
This commit is contained in:
catborise 2026-11-01 14:05:21 +03:00
parent 40db3dcf3d
commit 005fa59bf0
20 changed files with 399 additions and 228 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -77,6 +77,13 @@
{% endfor %}
</select>
{% field_error vm_form.firmware "vm_firmware" %}
{% if tpm_support %}
<div class="form-check mt-2">
<input id="vm_tpm" class="form-check-input" type="checkbox" name="tpm" value="true"{% if vm_form.tpm.value %} checked{% endif %} aria-describedby="vm_tpm_help">
<label for="vm_tpm" class="form-check-label">{% trans "TPM 2.0" %}</label>
<div id="vm_tpm_help" class="form-text">{% trans "Windows 11 needs it, with UEFI Secure Boot" %}</div>
</div>
{% endif %}
</div>
{% endif %}
{% if dom_caps.cpu_modes %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value><value>no</value></enum>
<enum name='enrolledKeys'><value>yes</value><value>no</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='yes'>
<enum name='model'><value>tpm-tis</value><value>tpm-crb</value></enum>
<enum name='backendModel'><value>emulator</value><value>external</value></enum>
<enum name='backendVersion'><value>2.0</value></enum>
</tpm></devices></domainCapabilities>"""
# Secure Boot firmware without Microsoft's keys, and no swtpm
BARE = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value><value>no</value></enum>
<enum name='enrolledKeys'><value>no</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='no'/></devices></domainCapabilities>"""
# only Secure Boot firmware, and a TPM of another machine
SECURE_ONLY = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value></enum>
<enum name='enrolledKeys'><value>yes</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='yes'>
<enum name='model'><value>tpm-spapr</value></enum>
<enum name='backendModel'><value>emulator</value></enum>
<enum name='backendVersion'><value>2.0</value></enum>
</tpm></devices></domainCapabilities>"""
# libvirt before 12.1: no firmwareFeatures, the loaders say whether they do Secure Boot
EARLIER = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<loader supported='yes'><enum name='secure'><value>yes</value><value>no</value></enum></loader>
</os></domainCapabilities>"""
EARLIER_PLAIN = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<loader supported='yes'/>
</os></domainCapabilities>"""
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"))

View file

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

View file

@ -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']")]

View file

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

Binary file not shown.

View file

@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-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 <muhammetalisag@gmail.com>, 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 "

View file

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

View file

@ -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 unit='KiB'>{memory}</memory>
<vcpu>{vcpu}</vcpu>"""
secure_boot = firmware == "UEFI Secure Boot"
if dom_caps["os_support"] == "yes":
xml += f"""<os>
# 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"""<os{efi}>
<type arch='{arch}' machine='{machine}'>{caps["os_type"]}</type>"""
if secure_boot:
xml += """<firmware>
<feature enabled='yes' name='secure-boot'/>
<feature enabled='yes' name='enrolled-keys'/>
</firmware>"""
elif efi:
xml += """<firmware><feature enabled='no' name='secure-boot'/></firmware>"""
xml += """ <boot dev='hd'/>
<boot dev='cdrom'/>
<bootmenu enable='yes'/>"""
if firmware:
if firmware["secure"] == "yes":
xml += """<loader readonly='%s' type='%s' secure='%s'>%s</loader>""" % (
firmware["readonly"],
firmware["type"],
firmware["secure"],
firmware["loader"],
)
if firmware["secure"] == "no":
xml += """<loader readonly='%s' type='%s'>%s</loader>""" % (
firmware["readonly"],
firmware["type"],
firmware["loader"],
)
xml += """</os>"""
if caps["features"]:
xml += """<features>"""
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 += """<smm state="on"/>"""
xml += """</features>"""
@ -382,6 +381,8 @@ class wvmCreate(wvmConnect):
xml += f""" <video>
<model type='{video}'/>
</video>"""
if tpm_model:
xml += f"""<tpm model='{tpm_model}'><backend type='emulator' version='2.0'/></tpm>"""
# 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

View file

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