[Libvirt] Support firmware auto-selection

Firmware auto-slection in Libvirt uses the QEMU firmware
description files to ease the burden on users.

This change adds support for get/set boot mode and secure
boot for domains created with firmware auto-selection.

Prior to this change operations on fw auto-selection domains
with errors such as:

  libvirt: Domain Config error : loader attribute 'readonly'
           cannot be specified when firmware autoselection is
           enabled

Change-Id: I533edb7a2a296026bb98977f7dd0de2acf553b7e
This commit is contained in:
Harald Jensås 2024-03-27 03:46:11 +01:00
parent cbc7e29ca8
commit cc738adf26
6 changed files with 365 additions and 4 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
Support for domains utilizing firmware auto-selection has been added to
the libvirt driver.

View File

@ -92,6 +92,14 @@ class LibvirtDriver(AbstractSystemsDriver):
LIBVIRT_URI = 'qemu:///system'
BOOT_MODE_AUTO_FW_MAP = {
'UEFI': 'efi',
'Legacy': 'bios'
}
BOOT_MODE_AUTO_FW_MAP_REV = {v: k for k, v
in BOOT_MODE_AUTO_FW_MAP.items()}
BOOT_MODE_MAP = {
'Legacy': 'rom',
'UEFI': 'pflash'
@ -503,6 +511,18 @@ class LibvirtDriver(AbstractSystemsDriver):
self._defineDomain(tree)
def _is_firmware_autoselection(self, tree):
"""Get libvirt firmware autoselection mode
:param tree: libvirt domain XML tree
:returns: True if firmware autoselection is enabled
"""
os_element = tree.find('.//os')
return True if os_element.get('firmware') else False
def get_boot_mode(self, identity):
"""Get computer system boot mode.
@ -516,8 +536,15 @@ class LibvirtDriver(AbstractSystemsDriver):
# XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
loader_element = tree.find('.//loader')
if self._is_firmware_autoselection(tree):
os_element = tree.find('.//os')
boot_mode = (
self.BOOT_MODE_AUTO_FW_MAP_REV.get(os_element.get('firmware'))
)
return boot_mode
loader_element = tree.find('.//loader')
if loader_element is not None:
boot_mode = (
self.BOOT_MODE_MAP_REV.get(loader_element.get('type'))
@ -558,9 +585,6 @@ class LibvirtDriver(AbstractSystemsDriver):
def _build_os_element(self, identity, tree, boot_mode, secure=None):
"""Set the boot mode and secure boot on the os element
This also converts from the previous manual layout to the automatic
approach.
:raises: `error.FishyError` if boot mode can't be set
"""
try:
@ -581,6 +605,54 @@ class LibvirtDriver(AbstractSystemsDriver):
os_element = os_elements[0]
if self._is_firmware_autoselection(tree):
self._build_os_element_fw_autoselection(boot_mode, secure,
os_element)
else:
self._build_os_element_fw_manualselection(boot_mode, secure,
os_element, loader_type)
def _build_os_element_fw_autoselection(self, boot_mode, secure,
os_element):
"""Set the boot mode and secure boot (auto-selection)
:raises: `error.FishyError` if boot mode can't be set
"""
os_element.set('firmware', self.BOOT_MODE_AUTO_FW_MAP[boot_mode])
# Delete the secure-boot feature element
try:
firmware_element = os_element.findall('firmware').pop()
for e in firmware_element.findall('.feature'
'[@name="secure-boot"]'):
firmware_element.remove(e)
except IndexError:
firmware_element = None
if boot_mode != 'UEFI':
return
if firmware_element is None:
firmware_element = ET.SubElement(os_element, 'firmware')
if secure:
secure_boot_element = ET.SubElement(firmware_element, 'feature')
secure_boot_element.set('name', 'secure-boot')
secure_boot_element.set('enabled', 'yes')
else:
secure_boot_element = ET.SubElement(firmware_element, 'feature')
secure_boot_element.set('name', 'secure-boot')
secure_boot_element.set('enabled', 'no')
def _build_os_element_fw_manualselection(self, boot_mode, secure,
os_element, loader_type):
"""Set the boot mode and secure boot (manual-selection)
This also converts from the previous manual layout to the automatic
approach.
:raises: `error.FishyError` if boot mode can't be set
"""
type_element = os_element.find('type')
if type_element is None:
os_arch = None
@ -656,6 +728,43 @@ class LibvirtDriver(AbstractSystemsDriver):
# https://libvirt.org/formatdomain.html#operating-system-booting
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
if self._is_firmware_autoselection(tree):
return self._get_secureboot_fw_auto_selection(identity, tree)
else:
return self._get_secureboot_fw_manual_selection(identity, tree)
def _get_secureboot_fw_auto_selection(self, identity, tree):
os_element = tree.find('os')
firmware_element = os_element.findall('firmware')
if len(firmware_element) == 0:
msg = ('Can\'t get secure boot state because "firmware" element '
'is not present in domain "%(identity)s" configuration'
% {'identity': identity})
raise error.FishyError(msg)
if len(firmware_element) > 1:
msg = ('Can\'t get secure boot state because "firmware" element '
'must be present exactly once in domain "%(identity)s" '
'configuration' % {'identity': identity})
raise error.FishyError(msg)
feature_secure_boot = os_element.findall('./firmware/feature'
'[@name="secure-boot"]')
if len(feature_secure_boot) > 1:
msg = ('Can\'t get secure boot state because the "firmware" '
'element contains multiple "feature" elements with the '
'"secure-boot" name attribute. "secure-boot" feature '
'should be present exactly once in domain %(identity)s" '
'configuration' % {'identity': identity})
raise error.FishyError(msg)
enabled = feature_secure_boot[0].get('enabled', "no")
return True if enabled == "yes" else False
def _get_secureboot_fw_manual_selection(self, identity, tree):
os_element = tree.find('os')
nvram = os_element.findall('nvram')

View File

@ -0,0 +1,30 @@
<domain type='qemu'>
<name>QEmu-fedora-i686</name>
<uuid>c7a5fdbd-cdaf-9455-926a-d65c16db1809</uuid>
<memory>219200</memory>
<currentMemory>219200</currentMemory>
<vcpu>2</vcpu>
<os firmware="efi">
<type arch="x86_64" machine="q35">hvm</type>
<firmware>
<feature enabled="no" name="secure-boot"/>
</firmware>
<boot dev="disk"/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/user/boot.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/user/fedora.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1'/>
</devices>
</domain>

View File

@ -0,0 +1,30 @@
<domain type='qemu'>
<name>QEmu-fedora-i686</name>
<uuid>c7a5fdbd-cdaf-9455-926a-d65c16db1809</uuid>
<memory>219200</memory>
<currentMemory>219200</currentMemory>
<vcpu>2</vcpu>
<os firmware="efi">
<type arch="x86_64" machine="q35">hvm</type>
<firmware>
<feature enabled="yes" name="secure-boot"/>
</firmware>
<boot dev="disk"/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/user/boot.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/user/fedora.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1'/>
</devices>
</domain>

View File

@ -0,0 +1,27 @@
<domain type='qemu'>
<name>QEmu-fedora-i686</name>
<uuid>c7a5fdbd-cdaf-9455-926a-d65c16db1809</uuid>
<memory>219200</memory>
<currentMemory>219200</currentMemory>
<vcpu>2</vcpu>
<os firmware='bios'>
<type arch='x86_64' machine='pc'>hvm</type>
<boot dev='cdrom'/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/user/boot.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/user/fedora.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1'/>
</devices>
</domain>

View File

@ -387,6 +387,27 @@ class LibvirtDriverTestCase(base.BaseTestCase):
'<graphics type="vnc" port="-1" />\n </devices>\n</domain>'
self.assertIn(expected, conn_mock.defineXML.call_args[0][0])
def test__is_firmware_autoselection_disabled(self):
with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f:
domain = f.read()
tree = ET.fromstring(domain)
fw_auto = self.test_driver._is_firmware_autoselection(tree)
self.assertEqual(False, fw_auto)
def test__is_firmware_autoselection_enabled(self):
with open(('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi.xml'), 'r') as f:
domain = f.read()
tree = ET.fromstring(domain)
fw_auto = self.test_driver._is_firmware_autoselection(tree)
self.assertEqual(True, fw_auto)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_boot_mode_legacy(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f:
@ -414,6 +435,20 @@ class LibvirtDriverTestCase(base.BaseTestCase):
self.assertEqual('UEFI', boot_mode)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_boot_mode_fw_auto_uefi(self, libvirt_mock):
with open(('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi.xml'), 'r') as f:
data = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
boot_mode = self.test_driver.get_boot_mode(self.uuid)
self.assertEqual('UEFI', boot_mode)
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode(self, libvirt_mock, libvirt_rw_mock):
@ -431,6 +466,58 @@ class LibvirtDriverTestCase(base.BaseTestCase):
conn_mock = libvirt_rw_mock.return_value
conn_mock.defineXML.assert_called_once_with(mock.ANY)
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode_auto_fw_uefi(self, libvirt_mock, libvirt_rw_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain_fw_auto.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_rw_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
with mock.patch.object(
self.test_driver, 'get_power_state', return_value='Off'):
self.test_driver.set_boot_mode(self.uuid, 'UEFI')
conn_mock = libvirt_rw_mock.return_value
xml_document = conn_mock.defineXML.call_args[0][0]
tree = ET.fromstring(xml_document)
os_element = tree.find('os')
self.assertEqual('efi', os_element.get('firmware'))
secure_boot = os_element.findall(
'./firmware/feature[@name="secure-boot"]')
self.assertEqual('secure-boot', secure_boot[0].get('name'))
self.assertEqual('no', secure_boot[0].get('enabled'))
conn_mock.defineXML.assert_called_once_with(mock.ANY)
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode_auto_fw_legacy(self, libvirt_mock, libvirt_rw_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_rw_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
with mock.patch.object(
self.test_driver, 'get_power_state', return_value='Off'):
self.test_driver.set_boot_mode(self.uuid, 'Legacy')
conn_mock = libvirt_rw_mock.return_value
xml_document = conn_mock.defineXML.call_args[0][0]
tree = ET.fromstring(xml_document)
os_element = tree.find('os')
self.assertEqual('bios', os_element.get('firmware'))
# There should be no secure-boot feature element
secure_boot = os_element.findall(
'./firmware/feature[@name="secure-boot"]')
self.assertEqual([], secure_boot)
conn_mock.defineXML.assert_called_once_with(mock.ANY)
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode_legacy(self, libvirt_mock, libvirt_rw_mock):
@ -1155,6 +1242,18 @@ class LibvirtDriverTestCase(base.BaseTestCase):
self.assertFalse(self.test_driver.get_secure_boot(self.uuid))
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_fw_auto_off(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
self.assertFalse(self.test_driver.get_secure_boot(self.uuid))
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_on(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml',
@ -1167,6 +1266,18 @@ class LibvirtDriverTestCase(base.BaseTestCase):
self.assertTrue(self.test_driver.get_secure_boot(self.uuid))
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_fw_auto_on(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi_secure.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
self.assertTrue(self.test_driver.get_secure_boot(self.uuid))
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_not_uefi(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain-q35.xml', 'r') as f:
@ -1259,3 +1370,51 @@ class LibvirtDriverTestCase(base.BaseTestCase):
uri = 'http://host.path/meow'
self.test_driver.set_http_boot_uri(uri)
self.assertEqual(uri, self.test_driver.get_http_boot_uri(None))
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_secure_boot_on_auto_fw(self, libvirt_mock, libvirt_rw_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
self.test_driver.set_secure_boot(self.uuid, True)
conn_mock = libvirt_rw_mock.return_value
xml_document = conn_mock.defineXML.call_args[0][0]
tree = ET.fromstring(xml_document)
os_element = tree.find('os')
self.assertEqual('efi', os_element.get('firmware'))
secure_boot = os_element.findall(
'./firmware/feature[@name="secure-boot"]')
self.assertEqual('secure-boot', secure_boot[0].get('name'))
self.assertEqual('yes', secure_boot[0].get('enabled'))
conn_mock.defineXML.assert_called_once_with(mock.ANY)
@mock.patch('libvirt.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_secure_boot_off_auto_fw(self, libvirt_mock, libvirt_rw_mock):
with open('sushy_tools/tests/unit/emulator/'
'domain-q35_fw_auto_uefi_secure.xml', 'r') as f:
data = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByUUID.return_value
domain_mock.XMLDesc.return_value = data
self.test_driver.set_secure_boot(self.uuid, False)
conn_mock = libvirt_rw_mock.return_value
xml_document = conn_mock.defineXML.call_args[0][0]
tree = ET.fromstring(xml_document)
os_element = tree.find('os')
self.assertEqual('efi', os_element.get('firmware'))
secure_boot = os_element.findall(
'./firmware/feature[@name="secure-boot"]')
self.assertEqual('secure-boot', secure_boot[0].get('name'))
self.assertEqual('no', secure_boot[0].get('enabled'))
conn_mock.defineXML.assert_called_once_with(mock.ANY)