Skip to content

Commit ebc2847

Browse files
author
Michal Hecko
committed
feat(arm,bootloader,efi): use separate BLS directory for upgrades
Use a separate BLS directory '/boot/upgrade-loader/entries' that mimics '/boot/loader/entries'. This allows very fine control of what boot entries are available when booting into upgrade environment via a separate EFI entry.
1 parent 664dae4 commit ebc2847

File tree

11 files changed

+232
-23
lines changed

11 files changed

+232
-23
lines changed

Diff for: repos/system_upgrade/common/actors/addupgradebootentry/actor.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from leapp.exceptions import StopActorExecutionError
55
from leapp.libraries.actor.addupgradebootentry import add_boot_entry, fix_grub_config_error
66
from leapp.models import (
7+
ArmWorkaroundEFIBootloaderInfo,
78
BootContent,
89
FirmwareFacts,
910
GrubConfigError,
@@ -28,6 +29,7 @@ class AddUpgradeBootEntry(Actor):
2829

2930
name = 'add_upgrade_boot_entry'
3031
consumes = (
32+
ArmWorkaroundEFIBootloaderInfo,
3133
BootContent,
3234
GrubConfigError,
3335
FirmwareFacts,

Diff for: repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py

+97
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import itertools
22
import os
33
import re
4+
import shutil
45

56
from leapp.exceptions import StopActorExecutionError
67
from leapp.libraries.common.config import architecture, get_env
78
from leapp.libraries.stdlib import api, CalledProcessError, run
89
from leapp.models import (
10+
ArmWorkaroundEFIBootloaderInfo,
911
BootContent,
1012
KernelCmdline,
1113
KernelCmdlineArg,
@@ -197,6 +199,8 @@ def run_commands_adding_entry(extra_command_suffix=None):
197199
details={'details': '{}: {}'.format(str(e), e.stderr)}
198200
)
199201

202+
apply_arm_specific_modifications()
203+
200204

201205
def _remove_old_upgrade_boot_entry(kernel_dst_path, configs=None):
202206
"""
@@ -357,3 +361,96 @@ def construct_cmdline_args_for_livemode():
357361
api.current_logger().info('The use of live mode image implies the following cmdline args: %s', args)
358362

359363
return args
364+
365+
366+
def _list_grubenv_variables():
367+
try:
368+
output_lines = run(['grub2-editenv', 'list'], split=True)['stdout']
369+
except CalledProcessError:
370+
raise StopActorExecutionError('Failed to list grubenv variables used by the system')
371+
372+
vars_with_values = {}
373+
for line in output_lines:
374+
var_with_value = line.split('=', 1)
375+
if len(var_with_value) <= 1:
376+
api.current_logger().warning(
377+
'Skipping \'{}\' in grub2-editenv output, the line does not have the form <var>=<value>'
378+
)
379+
continue
380+
vars_with_values[var_with_value[0]] = var_with_value[1]
381+
382+
return vars_with_values
383+
384+
385+
def apply_arm_specific_modifications():
386+
arm_efi_info = next(api.consume(ArmWorkaroundEFIBootloaderInfo), None)
387+
if not arm_efi_info:
388+
return
389+
390+
modify_our_grubenv_to_have_separate_blsdir(arm_efi_info)
391+
392+
393+
def modify_our_grubenv_to_have_separate_blsdir(efi_info):
394+
""" Create a new blsdir for the upgrade entry if using a separate EFI entry. """
395+
leapp_efi_grubenv_path = os.path.join(efi_info.upgrade_entry_efi_path, 'grubenv')
396+
397+
api.current_logger().debug(
398+
'Setting up separate blsdir for the upgrade using grubenv: {}'.format(leapp_efi_grubenv_path)
399+
)
400+
401+
grubenv_vars = _list_grubenv_variables()
402+
system_bls_dir = grubenv_vars.get('blsdir', '/loader/entries').lstrip('/')
403+
404+
# BLS dir is relative to /boot, prepend it so we can list its contents
405+
system_bls_dir = os.path.join('/boot', system_bls_dir)
406+
407+
# Find our loader entry
408+
try:
409+
bls_entries = os.listdir(system_bls_dir)
410+
except IOError: # Technically, we want FileNotFoundError, but that is only Python3.3+, so this is fine
411+
details = {
412+
'details': 'Failed to list {}.'.format(system_bls_dir)
413+
}
414+
raise StopActorExecutionError('Failed to set up bootloader for the upgrade.', details=details)
415+
416+
leapp_bls_entry = None
417+
for bls_entry in bls_entries:
418+
if bls_entry.endswith('upgrade.aarch64.conf'):
419+
leapp_bls_entry = bls_entry
420+
break
421+
422+
if not leapp_bls_entry:
423+
details = {
424+
'details': 'Failed to identify BLS entry that belongs to leapp in {}'.format(system_bls_dir)
425+
}
426+
raise StopActorExecutionError('Failed to set up bootloader for the upgrade.')
427+
428+
# The 'blsdir' grubenv variable specifies location of bls directory relative to /boot
429+
if os.path.exists(efi_info.upgrade_bls_dir):
430+
msg = 'The {} directory exists, probably a left-over from previous executions. Removing.'
431+
api.current_logger().debug(msg.format(efi_info.upgrade_bls_dir))
432+
shutil.rmtree(efi_info.upgrade_bls_dir)
433+
434+
os.makedirs(efi_info.upgrade_bls_dir)
435+
api.current_logger().debug('Successfully created upgrade BLS directory: {}'.format(efi_info.upgrade_bls_dir))
436+
437+
leapp_bls_entry_fullpath = os.path.join(system_bls_dir, leapp_bls_entry)
438+
bls_entry_dst = os.path.join(efi_info.upgrade_bls_dir, leapp_bls_entry)
439+
api.current_logger().debug(
440+
'Moving leapp\'s BLS entry ({}) into a separate BLS dir located at {}'.format(
441+
leapp_bls_entry, efi_info.upgrade_bls_dir
442+
)
443+
)
444+
445+
shutil.move(leapp_bls_entry_fullpath, bls_entry_dst)
446+
447+
upgrade_bls_dir_rel_to_boot = efi_info.upgrade_bls_dir[len('/boot'):]
448+
449+
# Modify leapp's grubenv to define our own BLSDIR
450+
try:
451+
run(['grub2-editenv', leapp_efi_grubenv_path, 'set', 'blsdir={}'.format(upgrade_bls_dir_rel_to_boot)])
452+
except CalledProcessError as error:
453+
details = {
454+
'details': 'Failed to modify upgrade grubenv to contain a custom blsdir definition. Error {}'.format(error)
455+
}
456+
raise StopActorExecutionError('Failed to set up bootloader for the upgrade.', details=details)

Diff for: repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py

+53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shutil
23
from collections import namedtuple
34

45
import pytest
@@ -9,7 +10,9 @@
910
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
1011
from leapp.libraries.stdlib import api
1112
from leapp.models import (
13+
ArmWorkaroundEFIBootloaderInfo,
1214
BootContent,
15+
EFIBootEntry,
1316
KernelCmdline,
1417
KernelCmdlineArg,
1518
LateTargetKernelCmdlineArgTasks,
@@ -326,3 +329,53 @@ def readlink_mock(path):
326329
uuid = addupgradebootentry._get_device_uuid(path)
327330

328331
assert uuid == 'MY_UUID1'
332+
333+
334+
def test_modify_grubenv_to_have_separate_blsdir(monkeypatch):
335+
efi_info = ArmWorkaroundEFIBootloaderInfo(
336+
original_entry=EFIBootEntry(
337+
boot_number='0001',
338+
label='Redhat',
339+
active=True,
340+
efi_bin_source="HD(.*)/File(\\EFI\\redhat\\shimx64.efi)",
341+
),
342+
upgrade_entry=EFIBootEntry(
343+
boot_number='0002',
344+
label='Leapp',
345+
active=True,
346+
efi_bin_source="HD(.*)/File(\\EFI\\leapp\\shimx64.efi)",
347+
),
348+
upgrade_bls_dir='/boot/upgrade-loader/entries',
349+
upgrade_entry_efi_path='/boot/efi/EFI/leapp'
350+
)
351+
352+
def list_grubenv_variables_mock():
353+
return {
354+
'blsdir': '/blsdir'
355+
}
356+
357+
def listdir_mock(dir_path):
358+
assert dir_path == '/boot/blsdir'
359+
return [
360+
'4a9c76478b98444fb5e0fbf533950edf-6.12.5-200.fc41.x86_64.conf',
361+
'4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf',
362+
]
363+
364+
def assert_path_correct(path):
365+
assert path == efi_info.upgrade_bls_dir
366+
367+
def move_mocked(src, dst):
368+
assert src == '/boot/blsdir/4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf'
369+
assert dst == '/boot/upgrade-loader/entries/4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf'
370+
371+
def run_mocked(cmd, *arg, **kwargs):
372+
assert cmd == ['grub2-editenv', '/boot/efi/EFI/leapp/grubenv', 'set', 'blsdir=/upgrade-loader/entries']
373+
374+
monkeypatch.setattr(addupgradebootentry, '_list_grubenv_variables', list_grubenv_variables_mock)
375+
monkeypatch.setattr(os, 'listdir', listdir_mock)
376+
monkeypatch.setattr(os.path, 'exists', assert_path_correct)
377+
monkeypatch.setattr(os, 'makedirs', assert_path_correct)
378+
monkeypatch.setattr(shutil, 'move', move_mocked)
379+
monkeypatch.setattr(addupgradebootentry, 'run', run_mocked)
380+
381+
addupgradebootentry.modify_our_grubenv_to_have_separate_blsdir(efi_info)

Diff for: repos/system_upgrade/common/actors/removeupgradebootentry/actor.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from leapp.actors import Actor
22
from leapp.libraries.actor.removeupgradebootentry import remove_boot_entry
3-
from leapp.models import BootContent, FirmwareFacts
3+
from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts
44
from leapp.tags import InitRamStartPhaseTag, IPUWorkflowTag
55

66

@@ -12,7 +12,7 @@ class RemoveUpgradeBootEntry(Actor):
1212
"""
1313

1414
name = 'remove_upgrade_boot_entry'
15-
consumes = (BootContent, FirmwareFacts)
15+
consumes = (ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts)
1616
produces = ()
1717
tags = (IPUWorkflowTag, InitRamStartPhaseTag)
1818

Diff for: repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from leapp.exceptions import StopActorExecutionError
22
from leapp.libraries.common.config import architecture
33
from leapp.libraries.stdlib import api, CalledProcessError, run
4-
from leapp.models import BootContent, FirmwareFacts
4+
from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts
55

66

77
def remove_boot_entry():
@@ -25,6 +25,14 @@ def remove_boot_entry():
2525
# partitions have been most likely already mounted
2626
pass
2727
kernel_filepath = get_upgrade_kernel_filepath()
28+
29+
arm_bootloader_workaround_info = next(api.consume(ArmWorkaroundEFIBootloaderInfo), None)
30+
if arm_bootloader_workaround_info and arm_bootloader_workaround_info.upgrade_bls_dir:
31+
# Leapp has a separate BLS dir and grubby will not know about it. We don't need to call
32+
# grubby here - we are removing the entire BLS dir in another actor.
33+
api.current_logger().debug('Skipping removal of upgrade kernel entry since we are using a separate BLS dir.')
34+
return
35+
2836
run([
2937
'/usr/sbin/grubby',
3038
'--remove-kernel={0}'.format(kernel_filepath)

Diff for: repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py

+25-16
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,38 @@
55
from leapp.libraries.common.config import architecture
66
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
77
from leapp.libraries.stdlib import api
8-
from leapp.models import BootContent, FirmwareFacts
8+
from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, EFIBootEntry, FirmwareFacts
99

1010

1111
class run_mocked(object):
12-
args = []
12+
def __init__(self):
13+
self.args = []
1314

1415
def __call__(self, args, split=True):
1516
self.args.append(args)
1617

1718

1819
@pytest.mark.parametrize('firmware', ['bios', 'efi'])
1920
@pytest.mark.parametrize('arch', [architecture.ARCH_X86_64, architecture.ARCH_S390X])
20-
def test_remove_boot_entry(firmware, arch, monkeypatch):
21+
@pytest.mark.parametrize('has_separate_bls_dir', [True, False])
22+
def test_remove_boot_entry(firmware, arch, has_separate_bls_dir, monkeypatch):
2123
def get_upgrade_kernel_filepath_mocked():
2224
return '/abc'
2325

24-
def consume_systemfacts_mocked(*models):
25-
yield FirmwareFacts(firmware=firmware)
26-
27-
monkeypatch.setattr(removeupgradebootentry, 'get_upgrade_kernel_filepath', get_upgrade_kernel_filepath_mocked, )
28-
monkeypatch.setattr(api, 'consume', consume_systemfacts_mocked)
26+
messages = [FirmwareFacts(firmware=firmware)]
27+
if has_separate_bls_dir:
28+
some_efi_entry = EFIBootEntry(boot_number='0001', label='entry', active=True, efi_bin_source='')
29+
workaround_info = ArmWorkaroundEFIBootloaderInfo(
30+
original_entry=some_efi_entry,
31+
upgrade_entry=some_efi_entry,
32+
upgrade_bls_dir='/boot/upgrade-loader/entries',
33+
upgrade_entry_efi_path='/boot/efi/EFI/leapp/'
34+
)
35+
messages.append(workaround_info)
36+
37+
monkeypatch.setattr(removeupgradebootentry, 'get_upgrade_kernel_filepath', get_upgrade_kernel_filepath_mocked)
2938
monkeypatch.setattr(removeupgradebootentry, 'run', run_mocked())
30-
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch))
39+
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch, msgs=messages))
3140
monkeypatch.setattr(api, 'current_logger', logger_mocked())
3241

