[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' 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 = { BOOT_MODE_MAP = {
'Legacy': 'rom', 'Legacy': 'rom',
'UEFI': 'pflash' 'UEFI': 'pflash'
@ -503,6 +511,18 @@ class LibvirtDriver(AbstractSystemsDriver):
self._defineDomain(tree) 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): def get_boot_mode(self, identity):
"""Get computer system boot mode. """Get computer system boot mode.
@ -516,8 +536,15 @@ class LibvirtDriver(AbstractSystemsDriver):
# XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS # XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) 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: if loader_element is not None:
boot_mode = ( boot_mode = (
self.BOOT_MODE_MAP_REV.get(loader_element.get('type')) 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): def _build_os_element(self, identity, tree, boot_mode, secure=None):
"""Set the boot mode and secure boot on the os element """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 :raises: `error.FishyError` if boot mode can't be set
""" """
try: try:
@ -581,6 +605,54 @@ class LibvirtDriver(AbstractSystemsDriver):
os_element = os_elements[0] 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') type_element = os_element.find('type')
if type_element is None: if type_element is None:
os_arch = None os_arch = None
@ -656,6 +728,43 @@ class LibvirtDriver(AbstractSystemsDriver):
# https://libvirt.org/formatdomain.html#operating-system-booting # https://libvirt.org/formatdomain.html#operating-system-booting
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) 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') os_element = tree.find('os')
nvram = os_element.findall('nvram') 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>' '<graphics type="vnc" port="-1" />\n </devices>\n</domain>'
self.assertIn(expected, conn_mock.defineXML.call_args[0][0]) 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) @mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_boot_mode_legacy(self, libvirt_mock): def test_get_boot_mode_legacy(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: 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) 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.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True) @mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode(self, libvirt_mock, libvirt_rw_mock): 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 = libvirt_rw_mock.return_value
conn_mock.defineXML.assert_called_once_with(mock.ANY) 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.open', autospec=True)
@mock.patch('libvirt.openReadOnly', autospec=True) @mock.patch('libvirt.openReadOnly', autospec=True)
def test_set_boot_mode_legacy(self, libvirt_mock, libvirt_rw_mock): 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)) 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) @mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_on(self, libvirt_mock): def test_get_secure_boot_on(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml', 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)) 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) @mock.patch('libvirt.openReadOnly', autospec=True)
def test_get_secure_boot_not_uefi(self, libvirt_mock): def test_get_secure_boot_not_uefi(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain-q35.xml', 'r') as f: 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' uri = 'http://host.path/meow'
self.test_driver.set_http_boot_uri(uri) self.test_driver.set_http_boot_uri(uri)
self.assertEqual(uri, self.test_driver.get_http_boot_uri(None)) 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)