From 22fec235c7091d684b334280f53c1a79006671f7 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 19 Feb 2024 06:42:05 +0100 Subject: [PATCH] Add TPM when switching to EFI This is useful for OSes which default to BIOS. It matches what `virt-install --boot uefi` does. This fixes e.g. secure boot on Fedora VMs. https://issues.redhat.com/browse/RHEL-24522 --- src/components/vm/overview/firmware.jsx | 6 ++- src/libvirt-xml-parse.js | 14 ++++++ src/libvirtApi/domain.js | 15 +++++++ test/browser/run-test.sh | 1 + test/check-machines-create | 60 +++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/components/vm/overview/firmware.jsx b/src/components/vm/overview/firmware.jsx index 1d653972e..e99c893ab 100644 --- a/src/components/vm/overview/firmware.jsx +++ b/src/components/vm/overview/firmware.jsx @@ -27,7 +27,7 @@ import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip"; import { useDialogs, DialogsContext } from 'dialogs.jsx'; import { ModalError } from 'cockpit-components-inline-notification.jsx'; -import { domainSetOSFirmware, domainCanInstall } from "../../../libvirtApi/domain.js"; +import { domainAddTPM, domainCanInstall, domainSetOSFirmware } from "../../../libvirtApi/domain.js"; import { supportsUefiXml, labelForFirmwarePath } from './helpers.jsx'; const _ = cockpit.gettext; @@ -61,6 +61,10 @@ class FirmwareModal extends React.Component { objPath: vm.id, loaderType: stateToXml(this.state.firmware) }); + + if (this.state.firmware == 'efi' && !vm.hasTPM && vm.capabilities.supportsTPM) + await domainAddTPM({ connectionName: vm.connectionName, vmName: vm.name }); + Dialogs.close(); } catch (exc) { this.dialogErrorSet(_("Failed to change firmware"), exc.message); diff --git a/src/libvirt-xml-parse.js b/src/libvirt-xml-parse.js index c2eccada6..74492abc0 100644 --- a/src/libvirt-xml-parse.js +++ b/src/libvirt-xml-parse.js @@ -197,6 +197,14 @@ export function getDomainCapSupportsSpice(capsXML) { return hasSpiceGraphics || hasSpiceChannel; } +export function getDomainCapSupportsTPM(capsXML) { + const domainCapsElem = getElem(capsXML); + const tpmCapsElems = domainCapsElem.getElementsByTagName("tpm")?.[0] + ?.getElementsByTagName("enum")?.[0] + ?.getElementsByTagName("value"); + return tpmCapsElems?.length > 0; +} + export function getSingleOptionalElem(parent, name) { const subElems = parent.getElementsByTagName(name); return subElems.length > 0 ? subElems[0] : undefined; // optional @@ -262,6 +270,7 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) { const watchdog = parseDumpxmlForWatchdog(devicesElem); const vsock = parseDumpxmlForVsock(devicesElem); const hasSpice = parseDumpxmlForSpice(devicesElem); + const hasTPM = parseDumpxmlForTPM(devicesElem); const hasInstallPhase = parseDumpxmlMachinesMetadataElement(metadataElem, 'has_install_phase') === 'true'; const installSourceType = parseDumpxmlMachinesMetadataElement(metadataElem, 'install_source_type'); @@ -306,6 +315,7 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) { vsock, metadata, hasSpice, + hasTPM, }; } @@ -549,6 +559,10 @@ function parseDumpxmlForSpice(devicesElem) { return false; } +function parseDumpxmlForTPM(devicesElem) { + return devicesElem.getElementsByTagName('tpm').length > 0; +} + export function parseDumpxmlForFilesystems(devicesElem) { const filesystems = []; const filesystemElems = devicesElem.getElementsByTagName('filesystem'); diff --git a/src/libvirtApi/domain.js b/src/libvirtApi/domain.js index 7244b46df..428201df0 100644 --- a/src/libvirtApi/domain.js +++ b/src/libvirtApi/domain.js @@ -59,6 +59,7 @@ import { getDomainCapCPUHostModel, getDomainCapDiskBusTypes, getDomainCapSupportsSpice, + getDomainCapSupportsTPM, getSingleOptionalElem, parseDomainDumpxml, getHostDevElemBySource, @@ -661,6 +662,7 @@ export async function domainGet({ cpuHostModel: getDomainCapCPUHostModel(domCaps), supportedDiskBusTypes: getDomainCapDiskBusTypes(domCaps), supportsSpice: getDomainCapSupportsSpice(domCaps), + supportsTPM: getDomainCapSupportsTPM(domCaps), }; const [state] = await call(connectionName, objPath, 'org.libvirt.Domain', 'GetState', [0], { timeout, type: 'u' }); @@ -1054,3 +1056,16 @@ export async function domainReplaceSpice({ connectionName, id: objPath }) { throw ex; // not-covered: see above } } + +export async function domainAddTPM({ connectionName, vmName }) { + const args = ["virt-xml", "-c", `qemu:///${connectionName}`, "--add-device", "--tpm", "default", vmName]; + return cockpit.spawn(args, { err: "message", superuser: connectionName === "system" ? "try" : null }) + // RHEL 8 does not support --tpm default; arch (and likely other system setups) fails with + // unsupported configuration: TPM version '2.0' is not supported + .catch(ex => { + if (ex.message?.includes("Unknown TPM backend type") || ex.message?.includes("unsupported configuration")) + console.log("Failed to add TPM:", ex); + else + return Promise.reject(ex); + }); +} diff --git a/test/browser/run-test.sh b/test/browser/run-test.sh index 92326cde6..e9618ea1f 100755 --- a/test/browser/run-test.sh +++ b/test/browser/run-test.sh @@ -46,6 +46,7 @@ fi EXCLUDES="$EXCLUDES TestMachinesCreate.testConfigureBeforeInstall TestMachinesCreate.testConfigureBeforeInstallBios + TestMachinesCreate.testConfigureBeforeInstallBiosTPM TestMachinesCreate.testCreateBasicValidation TestMachinesCreate.testCreateNameGeneration TestMachinesCreate.testCreateDownloadRhel diff --git a/test/check-machines-create b/test/check-machines-create index af2a913a9..e8bfec572 100755 --- a/test/check-machines-create +++ b/test/check-machines-create @@ -2126,6 +2126,9 @@ vnc_password= "{vnc_passwd}" # Attach some interface m.execute("virsh attach-interface --persistent VmNotInstalled bridge virbr0") + # BIOS machine doesn't have TPM by default + self.assertNotIn("tpm", m.execute("virsh dumpxml VmNotInstalled")) + # Change the os boot firmware configuration b.wait_in_text("#vm-VmNotInstalled-firmware", "BIOS") b.click("#vm-VmNotInstalled-firmware") @@ -2134,6 +2137,13 @@ vnc_password= "{vnc_passwd}" b.click("#firmware-dialog-apply") b.wait_not_present(".pf-v5-c-modal-box__body") b.wait_in_text("#vm-VmNotInstalled-firmware", "UEFI") + self.assertIn("", m.execute("virsh dumpxml VmNotInstalled")) + + # auto-enabled software TPM (doesn't work on RHEL 8 or arch) + if not m.image.startswith("rhel-8") and not m.image.startswith("centos-8") and m.image != 'arch': + tpm_xml = m.execute("virsh dumpxml VmNotInstalled | xmllint --xpath /domain/devices/tpm -") + self.assertIn('model="tpm-crb"', tpm_xml) + self.assertIn('type="emulator"', tpm_xml) # Temporarily delete the OVMF binary and check the firmware options again if "fedora" in m.image or "rhel" in m.image or "centos" in m.image: @@ -2244,6 +2254,56 @@ vnc_password= "{vnc_passwd}" self.assertIn("", domainXML) self.assertNotIn("efi", domainXML) + @testlib.skipImage("does not support virt-xml --tpm", "rhel-8*", "centos-8*") + @testlib.skipImage("does not support TPM 2.0", "arch") + def testConfigureBeforeInstallBiosTPM(self): + m = self.machine + b = self.browser + TestMachinesCreate.CreateVmRunner(self) + + # create VM with BIOS + vmName = "subVmTest1" + self.login_and_go("/machines") + self.waitPageInit() + dialog = TestMachinesCreate.VmDialog(self, sourceType='file', + name=vmName, + location=TestMachinesCreate.TestCreateConfig.NOVELL_MOCKUP_ISO_PATH, + memory_size=128, memory_size_unit='MiB', + storage_size=256, storage_size_unit='MiB', + create_and_run=False) + dialog.open().fill() + b.click(".pf-v5-c-modal-box__footer #create-and-edit") + b.wait_not_present("#create-vm-dialog") + b.wait_in_text(f"#vm-{vmName}-firmware", "BIOS") + + # manually add TPM to BIOS + m.execute(f"virt-xml --add-device --tpm default {vmName}") + self.assertIn("tpm", m.execute(f"virsh dumpxml {vmName}")) + # this doesn't change the UI, so we have to reload to ensure that the UI picks up the change + # otherwise this is a race condition + b.reload() + b.enter_page('/machines') + + # Change the os boot firmware to EFI + b.click(f"#vm-{vmName}-firmware") + b.wait_visible(".pf-v5-c-modal-box__body") + b.select_from_dropdown(".pf-v5-c-modal-box__body select", "efi") + b.click("#firmware-dialog-apply") + b.wait_not_present(".pf-v5-c-modal-box__body") + b.wait_in_text(f"#vm-{vmName}-firmware", "UEFI") + + # did not add a second TPM; that would fail in the dialog anyway, but let's double-check the backend + out = m.execute(f"virsh dumpxml {vmName} | xmllint --xpath /domain/devices/tpm - | grep --count '