3342
removeupgradebootentry.remove_boot_entry()
@@ -36,16 +45,16 @@ def consume_systemfacts_mocked(*models):
3645
if firmware == 'efi':
3746
boot_mounts.append(['/bin/mount', '/boot/efi'])
3847

39-
calls = boot_mounts + [['/usr/sbin/grubby', '--remove-kernel=/abc']]
40-
if arch == architecture.ARCH_S390X:
41-
calls.append(['/usr/sbin/zipl'])
42-
calls.append(['/bin/mount', '-a'])
48+
calls = boot_mounts
49+
if not has_separate_bls_dir:
50+
# If we are using a separate BLS dir (ARM specific), then do not call anything
51+
calls += [['/usr/sbin/grubby', '--remove-kernel=/abc']]
52+
if arch == architecture.ARCH_S390X:
53+
calls.append(['/usr/sbin/zipl'])
54+
calls.append(['/bin/mount', '-a'])
4355

4456
assert removeupgradebootentry.run.args == calls
4557

46-
# clear args for next run
47-
del removeupgradebootentry.run.args[:]
48-
4958

5059
def test_get_upgrade_kernel_filepath(monkeypatch):
5160
# BootContent message available

Diff for: repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
EFI_MOUNTPOINT = '/boot/efi/'
2525
LEAPP_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/leapp/')
2626
RHEL_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/redhat/')
27+
UPGRADE_BLS_DIR = '/boot/upgrade-loader'
2728

