Skip to content

Commit c887b0e

Browse files
authored
Merge pull request #17 from cloudlinux/clos-2610-add-safe-grub-space-inhibitor
Add safe grub embedded space inhibitor
2 parents 713f991 + bfca2c4 commit c887b0e

File tree

11 files changed

+533
-3
lines changed

11 files changed

+533
-3
lines changed

repos/system_upgrade/common/actors/checkgrubcore/actor.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,16 @@ def process(self):
4545
create_report([
4646
reporting.Title('Leapp could not identify where GRUB core is located'),
4747
reporting.Summary(
48-
'We assume GRUB core is located on the same device as /boot. Leapp needs to '
49-
'update GRUB core as it is not done automatically on legacy (BIOS) systems. '
48+
'We assumed GRUB2 core is located on the same device(s) as /boot, '
49+
'however Leapp could not detect GRUB2 on those device(s). '
50+
'This means GRUB2 core will not be updated during the upgrade process and '
51+
'the system will probably ' 'boot into the old kernel after the upgrade. '
52+
'GRUB2 core needs to be updated manually on legacy (BIOS) systems to '
53+
'fix this.'
5054
),
5155
reporting.Severity(reporting.Severity.HIGH),
5256
reporting.Tags([reporting.Tags.BOOT]),
5357
reporting.Remediation(
54-
hint='Please run "grub2-install <GRUB_DEVICE> command manually after upgrade'),
58+
hint='Please run the "grub2-install <GRUB_DEVICE>" command manually '
59+
'after the upgrade'),
5560
])
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor import check_first_partition_offset
3+
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout
4+
from leapp.reporting import Report
5+
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
6+
7+
8+
class CheckFirstPartitionOffset(Actor):
9+
"""
10+
Check whether the first partition starts at the offset >=1MiB.
11+
12+
The alignment of the first partition plays role in disk access speeds. Older tools placed the start of the first
13+
partition at cylinder 63 (due to historical reasons connected to the INT13h BIOS API). However, grub core
14+
binary is placed before the start of the first partition, meaning that not enough space causes bootloader
15+
installation to fail. Modern partitioning tools place the first partition at >= 1MiB (cylinder 2048+).
16+
"""
17+
18+
name = 'check_first_partition_offset'
19+
consumes = (FirmwareFacts, GRUBDevicePartitionLayout,)
20+
produces = (Report,)
21+
tags = (ChecksPhaseTag, IPUWorkflowTag,)
22+
23+
def process(self):
24+
check_first_partition_offset.check_first_partition_offset()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from leapp import reporting
2+
from leapp.libraries.common.config import architecture
3+
from leapp.libraries.stdlib import api
4+
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout
5+
6+
SAFE_OFFSET_BYTES = 1024*1024 # 1MiB
7+
8+
9+
def check_first_partition_offset():
10+
if architecture.matches_architecture(architecture.ARCH_S390X):
11+
return
12+
13+
for fact in api.consume(FirmwareFacts):
14+
if fact.firmware == 'efi':
15+
return # Skip EFI system
16+
17+
problematic_devices = []
18+
for grub_dev in api.consume(GRUBDevicePartitionLayout):
19+
if not grub_dev.partitions:
20+
# NOTE(pstodulk): In case of empty partition list we have nothing to do.
21+
# This can could happen when the fdisk output is different then expected.
22+
# E.g. when GPT partition table is used on the disk. We are right now
23+
# interested strictly about MBR only, so ignoring these cases.
24+
# This is seatbelt, as the msg should not be produced for GPT at all.
25+
continue
26+
first_partition = min(grub_dev.partitions, key=lambda partition: partition.start_offset)
27+
if first_partition.start_offset < SAFE_OFFSET_BYTES:
28+
problematic_devices.append(grub_dev.device)
29+
30+
if problematic_devices:
31+
summary = (
32+
'On the system booting by using BIOS, the in-place upgrade fails '
33+
'when upgrading the GRUB2 bootloader if the boot disk\'s embedding area '
34+
'does not contain enough space for the core image installation. '
35+
'This results in a broken system, and can occur when the disk has been '
36+
'partitioned manually, for example using the RHEL 6 fdisk utility.\n\n'
37+
38+
'The list of devices with small embedding area:\n'
39+
'{0}.'
40+
)
41+
problematic_devices_fmt = ['- {0}'.format(dev) for dev in problematic_devices]
42+
43+
hint = (
44+
'We recommend to perform a fresh installation of the RHEL 8 system '
45+
'instead of performing the in-place upgrade.\n'
46+
'Another possibility is to reformat the devices so that there is '
47+
'at least {0} kiB space before the first partition. If reformatting the drive is not possible, '
48+
'consider migrating your /boot folder and grub2 configuration to another drive '
49+
'(refer to https://cloudlinux.zendesk.com/hc/en-us/articles/14549594244508). '
50+
'Note that this operation is not supported and does not have to be '
51+
'always possible.'
52+
)
53+
54+
reporting.create_report([
55+
reporting.Title('Found GRUB devices with too little space reserved before the first partition'),
56+
reporting.Summary(summary.format('\n'.join(problematic_devices_fmt))),
57+
reporting.Remediation(hint=hint.format(SAFE_OFFSET_BYTES // 1024)),
58+
reporting.Severity(reporting.Severity.HIGH),
59+
reporting.Tags([reporting.Tags.BOOT]),
60+
reporting.Flags([reporting.Flags.INHIBITOR]),
61+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
3+
from leapp import reporting
4+
from leapp.libraries.actor import check_first_partition_offset
5+
from leapp.libraries.common import grub
6+
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
7+
from leapp.libraries.stdlib import api
8+
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout, PartitionInfo
9+
from leapp.reporting import Report
10+
from leapp.utils.report import is_inhibitor
11+
12+
13+
@pytest.mark.parametrize(
14+
('devices', 'should_report'),
15+
[
16+
(
17+
[
18+
GRUBDevicePartitionLayout(device='/dev/vda',
19+
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=32256)])
20+
],
21+
True
22+
),
23+
(
24+
[
25+
GRUBDevicePartitionLayout(device='/dev/vda',
26+
partitions=[
27+
PartitionInfo(part_device='/dev/vda2', start_offset=1024*1025),
28+
PartitionInfo(part_device='/dev/vda1', start_offset=32256)
29+
])
30+
],
31+
True
32+
),
33+
(
34+
[
35+
GRUBDevicePartitionLayout(device='/dev/vda',
36+
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1025)])
37+
],
38+
False
39+
),
40+
(
41+
[
42+
GRUBDevicePartitionLayout(device='/dev/vda',
43+
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1024)])
44+
],
45+
False
46+
),
47+
(
48+
[
49+
GRUBDevicePartitionLayout(device='/dev/vda', partitions=[])
50+
],
51+
False
52+
)
53+
]
54+
)
55+
def test_bad_offset_reported(monkeypatch, devices, should_report):
56+
def consume_mocked(model_cls):
57+
if model_cls == FirmwareFacts:
58+
return [FirmwareFacts(firmware='bios')]
59+
return devices
60+
61+
monkeypatch.setattr(api, 'consume', consume_mocked)
62+
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
63+
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
64+
65+
check_first_partition_offset.check_first_partition_offset()
66+
67+
assert bool(reporting.create_report.called) == should_report
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor import check_legacy_grub as check_legacy_grub_lib
3+
from leapp.reporting import Report
4+
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
5+
6+
7+
class CheckLegacyGrub(Actor):
8+
"""
9+
Check whether GRUB Legacy is installed in the MBR.
10+
11+
GRUB Legacy is deprecated since RHEL 7 in favour of GRUB2.
12+
"""
13+
14+
name = 'check_grub_legacy'
15+
consumes = ()
16+
produces = (Report,)
17+
tags = (FactsPhaseTag, IPUWorkflowTag)
18+
19+
def process(self):
20+
check_legacy_grub_lib.check_grub_disks_for_legacy_grub()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from leapp import reporting
2+
from leapp.exceptions import StopActorExecution
3+
from leapp.libraries.common import grub as grub_lib
4+
from leapp.libraries.stdlib import api, CalledProcessError, run
5+
from leapp.reporting import create_report
6+
7+
# There is no grub legacy package on RHEL7, therefore, the system must have been upgraded from RHEL6
8+
MIGRATION_TO_GRUB2_GUIDE_URL = 'https://access.redhat.com/solutions/2643721'
9+
10+
11+
def has_legacy_grub(device):
12+
try:
13+
output = run(['file', '-s', device])
14+
except CalledProcessError as err:
15+
msg = 'Failed to determine the file type for the special device `{0}`. Full error: `{1}`'
16+
api.current_logger().warning(msg.format(device, str(err)))
17+
18+
# According to `file` manpage, the exit code > 0 iff the file does not exists (meaning)
19+
# that grub_lib.get_grub_devices() is unreliable for some reason (better stop the upgrade),
20+
# or because the file type could not be determined. However, its manpage directly gives examples
21+
# of file -s being used on block devices, so this should be unlikely - especially if one would
22+
# consider that get_grub_devices was able to determine that it is a grub device.
23+
raise StopActorExecution()
24+
25+
grub_legacy_version_string = 'GRUB version 0.94'
26+
return grub_legacy_version_string in output['stdout']
27+
28+
29+
def check_grub_disks_for_legacy_grub():
30+
# Both GRUB2 and Grub Legacy are recognized by `get_grub_devices`
31+
# oshyshatskyi: newer versions of leapp support multiple grub devices e.g. for raid
32+
# because our version does not support that, we always check only one device
33+
# https://github.com/oamg/leapp-repository/commit/2ba44076625e35aabfd2a1f9e45b2934f99f1e8d
34+
grub_device = grub_lib.get_grub_device()
35+
grub_devices = []
36+
if grub_device:
37+
grub_devices.append(grub_device)
38+
39+
legacy_grub_devices = []
40+
for device in grub_devices:
41+
if has_legacy_grub(device):
42+
legacy_grub_devices.append(device)
43+
44+
if legacy_grub_devices:
45+
details = (
46+
'Leapp detected GRUB Legacy to be installed on the system. '
47+
'The GRUB Legacy bootloader is unsupported on RHEL7 and GRUB2 must be used instead. '
48+
'The presence of GRUB Legacy is possible on systems that have been upgraded from RHEL 6 in the past, '
49+
'but required manual post-upgrade steps have not been performed. '
50+
'Note that the in-place upgrade from RHEL 6 to RHEL 7 systems is in such a case '
51+
'considered as unfinished.\n\n'
52+
53+
'GRUB Legacy has been detected on following devices:\n'
54+
'{block_devices_fmt}\n'
55+
)
56+
57+
hint = (
58+
'Migrate to the GRUB2 bootloader on the reported devices. '
59+
'Also finish other post-upgrade steps related to the previous in-place upgrade, the majority of which '
60+
'is a part of the related preupgrade report for upgrades from RHEL 6 to RHEL 7.'
61+
'If you are not sure whether all previously required post-upgrade steps '
62+
'have been performed, consider a clean installation of the RHEL 8 system instead. '
63+
'Note that the in-place upgrade to RHEL 8 can fail in various ways '
64+
'if the RHEL 7 system is misconfigured.'
65+
)
66+
67+
block_devices_fmt = '\n'.join(legacy_grub_devices)
68+
create_report([
69+
reporting.Title("GRUB Legacy is used on the system"),
70+
reporting.Summary(details.format(block_devices_fmt=block_devices_fmt)),
71+
reporting.Severity(reporting.Severity.HIGH),
72+
reporting.Tags([reporting.Tags.BOOT]),
73+
reporting.Remediation(hint=hint),
74+
reporting.Flags([reporting.Flags.INHIBITOR]),
75+
reporting.ExternalLink(url=MIGRATION_TO_GRUB2_GUIDE_URL,
76+
title='How to install GRUB2 after a RHEL6 to RHEL7 upgrade'),
77+
])
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
3+
from leapp.libraries.actor import check_legacy_grub as check_legacy_grub_lib
4+
from leapp.libraries.common import grub as grub_lib
5+
from leapp.libraries.common.testutils import create_report_mocked
6+
from leapp.utils.report import is_inhibitor
7+
8+
VDA_WITH_LEGACY_GRUB = (
9+
'/dev/vda: x86 boot sector; GRand Unified Bootloader, stage1 version 0x3, '
10+
'stage2 address 0x2000, stage2 segment 0x200, GRUB version 0.94; partition 1: ID=0x83, '
11+
'active, starthead 32, startsector 2048, 1024000 sectors; partition 2: ID=0x83, starthead 221, '
12+
'startsector 1026048, 19945472 sectors, code offset 0x48\n'
13+
)
14+
15+
NVME0N1_VDB_WITH_GRUB = (
16+
'/dev/nvme0n1: x86 boot sector; partition 1: ID=0x83, active, starthead 32, startsector 2048, 6291456 sectors; '
17+
'partition 2: ID=0x83, starthead 191, startsector 6293504, 993921024 sectors, code offset 0x63'
18+
)
19+
20+
21+
@pytest.mark.parametrize(
22+
('grub_device_to_file_output', 'should_inhibit'),
23+
[
24+
({'/dev/vda': VDA_WITH_LEGACY_GRUB}, True),
25+
({'/dev/nvme0n1': NVME0N1_VDB_WITH_GRUB}, False),
26+
({'/dev/vda': VDA_WITH_LEGACY_GRUB, '/dev/nvme0n1': NVME0N1_VDB_WITH_GRUB}, True)
27+
]
28+
)
29+
def test_check_legacy_grub(monkeypatch, grub_device_to_file_output, should_inhibit):
30+
31+
def file_cmd_mock(cmd, *args, **kwargs):
32+
assert cmd[:2] == ['file', '-s']
33+
return {'stdout': grub_device_to_file_output[cmd[2]]}
34+
35+
monkeypatch.setattr(check_legacy_grub_lib, 'create_report', create_report_mocked())
36+
monkeypatch.setattr(grub_lib, 'get_grub_devices', lambda: list(grub_device_to_file_output.keys()))
37+
monkeypatch.setattr(check_legacy_grub_lib, 'run', file_cmd_mock)
38+
39+
check_legacy_grub_lib.check_grub_disks_for_legacy_grub()
40+
41+
assert bool(check_legacy_grub_lib.create_report.called) == should_inhibit
42+
if should_inhibit:
43+
assert len(check_legacy_grub_lib.create_report.reports) == 1
44+
report = check_legacy_grub_lib.create_report.reports[0]
45+
assert is_inhibitor(report)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor import scan_layout as scan_layout_lib
3+
from leapp.models import GRUBDevicePartitionLayout, GrubInfo
4+
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
5+
6+
7+
class ScanGRUBDevicePartitionLayout(Actor):
8+
"""
9+
Scan all identified GRUB devices for their partition layout.
10+
"""
11+
12+
name = 'scan_grub_device_partition_layout'
13+
consumes = ()
14+
produces = (GRUBDevicePartitionLayout,)
15+
tags = (FactsPhaseTag, IPUWorkflowTag,)
16+
17+
def process(self):
18+
scan_layout_lib.scan_grub_device_partition_layout()

0 commit comments

Comments
 (0)