Skip to content

Commit 867f7f2

Browse files
committed
Update Grub on component devices if /boot is on md device
Previously, if /boot was on md device such as RAID consisting of multiple partitions on different drives, the part of Grub residing in the 512 Mb after MBR was only updated for one of the drives. This resulted in broken Grub. Now, Grub is updated on all the component devices of an md array if Grub was already installed on them before the upgrade.
1 parent 948c782 commit 867f7f2

12 files changed

Lines changed: 296 additions & 52 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def process(self):
3232
grub_info = next(self.consume(GrubInfo), None)
3333
if not grub_info:
3434
raise StopActorExecutionError('Actor did not receive any GrubInfo message.')
35-
if grub_info.orig_device_name:
35+
if grub_info.orig_devices:
3636
create_report([
3737
reporting.Title(
3838
'GRUB2 core will be automatically updated during the upgrade'
@@ -45,8 +45,9 @@ def process(self):
4545
create_report([
4646
reporting.Title('Leapp could not identify where GRUB2 core is located'),
4747
reporting.Summary(
48-
'We assumed GRUB2 core is located on the same device as /boot, however Leapp could not '
49-
'detect GRUB2 on the device. GRUB2 core needs to be updated maually 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 the device(s). '
50+
'GRUB2 core needs to be updated maually on legacy (BIOS) systems. '
5051
),
5152
reporting.Severity(reporting.Severity.HIGH),
5253
reporting.Groups([reporting.Groups.BOOT]),

repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import pytest
2-
3-
from leapp.exceptions import StopActorExecutionError
41
from leapp.libraries.common.config import mock_configs
52
from leapp.models import FirmwareFacts, GrubInfo
63
from leapp.reporting import Report
74

85
NO_GRUB = 'Leapp could not identify where GRUB2 core is located'
6+
GRUB = 'GRUB2 core will be automatically updated during the upgrade'
97

108

119
def test_actor_update_grub(current_actor_context):
1210
current_actor_context.feed(FirmwareFacts(firmware='bios'))
13-
current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
11+
current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
1412
current_actor_context.run(config_model=mock_configs.CONFIG)
1513
assert current_actor_context.consume(Report)
14+
assert current_actor_context.consume(Report)[0].report['title'].startswith(GRUB)
1615

1716

1817
def test_actor_no_grub_device(current_actor_context):
@@ -31,6 +30,6 @@ def test_actor_with_efi(current_actor_context):
3130

3231
def test_s390x(current_actor_context):
3332
current_actor_context.feed(FirmwareFacts(firmware='bios'))
34-
current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
33+
current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
3534
current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
3635
assert not current_actor_context.consume(Report)

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class ScanGrubDeviceName(Actor):
99
"""
10-
Find the name of the block device where GRUB is located
10+
Find the name of the block devices where GRUB is located
1111
"""
1212

1313
name = 'scan_grub_device_name'
@@ -19,8 +19,5 @@ def process(self):
1919
if architecture.matches_architecture(architecture.ARCH_S390X):
2020
return
2121

22-
device_name = grub.get_grub_device()
23-
if device_name:
24-
self.produce(GrubInfo(orig_device_name=device_name))
25-
else:
26-
self.produce(GrubInfo())
22+
devices = grub.get_grub_devices()
23+
self.produce(GrubInfo(orig_devices=devices))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from leapp.libraries.common import grub
2+
from leapp.libraries.common.config import mock_configs
3+
from leapp.models import GrubInfo
4+
5+
6+
def _get_grub_devices_mocked():
7+
return ['/dev/vda', '/dev/vdb']
8+
9+
10+
def test_actor_scan_grub_device(current_actor_context, monkeypatch):
11+
monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
12+
current_actor_context.run(config_model=mock_configs.CONFIG)
13+
info = current_actor_context.consume(GrubInfo)
14+
assert info and info[0].orig_devices == ['/dev/vda', '/dev/vdb']
15+
16+
17+
def test_actor_scan_grub_device_s390x(current_actor_context, monkeypatch):
18+
monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
19+
current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
20+
assert not current_actor_context.consume(GrubInfo)

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class UpdateGrubCore(Actor):
2121
def process(self):
2222
ff = next(self.consume(FirmwareFacts), None)
2323
if ff and ff.firmware == 'bios':
24-
grub_dev = grub.get_grub_device()
25-
if grub_dev:
26-
update_grub_core(grub_dev)
24+
grub_devs = grub.get_grub_devices()
25+
if grub_devs:
26+
update_grub_core(grub_devs)
2727
else:
28-
api.current_logger().warning('Leapp could not detect GRUB on {}'.format(grub_dev))
28+
api.current_logger().warning('Leapp could not detect GRUB devices')
Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,42 @@
11
from leapp import reporting
2-
from leapp.exceptions import StopActorExecution
32
from leapp.libraries.stdlib import api, CalledProcessError, config, run
43

54

6-
def update_grub_core(grub_dev):
5+
def update_grub_core(grub_devs):
76
"""
87
Update GRUB core after upgrade from RHEL7 to RHEL8
98
109
On legacy systems, GRUB core does not get automatically updated when GRUB packages
1110
are updated.
1211
"""
13-
cmd = ['grub2-install', grub_dev]
14-
if config.is_debug():
15-
cmd += ['-v']
16-
try:
17-
run(cmd)
18-
except CalledProcessError as err:
19-
reporting.create_report([
20-
reporting.Title('GRUB core update failed'),
21-
reporting.Summary(str(err)),
22-
reporting.Groups([reporting.Groups.BOOT]),
23-
reporting.Severity(reporting.Severity.HIGH),
24-
reporting.Remediation(
25-
hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
26-
)
27-
])
28-
api.current_logger().warning('GRUB core update on {} failed'.format(grub_dev))
29-
raise StopActorExecution()
12+
13+
successful = []
14+
failed = []
15+
for dev in grub_devs:
16+
cmd = ['grub2-install', dev]
17+
if config.is_debug():
18+
cmd += ['-v']
19+
try:
20+
run(cmd)
21+
except CalledProcessError as err:
22+
api.current_logger().warning('GRUB core update on {} failed: {}'.format(dev, err))
23+
failed.append(dev)
24+
25+
successful.append(dev)
26+
27+
reporting.create_report([
28+
reporting.Title('GRUB core update failed'),
29+
reporting.Summary('Leapp failed to update GRUB on {}'.format(', '.join(failed))),
30+
reporting.Groups([reporting.Groups.BOOT]),
31+
reporting.Severity(reporting.Severity.HIGH),
32+
reporting.Remediation(
33+
hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
34+
)
35+
])
36+
3037
reporting.create_report([
3138
reporting.Title('GRUB core successfully updated'),
32-
reporting.Summary('GRUB core on {} was successfully updated'.format(grub_dev)),
39+
reporting.Summary('GRUB core on {} was successfully updated'.format(', '.join(successful))),
3340
reporting.Groups([reporting.Groups.BOOT]),
3441
reporting.Severity(reporting.Severity.INFO)
3542
])

repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pytest
22

33
from leapp import reporting
4-
from leapp.exceptions import StopActorExecution
54
from leapp.libraries.actor import updategrubcore
65
from leapp.libraries.common import testutils
76
from leapp.libraries.stdlib import api, CalledProcessError
@@ -32,21 +31,24 @@ def __call__(self, *args):
3231
raise_call_error(args)
3332

3433

35-
def test_update_grub(monkeypatch):
34+
@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
35+
def test_update_grub(monkeypatch, devices):
3636
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
3737
monkeypatch.setattr(updategrubcore, 'run', run_mocked())
38-
updategrubcore.update_grub_core('/dev/vda')
38+
updategrubcore.update_grub_core(devices)
3939
assert reporting.create_report.called
40-
assert UPDATE_OK_TITLE == reporting.create_report.report_fields['title']
40+
assert UPDATE_OK_TITLE == reporting.create_report.reports[1]['title']
41+
assert all(dev in reporting.create_report.reports[1]['summary'] for dev in devices)
4142

4243

43-
def test_update_grub_failed(monkeypatch):
44+
@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
45+
def test_update_grub_failed(monkeypatch, devices):
4446
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
4547
monkeypatch.setattr(updategrubcore, 'run', run_mocked(raise_err=True))
46-
with pytest.raises(StopActorExecution):
47-
updategrubcore.update_grub_core('/dev/vda')
48+
updategrubcore.update_grub_core(devices)
4849
assert reporting.create_report.called
49-
assert UPDATE_FAILED_TITLE == reporting.create_report.report_fields['title']
50+
assert UPDATE_FAILED_TITLE == reporting.create_report.reports[0]['title']
51+
assert all(dev in reporting.create_report.reports[0]['summary'] for dev in devices)
5052

5153

5254
def test_update_grub_negative(current_actor_context):

repos/system_upgrade/common/libraries/grub.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22

33
from leapp.exceptions import StopActorExecution
4+
from leapp.libraries.common import mdraid
45
from leapp.libraries.stdlib import api, CalledProcessError, run
6+
from leapp.utils.deprecation import deprecated
57

68

79
def has_grub(blk_dev):
@@ -59,6 +61,32 @@ def get_boot_partition():
5961
return boot_partition
6062

6163

64+
def get_grub_devices():
65+
"""
66+
Get block devices where GRUB is located. We assume GRUB is on the same device
67+
as /boot partition is. In case that device is an md (Multiple Device) device, all
68+
of the component devices of such a device are considered.
69+
70+
:return: Devices where GRUB is located
71+
:rtype: list
72+
"""
73+
boot_device = get_boot_partition()
74+
devices = []
75+
if mdraid.is_mdraid_dev(boot_device):
76+
component_devs = mdraid.get_component_devices(boot_device)
77+
blk_devs = [blk_dev_from_partition(dev) for dev in component_devs]
78+
# remove duplicates as there might be raid on partitions on the same drive
79+
# even if that's very unusual
80+
devices = list(set(blk_devs))
81+
else:
82+
devices.append(blk_dev_from_partition(boot_device))
83+
84+
have_grub = [dev for dev in devices if has_grub(dev)]
85+
api.current_logger().info('GRUB is installed on {}'.format(",".join(have_grub)))
86+
return have_grub
87+
88+
89+
@deprecated(since='2023-06-23', message='This function has been replaced by get_grub_devices')
6290
def get_grub_device():
6391
"""
6492
Get block device where GRUB is located. We assume GRUB is on the same device
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from leapp.exceptions import StopActorExecution
2+
from leapp.libraries.stdlib import api, CalledProcessError, run
3+
4+
5+
def is_mdraid_dev(dev):
6+
"""
7+
Check if a given device is an md (Multiple Device) device
8+
9+
:raises: StopActorExecution in case of error
10+
"""
11+
try:
12+
result = run(['mdadm', '--query', dev])
13+
except CalledProcessError as err:
14+
api.current_logger().warning(
15+
'Could not check if device is an md device: {}'.format(err)
16+
)
17+
raise StopActorExecution()
18+
return '--detail' in result['stdout']
19+
20+
21+
def get_component_devices(raid_dev):
22+
"""
23+
Get list of component devices in an md (Multiple Device) array
24+
25+
:return: The list of component devices or None in case of error
26+
"""
27+
try:
28+
# using both --verbose and --brief for medium verbosity
29+
result = run(['mdadm', '--detail', '--verbose', '--brief', raid_dev])
30+
except CalledProcessError as err:
31+
api.current_logger().warning(
32+
'Could not get md array component devices: {}'.format(err)
33+
)
34+
return None
35+
# example output:
36+
# ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 name=localhost.localdomain:0 UUID=c4acea6e:d56e1598:91822e3f:fb26832c # noqa: E501; pylint: disable=line-too-long
37+
# devices=/dev/vda1,/dev/vdb1
38+
if 'does not appear to be an md device' in result['stdout']:
39+
raise ValueError("Expected md device, but got: {}".format(raid_dev))
40+
41+
return result['stdout'].rsplit('=', 2)[-1].strip().split(',')

0 commit comments

Comments
 (0)