2829
CONTAINER_DOWNLOAD_DIR = '/tmp_pkg_download_dir'
2930

@@ -64,7 +65,6 @@ def process():
6465
current_boot_entry = efibootinfo.entries[efibootinfo.current_bootnum]
6566
upgrade_boot_entry = _add_upgrade_boot_entry(efibootinfo)
6667

67-
leapp_efi_grubenv = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv')
6868
patch_efi_redhat_grubcfg_to_load_correct_grubenv()
6969

7070
_set_bootnext(upgrade_boot_entry.boot_number)
@@ -74,6 +74,8 @@ def process():
7474
ArmWorkaroundEFIBootloaderInfo(
7575
original_entry=EFIBootEntry(**{f: getattr(current_boot_entry, f) for f in efibootentry_fields}),
7676
upgrade_entry=EFIBootEntry(**{f: getattr(upgrade_boot_entry, f) for f in efibootentry_fields}),
77+
upgrade_bls_dir=UPGRADE_BLS_DIR,
78+
upgrade_entry_efi_path=os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH),
7779
)
7880
)
7981

Diff for: repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def mock_add_upgrade_boot_entry(efibootinfo):
310310
expected = ArmWorkaroundEFIBootloaderInfo(
311311
original_entry=EFIBootEntry(**{f: getattr(TEST_RHEL_EFI_ENTRY, f) for f in efibootentry_fields}),
312312
upgrade_entry=EFIBootEntry(**{f: getattr(TEST_UPGRADE_EFI_ENTRY, f) for f in efibootentry_fields}),
313-
upgrade_bls_dir='/boot/upgrade-loader/entries',
313+
upgrade_bls_dir=addupgradebootloader.UPGRADE_BLS_DIR,
314314
upgrade_entry_efi_path='/boot/efi/EFI/leapp/',
315315
)
316316
actual = api.produce.model_instances[0]
@@ -323,6 +323,7 @@ def test_patch_grubcfg(is_config_ok, monkeypatch):
323323
expected_grubcfg_path = os.path.join(addupgradebootloader.EFI_MOUNTPOINT,
324324
addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH,
325325
'grub.cfg')
326+
326327
def isfile_mocked(path):
327328
assert expected_grubcfg_path == path
328329
return True

