Skip to content

Add safe grub embedded space inhibitor #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions repos/system_upgrade/common/actors/checkgrubcore/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ def process(self):
create_report([
reporting.Title('Leapp could not identify where GRUB core is located'),
reporting.Summary(
'We assume GRUB core is located on the same device as /boot. Leapp needs to '
'update GRUB core as it is not done automatically on legacy (BIOS) systems. '
'We assumed GRUB2 core is located on the same device(s) as /boot, '
'however Leapp could not detect GRUB2 on those device(s). '
'This means GRUB2 core will not be updated during the upgrade process and '
'the system will probably ' 'boot into the old kernel after the upgrade. '
'GRUB2 core needs to be updated manually on legacy (BIOS) systems to '
'fix this.'
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Tags([reporting.Tags.BOOT]),
reporting.Remediation(
hint='Please run "grub2-install <GRUB_DEVICE> command manually after upgrade'),
hint='Please run the "grub2-install <GRUB_DEVICE>" command manually '
'after the upgrade'),
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from leapp.actors import Actor
from leapp.libraries.actor import check_first_partition_offset
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckFirstPartitionOffset(Actor):
"""
Check whether the first partition starts at the offset >=1MiB.

The alignment of the first partition plays role in disk access speeds. Older tools placed the start of the first
partition at cylinder 63 (due to historical reasons connected to the INT13h BIOS API). However, grub core
binary is placed before the start of the first partition, meaning that not enough space causes bootloader
installation to fail. Modern partitioning tools place the first partition at >= 1MiB (cylinder 2048+).
"""

name = 'check_first_partition_offset'
consumes = (FirmwareFacts, GRUBDevicePartitionLayout,)
produces = (Report,)
tags = (ChecksPhaseTag, IPUWorkflowTag,)

def process(self):
check_first_partition_offset.check_first_partition_offset()
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from leapp import reporting
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout

SAFE_OFFSET_BYTES = 1024*1024 # 1MiB


def check_first_partition_offset():
if architecture.matches_architecture(architecture.ARCH_S390X):
return

for fact in api.consume(FirmwareFacts):
if fact.firmware == 'efi':
return # Skip EFI system

problematic_devices = []
for grub_dev in api.consume(GRUBDevicePartitionLayout):
if not grub_dev.partitions:
# NOTE(pstodulk): In case of empty partition list we have nothing to do.
# This can could happen when the fdisk output is different then expected.
# E.g. when GPT partition table is used on the disk. We are right now
# interested strictly about MBR only, so ignoring these cases.
# This is seatbelt, as the msg should not be produced for GPT at all.
continue
first_partition = min(grub_dev.partitions, key=lambda partition: partition.start_offset)
if first_partition.start_offset < SAFE_OFFSET_BYTES:
problematic_devices.append(grub_dev.device)

if problematic_devices:
summary = (
'On the system booting by using BIOS, the in-place upgrade fails '
'when upgrading the GRUB2 bootloader if the boot disk\'s embedding area '
'does not contain enough space for the core image installation. '
'This results in a broken system, and can occur when the disk has been '
'partitioned manually, for example using the RHEL 6 fdisk utility.\n\n'

'The list of devices with small embedding area:\n'
'{0}.'
)
problematic_devices_fmt = ['- {0}'.format(dev) for dev in problematic_devices]

hint = (
'We recommend to perform a fresh installation of the RHEL 8 system '
'instead of performing the in-place upgrade.\n'
'Another possibility is to reformat the devices so that there is '
'at least {0} kiB space before the first partition. If reformatting the drive is not possible, '
'consider migrating your /boot folder and grub2 configuration to another drive '
'(refer to https://cloudlinux.zendesk.com/hc/en-us/articles/14549594244508). '
'Note that this operation is not supported and does not have to be '
'always possible.'
)

reporting.create_report([
reporting.Title('Found GRUB devices with too little space reserved before the first partition'),
reporting.Summary(summary.format('\n'.join(problematic_devices_fmt))),
reporting.Remediation(hint=hint.format(SAFE_OFFSET_BYTES // 1024)),
reporting.Severity(reporting.Severity.HIGH),
reporting.Tags([reporting.Tags.BOOT]),
reporting.Flags([reporting.Flags.INHIBITOR]),
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest

from leapp import reporting
from leapp.libraries.actor import check_first_partition_offset
from leapp.libraries.common import grub
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, GRUBDevicePartitionLayout, PartitionInfo
from leapp.reporting import Report
from leapp.utils.report import is_inhibitor


@pytest.mark.parametrize(
('devices', 'should_report'),
[
(
[
GRUBDevicePartitionLayout(device='/dev/vda',
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=32256)])
],
True
),
(
[
GRUBDevicePartitionLayout(device='/dev/vda',
partitions=[
PartitionInfo(part_device='/dev/vda2', start_offset=1024*1025),
PartitionInfo(part_device='/dev/vda1', start_offset=32256)
])
],
True
),
(
[
GRUBDevicePartitionLayout(device='/dev/vda',
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1025)])
],
False
),
(
[
GRUBDevicePartitionLayout(device='/dev/vda',
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1024)])
],
False
),
(
[
GRUBDevicePartitionLayout(device='/dev/vda', partitions=[])
],
False
)
]
)
def test_bad_offset_reported(monkeypatch, devices, should_report):
def consume_mocked(model_cls):
if model_cls == FirmwareFacts:
return [FirmwareFacts(firmware='bios')]
return devices

