Skip to content

Commit

Permalink
Restructure hybrid image detection
Browse files Browse the repository at this point in the history
Previosly detection of Azure hybrid image was tightly coupled with
process of converting grubenv symlink to a regular file. Since there
exists other issues relating to hybrid images it is worth to separate
these two concepts.

This commit modifies the CheckHybridImage actor so that it produces a
message whel WALinuxAgent is detected or we are booted in bios and ESP
partition is mounted (sign of a hybrid image).

New CheckGrubenvToFile actor is responsible for detection of grubenv
symlink on hybrid images and tasks ConvertGrubenvToFile that is later
responsible for the actual conversion.
  • Loading branch information
dkubek committed Aug 23, 2024
1 parent cc3fa6f commit 92bf4b6
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 161 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from leapp.actors import Actor
from leapp.libraries.actor import checkgrubenvtofile
from leapp.models import HybridImage, ConvertGrubenvTask, FirmwareFacts
from leapp.reporting import Report
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class CheckGrubenvToFile(Actor):
"""
Check whether grubenv is a symlink on Azure hybrid images using BIOS.
Azure images provided by Red Hat aim for hybrid (BIOS/EFI) functionality,
however, currently GRUB is not able to see the "grubenv" file if it is a
symlink to a different partition (default on EFI with grub2-efi pkg
installed) and fails on BIOS systems.
These images have a default relative symlink to EFI partition even when
booted using BIOS and in such cases GRUB is not able to find "grubenv" and
fails to get the kernel cmdline options resulting in system failing to boot
after upgrade.
The symlink needs to be converted to a normal file with the content of
grubenv on the EFI partition in case the system is using BIOS and running
on the Azure cloud. This action is reported in the preupgrade phase.
"""

name = 'check_grubenv_to_file'
consumes = (FirmwareFacts, HybridImage,)
produces = (ConvertGrubenvTask, Report)
tags = (FinalizationPhaseTag, IPUWorkflowTag)

def process(self):
checkgrubenvtofile.process()
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, HybridImage, ConvertGrubenvTask

GRUBENV_BIOS_PATH = '/boot/grub2/grubenv'
GRUBENV_EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def process():
hybrid_image = next(api.consume(HybridImage), None)

if hybrid_image and is_bios() and is_grubenv_symlink_to_efi():
reporting.create_report([
reporting.Title(
'Azure hybrid (BIOS/EFI) image detected. "grubenv" symlink will be converted to a regular file'
),
reporting.Summary(
'Leapp detected the system is running on Azure cloud, booted using BIOS and '
'the "/boot/grub2/grubenv" file is a symlink to "../efi/EFI/redhat/grubenv". In case of such a '
'hybrid image scenario GRUB is not able to locate "grubenv" as it is a symlink to different '
'partition and fails to boot. If the system needs to be run in EFI mode later, please re-create '
'the relative symlink again.'
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.PUBLIC_CLOUD]),
])

api.produce(ConvertGrubenvTask())


def is_bios():
"""
Check whether system is booted into BIOS
"""

ff = next(api.consume(FirmwareFacts), None)
return ff and ff.firmware == 'bios'


def is_grubenv_symlink_to_efi():
"""
Check whether '/boot/grub2/grubenv' is a relative symlink to '/boot/efi/EFI/redhat/grubenv'.
"""

is_symlink = os.path.islink(GRUBENV_BIOS_PATH)
realpaths_match = os.path.realpath(GRUBENV_BIOS_PATH) == os.path.realpath(GRUBENV_EFI_PATH)

return is_symlink and realpaths_match
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest

from leapp import reporting
from leapp.libraries.actor import checkgrubenvtofile
from leapp.libraries.stdlib import api
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.models import HybridImage, FirmwareFacts


BIOS_FIRMWARE = FirmwareFacts(firmware='bios')
EFI_FIRMWARE = FirmwareFacts(firmware='efi')


@pytest.mark.parametrize('is_symlink', [True, False])
@pytest.mark.parametrize('realpath_match', [True, False])
def test_is_grubenv_symlink_to_efi(monkeypatch, tmpdir, is_symlink, realpath_match):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')
grubenv_efi.write('nope')
grubenv_boot = tmpdir.join('grubenv_boot')

grubenv_efi_false = tmpdir.join('grubenv_efi_false')

