Add a new configuration option, `bootloader_by_arch`

Adds a new configuration option ``bootloader_by_arch`` to support
architecture-specific ESP images for virtual media boot, similar to
how ``pxe_bootfile_name_by_arch`` works for PXE.

Closes-Bug: #2110132
Change-Id: I54fb4b2f379c2d06a7c49402d32403aa2ee67e70
Signed-off-by: Afonne-CID <afonnepaulc@gmail.com>
This commit is contained in:
Afonne-CID
2025-05-13 20:40:15 +01:00
parent 10590b36f5
commit 6d11954816
7 changed files with 90 additions and 9 deletions

View File

@@ -279,7 +279,8 @@ opts = [
'partition image containing EFI boot loader. This image '
'will be used by ironic when building UEFI-bootable ISO '
'out of kernel and ramdisk. Required for UEFI boot from '
'partition images.')),
'partition images. Can be overridden per-architecture '
'using the bootloader_by_arch option.')),
cfg.MultiOpt('clean_step_priority_override',
item_type=types.Dict(),
default={},
@@ -546,6 +547,16 @@ opts = [
'here are validated as absolute paths and will be rejected'
'if they contain path traversal mechanisms, such as "..".'
)),
cfg.DictOpt('bootloader_by_arch',
default={},
help=_(
'Bootloader ESP image parameter per node architecture. '
'For example: x86_64:bootx64.efi,aarch64:grubaa64.efi. '
'A node\'s cpu_arch property is used as the key to get '
'the appropriate bootloader ESP image. If the node\'s '
'cpu_arch is not in the dictionary, '
'the [conductor]bootloader value will be used instead.'
)),
]

View File

@@ -567,6 +567,11 @@ def prepare_deploy_iso(task, params, mode, d_info):
kernel_href = _find_param(kernel_str, d_info)
ramdisk_href = _find_param(ramdisk_str, d_info)
iso_href = _find_param(iso_str, d_info)
if not d_info.get('bootloader'):
d_info['bootloader'] = driver_utils.get_field(
task.node, 'bootloader', use_conf=True)
bootloader_href = _find_param(bootloader_str, d_info)
params = override_api_url(params)

View File

@@ -122,6 +122,9 @@ def _parse_driver_info(node):
{option: d_info.get(option, getattr(CONF.conductor, option, None))
for option in OPTIONAL_PROPERTIES})
deploy_info['bootloader'] = driver_utils.get_field(
node, 'bootloader', use_conf=True)
if (d_info.get('config_via_removable') is None
and d_info.get('config_via_floppy') is not None):
LOG.warning('The config_via_floppy driver_info option is deprecated, '

View File

@@ -451,18 +451,27 @@ def get_field(node, name, deprecated_prefix=None, use_conf=False,
"""Get a driver_info field with deprecated prefix."""
node_coll = getattr(node, collection)
value = node_coll.get(name)
if value or not deprecated_prefix:
if value:
return value
deprecated_name = f'{deprecated_prefix}_{name}'
value = node_coll.get(deprecated_name)
if value:
LOG.warning("The %s field %s of node %s is deprecated, "
"please use %s instead",
collection, deprecated_name, node.uuid, name)
return value
if deprecated_prefix:
deprecated_name = f'{deprecated_prefix}_{name}'
value = node_coll.get(deprecated_name)
if value:
LOG.warning("The %s field %s of node %s is deprecated, "
"please use %s instead",
collection, deprecated_name, node.uuid, name)
return value
if use_conf:
if name == 'bootloader':
cpu_arch = node.properties.get('cpu_arch')
if cpu_arch:
bootloader_by_arch = getattr(
CONF.conductor, 'bootloader_by_arch', {})
bootloader_href = bootloader_by_arch.get(cpu_arch)
if bootloader_href:
return bootloader_href
return getattr(CONF.conductor, name)

View File

@@ -848,6 +848,29 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
task, 'kernel', 'ramdisk', 'bootloader', params={},
inject_files={}, base_iso=None)
@mock.patch.object(image_utils, '_prepare_iso_image', autospec=True)
def test_prepare_deploy_iso_bootloader_by_arch(self,
mock__prepare_iso_image):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.config(bootloader_by_arch={'x86_64': 'bootx64.efi'},
group='conductor')
d_info = {
'deploy_kernel': 'kernel',
'deploy_ramdisk': 'ramdisk',
}
task.node.driver_info.update(d_info)
task.node.instance_info.update(deploy_boot_mode='uefi')
image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info)
mock__prepare_iso_image.assert_called_once_with(
task, 'kernel', 'ramdisk', 'bootx64.efi', params={},
inject_files={}, base_iso=None)
@mock.patch.object(image_utils, '_prepare_iso_image', autospec=True)
def test_prepare_deploy_iso_existing_iso(self, mock__prepare_iso_image):
with task_manager.acquire(self.context, self.node.uuid,

View File

@@ -221,6 +221,28 @@ class UtilsTestCase(db_base.DbTestCase):
mac_clean = driver_utils.normalize_mac(mac_raw)
self.assertEqual("0a1b2c3d4f", mac_clean)
def test_get_field_bootloader(self):
driver_info = self.node.driver_info
driver_info['bootloader'] = 'custom.efi'
self.node.driver_info = driver_info
result = driver_utils.get_field(self.node, 'bootloader', use_conf=True)
self.assertEqual('custom.efi', result)
self.config(bootloader='global.efi', group='conductor')
del self.node.driver_info['bootloader']
result = driver_utils.get_field(self.node, 'bootloader', use_conf=True)
self.assertEqual('global.efi', result)
def test_get_field_bootloader_by_arch(self):
self.config(bootloader_by_arch={'aarch64': 'grubaa64.efi'},
group='conductor')
properties = self.node.properties
properties['cpu_arch'] = 'aarch64'
self.node.properties = properties
result = driver_utils.get_field(self.node, 'bootloader', use_conf=True)
self.assertEqual('grubaa64.efi', result)
class UtilsRamdiskLogsTestCase(tests_base.TestCase):

View File

@@ -0,0 +1,8 @@
---
features:
- |
Adds a new configuration option ``bootloader_by_arch``, a dictionary value
that maps architecture names to a Glance ID, http:// or file:// URL
of an EFI system partition image containing EFI boot loader, to support
architecture-specific images for virtual media boot in mixed-architecture
clouds.