monkeypatch.setattr(api, 'consume', consume_mocked)
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

check_first_partition_offset.check_first_partition_offset()

assert bool(reporting.create_report.called) == should_report
20 changes: 20 additions & 0 deletions repos/system_upgrade/el7toel8/actors/checklegacygrub/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from leapp.actors import Actor
from leapp.libraries.actor import check_legacy_grub as check_legacy_grub_lib
from leapp.reporting import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CheckLegacyGrub(Actor):
"""
Check whether GRUB Legacy is installed in the MBR.

GRUB Legacy is deprecated since RHEL 7 in favour of GRUB2.
"""

name = 'check_grub_legacy'
consumes = ()
produces = (Report,)
tags = (FactsPhaseTag, IPUWorkflowTag)

def process(self):
check_legacy_grub_lib.check_grub_disks_for_legacy_grub()
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.common import grub as grub_lib
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.reporting import create_report

# There is no grub legacy package on RHEL7, therefore, the system must have been upgraded from RHEL6
MIGRATION_TO_GRUB2_GUIDE_URL = 'https://access.redhat.com/solutions/2643721'


def has_legacy_grub(device):
try:
output = run(['file', '-s', device])
except CalledProcessError as err:
msg = 'Failed to determine the file type for the special device `{0}`. Full error: `{1}`'
api.current_logger().warning(msg.format(device, str(err)))

# According to `file` manpage, the exit code > 0 iff the file does not exists (meaning)
# that grub_lib.get_grub_devices() is unreliable for some reason (better stop the upgrade),
# or because the file type could not be determined. However, its manpage directly gives examples
# of file -s being used on block devices, so this should be unlikely - especially if one would
# consider that get_grub_devices was able to determine that it is a grub device.
raise StopActorExecution()

grub_legacy_version_string = 'GRUB version 0.94'
return grub_legacy_version_string in output['stdout']


def check_grub_disks_for_legacy_grub():
# Both GRUB2 and Grub Legacy are recognized by `get_grub_devices`
# oshyshatskyi: newer versions of leapp support multiple grub devices e.g. for raid
# because our version does not support that, we always check only one device
# https://github.com/oamg/leapp-repository/commit/2ba44076625e35aabfd2a1f9e45b2934f99f1e8d
grub_device = grub_lib.get_grub_device()
grub_devices = []
if grub_device:
grub_devices.append(grub_device)

legacy_grub_devices = []
for device in grub_devices:
if has_legacy_grub(device):
legacy_grub_devices.append(device)

if legacy_grub_devices:
details = (
'Leapp detected GRUB Legacy to be installed on the system. '
'The GRUB Legacy bootloader is unsupported on RHEL7 and GRUB2 must be used instead. '
'The presence of GRUB Legacy is possible on systems that have been upgraded from RHEL 6 in the past, '
'but required manual post-upgrade steps have not been performed. '
'Note that the in-place upgrade from RHEL 6 to RHEL 7 systems is in such a case '
'considered as unfinished.\n\n'

'GRUB Legacy has been detected on following devices:\n'
'{block_devices_fmt}\n'
)