monkeypatch.setattr(checkgrubenvtofile, 'GRUBENV_BIOS_PATH', grubenv_boot.strpath)
monkeypatch.setattr(checkgrubenvtofile, 'GRUBENV_EFI_PATH', grubenv_efi.strpath)

grubenv_target = grubenv_efi if realpath_match else grubenv_efi_false
if is_symlink:
grubenv_boot.mksymlinkto(grubenv_target)

ans = checkgrubenvtofile.is_grubenv_symlink_to_efi()

Check failure on line 31 in repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py

View workflow job for this annotation

GitHub Actions / Check for spelling errors

ans ==> and

assert ans == (is_symlink and realpath_match)

Check failure on line 33 in repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py

View workflow job for this annotation

GitHub Actions / Check for spelling errors

ans ==> and


@pytest.mark.parametrize('is_hybrid', [True, False])
@pytest.mark.parametrize('is_bios', [True, False])
@pytest.mark.parametrize('is_symlink', [True, False])
def test_check_grubenv_to_file(monkeypatch, tmpdir, is_hybrid, is_bios, is_symlink):

should_report = all([is_hybrid, is_bios, is_symlink])

monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

firmware = BIOS_FIRMWARE if is_bios else EFI_FIRMWARE
msgs = [firmware] + ([HybridImage()] if is_hybrid else [])
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=msgs))
monkeypatch.setattr(api, "produce", produce_mocked())
monkeypatch.setattr(checkgrubenvtofile, "is_grubenv_symlink_to_efi", lambda: is_symlink)

checkgrubenvtofile.process()

if should_report:
assert reporting.create_report.called == 1
assert 'hybrid' in reporting.create_report.report_fields['title']
assert api.produce.called == 1
else:
assert reporting.create_report.called == 0
assert api.produce.called == 0
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@
class CheckHybridImage(Actor):
"""
Check if the system is using Azure hybrid image.
These images have a default relative symlink to EFI
partition even when booted using BIOS and in such cases
GRUB is not able find "grubenv" to get the kernel cmdline
options and fails to boot after upgrade`.
"""

name = 'checkhybridimage'
name = 'check_hybrid_image'
consumes = (InstalledRPM, FirmwareFacts)
produces = (HybridImage, Report)
tags = (ChecksPhaseTag, IPUWorkflowTag)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import os

from leapp import reporting
from leapp.libraries.common import rhui
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, HybridImage, InstalledRPM

BIOS_PATH = '/boot/grub2/grubenv'
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'
EFI_MOUNTPOINT = "/boot/efi/"


def is_grubenv_symlink_to_efi():
def is_azure_agent_installed():
"""
Check whether '/boot/grub2/grubenv' is a relative symlink to
'/boot/efi/EFI/redhat/grubenv'.
Check whether 'WALinuxAgent' package is installed.
"""
return os.path.islink(BIOS_PATH) and os.path.realpath(BIOS_PATH) == os.path.realpath(EFI_PATH)


def is_azure_agent_installed():
"""Check whether 'WALinuxAgent' package is installed."""
src_ver_major = get_source_major_version()

family = rhui.RHUIFamily(rhui.RHUIProvider.AZURE)
Expand All @@ -39,27 +32,27 @@ def is_azure_agent_installed():
return has_package(InstalledRPM, agent_pkg)


def has_efi_partition():
"""
Check whether ESP partition exists and is mounted.
"""

return os.path.exists(EFI_MOUNTPOINT) and os.path.ismount(EFI_MOUNTPOINT)


def is_bios():
"""Check whether system is booted into BIOS"""
"""
Check whether system is booted into BIOS
"""

ff = next(api.consume(FirmwareFacts), None)
return ff and ff.firmware == 'bios'


def check_hybrid_image():
"""Check whether the system is using Azure hybrid image."""
if all([is_grubenv_symlink_to_efi(), is_azure_agent_installed(), is_bios()]):
api.produce(HybridImage(detected=True))
reporting.create_report([
reporting.Title(
'Azure hybrid (BIOS/EFI) image detected. "grubenv" symlink will be converted to a regular file'
),
reporting.Summary(
'Leapp detected the system is running on Azure cloud, booted using BIOS and '
'the "/boot/grub2/grubenv" file is a symlink to "../efi/EFI/redhat/grubenv". In case of such a '
'hybrid image scenario GRUB is not able to locate "grubenv" as it is a symlink to different '
'partition and fails to boot. If the system needs to be run in EFI mode later, please re-create '
'the relative symlink again.'
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.PUBLIC_CLOUD]),
])
"""
Check whether the system is using Azure hybrid image.
"""

