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 and we are running on Hyper-V(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 27, 2024
1 parent cc3fa6f commit 807c270
Show file tree
Hide file tree
Showing 17 changed files with 439 additions and 171 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 ConvertGrubenvTask, FirmwareFacts, HybridImage
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, 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 = (ChecksPhaseTag, 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 ConvertGrubenvTask, FirmwareFacts, HybridImage

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,58 @@
import pytest

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

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)

result = checkgrubenvtofile.is_grubenv_symlink_to_efi()

assert result == (is_symlink and realpath_match)


@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,32 @@
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.libraries.stdlib import api, CalledProcessError, run
from leapp.models import FirmwareFacts, HybridImage, InstalledRPM

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


def is_grubenv_symlink_to_efi():
def check_hybrid_image():
"""
Check whether '/boot/grub2/grubenv' is a relative symlink to
'/boot/efi/EFI/redhat/grubenv'.
Check whether the system is using Azure hybrid image.
"""
return os.path.islink(BIOS_PATH) and os.path.realpath(BIOS_PATH) == os.path.realpath(EFI_PATH)

hybrid_image_condition_1 = is_azure_agent_installed() and is_bios()
hybrid_image_condition_2 = has_efi_partition() and is_bios() and is_running_on_azure_hypervisor()

if any([hybrid_image_condition_1, hybrid_image_condition_2]):
api.produce(HybridImage())


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

src_ver_major = get_source_major_version()

family = rhui.RHUIFamily(rhui.RHUIProvider.AZURE)
Expand All @@ -39,27 +45,40 @@ 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]),
])
def is_running_on_azure_hypervisor():
"""
Check if system is running on Azure hypervisor (Hyper-V)
"""

return detect_virt() == AZURE_HYPERVISOR_ID


def detect_virt():
"""
Detect execution in a virtualized environment
"""

try:
result = run(['systemd-detect-virt'])
except CalledProcessError as e:
api.current_logger().warning('Unable to detect virtualization environment! {}'.format(e))
return ''

return result['stdout']
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from leapp import reporting
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.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked, produce_mocked
from leapp.libraries.stdlib import api, CalledProcessError
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,70 @@
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def test_hybrid_image(monkeypatch, tmpdir):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')
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'}
)

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())
class run_mocked(object):
def __init__(self, hypervisor='', raise_err=False):
self.hypervisor = hypervisor
self.called = 0
self.args = []
self.raise_err = raise_err

def __call__(self, *args): # pylint: disable=inconsistent-return-statements
self.called += 1
self.args.append(args)

if self.raise_err:
raise_call_error(args)

if args[0] == ['systemd-detect-virt']:
return {'stdout': self.hypervisor}


@pytest.mark.parametrize('hypervisor, expected', [('none', False), ('microsoft', True)])
def test_is_running_on_azure_hypervisor(monkeypatch, hypervisor, expected):
monkeypatch.setattr(checkhybridimage, 'run', run_mocked(hypervisor))

assert checkhybridimage.is_running_on_azure_hypervisor() == expected


def test_is_running_on_azure_hypervisor_error(monkeypatch):
monkeypatch.setattr(checkhybridimage, 'run', run_mocked('microsoft', raise_err=True))
monkeypatch.setattr(api, 'current_logger', logger_mocked())

result = checkhybridimage.is_running_on_azure_hypervisor()

assert result is False
assert any('Unable to detect' in msg for msg in api.current_logger.warnmsg)


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

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())
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)
monkeypatch.setattr(checkhybridimage, 'is_running_on_azure_hypervisor', lambda: is_microsoft)

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
Loading

0 comments on commit 807c270

Please sign in to comment.