hint = (
'Migrate to the GRUB2 bootloader on the reported devices. '
'Also finish other post-upgrade steps related to the previous in-place upgrade, the majority of which '
'is a part of the related preupgrade report for upgrades from RHEL 6 to RHEL 7.'
'If you are not sure whether all previously required post-upgrade steps '
'have been performed, consider a clean installation of the RHEL 8 system instead. '
'Note that the in-place upgrade to RHEL 8 can fail in various ways '
'if the RHEL 7 system is misconfigured.'
)

block_devices_fmt = '\n'.join(legacy_grub_devices)
create_report([
reporting.Title("GRUB Legacy is used on the system"),
reporting.Summary(details.format(block_devices_fmt=block_devices_fmt)),
reporting.Severity(reporting.Severity.HIGH),
reporting.Tags([reporting.Tags.BOOT]),
reporting.Remediation(hint=hint),
reporting.Flags([reporting.Flags.INHIBITOR]),
reporting.ExternalLink(url=MIGRATION_TO_GRUB2_GUIDE_URL,
title='How to install GRUB2 after a RHEL6 to RHEL7 upgrade'),
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest

from leapp.libraries.actor import check_legacy_grub as check_legacy_grub_lib
from leapp.libraries.common import grub as grub_lib
from leapp.libraries.common.testutils import create_report_mocked
from leapp.utils.report import is_inhibitor

VDA_WITH_LEGACY_GRUB = (
'/dev/vda: x86 boot sector; GRand Unified Bootloader, stage1 version 0x3, '
'stage2 address 0x2000, stage2 segment 0x200, GRUB version 0.94; partition 1: ID=0x83, '
'active, starthead 32, startsector 2048, 1024000 sectors; partition 2: ID=0x83, starthead 221, '
'startsector 1026048, 19945472 sectors, code offset 0x48\n'
)

NVME0N1_VDB_WITH_GRUB = (
'/dev/nvme0n1: x86 boot sector; partition 1: ID=0x83, active, starthead 32, startsector 2048, 6291456 sectors; '
'partition 2: ID=0x83, starthead 191, startsector 6293504, 993921024 sectors, code offset 0x63'
)


@pytest.mark.parametrize(
('grub_device_to_file_output', 'should_inhibit'),
[
({'/dev/vda': VDA_WITH_LEGACY_GRUB}, True),
({'/dev/nvme0n1': NVME0N1_VDB_WITH_GRUB}, False),
({'/dev/vda': VDA_WITH_LEGACY_GRUB, '/dev/nvme0n1': NVME0N1_VDB_WITH_GRUB}, True)
]
)
def test_check_legacy_grub(monkeypatch, grub_device_to_file_output, should_inhibit):

def file_cmd_mock(cmd, *args, **kwargs):
assert cmd[:2] == ['file', '-s']
return {'stdout': grub_device_to_file_output[cmd[2]]}

monkeypatch.setattr(check_legacy_grub_lib, 'create_report', create_report_mocked())
monkeypatch.setattr(grub_lib, 'get_grub_devices', lambda: list(grub_device_to_file_output.keys()))
monkeypatch.setattr(check_legacy_grub_lib, 'run', file_cmd_mock)

check_legacy_grub_lib.check_grub_disks_for_legacy_grub()

assert bool(check_legacy_grub_lib.create_report.called) == should_inhibit
if should_inhibit:
assert len(check_legacy_grub_lib.create_report.reports) == 1
report = check_legacy_grub_lib.create_report.reports[0]
assert is_inhibitor(report)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import scan_layout as scan_layout_lib
from leapp.models import GRUBDevicePartitionLayout, GrubInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanGRUBDevicePartitionLayout(Actor):
"""
Scan all identified GRUB devices for their partition layout.
"""

name = 'scan_grub_device_partition_layout'
consumes = ()
produces = (GRUBDevicePartitionLayout,)
tags = (FactsPhaseTag, IPUWorkflowTag,)

def process(self):
scan_layout_lib.scan_grub_device_partition_layout()
Loading