SoftwareRAID: Use efibootmgr (and drop grub2-install)

Move the software RAID code path from grub2-install to
efibootmgr:

- remove the UEFI efibootmgr exception for software RAID
- create and populate the ESPs on the holder disks
- update the NVRAM with all ESPs (the component devices
  of the ESP mirror, use unique labels to avoid unintentional
  deduplication of entries in the NVRAM)

Story: #2009794

Change-Id: I7ed34e595215194a589c2f1cd0b39ff0336da8f1
This commit is contained in:
Arne Wiebalck 2022-01-24 15:00:05 +01:00
parent e06dd22e78
commit 62c5674a60
7 changed files with 172 additions and 45 deletions

View File

@ -20,6 +20,8 @@ from oslo_concurrency import processutils
from oslo_log import log
from ironic_python_agent import errors
from ironic_python_agent.extensions import image
from ironic_python_agent import hardware
from ironic_python_agent import partition_utils
from ironic_python_agent import utils
@ -92,16 +94,60 @@ def manage_uefi(device, efi_system_part_uuid=None):
efi_mounted = True
valid_efi_bootloaders = _get_efi_bootloaders(efi_partition_mount_point)
if valid_efi_bootloaders:
_run_efibootmgr(valid_efi_bootloaders, device, efi_partition,
efi_partition_mount_point)
return True
else:
if not valid_efi_bootloaders:
# NOTE(dtantsur): if we have an empty EFI partition, try to use
# grub-install to populate it.
LOG.warning('Empty EFI partition detected.')
return False
if not hardware.is_md_device(device):
efi_devices = [device]
efi_partition_numbers = [efi_partition]
efi_label_suffix = ''
else:
# umount to allow for signature removal (to avoid confusion about
# which ESP to mount once the instance is deployed)
utils.execute('umount', efi_partition_mount_point, attempts=3,
delay_on_retry=True)
efi_mounted = False
holders = hardware.get_holder_disks(device)
efi_md_device = image.prepare_boot_partitions_for_softraid(
device, holders, efi_device_part, target_boot_mode='uefi'
)
efi_devices = hardware.get_component_devices(efi_md_device)
efi_partition_numbers = []
_PARTITION_NUMBER = re.compile(r'(\d+)$')
for dev in efi_devices:
match = _PARTITION_NUMBER.search(dev)
if match:
partition_number = match.group(1)
efi_partition_numbers.append(partition_number)
else:
raise errors.DeviceNotFound(
"Could not extract the partition number "
"from %s!" % dev)
efi_label_suffix = "(RAID, part%s)"
# remount for _run_efibootmgr
utils.execute('mount', efi_device_part, efi_partition_mount_point)
efi_mounted = True
efi_dev_part = zip(efi_devices, efi_partition_numbers)
for i, (efi_dev, efi_part) in enumerate(efi_dev_part):
LOG.debug("Calling efibootmgr with dev %s part %s",
efi_dev, efi_part)
if efi_label_suffix:
# NOTE (arne_wiebalck): uniqify the labels to prevent
# unintentional boot entry cleanup
_run_efibootmgr(valid_efi_bootloaders, efi_dev, efi_part,
efi_partition_mount_point,
efi_label_suffix % i)
else:
_run_efibootmgr(valid_efi_bootloaders, efi_dev, efi_part,
efi_partition_mount_point)
return True
except processutils.ProcessExecutionError as e:
error_msg = ('Could not verify uefi on device %(dev)s, '
'failed with %(err)s.' % {'dev': device, 'err': e})
@ -227,7 +273,7 @@ def remove_boot_record(boot_num):
def _run_efibootmgr(valid_efi_bootloaders, device, efi_partition,
mount_point):
mount_point, label_suffix=None):
"""Executes efibootmgr and removes duplicate entries.
:param valid_efi_bootloaders: the list of valid efi bootloaders
@ -236,6 +282,9 @@ def _run_efibootmgr(valid_efi_bootloaders, device, efi_partition,
:param mount_point: The mountpoint for the EFI partition so we can
read contents of files if necessary to perform
proper bootloader injection operations.
:param label_suffix: a string to be appended to the EFI label,
mainly used in the case of software to uniqify
the entries for the md components.
"""
# Before updating let's get information about the bootorder
@ -255,9 +304,13 @@ def _run_efibootmgr(valid_efi_bootloaders, device, efi_partition,
v_efi_bl_path = v_bl.replace(csv_filename, str(csv_contents[0]))
v_efi_bl_path = '\\' + v_efi_bl_path.replace('/', '\\')
label = csv_contents[1]
if label_suffix:
label = label + " " + str(label_suffix)
else:
v_efi_bl_path = '\\' + v_bl.replace('/', '\\')
label = 'ironic' + str(label_id)
if label_suffix:
label = label + " " + str(label_suffix)
# Iterate through standard out, and look for duplicates
for boot_num, boot_rec in boot_records:
@ -268,9 +321,11 @@ def _run_efibootmgr(valid_efi_bootloaders, device, efi_partition,
LOG.debug("Found bootnum %s matching label", boot_num)
remove_boot_record(boot_num)
LOG.debug("Adding loader %(path)s on partition %(part)s of device "
" %(dev)s", {'path': v_efi_bl_path, 'part': efi_partition,
'dev': device})
LOG.info("Adding loader %(path)s on partition %(part)s of device "
" %(dev)s with label %(label)s",
{'path': v_efi_bl_path, 'part': efi_partition,
'dev': device, 'label': label})
# Update the nvram using efibootmgr
add_boot_record(device, efi_partition, v_efi_bl_path, label)
# Increment the ID in case the loop runs again.

View File

@ -105,8 +105,8 @@ def _is_bootloader_loaded(dev):
# TODO(rg): handle PreP boot parts relocation as well
def _prepare_boot_partitions_for_softraid(device, holders, efi_part,
target_boot_mode):
def prepare_boot_partitions_for_softraid(device, holders, efi_part,
target_boot_mode):
"""Prepare boot partitions when relevant.
Create either a RAIDed EFI partition or bios boot partitions for software
@ -311,7 +311,7 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
efi_partition = efi_part
if hardware.is_md_device(device):
holders = hardware.get_holder_disks(device)
efi_partition = _prepare_boot_partitions_for_softraid(
efi_partition = prepare_boot_partitions_for_softraid(
device, holders, efi_part, target_boot_mode
)
@ -648,9 +648,7 @@ def _efi_boot_setup(device, efi_system_part_uuid=None, target_boot_mode=None):
{'target': target_boot_mode,
'current': boot.current_boot_mode})
# FIXME(arne_wiebalck): make software RAID work with efibootmgr
if (boot.current_boot_mode == 'uefi'
and not hardware.is_md_device(device)):
if boot.current_boot_mode == 'uefi':
try:
utils.execute('efibootmgr', '--version')
except FileNotFoundError:

View File

@ -181,7 +181,7 @@ def _get_md_uuid(raid_device):
return match.group(1)
def _get_component_devices(raid_device):
def get_component_devices(raid_device):
"""Get the component devices of a Software RAID device.
Get the UUID of the md device and scan all other devices
@ -325,7 +325,7 @@ def md_restart(raid_device):
"""
try:
LOG.debug('Restarting software RAID device %s', raid_device)
component_devices = _get_component_devices(raid_device)
component_devices = get_component_devices(raid_device)
il_utils.execute('mdadm', '--stop', raid_device)
il_utils.execute('mdadm', '--assemble', raid_device,
*component_devices)
@ -2221,7 +2221,7 @@ class GenericHardwareManager(HardwareManager):
def _delete_config_pass(self, raid_devices):
all_holder_disks = []
for raid_device in raid_devices:
component_devices = _get_component_devices(raid_device.name)
component_devices = get_component_devices(raid_device.name)
if not component_devices:
# A "Software RAID device" without components is usually
# a partition on an md device (as, for instance, created

View File

@ -1654,7 +1654,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
self.assertFalse(mock_dispatch.called)
@mock.patch.object(disk_utils, 'find_efi_partition', autospec=True)
def test__prepare_boot_partitions_for_softraid_uefi_gpt(
def test_prepare_boot_partitions_for_softraid_uefi_gpt(
self, mock_efi_part, mock_execute, mock_dispatch):
mock_efi_part.return_value = {'number': '12'}
mock_execute.side_effect = [
@ -1673,7 +1673,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
(None, None), # wipefs
]
efi_part = image._prepare_boot_partitions_for_softraid(
efi_part = image.prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], None,
target_boot_mode='uefi')
@ -1704,7 +1704,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
@mock.patch.object(disk_utils, 'find_efi_partition', autospec=True)
@mock.patch.object(ilib_utils, 'mkfs', autospec=True)
def test__prepare_boot_partitions_for_softraid_uefi_gpt_esp_not_found(
def test_prepare_boot_partitions_for_softraid_uefi_gpt_esp_not_found(
self, mock_mkfs, mock_efi_part, mock_execute, mock_dispatch):
mock_efi_part.return_value = None
mock_execute.side_effect = [
@ -1721,7 +1721,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
(None, None), # mdadm
]
efi_part = image._prepare_boot_partitions_for_softraid(
efi_part = image.prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], None,
target_boot_mode='uefi')
@ -1748,7 +1748,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
], any_order=False)
self.assertEqual(efi_part, '/dev/md/esp')
def test__prepare_boot_partitions_for_softraid_uefi_gpt_efi_provided(
def test_prepare_boot_partitions_for_softraid_uefi_gpt_efi_provided(
self, mock_execute, mock_dispatch):
mock_execute.side_effect = [
('451', None), # sgdisk -F
@ -1766,7 +1766,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
(None, None), # wipefs
]
efi_part = image._prepare_boot_partitions_for_softraid(
efi_part = image.prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], '/dev/md0p15',
target_boot_mode='uefi')
@ -1796,10 +1796,10 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
@mock.patch.object(disk_utils, 'get_partition_table_type', autospec=True,
return_value='msdos')
def test__prepare_boot_partitions_for_softraid_bios_msdos(
def test_prepare_boot_partitions_for_softraid_bios_msdos(
self, mock_label_scan, mock_execute, mock_dispatch):
efi_part = image._prepare_boot_partitions_for_softraid(
efi_part = image.prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], 'notusedanyway',
target_boot_mode='bios')
@ -1812,7 +1812,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
@mock.patch.object(disk_utils, 'get_partition_table_type', autospec=True,
return_value='gpt')
def test__prepare_boot_partitions_for_softraid_bios_gpt(
def test_prepare_boot_partitions_for_softraid_bios_gpt(
self, mock_label_scan, mock_execute, mock_dispatch):
mock_execute.side_effect = [
@ -1822,7 +1822,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
(None, None), # bios boot grub
]
efi_part = image._prepare_boot_partitions_for_softraid(
efi_part = image.prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], 'notusedanyway',
target_boot_mode='bios')
@ -1854,7 +1854,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
@mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(partition_utils, 'get_partition', autospec=True)
@mock.patch.object(image, '_prepare_boot_partitions_for_softraid',
@mock.patch.object(image, 'prepare_boot_partitions_for_softraid',
autospec=True,
return_value='/dev/md/esp')
@mock.patch.object(image, '_has_dracut',
@ -1972,7 +1972,7 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640
@mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(partition_utils, 'get_partition', autospec=True)
@mock.patch.object(image, '_prepare_boot_partitions_for_softraid',
@mock.patch.object(image, 'prepare_boot_partitions_for_softraid',
autospec=True,
return_value=[])
@mock.patch.object(image, '_has_dracut',

View File

@ -19,6 +19,8 @@ from ironic_lib import disk_utils
from ironic_python_agent import efi_utils
from ironic_python_agent import errors
from ironic_python_agent.extensions import image
from ironic_python_agent import hardware
from ironic_python_agent import partition_utils
from ironic_python_agent.tests.unit import base
from ironic_python_agent import utils
@ -158,12 +160,15 @@ class TestManageUefi(base.IronicAgentTest):
mock_rescan.assert_called_once_with(self.fake_dev)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(efi_utils, '_get_efi_bootloaders', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_ok(self, mkdir_mock, mock_efi_bl, mock_utils_efi_part,
mock_get_part_uuid, mock_execute, mock_rescan):
def test_ok(self, mkdir_mock, mock_efi_bl, mock_is_md_device,
mock_utils_efi_part, mock_get_part_uuid, mock_execute,
mock_rescan):
mock_utils_efi_part.return_value = {'number': '1'}
mock_get_part_uuid.return_value = self.fake_dev
mock_is_md_device.return_value = False
mock_efi_bl.return_value = ['EFI/BOOT/BOOTX64.EFI']
@ -192,13 +197,16 @@ class TestManageUefi(base.IronicAgentTest):
mock_rescan.assert_called_once_with(self.fake_dev)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(efi_utils, '_get_efi_bootloaders', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_found_csv(self, mkdir_mock, mock_efi_bl, mock_utils_efi_part,
mock_get_part_uuid, mock_execute, mock_rescan):
def test_found_csv(self, mkdir_mock, mock_efi_bl, mock_is_md_device,
mock_utils_efi_part, mock_get_part_uuid, mock_execute,
mock_rescan):
mock_utils_efi_part.return_value = {'number': '1'}
mock_get_part_uuid.return_value = self.fake_dev
mock_efi_bl.return_value = ['EFI/vendor/BOOTX64.CSV']
mock_is_md_device.return_value = False
# Format is <file>,<entry_name>,<options>,humanfriendlytextnotused
# https://www.rodsbooks.com/efi-bootloaders/fallback.html
@ -242,12 +250,15 @@ Boot0002: VENDMAGIC FvFile(9f3c6294-bf9b-4208-9808-be45dfc34b51)
mock_execute.assert_has_calls(expected)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(efi_utils, '_get_efi_bootloaders', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_nvme_device(self, mkdir_mock, mock_efi_bl, mock_utils_efi_part,
mock_get_part_uuid, mock_execute, mock_rescan):
def test_nvme_device(self, mkdir_mock, mock_efi_bl, mock_is_md_device,
mock_utils_efi_part, mock_get_part_uuid,
mock_execute, mock_rescan):
mock_utils_efi_part.return_value = {'number': '1'}
mock_get_part_uuid.return_value = '/dev/fakenvme0p1'
mock_is_md_device.return_value = False
mock_efi_bl.return_value = ['EFI/BOOT/BOOTX64.EFI']
@ -274,12 +285,15 @@ Boot0002: VENDMAGIC FvFile(9f3c6294-bf9b-4208-9808-be45dfc34b51)
mock_execute.assert_has_calls(expected)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(efi_utils, '_get_efi_bootloaders', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_wholedisk(self, mkdir_mock, mock_efi_bl, mock_utils_efi_part,
mock_get_part_uuid, mock_execute, mock_rescan):
def test_wholedisk(self, mkdir_mock, mock_efi_bl, mock_is_md_device,
mock_utils_efi_part, mock_get_part_uuid, mock_execute,
mock_rescan):
mock_utils_efi_part.return_value = {'number': '1'}
mock_get_part_uuid.side_effect = Exception
mock_is_md_device.return_value = False
mock_efi_bl.return_value = ['EFI/BOOT/BOOTX64.EFI']
@ -304,3 +318,56 @@ Boot0002: VENDMAGIC FvFile(9f3c6294-bf9b-4208-9808-be45dfc34b51)
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
mock_execute.assert_has_calls(expected)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(hardware, 'get_component_devices', autospec=True)
@mock.patch.object(image,
'prepare_boot_partitions_for_softraid',
autospec=True)
@mock.patch.object(hardware, 'get_holder_disks', autospec=True)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(efi_utils, '_get_efi_bootloaders', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_software_raid(self, mkdir_mock, mock_efi_bl, mock_is_md_device,
mock_get_holder_disks, mock_prepare,
mock_get_component_devices,
mock_utils_efi_part, mock_get_part_uuid,
mock_execute, mock_rescan):
mock_utils_efi_part.return_value = {'number': '1'}
mock_get_part_uuid.side_effect = Exception
mock_is_md_device.return_value = True
mock_get_holder_disks.return_value = ['/dev/sda', '/dev/sdb']
mock_prepare.return_value = '/dev/md125'
mock_get_component_devices.return_value = ['/dev/sda3', '/dev/sdb3']
mock_efi_bl.return_value = ['EFI/BOOT/BOOTX64.EFI']
mock_execute.side_effect = iter([('', ''), ('', ''),
('', ''), ('', ''),
('', ''), ('', ''),
('', ''), ('', ''),
('', '')])
expected = [mock.call('mount', self.fake_efi_system_part,
self.fake_dir + '/boot/efi'),
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('mount', self.fake_efi_system_part,
self.fake_dir + '/boot/efi'),
mock.call('efibootmgr', '-v'),
mock.call('efibootmgr', '-v', '-c', '-d', '/dev/sda3',
'-p', '3', '-w', '-L', 'ironic1 (RAID, part0)',
'-l', '\\EFI\\BOOT\\BOOTX64.EFI'),
mock.call('efibootmgr', '-v'),
mock.call('efibootmgr', '-v', '-c', '-d', '/dev/sdb3',
'-p', '3', '-w', '-L', 'ironic1 (RAID, part1)',
'-l', '\\EFI\\BOOT\\BOOTX64.EFI'),
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('sync')]
result = efi_utils.manage_uefi(self.fake_dev, None)
self.assertTrue(result)
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
mock_execute.assert_has_calls(expected)

View File

@ -3660,9 +3660,9 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(hardware, '_get_md_uuid', autospec=True)
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
def test__get_component_devices(self, mocked_execute,
mocked_list_all_block_devices,
mocked_md_uuid):
def test_get_component_devices(self, mocked_execute,
mocked_list_all_block_devices,
mocked_md_uuid):
raid_device1 = hardware.BlockDevice('/dev/md0', 'RAID-1',
107374182400, True)
sda = hardware.BlockDevice('/dev/sda', 'model12', 21, True)
@ -3682,7 +3682,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
[hws.MDADM_EXAMINE_OUTPUT_NON_MEMBER, '_'],
]
component_devices = hardware._get_component_devices(raid_device1)
component_devices = hardware.get_component_devices(raid_device1)
self.assertEqual(['/dev/sda1'], component_devices)
mocked_execute.assert_has_calls([
mock.call('mdadm', '--examine', '/dev/sda',
@ -3739,7 +3739,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual(['/dev/sda'], holder_disks)
@mock.patch.object(hardware, 'get_holder_disks', autospec=True)
@mock.patch.object(hardware, '_get_component_devices', autospec=True)
@mock.patch.object(hardware, 'get_component_devices', autospec=True)
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_delete_configuration(self, mocked_execute, mocked_list,
@ -3827,7 +3827,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mock.call('mdadm', '--assemble', '--scan', check_exit_code=False),
])
@mock.patch.object(hardware, '_get_component_devices', autospec=True)
@mock.patch.object(hardware, 'get_component_devices', autospec=True)
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_delete_configuration_partition(self, mocked_execute, mocked_list,
@ -3852,7 +3852,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mock.call('mdadm', '--assemble', '--scan', check_exit_code=False),
])
@mock.patch.object(hardware, '_get_component_devices', autospec=True)
@mock.patch.object(hardware, 'get_component_devices', autospec=True)
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_delete_configuration_failure_blocks_remaining(

View File

@ -0,0 +1,7 @@
---
fixes:
- |
Use efibootmgr instead of grub2-install for software RAID.
This fixes an issue with images which include newer versions
of grub2-install as they refuse bootloader installations in
UEFI boot mode due to the lack of secure boot support.