if any([is_azure_agent_installed(), has_efi_partition() and is_bios()]):
api.produce(HybridImage())
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from leapp.libraries.actor import checkhybridimage
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, InstalledRPM, RPM
from leapp.reporting import Report
from leapp.models import FirmwareFacts, HybridImage, InstalledRPM, RPM

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'
WA_AGENT_RPM = RPM(
Expand All @@ -27,56 +26,25 @@
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def test_hybrid_image(monkeypatch, tmpdir):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')
@pytest.mark.parametrize('is_bios', [True, False])
@pytest.mark.parametrize('has_efi_partition', [True, False])
@pytest.mark.parametrize('agent_installed', [True, False])
def test_hybrid_image(monkeypatch, tmpdir, is_bios, has_efi_partition, agent_installed):
should_produce = agent_installed or (has_efi_partition and is_bios)

grubenv_boot = tmpdir.join('grubenv_boot')
grubenv_boot.mksymlinkto('grubenv_efi')

monkeypatch.setattr(checkhybridimage, 'BIOS_PATH', grubenv_boot.strpath)
monkeypatch.setattr(checkhybridimage, 'EFI_PATH', grubenv_efi.strpath)
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(
api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[BIOS_FIRMWARE, INSTALLED_AGENT])
)
monkeypatch.setattr(api, "produce", produce_mocked())
msgs = [
BIOS_FIRMWARE if is_bios else EFI_FIRMWARE,
INSTALLED_AGENT if agent_installed else NOT_INSTALLED_AGENT
]
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(checkhybridimage, 'has_efi_partition', lambda: has_efi_partition)

checkhybridimage.check_hybrid_image()
assert reporting.create_report.called == 1
assert 'hybrid' in reporting.create_report.report_fields['title']
assert api.produce.called == 1


@pytest.mark.parametrize('is_symlink, realpath_match, is_bios, agent_installed', [
(False, True, True, True),
(True, False, True, True),
(True, True, False, True),
(True, True, True, False),
])
def test_no_hybrid_image(monkeypatch, is_symlink, realpath_match, is_bios, agent_installed, tmpdir):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')
grubenv_efi_false = tmpdir.join('grubenv_efi_false')
grubenv_efi.write('nope')
grubenv_boot = tmpdir.join('grubenv_boot')

grubenv_target = grubenv_efi if realpath_match else grubenv_efi_false

if is_symlink:
grubenv_boot.mksymlinkto(grubenv_target)

firmw = BIOS_FIRMWARE if is_bios else EFI_FIRMWARE
inst_rpms = INSTALLED_AGENT if agent_installed else NOT_INSTALLED_AGENT

monkeypatch.setattr(checkhybridimage, 'BIOS_PATH', grubenv_boot.strpath)
monkeypatch.setattr(checkhybridimage, 'EFI_PATH', grubenv_efi.strpath)
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(
api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[firmw, inst_rpms])
)
monkeypatch.setattr(api, "produce", produce_mocked())

checkhybridimage.check_hybrid_image()
assert not reporting.create_report.called
assert not api.produce.called
if should_produce:
assert api.produce.called == 1
assert HybridImage() in api.produce.model_instances
else:
assert not api.produce.called
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from leapp.actors import Actor
from leapp.libraries.actor import convertgrubenvtofile
from leapp.models import ConvertGrubenvTask
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class ConvertGrubenvToFile(Actor):
"""
Convert "grubenv" symlink to a regular file on Azure hybrid images using BIOS.
For more information see CheckGrubenvToFile actor.
"""

name = 'convert_grubenv_to_file'
consumes = (ConvertGrubenvTask,)
produces = ()
tags = (FinalizationPhaseTag, IPUWorkflowTag)

def process(self):
convertgrubenvtofile.process()
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
from leapp.models import ConvertGrubenvTask
from leapp.libraries.stdlib import api, CalledProcessError, run

BIOS_PATH = '/boot/grub2/grubenv'
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def process():
convert_grubenv_task = next(api.consume(ConvertGrubenvTask), None)

if convert_grubenv_task:
grubenv_to_file()


def grubenv_to_file():
try:
run(['unlink', BIOS_PATH])
Expand Down
Loading

0 comments on commit 92bf4b6

Please sign in to comment.