Skip to content

Commit ea6cd79

Browse files
mheckopirat89
authored andcommitted
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
1 parent 6d05575 commit ea6cd79

File tree

7 files changed

+315
-0
lines changed

7 files changed

+315
-0
lines changed
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,52 @@
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+
first_partition = min(grub_dev.partitions, key=lambda partition: partition.start_offset)
20+
if first_partition.start_offset < SAFE_OFFSET_BYTES:
21+
problematic_devices.append(grub_dev.device)
22+
23+
if problematic_devices:
24+
summary = (
25+
'On the system booting by using BIOS, the in-place upgrade fails '
26+
'when upgrading the GRUB2 bootloader if the boot disk\'s embedding area '
27+
'does not contain enough space for the core image installation. '
28+
'This results in a broken system, and can occur when the disk has been '
29+
'partitioned manually, for example using the RHEL 6 fdisk utility.\n\n'
30+
31+
'The list of devices with small embedding area:\n'
32+
'{0}.'
33+
)
34+
problematic_devices_fmt = ['- {0}'.format(dev) for dev in problematic_devices]
35+
36+
hint = (
37+
'We recommend to perform a fresh installation of the RHEL 8 system '
38+
'instead of performing the in-place upgrade.\n'
39+
'Another possibility is to reformat the devices so that there is '
40+
'at least {0} kiB space before the first partition. '
41+
'Note that this operation is not supported and does not have to be '
42+
'always possible.'
43+
)
44+
45+
reporting.create_report([
46+
reporting.Title('Found GRUB devices with too little space reserved before the first partition'),
47+
reporting.Summary(summary.format('\n'.join(problematic_devices_fmt))),
48+
reporting.Remediation(hint=hint.format(SAFE_OFFSET_BYTES // 1024)),
49+
reporting.Severity(reporting.Severity.HIGH),
50+
reporting.Groups([reporting.Groups.BOOT]),
51+
reporting.Groups([reporting.Groups.INHIBITOR]),
52+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1025)])
27+
],
28+
False
29+
),
30+
(
31+
[
32+
GRUBDevicePartitionLayout(device='/dev/vda',
33+
partitions=[PartitionInfo(part_device='/dev/vda1', start_offset=1024*1024)])
34+
],
35+
False
36+
)
37+
]
38+
)
39+
def test_bad_offset_reported(monkeypatch, devices, should_report):
40+
def consume_mocked(model_cls):
41+
if model_cls == FirmwareFacts:
42+
return [FirmwareFacts(firmware='bios')]
43+
return devices
44+
45+
monkeypatch.setattr(api, 'consume', consume_mocked)
46+
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
47+
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
48+
49+
check_first_partition_offset.check_first_partition_offset()
50+
51+
assert bool(reporting.create_report.called) == should_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 = (GrubInfo,)
14+
produces = (GRUBDevicePartitionLayout,)
15+
tags = (FactsPhaseTag, IPUWorkflowTag,)
16+
17+
def process(self):
18+
scan_layout_lib.scan_grub_device_partition_layout()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from leapp.libraries.stdlib import api, CalledProcessError, run
2+
from leapp.models import GRUBDevicePartitionLayout, GrubInfo, PartitionInfo
3+
4+
SAFE_OFFSET_BYTES = 1024*1024 # 1MiB
5+
6+
7+
def split_on_space_segments(line):
8+
fragments = (fragment.strip() for fragment in line.split(' '))
9+
return [fragment for fragment in fragments if fragment]
10+
11+
12+
def get_partition_layout(device):
13+
try:
14+
partition_table = run(['fdisk', '-l', '-u=sectors', device], split=True)['stdout']
15+
except CalledProcessError as err:
16+
# Unlikely - if the disk has no partition table, `fdisk` terminates with 0 (no err). Fdisk exits with an err
17+
# when the device does not exists, or if it is too small to contain a partition table.
18+
19+
err_msg = 'Failed to run `fdisk` to obtain the partition table of the device {0}. Full error: \'{1}\''
20+
api.current_logger().error(err_msg.format(device, str(err)))
21+
return None
22+
23+
table_iter = iter(partition_table)
24+
25+
for line in table_iter:
26+
if not line.startswith('Units'):
27+
# We are still reading general device information and not the table itself
28+
continue
29+
30+
unit = line.split('=')[2].strip() # Contains '512 bytes'
31+
unit = int(unit.split(' ')[0].strip())
32+
break # First line of the partition table header
33+
34+
for line in table_iter:
35+
line = line.strip()
36+
if not line.startswith('Device'):
37+
continue
38+
39+
part_all_attrs = split_on_space_segments(line)
40+
break
41+
42+
partitions = []
43+
for partition_line in table_iter:
44+
# Fields: Device Boot Start End Sectors Size Id Type
45+
# The line looks like: `/dev/vda1 * 2048 2099199 2097152 1G 83 Linux`
46+
part_info = split_on_space_segments(partition_line)
47+
48+
# If the partition is not bootable, the Boot column might be empty
49+
part_device = part_info[0]
50+
part_start = int(part_info[2]) if len(part_info) == len(part_all_attrs) else int(part_info[1])
51+
partitions.append(PartitionInfo(part_device=part_device, start_offset=part_start*unit))
52+
53+
return GRUBDevicePartitionLayout(device=device, partitions=partitions)
54+
55+
56+
def scan_grub_device_partition_layout():
57+
grub_devices = next(api.consume(GrubInfo), None)
58+
if not grub_devices:
59+
return
60+
61+
for device in grub_devices.orig_devices:
62+
dev_info = get_partition_layout(device)
63+
if dev_info:
64+
api.produce(dev_info)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from collections import namedtuple
2+
3+
import pytest
4+
5+
from leapp.libraries.actor import scan_layout as scan_layout_lib
6+
from leapp.libraries.common import grub
7+
from leapp.libraries.common.testutils import create_report_mocked, produce_mocked
8+
from leapp.libraries.stdlib import api
9+
from leapp.models import GRUBDevicePartitionLayout, GrubInfo
10+
from leapp.utils.report import is_inhibitor
11+
12+
Device = namedtuple('Device', ['name', 'partitions', 'sector_size'])
13+
Partition = namedtuple('Partition', ['name', 'start_offset'])
14+
15+
16+
@pytest.mark.parametrize(
17+
'devices',
18+
[
19+
(
20+
Device(name='/dev/vda', sector_size=512,
21+
partitions=[Partition(name='/dev/vda1', start_offset=63),
22+
Partition(name='/dev/vda2', start_offset=1000)]),
23+
Device(name='/dev/vdb', sector_size=1024,
24+
partitions=[Partition(name='/dev/vdb1', start_offset=100),
25+
Partition(name='/dev/vdb2', start_offset=20000)])
26+
),
27+
(
28+
Device(name='/dev/vda', sector_size=512,
29+
partitions=[Partition(name='/dev/vda1', start_offset=111),
30+
Partition(name='/dev/vda2', start_offset=1000)]),
31+
)
32+
]
33+
)
34+
def test_get_partition_layout(monkeypatch, devices):
35+
device_to_fdisk_output = {}
36+
for device in devices:
37+
fdisk_output = [
38+
'Disk {0}: 42.9 GB, 42949672960 bytes, 83886080 sectors'.format(device.name),
39+
'Units = sectors of 1 * {sector_size} = {sector_size} bytes'.format(sector_size=device.sector_size),
40+
'Sector size (logical/physical): 512 bytes / 512 bytes',
41+
'I/O size (minimum/optimal): 512 bytes / 512 bytes',
42+
'Disk label type: dos',
43+
'Disk identifier: 0x0000000da',
44+
'',
45+
' Device Boot Start End Blocks Id System',
46+
]
47+
for part in device.partitions:
48+
part_line = '{0} * {1} 2099199 1048576 83 Linux'.format(part.name, part.start_offset)
49+
fdisk_output.append(part_line)
50+
51+
device_to_fdisk_output[device.name] = fdisk_output
52+
53+
def mocked_run(cmd, *args, **kwargs):
54+
assert cmd[:3] == ['fdisk', '-l', '-u=sectors']
55+
device = cmd[3]
56+
output = device_to_fdisk_output[device]
57+
return {'stdout': output}
58+
59+
def consume_mocked(*args, **kwargs):
60+
yield GrubInfo(orig_devices=[device.name for device in devices])
61+
62+
monkeypatch.setattr(scan_layout_lib, 'run', mocked_run)
63+
monkeypatch.setattr(api, 'produce', produce_mocked())
64+
monkeypatch.setattr(api, 'consume', consume_mocked)
65+
66+
scan_layout_lib.scan_grub_device_partition_layout()
67+
68+
assert api.produce.called == len(devices)
69+
70+
dev_name_to_desc = {dev.name: dev for dev in devices}
71+
72+
for message in api.produce.model_instances:
73+
assert isinstance(message, GRUBDevicePartitionLayout)
74+
dev = dev_name_to_desc[message.device]
75+
76+
expected_part_name_to_start = {part.name: part.start_offset*dev.sector_size for part in dev.partitions}
77+
actual_part_name_to_start = {part.part_device: part.start_offset for part in message.partitions}
78+
assert expected_part_name_to_start == actual_part_name_to_start
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from leapp.models import fields, Model
2+
from leapp.topics import SystemInfoTopic
3+
4+
5+
class PartitionInfo(Model):
6+
"""
7+
Information about a single partition.
8+
"""
9+
topic = SystemInfoTopic
10+
11+
part_device = fields.String()
12+
""" Partition device """
13+
14+
start_offset = fields.Integer()
15+
""" Partition start - offset from the start of the block device in bytes """
16+
17+
18+
class GRUBDevicePartitionLayout(Model):
19+
"""
20+
Information about partition layout of a GRUB device.
21+
"""
22+
topic = SystemInfoTopic
23+
24+
device = fields.String()
25+
""" GRUB device """
26+
27+
partitions = fields.List(fields.Model(PartitionInfo))
28+
""" List of partitions present on the device """

0 commit comments

Comments
 (0)