leapp-repository/0014-boot-check-first-partition-offset-on-GRUB-devices.patch

389 lines
16 KiB
Diff
Raw Normal View History

From ea6cd7912ce650f033a972921a2b29636ac304db Mon Sep 17 00:00:00 2001
From: mhecko <mhecko@redhat.com>
Date: Tue, 2 Apr 2024 19:29:16 +0200
Subject: [PATCH 14/34] boot: check first partition offset on GRUB devices
Check that the first partition starts at least at 1MiB (2048 cylinders),
as too small first-partition offsets lead to failures when doing
grub2-install. The limit (1MiB) has been chosen as it is a common
value set by the disk formatting tools nowadays.
jira: https://issues.redhat.com/browse/RHEL-3341
---
.../actors/checkfirstpartitionoffset/actor.py | 24 ++++++
.../libraries/check_first_partition_offset.py | 52 +++++++++++++
.../test_check_first_partition_offset.py | 51 ++++++++++++
.../scangrubdevpartitionlayout/actor.py | 18 +++++
.../libraries/scan_layout.py | 64 +++++++++++++++
.../tests/test_scan_partition_layout.py | 78 +++++++++++++++++++
.../el7toel8/models/partitionlayout.py | 28 +++++++
7 files changed, 315 insertions(+)
create mode 100644 repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/actor.py
create mode 100644 repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/libraries/check_first_partition_offset.py
create mode 100644 repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/tests/test_check_first_partition_offset.py
create mode 100644 repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/actor.py
create mode 100644 repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/libraries/scan_layout.py
create mode 100644 repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/tests/test_scan_partition_layout.py
create mode 100644 repos/system_upgrade/el7toel8/models/partitionlayout.py
diff --git a/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/actor.py b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/actor.py
new file mode 100644
index 00000000..cde27c2a
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/actor.py
@@ -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()
diff --git a/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/libraries/check_first_partition_offset.py b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/libraries/check_first_partition_offset.py
new file mode 100644
index 00000000..fbd4e178
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/libraries/check_first_partition_offset.py
@@ -0,0 +1,52 @@
+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):
+ 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. '
+ '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.Groups([reporting.Groups.BOOT]),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ ])
diff --git a/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/tests/test_check_first_partition_offset.py b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/tests/test_check_first_partition_offset.py
new file mode 100644
index 00000000..e349ff7d
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/checkfirstpartitionoffset/tests/test_check_first_partition_offset.py
@@ -0,0 +1,51 @@
+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/vda1', start_offset=1024*1025)])
+ ],
+ False
+ ),
+ (
+ [
+ GRUBDevicePartitionLayout(device='/dev/vda',
+ partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1024)])
+ ],
+ 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
diff --git a/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/actor.py b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/actor.py
new file mode 100644
index 00000000..0db93aba
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/actor.py
@@ -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 = (GrubInfo,)
+ produces = (GRUBDevicePartitionLayout,)
+ tags = (FactsPhaseTag, IPUWorkflowTag,)
+
+ def process(self):
+ scan_layout_lib.scan_grub_device_partition_layout()
diff --git a/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/libraries/scan_layout.py b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/libraries/scan_layout.py
new file mode 100644
index 00000000..bb2e6d9e
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/libraries/scan_layout.py
@@ -0,0 +1,64 @@
+from leapp.libraries.stdlib import api, CalledProcessError, run
+from leapp.models import GRUBDevicePartitionLayout, GrubInfo, PartitionInfo
+
+SAFE_OFFSET_BYTES = 1024*1024 # 1MiB
+
+
+def split_on_space_segments(line):
+ fragments = (fragment.strip() for fragment in line.split(' '))
+ return [fragment for fragment in fragments if fragment]
+
+
+def get_partition_layout(device):
+ try:
+ partition_table = run(['fdisk', '-l', '-u=sectors', device], split=True)['stdout']
+ except CalledProcessError as err:
+ # Unlikely - if the disk has no partition table, `fdisk` terminates with 0 (no err). Fdisk exits with an err
+ # when the device does not exists, or if it is too small to contain a partition table.
+
+ err_msg = 'Failed to run `fdisk` to obtain the partition table of the device {0}. Full error: \'{1}\''
+ api.current_logger().error(err_msg.format(device, str(err)))
+ return None
+
+ table_iter = iter(partition_table)
+
+ for line in table_iter:
+ if not line.startswith('Units'):
+ # We are still reading general device information and not the table itself
+ continue
+
+ unit = line.split('=')[2].strip() # Contains '512 bytes'
+ unit = int(unit.split(' ')[0].strip())
+ break # First line of the partition table header
+
+ for line in table_iter:
+ line = line.strip()
+ if not line.startswith('Device'):
+ continue
+
+ part_all_attrs = split_on_space_segments(line)
+ break
+
+ partitions = []
+ for partition_line in table_iter:
+ # Fields: Device Boot Start End Sectors Size Id Type
+ # The line looks like: `/dev/vda1 * 2048 2099199 2097152 1G 83 Linux`
+ part_info = split_on_space_segments(partition_line)
+
+ # If the partition is not bootable, the Boot column might be empty
+ part_device = part_info[0]
+ part_start = int(part_info[2]) if len(part_info) == len(part_all_attrs) else int(part_info[1])
+ partitions.append(PartitionInfo(part_device=part_device, start_offset=part_start*unit))
+
+ return GRUBDevicePartitionLayout(device=device, partitions=partitions)
+
+
+def scan_grub_device_partition_layout():
+ grub_devices = next(api.consume(GrubInfo), None)
+ if not grub_devices:
+ return
+
+ for device in grub_devices.orig_devices:
+ dev_info = get_partition_layout(device)
+ if dev_info:
+ api.produce(dev_info)
diff --git a/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/tests/test_scan_partition_layout.py b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/tests/test_scan_partition_layout.py
new file mode 100644
index 00000000..37bb5bcf
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/actors/scangrubdevpartitionlayout/tests/test_scan_partition_layout.py
@@ -0,0 +1,78 @@
+from collections import namedtuple
+
+import pytest
+
+from leapp.libraries.actor import scan_layout as scan_layout_lib
+from leapp.libraries.common import grub
+from leapp.libraries.common.testutils import create_report_mocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import GRUBDevicePartitionLayout, GrubInfo
+from leapp.utils.report import is_inhibitor
+
+Device = namedtuple('Device', ['name', 'partitions', 'sector_size'])
+Partition = namedtuple('Partition', ['name', 'start_offset'])
+
+
+@pytest.mark.parametrize(
+ 'devices',
+ [
+ (
+ Device(name='/dev/vda', sector_size=512,
+ partitions=[Partition(name='/dev/vda1', start_offset=63),
+ Partition(name='/dev/vda2', start_offset=1000)]),
+ Device(name='/dev/vdb', sector_size=1024,
+ partitions=[Partition(name='/dev/vdb1', start_offset=100),
+ Partition(name='/dev/vdb2', start_offset=20000)])
+ ),
+ (
+ Device(name='/dev/vda', sector_size=512,
+ partitions=[Partition(name='/dev/vda1', start_offset=111),
+ Partition(name='/dev/vda2', start_offset=1000)]),
+ )
+ ]
+)
+def test_get_partition_layout(monkeypatch, devices):
+ device_to_fdisk_output = {}
+ for device in devices:
+ fdisk_output = [
+ 'Disk {0}: 42.9 GB, 42949672960 bytes, 83886080 sectors'.format(device.name),
+ 'Units = sectors of 1 * {sector_size} = {sector_size} bytes'.format(sector_size=device.sector_size),
+ 'Sector size (logical/physical): 512 bytes / 512 bytes',
+ 'I/O size (minimum/optimal): 512 bytes / 512 bytes',
+ 'Disk label type: dos',
+ 'Disk identifier: 0x0000000da',
+ '',
+ ' Device Boot Start End Blocks Id System',
+ ]
+ for part in device.partitions:
+ part_line = '{0} * {1} 2099199 1048576 83 Linux'.format(part.name, part.start_offset)
+ fdisk_output.append(part_line)
+
+ device_to_fdisk_output[device.name] = fdisk_output
+
+ def mocked_run(cmd, *args, **kwargs):
+ assert cmd[:3] == ['fdisk', '-l', '-u=sectors']
+ device = cmd[3]
+ output = device_to_fdisk_output[device]
+ return {'stdout': output}
+
+ def consume_mocked(*args, **kwargs):
+ yield GrubInfo(orig_devices=[device.name for device in devices])
+
+ monkeypatch.setattr(scan_layout_lib, 'run', mocked_run)
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+ monkeypatch.setattr(api, 'consume', consume_mocked)
+
+ scan_layout_lib.scan_grub_device_partition_layout()
+
+ assert api.produce.called == len(devices)
+
+ dev_name_to_desc = {dev.name: dev for dev in devices}
+
+ for message in api.produce.model_instances:
+ assert isinstance(message, GRUBDevicePartitionLayout)
+ dev = dev_name_to_desc[message.device]
+
+ expected_part_name_to_start = {part.name: part.start_offset*dev.sector_size for part in dev.partitions}
+ actual_part_name_to_start = {part.part_device: part.start_offset for part in message.partitions}
+ assert expected_part_name_to_start == actual_part_name_to_start
diff --git a/repos/system_upgrade/el7toel8/models/partitionlayout.py b/repos/system_upgrade/el7toel8/models/partitionlayout.py
new file mode 100644
index 00000000..c6483283
--- /dev/null
+++ b/repos/system_upgrade/el7toel8/models/partitionlayout.py
@@ -0,0 +1,28 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class PartitionInfo(Model):
+ """
+ Information about a single partition.
+ """
+ topic = SystemInfoTopic
+
+ part_device = fields.String()
+ """ Partition device """
+
+ start_offset = fields.Integer()
+ """ Partition start - offset from the start of the block device in bytes """
+
+
+class GRUBDevicePartitionLayout(Model):
+ """
+ Information about partition layout of a GRUB device.
+ """
+ topic = SystemInfoTopic
+
+ device = fields.String()
+ """ GRUB device """
+
+ partitions = fields.List(fields.Model(PartitionInfo))
+ """ List of partitions present on the device """
--
2.42.0