Skip to content

Commit

Permalink
Update Grub on component devices if /boot is on md device
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matejmatuska committed Jun 24, 2023
1 parent 948c782 commit b37fb06
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 38 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
9 changes: 3 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,5 @@ 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()
self.produce(GrubInfo(orig_devices=devices))
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
Expand Up @@ -3,33 +3,39 @@
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 = []
for dev in grub_devs:
cmd = ['grub2-install', 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(dev))
raise StopActorExecution()

successful.append(dev)

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)
])
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 = 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
41 changes: 41 additions & 0 deletions repos/system_upgrade/common/libraries/mdraid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api, CalledProcessError, run


def is_mdraid_dev(dev):
"""
Check if a given device is an md (Multiple Device) device
:raises: StopActorExecution in case of error
"""
try:
result = run(['mdadm', '--query', dev])
except CalledProcessError as err:
api.current_logger().warning(
'Could not check if device is an md device: {}'.format(err)
)
raise StopActorExecution()
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
"""
try:
# using both --verbose and --brief for medium verbosity
result = run(['mdadm', '--detail', '--verbose', '--brief', raid_dev])
except 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 result['stdout'].rsplit('=', 2)[-1].strip().split(',')
64 changes: 58 additions & 6 deletions repos/system_upgrade/common/libraries/tests/test_grub.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import pytest

from leapp.exceptions import StopActorExecution
from leapp.libraries.common import grub
from leapp.libraries.common import grub, mdraid
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import DefaultGrub, DefaultGrubInfo

BOOT_PARTITION = '/dev/vda1'
BOOT_DEVICE = '/dev/vda'

MD_BOOT_DEVICE = '/dev/md0'
MD_BOOT_REAL_DEVICES = ['/dev/sda', '/dev/sdb']

VALID_DD = b'GRUB GeomHard DiskRead Error'
INVALID_DD = b'Nothing to see here!'

Expand All @@ -27,10 +30,11 @@ def raise_call_error(args=None):

class RunMocked(object):

def __init__(self, raise_err=False):
def __init__(self, raise_err=False, boot_on_raid=False):
self.called = 0
self.args = None
self.raise_err = raise_err
self.boot_on_raid = boot_on_raid

def __call__(self, args, encoding=None):
self.called += 1
Expand All @@ -39,18 +43,24 @@ def __call__(self, args, encoding=None):
raise_call_error(args)

if self.args == ['grub2-probe', '--target=device', '/boot']:
stdout = BOOT_PARTITION
stdout = MD_BOOT_DEVICE if self.boot_on_raid else BOOT_PARTITION

elif self.args == ['lsblk', '-spnlo', 'name', BOOT_PARTITION]:
stdout = BOOT_DEVICE
elif self.args == ['lsblk', '-spnlo', 'name', '/dev/sda1']:
stdout = '/dev/sda'
elif self.args == ['lsblk', '-spnlo', 'name', '/dev/sdb1']:
stdout = '/dev/sdb'

return {'stdout': stdout}


def open_mocked(fn, flags):
return open(
os.path.join(CUR_DIR, 'grub_valid') if fn == BOOT_DEVICE else os.path.join(CUR_DIR, 'grub_invalid'), 'r'
)
if fn == BOOT_DEVICE or fn in MD_BOOT_REAL_DEVICES:
path = os.path.join(CUR_DIR, 'grub_valid')
else:
path = os.path.join(CUR_DIR, 'grub_invalid')
return open(path, 'r')


def open_invalid(fn, flags):
Expand Down Expand Up @@ -122,3 +132,45 @@ def test_is_blscfg_library(monkeypatch, enabled):
assert result
else:
assert not result


def is_mdraid_dev_mocked(dev):
return dev == '/dev/md0'


def test_get_grub_devices_one_device(monkeypatch):
run_mocked = RunMocked()
monkeypatch.setattr(grub, 'run', run_mocked)
monkeypatch.setattr(os, 'open', open_mocked)
monkeypatch.setattr(os, 'read', read_mocked)
monkeypatch.setattr(os, 'close', close_mocked)
monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(mdraid, 'is_mdraid_dev', is_mdraid_dev_mocked)

result = grub.get_grub_devices()
assert grub.run.called == 2
assert [BOOT_DEVICE] == result
assert not api.current_logger.warnmsg
assert 'GRUB is installed on {}'.format(",".join(result)) in api.current_logger.infomsg


def test_get_grub_devices_raid_device(monkeypatch):
run_mocked = RunMocked(boot_on_raid=True)
monkeypatch.setattr(grub, 'run', run_mocked)
monkeypatch.setattr(os, 'open', open_mocked)
monkeypatch.setattr(os, 'read', read_mocked)
monkeypatch.setattr(os, 'close', close_mocked)
monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(mdraid, 'is_mdraid_dev', is_mdraid_dev_mocked)

def get_component_devices_mocked(raid_dev):
assert raid_dev == MD_BOOT_DEVICE
return ['/dev/sda1', '/dev/sdb1']

monkeypatch.setattr(mdraid, 'get_component_devices', get_component_devices_mocked)

result = grub.get_grub_devices()
assert grub.run.called == 3 # grub2-probe + 2x lsblk
assert ['/dev/sda', '/dev/sdb'] == result
assert not api.current_logger.warnmsg
assert 'GRUB is installed on {}'.format(",".join(result)) in api.current_logger.infomsg
78 changes: 78 additions & 0 deletions repos/system_upgrade/common/libraries/tests/test_mdraid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os

import pytest

from leapp.libraries.common import mdraid
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError

MD_DEVICE = '/dev/md0'
NOT_MD_DEVICE = '/dev/sda'

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def raise_call_error(args=None):
raise CalledProcessError(
message='A Leapp Command Error occurred.',
command=args,
result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
)


class RunMocked(object):

def __init__(self, raise_err=False):
self.called = 0
self.args = None
self.raise_err = raise_err

def __call__(self, args, encoding=None):
self.called += 1
self.args = args
if self.raise_err:
raise_call_error(args)

if self.args == ['mdadm', '--query', MD_DEVICE]:
stdout = '/dev/md0: 1022.00MiB raid1 2 devices, 0 spares. Use mdadm --detail for more detail.'
elif self.args == ['mdadm', '--query', NOT_MD_DEVICE]:
stdout = '/dev/sda: is not an md array'

elif self.args == ['mdadm', '--detail', '--verbose', '--brief', MD_DEVICE]:
stdout = 'ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 name=localhost.localdomain:0 UUID=c4acea6e:d56e1598:91822e3f:fb26832c\n devices=/dev/sda1,/dev/sdb1' # noqa: E501; pylint: disable=line-too-long
elif self.args == ['mdadm', '--detail', '--verbose', '--brief', NOT_MD_DEVICE]:
stdout = 'mdadm: /dev/sda does not appear to be an md device'

return {'stdout': stdout}


@pytest.mark.parametrize('dev,expected', [(MD_DEVICE, True), (NOT_MD_DEVICE, False)])
def test_is_mdraid_dev(monkeypatch, dev, expected):
run_mocked = RunMocked()
monkeypatch.setattr(mdraid, 'run', run_mocked)
monkeypatch.setattr(api, 'current_logger', logger_mocked())

result = mdraid.is_mdraid_dev(dev)
assert mdraid.run.called == 1
assert expected == result
assert not api.current_logger.warnmsg


def test_get_component_devices_ok(monkeypatch):
run_mocked = RunMocked()
monkeypatch.setattr(mdraid, 'run', run_mocked)
monkeypatch.setattr(api, 'current_logger', logger_mocked())

result = mdraid.get_component_devices(MD_DEVICE)
assert mdraid.run.called == 1
assert ['/dev/sda1', '/dev/sdb1'] == result
assert not api.current_logger.warnmsg


def test_get_component_devices_not_md_device(monkeypatch):
run_mocked = RunMocked()
monkeypatch.setattr(mdraid, 'run', run_mocked)

with pytest.raises(ValueError):
mdraid.get_component_devices(NOT_MD_DEVICE)
assert mdraid.run.called == 1
Loading

0 comments on commit b37fb06

Please sign in to comment.