Skip to content

Commit

Permalink
fix(arm,bootloader,efi): patch grub.cfg used for upgrading
Browse files Browse the repository at this point in the history
Use the grub.cfg bundled within leapp if we detect that
system's grub.cfg contains problematic configuration which
will not load grubenv of the upgrade BLS entry. We need
to ensure that this grubenv is loaded, as without it we
cannot guarantee a successful boot into upgrade environment.
  • Loading branch information
Michal Hecko authored and pirat89 committed Jan 29, 2025
1 parent c861416 commit 1207dec
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
set timeout=0

# Make sure to load EFI/leapp/grubenv and not system's default path
if [ -f ${config_directory}/grubenv ]; then
load_env -f ${config_directory}/grubenv
elif [ -s $prefix/grubenv ]; then
load_env
fi

# EFI/leapp/grubenv contains our upgrade BLS entry as saved_entry
if [ "${next_entry}" ] ; then
set default="${next_entry}"
set next_entry=
save_env next_entry
set boot_once=true
else
set default="${saved_entry}"
fi

search --no-floppy --set=root --fs-uuid LEAPP_BOOT_UUID
set boot=${root}
function load_video {
insmod all_video
}
${serial}${terminal_input}${terminal_output}
blscfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from leapp.libraries.common.grub import (
canonical_path_to_efi_format,
EFIBootInfo,
get_boot_partition,
get_device_number,
get_efi_device,
get_efi_partition,
Expand All @@ -26,6 +27,15 @@

CONTAINER_DOWNLOAD_DIR = '/tmp_pkg_download_dir'

LEAPP_GRUB2_CFG_TEMPLATE = 'grub2_config_template'
"""
Our grub configuration file template that is used in case the system's grubcfg would not load our grubenv.
The template contains placeholders named with LEAPP_*, that need to be replaced in order to
obtain a valid config.
"""


def _copy_file(src_path, dst_path):
if os.path.exists(dst_path):
Expand All @@ -49,11 +59,14 @@ def process():
context.copytree_from(RHEL_EFIDIR_CANONICAL_PATH, LEAPP_EFIDIR_CANONICAL_PATH)

_copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg'])
_link_grubenv_to_upgrade_entry()

efibootinfo = EFIBootInfo()
current_boot_entry = efibootinfo.entries[efibootinfo.current_bootnum]
upgrade_boot_entry = _add_upgrade_boot_entry(efibootinfo)

leapp_efi_grubenv = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv')
patch_efi_redhat_grubcfg_to_load_correct_grubenv()

_set_bootnext(upgrade_boot_entry.boot_number)

efibootentry_fields = ['boot_number', 'label', 'active', 'efi_bin_source']
Expand Down Expand Up @@ -183,3 +196,90 @@ def _set_bootnext(boot_number):
run(['/usr/sbin/efibootmgr', '--bootnext', boot_number])
except CalledProcessError:
raise StopActorExecutionError('Could not set boot entry {} as BootNext.'.format(boot_number))


def _notify_user_to_check_grub2_cfg():
# Or maybe rather ask a question in a dialog? But this is rare, so maybe continuing is fine.
pass


def _will_grubcfg_read_our_grubenv(grubcfg_path):
with open(grubcfg_path) as grubcfg:
config_lines = grubcfg.readlines()

will_read = False
for line in config_lines:
if line.strip() == 'load_env -f ${config_directory}/grubenv':
will_read = True
break

return will_read


def _get_boot_device_uuid():
boot_device = get_boot_partition()
try:
raw_device_info_lines = run(['blkid', boot_device], split=True)['stdout']
raw_device_info = raw_device_info_lines[0] # There is only 1 output line

uuid_needle_start_pos = raw_device_info.index('UUID')
raw_device_info = raw_device_info[uuid_needle_start_pos:] # results in: "UUID="..." ....

uuid = raw_device_info.split(' ', 1)[0] # UUID cannot contain spaces
uuid = uuid[len('UUID='):] # Remove UUID=
uuid = uuid.strip('"')
return uuid

except CalledProcessError as error:
details = {'details': 'blkid failed with error: {}'.format(error)}
raise StopActorExecutionError('Failed to obtain UUID of /boot partition', details=details)


def _prepare_config_contents():
config_template_path = api.get_actor_file_path(LEAPP_GRUB2_CFG_TEMPLATE)
with open(config_template_path) as config_template_handle:
config_template = config_template_handle.read()

substitutions = {
'LEAPP_BOOT_UUID': _get_boot_device_uuid()
}

api.current_logger().debug(
'Applying the following substitution map to grub config template: {}'.format(substitutions)
)

for placeholder, placeholder_value in substitutions.items():
config_template = config_template.replace(placeholder, placeholder_value)

return config_template


def _write_config(config_path, config_contents):
with open(config_path, 'w') as grub_cfg_handle:
grub_cfg_handle.write(config_contents)


def patch_efi_redhat_grubcfg_to_load_correct_grubenv():
"""
Replaces /boot/efi/EFI/redhat/grub2.cfg with a patched grub2.cfg shipped in leapp.
The grub2.cfg shipped on some AWS images omits the section that loads grubenv different
EFI entries. Thus, we need to replace it with our own that will load grubenv shipped
of our UEFI boot entry.
"""
leapp_grub_cfg_path = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grub.cfg')

if not os.path.isfile(leapp_grub_cfg_path):
msg = 'The file {} does not exists, cannot check whether bootloader is configured properly.'
raise StopActorExecutionError(msg.format(leapp_grub_cfg_path))

if _will_grubcfg_read_our_grubenv(leapp_grub_cfg_path):
api.current_logger().debug('The current grub.cfg will read our grubenv without any modifications.')
return

api.current_logger().info('Current grub2.cfg is likely faulty (would not read our grubenv), patching.')

config_contents = _prepare_config_contents()
_write_config(leapp_grub_cfg_path, config_contents)

api.current_logger().info('New upgrade grub.cfg has been written to {}'.format(leapp_grub_cfg_path))
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ def mock_add_upgrade_boot_entry(efibootinfo):
monkeypatch.setattr(addupgradebootloader, '_add_upgrade_boot_entry', mock_add_upgrade_boot_entry)
monkeypatch.setattr(addupgradebootloader, '_set_bootnext', lambda _: None)

monkeypatch.setattr(addupgradebootloader, 'patch_efi_redhat_grubcfg_to_load_correct_grubenv',
lambda: None)

addupgradebootloader.process()

assert api.produce.called == 1
Expand All @@ -307,6 +310,32 @@ def mock_add_upgrade_boot_entry(efibootinfo):
expected = ArmWorkaroundEFIBootloaderInfo(
original_entry=EFIBootEntry(**{f: getattr(TEST_RHEL_EFI_ENTRY, f) for f in efibootentry_fields}),
upgrade_entry=EFIBootEntry(**{f: getattr(TEST_UPGRADE_EFI_ENTRY, f) for f in efibootentry_fields}),
upgrade_bls_dir='/boot/upgrade-loader/entries',
upgrade_entry_efi_path='/boot/efi/EFI/leapp/',
)
actual = api.produce.model_instances[0]
assert actual == expected


@pytest.mark.parametrize('is_config_ok', (True, False))
def test_patch_grubcfg(is_config_ok, monkeypatch):

expected_grubcfg_path = os.path.join(addupgradebootloader.EFI_MOUNTPOINT,
addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH,
'grub.cfg')
def isfile_mocked(path):
assert expected_grubcfg_path == path
return True

def prepare_config_contents_mocked():
return 'config contents'

def write_config(path, contents):
assert not is_config_ok # We should write only when the config is not OK
assert path == expected_grubcfg_path
assert contents == 'config contents'

monkeypatch.setattr(os.path, 'isfile', isfile_mocked)
monkeypatch.setattr(addupgradebootloader, '_will_grubcfg_read_our_grubenv', lambda cfg_path: is_config_ok)
monkeypatch.setattr(addupgradebootloader, '_prepare_config_contents', prepare_config_contents_mocked)
monkeypatch.setattr(addupgradebootloader, '_write_config', write_config)
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def remove_upgrade_efi_entry():

bootloader_info = get_workaround_efi_info()

_copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg'])
_link_grubenv_to_rhel_entry()
# _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg'])
# _link_grubenv_to_rhel_entry()

upgrade_boot_number = bootloader_info.upgrade_entry.boot_number
try:
Expand Down

0 comments on commit 1207dec

Please sign in to comment.