Diff for: repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ def remove_upgrade_efi_entry():
5454
except CalledProcessError:
5555
api.current_logger().warning('Unable to remove Leapp upgrade efi files.')
5656

57+
_remove_upgrade_blsdir(bootloader_info)
58+
5759
original_boot_number = bootloader_info.original_entry.boot_number
5860
run(['/usr/sbin/efibootmgr', '--bootnext', original_boot_number])
5961

@@ -82,7 +84,7 @@ def _copy_file(src_path, dst_path):
8284

8385
def _copy_grub_files(required, optional):
8486
"""
85-
Copy grub files from redhat/ dir to the /boot/efi/EFI/leapp/ dir.
87+
Copy grub files from /boot/efi/EFI/leapp/ dir to the /boot/efi/EFI/redhat/ dir.
8688
"""
8789

8890
all_files = required + optional
@@ -98,3 +100,13 @@ def _copy_grub_files(required, optional):
98100
continue
99101

100102
_copy_file(src_path, dst_path)
103+
104+
105+
def _remove_upgrade_blsdir(bootloader_info):
106+
api.current_logger().debug('Removing upgrade BLS directory: {}'.format(bootloader_info.upgrade_bls_dir))
107+
try:
108+
shutil.rmtree(bootloader_info.upgrade_bls_dir)
109+
except OSError as error:
110+
# I tried, no can do at this point
111+
msg = 'Failed to remove upgrade BLS directory: {} with error {}'
112+
api.current_logger().debug(msg.format(bootloader_info.upgrade_bls_dir, error))

0 commit comments

Comments
 (0)