Skip to content

Commit

Permalink
Update Grub on component drives if /boot is on md device
Browse files Browse the repository at this point in the history
On BIOS systems, previously, if /boot was on md device such as RAID
consisting of multiple partitions on different MBR/GPT partitioned
drives, the part of Grub residing in the 512 Mb after MBR was only
updated for one of the drives. Similar situation occurred on GPT
partitioned drives and the BIOS boot partition. This resulted in
outdated GRUB on the remaining drives which could cause the system to be
unbootable.

Now, Grub is updated on all the component devices of an md array if Grub
was already installed on them before the upgrade.

Jira: OAMG-7835
BZ#2219544
BZ#2140011
  • Loading branch information
matejmatuska committed Jul 17, 2023
1 parent 030e1fc commit c7b1e75
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 52 deletions.
7 changes: 4 additions & 3 deletions repos/system_upgrade/common/actors/checkgrubcore/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def process(self):
grub_info = next(self.consume(GrubInfo), None)
if not grub_info:
raise StopActorExecutionError('Actor did not receive any GrubInfo message.')
if grub_info.orig_device_name:
if grub_info.orig_devices:
create_report([
reporting.Title(
'GRUB2 core will be automatically updated during the upgrade'
Expand All @@ -45,8 +45,9 @@ def process(self):
create_report([
reporting.Title('Leapp could not identify where GRUB2 core is located'),
reporting.Summary(
'We assumed GRUB2 core is located on the same device as /boot, however Leapp could not '
'detect GRUB2 on the device. GRUB2 core needs to be updated maually on legacy (BIOS) systems. '
'We assumed GRUB2 core is located on the same device(s) as /boot, '
'however Leapp could not detect GRUB2 on the device(s). '
'GRUB2 core needs to be updated maually on legacy (BIOS) systems. '
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.BOOT]),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import mock_configs
from leapp.models import FirmwareFacts, GrubInfo
from leapp.reporting import Report

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


def test_actor_update_grub(current_actor_context):
current_actor_context.feed(FirmwareFacts(firmware='bios'))
current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
current_actor_context.run(config_model=mock_configs.CONFIG)
assert current_actor_context.consume(Report)
assert current_actor_context.consume(Report)[0].report['title'].startswith(GRUB)


def test_actor_no_grub_device(current_actor_context):
Expand All @@ -31,6 +30,6 @@ def test_actor_with_efi(current_actor_context):

def test_s390x(current_actor_context):
current_actor_context.feed(FirmwareFacts(firmware='bios'))
current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
assert not current_actor_context.consume(Report)
11 changes: 5 additions & 6 deletions repos/system_upgrade/common/actors/scangrubdevice/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class ScanGrubDeviceName(Actor):
"""
Find the name of the block device where GRUB is located
Find the name of the block devices where GRUB is located
"""

name = 'scan_grub_device_name'
Expand All @@ -19,8 +19,7 @@ def process(self):
if architecture.matches_architecture(architecture.ARCH_S390X):
return

device_name = grub.get_grub_device()
if device_name:
self.produce(GrubInfo(orig_device_name=device_name))
else:
self.produce(GrubInfo())
devices = grub.get_grub_devices()
grub_info = GrubInfo(orig_devices=devices)
grub_info.orig_device_name = devices[0] if len(devices) == 1 else None
self.produce(grub_info)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from leapp.libraries.common import grub
from leapp.libraries.common.config import mock_configs
from leapp.models import GrubInfo


def _get_grub_devices_mocked():
return ['/dev/vda', '/dev/vdb']


def test_actor_scan_grub_device(current_actor_context, monkeypatch):
monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
current_actor_context.run(config_model=mock_configs.CONFIG)
info = current_actor_context.consume(GrubInfo)
assert info and info[0].orig_devices == ['/dev/vda', '/dev/vdb']
assert len(info) == 1, 'Expected just one GrubInfo message'
assert not info[0].orig_device_name


def test_actor_scan_grub_device_one(current_actor_context, monkeypatch):

def _get_grub_devices_mocked():
return ['/dev/vda']

monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
current_actor_context.run(config_model=mock_configs.CONFIG)
info = current_actor_context.consume(GrubInfo)
assert info and info[0].orig_devices == ['/dev/vda']
assert len(info) == 1, 'Expected just one GrubInfo message'
assert info[0].orig_device_name == '/dev/vda'


def test_actor_scan_grub_device_s390x(current_actor_context, monkeypatch):
monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
assert not current_actor_context.consume(GrubInfo)
8 changes: 4 additions & 4 deletions repos/system_upgrade/common/actors/updategrubcore/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class UpdateGrubCore(Actor):
def process(self):
ff = next(self.consume(FirmwareFacts), None)
if ff and ff.firmware == 'bios':
grub_dev = grub.get_grub_device()
if grub_dev:
update_grub_core(grub_dev)
grub_devs = grub.get_grub_devices()
if grub_devs:
update_grub_core(grub_devs)
else:
api.current_logger().warning('Leapp could not detect GRUB on {}'.format(grub_dev))
api.current_logger().warning('Leapp could not detect GRUB devices')
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api, CalledProcessError, config, run


def update_grub_core(grub_dev):
def update_grub_core(grub_devs):
"""
Update GRUB core after upgrade from RHEL7 to RHEL8
On legacy systems, GRUB core does not get automatically updated when GRUB packages
are updated.
"""
cmd = ['grub2-install', grub_dev]
if config.is_debug():
cmd += ['-v']
try:
run(cmd)
except CalledProcessError as err:
reporting.create_report([
reporting.Title('GRUB core update failed'),
reporting.Summary(str(err)),
reporting.Groups([reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.HIGH),
reporting.Remediation(
hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
)
])
api.current_logger().warning('GRUB core update on {} failed'.format(grub_dev))
raise StopActorExecution()

successful = []
failed = []
for dev in grub_devs:
cmd = ['grub2-install', dev]
if config.is_debug():
cmd += ['-v']
try:
run(cmd)
except CalledProcessError as err:
api.current_logger().warning('GRUB core update on {} failed: {}'.format(dev, err))
failed.append(dev)
continue

successful.append(dev)

reporting.create_report([
reporting.Title('GRUB core update failed'),
reporting.Summary('Leapp failed to update GRUB on {}'.format(', '.join(failed))),
reporting.Groups([reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.HIGH),
reporting.Remediation(
hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
)
])

reporting.create_report([
reporting.Title('GRUB core successfully updated'),
reporting.Summary('GRUB core on {} was successfully updated'.format(grub_dev)),
reporting.Summary('GRUB core on {} was successfully updated'.format(', '.join(successful))),
reporting.Groups([reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.INFO)
])
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pytest

from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.actor import updategrubcore
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import api, CalledProcessError
Expand Down Expand Up @@ -32,21 +31,45 @@ def __call__(self, *args):
raise_call_error(args)


def test_update_grub(monkeypatch):
@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
def test_update_grub(monkeypatch, devices):
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
monkeypatch.setattr(updategrubcore, 'run', run_mocked())
updategrubcore.update_grub_core('/dev/vda')
updategrubcore.update_grub_core(devices)
assert reporting.create_report.called
assert UPDATE_OK_TITLE == reporting.create_report.report_fields['title']
assert UPDATE_OK_TITLE == reporting.create_report.reports[1]['title']
assert all(dev in reporting.create_report.reports[1]['summary'] for dev in devices)


def test_update_grub_failed(monkeypatch):
@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
def test_update_grub_failed(monkeypatch, devices):
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
monkeypatch.setattr(updategrubcore, 'run', run_mocked(raise_err=True))
with pytest.raises(StopActorExecution):
updategrubcore.update_grub_core('/dev/vda')
updategrubcore.update_grub_core(devices)
assert reporting.create_report.called
assert UPDATE_FAILED_TITLE == reporting.create_report.report_fields['title']
assert UPDATE_FAILED_TITLE == reporting.create_report.reports[0]['title']
assert all(dev in reporting.create_report.reports[0]['summary'] for dev in devices)


def test_update_grub_success_and_fail(monkeypatch):
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())

def run_mocked(args):
if args == ['grub2-install', '/dev/vdb']:
raise_call_error(args)
else:
assert args == ['grub2-install', '/dev/vda']

monkeypatch.setattr(updategrubcore, 'run', run_mocked)

devices = ['/dev/vda', '/dev/vdb']
updategrubcore.update_grub_core(devices)

assert reporting.create_report.called
assert UPDATE_FAILED_TITLE == reporting.create_report.reports[0]['title']
assert '/dev/vdb' in reporting.create_report.reports[0]['summary']
assert UPDATE_OK_TITLE == reporting.create_report.reports[1]['title']
assert '/dev/vda' in reporting.create_report.reports[1]['summary']


def test_update_grub_negative(current_actor_context):
Expand Down
28 changes: 28 additions & 0 deletions repos/system_upgrade/common/libraries/grub.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os

from leapp.exceptions import StopActorExecution
from leapp.libraries.common import mdraid
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.utils.deprecation import deprecated


def has_grub(blk_dev):
Expand Down Expand Up @@ -59,6 +61,32 @@ def get_boot_partition():
return boot_partition


def get_grub_devices():
"""
Get block devices where GRUB is located. We assume GRUB is on the same device
as /boot partition is. In case that device is an md (Multiple Device) device, all
of the component devices of such a device are considered.
:return: Devices where GRUB is located
:rtype: list
"""
boot_device = get_boot_partition()
devices = []
if mdraid.is_mdraid_dev(boot_device):
component_devs = mdraid.get_component_devices(boot_device)
blk_devs = [blk_dev_from_partition(dev) for dev in component_devs]
# remove duplicates as there might be raid on partitions on the same drive
# even if that's very unusual
devices = sorted(list(set(blk_devs)))
else:
devices.append(blk_dev_from_partition(boot_device))

have_grub = [dev for dev in devices if has_grub(dev)]
api.current_logger().info('GRUB is installed on {}'.format(",".join(have_grub)))
return have_grub


@deprecated(since='2023-06-23', message='This function has been replaced by get_grub_devices')
def get_grub_device():
"""
Get block device where GRUB is located. We assume GRUB is on the same device
Expand Down
48 changes: 48 additions & 0 deletions repos/system_upgrade/common/libraries/mdraid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from leapp.libraries.stdlib import api, CalledProcessError, run


def is_mdraid_dev(dev):
"""
Check if a given device is an md (Multiple Device) device
It is expected that the "mdadm" command is available,
if it's not it is assumed the device is not an md device.
:return: True if the device is an md device, False otherwise
:raises CalledProcessError: If an error occurred
"""
fail_msg = 'Could not check if device "{}" is an md device: {}'
try:
result = run(['mdadm', '--query', dev])
except OSError as err:
api.current_logger().warning(fail_msg.format(dev, err))
return False
except CalledProcessError as err:
err.message = fail_msg.format(dev, err)
raise # let the calling actor handle the exception

return '--detail' in result['stdout']


def get_component_devices(raid_dev):
"""
Get list of component devices in an md (Multiple Device) array
:return: The list of component devices or None in case of error
:raises ValueError: If the device is not an mdraid device
"""
try:
# using both --verbose and --brief for medium verbosity
result = run(['mdadm', '--detail', '--verbose', '--brief', raid_dev])
except (OSError, CalledProcessError) as err:
api.current_logger().warning(
'Could not get md array component devices: {}'.format(err)
)
return None
# example output:
# 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
# devices=/dev/vda1,/dev/vdb1
if 'does not appear to be an md device' in result['stdout']:
raise ValueError("Expected md device, but got: {}".format(raid_dev))

return sorted(result['stdout'].rsplit('=', 2)[-1].strip().split(','))

0 comments on commit c7b1e75

Please sign in to comment.