From 39d83e3f9df2ddeb3cca5f3c9a5c23cda1fa908d Mon Sep 17 00:00:00 2001 From: David Kubek Date: Thu, 22 Aug 2024 11:54:37 +0200 Subject: [PATCH] Restructure hybrid image detection 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 ScanHybridImage 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. --- .../actors/cloud/checkgrubenvtofile/actor.py | 34 +++++ .../libraries/checkgrubenvtofile.py | 44 +++++++ .../tests/test_checkgrubenvtofile.py | 35 +++++ .../actors/cloud/checkhybridimage/actor.py | 24 ---- .../libraries/checkhybridimage.py | 65 --------- .../tests/test_checkhybridimage.py | 82 ------------ .../cloud/convertgrubenvtofile/actor.py | 21 +++ .../libraries/convertgrubenvtofile.py} | 8 ++ .../tests/test_convertgrubenvtofile.py | 51 +++++++ .../actors/cloud/grubenvtofile/actor.py | 28 ---- .../grubenvtofile/tests/test_grubenvtofile.py | 43 ------ .../actors/cloud/scanhybridimage/actor.py | 19 +++ .../libraries/scanhybridimage.py | 102 ++++++++++++++ .../tests/test_scanhybridimage.py | 124 ++++++++++++++++++ repos/system_upgrade/common/models/grubenv.py | 11 +- .../common/models/hybridimage.py | 12 ++ .../cloud/checkvalidgrubcfghybrid/actor.py | 32 +++++ .../libraries/checkvalidgrubcfghybrid.py | 30 +++++ .../tests/test_checkvalidgrubcfghybrid.py | 25 ++++ .../cloud/ensurevalidgrubcfghybrid/actor.py | 18 +-- .../libraries/ensurevalidgrubcfghybrid.py | 4 +- .../tests/test_ensurevalidgrubcfghybrid.py | 8 +- 22 files changed, 555 insertions(+), 265 deletions(-) create mode 100644 repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/actor.py create mode 100644 repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/libraries/checkgrubenvtofile.py create mode 100644 repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py delete mode 100644 repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py delete mode 100644 repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py delete mode 100644 repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py create mode 100644 repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/actor.py rename repos/system_upgrade/common/actors/cloud/{grubenvtofile/libraries/grubenvtofile.py => convertgrubenvtofile/libraries/convertgrubenvtofile.py} (79%) create mode 100644 repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/tests/test_convertgrubenvtofile.py delete mode 100644 repos/system_upgrade/common/actors/cloud/grubenvtofile/actor.py delete mode 100644 repos/system_upgrade/common/actors/cloud/grubenvtofile/tests/test_grubenvtofile.py create mode 100644 repos/system_upgrade/common/actors/cloud/scanhybridimage/actor.py create mode 100644 repos/system_upgrade/common/actors/cloud/scanhybridimage/libraries/scanhybridimage.py create mode 100644 repos/system_upgrade/common/actors/cloud/scanhybridimage/tests/test_scanhybridimage.py create mode 100644 repos/system_upgrade/common/models/hybridimage.py create mode 100644 repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/actor.py create mode 100644 repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/libraries/checkvalidgrubcfghybrid.py create mode 100644 repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/tests/test_checkvalidgrubcfghybrid.py diff --git a/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/actor.py b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/actor.py new file mode 100644 index 0000000000..62ff764432 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/actor.py @@ -0,0 +1,34 @@ +from leapp.actors import Actor +from leapp.libraries.actor import checkgrubenvtofile +from leapp.models import ConvertGrubenvTask, FirmwareFacts, HybridImageAzure +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, HybridImageAzure,) + produces = (ConvertGrubenvTask, Report) + tags = (ChecksPhaseTag, IPUWorkflowTag) + + def process(self): + checkgrubenvtofile.process() diff --git a/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/libraries/checkgrubenvtofile.py b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/libraries/checkgrubenvtofile.py new file mode 100644 index 0000000000..a4c5ee1c09 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/libraries/checkgrubenvtofile.py @@ -0,0 +1,44 @@ +from leapp import reporting +from leapp.libraries.stdlib import api +from leapp.models import ConvertGrubenvTask, FirmwareFacts, HybridImageAzure + + +def process(): + hybrid_image = next(api.consume(HybridImageAzure), None) + + if not hybrid_image: + return + + if not is_bios() or not hybrid_image.grubenv_is_symlink_to_efi: + return + + 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, + reporting.Groups.BOOT + ]), + reporting.RelatedResource('file', '/boot/grub2/grubenv'), + reporting.RelatedResource('file', '/boot/efi/EFI/redhat/grubenv'), + ]) + + 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' diff --git a/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py new file mode 100644 index 0000000000..a5a203fdd5 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py @@ -0,0 +1,35 @@ +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, HybridImageAzure + +BIOS_FIRMWARE = FirmwareFacts(firmware='bios') +EFI_FIRMWARE = FirmwareFacts(firmware='efi') + + +@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] + ([HybridImageAzure(grubenv_is_symlink_to_efi=is_symlink)] if is_hybrid else []) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=msgs)) + monkeypatch.setattr(api, "produce", produce_mocked()) + + 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 diff --git a/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py b/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py deleted file mode 100644 index 3cd2d8645f..0000000000 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py +++ /dev/null @@ -1,24 +0,0 @@ -from leapp.actors import Actor -from leapp.libraries.actor.checkhybridimage import check_hybrid_image -from leapp.models import FirmwareFacts, HybridImage, InstalledRPM -from leapp.reporting import Report -from leapp.tags import ChecksPhaseTag, IPUWorkflowTag - - -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' - consumes = (InstalledRPM, FirmwareFacts) - produces = (HybridImage, Report) - tags = (ChecksPhaseTag, IPUWorkflowTag) - - def process(self): - check_hybrid_image() diff --git a/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py b/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py deleted file mode 100644 index a4eb6fa19a..0000000000 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py +++ /dev/null @@ -1,65 +0,0 @@ -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' - - -def is_grubenv_symlink_to_efi(): - """ - Check whether '/boot/grub2/grubenv' is a relative symlink to - '/boot/efi/EFI/redhat/grubenv'. - """ - 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) - azure_setups = rhui.RHUI_SETUPS.get(family, []) - - agent_pkg = None - for setup in azure_setups: - setup_major_ver = str(setup.os_version[0]) - if setup_major_ver == src_ver_major: - agent_pkg = setup.extra_info.get('agent_pkg') - break - - if not agent_pkg: - return False - - return has_package(InstalledRPM, agent_pkg) - - -def is_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]), - ]) diff --git a/repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py b/repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py deleted file mode 100644 index 16fbb44c37..0000000000 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest - -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 - -RH_PACKAGER = 'Red Hat, Inc. ' -WA_AGENT_RPM = RPM( - name='WALinuxAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch', - pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51' -) -NO_AGENT_RPM = RPM( - name='NoAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch', - pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51' -) - -INSTALLED_AGENT = InstalledRPM(items=[WA_AGENT_RPM]) -NOT_INSTALLED_AGENT = InstalledRPM(items=[NO_AGENT_RPM]) - -BIOS_FIRMWARE = FirmwareFacts(firmware='bios') -EFI_FIRMWARE = FirmwareFacts(firmware='efi') - -BIOS_PATH = '/boot/grub2/grubenv' -EFI_PATH = '/boot/efi/EFI/redhat/grubenv' - - -def test_hybrid_image(monkeypatch, tmpdir): - grubenv_efi = tmpdir.join('grubenv_efi') - grubenv_efi.write('grubenv') - - 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()) - - 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 diff --git a/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/actor.py b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/actor.py new file mode 100644 index 0000000000..68ef54bb9a --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/actor.py @@ -0,0 +1,21 @@ +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() diff --git a/repos/system_upgrade/common/actors/cloud/grubenvtofile/libraries/grubenvtofile.py b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/libraries/convertgrubenvtofile.py similarity index 79% rename from repos/system_upgrade/common/actors/cloud/grubenvtofile/libraries/grubenvtofile.py rename to repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/libraries/convertgrubenvtofile.py index 4d699ec3ff..1803c6c714 100644 --- a/repos/system_upgrade/common/actors/cloud/grubenvtofile/libraries/grubenvtofile.py +++ b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/libraries/convertgrubenvtofile.py @@ -1,9 +1,17 @@ from leapp.libraries.stdlib import api, CalledProcessError, run +from leapp.models import ConvertGrubenvTask 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]) diff --git a/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/tests/test_convertgrubenvtofile.py b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/tests/test_convertgrubenvtofile.py new file mode 100644 index 0000000000..c4534bd62e --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/convertgrubenvtofile/tests/test_convertgrubenvtofile.py @@ -0,0 +1,51 @@ +import pytest + +from leapp.libraries.actor import convertgrubenvtofile +from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api, CalledProcessError +from leapp.models import ConvertGrubenvTask + + +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 run_mocked(object): + def __init__(self, raise_err=False): + self.called = 0 + self.args = [] + self.raise_err = raise_err + + def __call__(self, *args): + self.called += 1 + self.args.append(args) + if self.raise_err: + raise_call_error(args) + + +def test_grubenv_to_file(monkeypatch): + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[ConvertGrubenvTask()])) + monkeypatch.setattr(convertgrubenvtofile, 'run', run_mocked(raise_err=False)) + convertgrubenvtofile.process() + assert convertgrubenvtofile.run.called == 2 + + +def test_no_grubenv_to_file(monkeypatch): + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[])) + monkeypatch.setattr(convertgrubenvtofile, 'run', run_mocked(raise_err=False)) + convertgrubenvtofile.process() + assert convertgrubenvtofile.run.called == 0 + + +def test_fail_grubenv_to_file(monkeypatch): + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[ConvertGrubenvTask()])) + monkeypatch.setattr(convertgrubenvtofile, 'run', run_mocked(raise_err=True)) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + convertgrubenvtofile.grubenv_to_file() + + assert convertgrubenvtofile.run.called == 1 + assert api.current_logger.warnmsg[0].startswith('Could not unlink') diff --git a/repos/system_upgrade/common/actors/cloud/grubenvtofile/actor.py b/repos/system_upgrade/common/actors/cloud/grubenvtofile/actor.py deleted file mode 100644 index fc94219c8e..0000000000 --- a/repos/system_upgrade/common/actors/cloud/grubenvtofile/actor.py +++ /dev/null @@ -1,28 +0,0 @@ -from leapp.actors import Actor -from leapp.libraries.actor.grubenvtofile import grubenv_to_file -from leapp.models import HybridImage -from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag - - -class GrubenvToFile(Actor): - """ - Convert "grubenv" symlink to a regular file 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. This actor converts the symlink to the 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 = 'grubenvtofile' - consumes = (HybridImage,) - produces = () - tags = (FinalizationPhaseTag, IPUWorkflowTag) - - def process(self): - grubenv_msg = next(self.consume(HybridImage), None) - - if grubenv_msg and grubenv_msg.detected: - grubenv_to_file() diff --git a/repos/system_upgrade/common/actors/cloud/grubenvtofile/tests/test_grubenvtofile.py b/repos/system_upgrade/common/actors/cloud/grubenvtofile/tests/test_grubenvtofile.py deleted file mode 100644 index 807f5efa37..0000000000 --- a/repos/system_upgrade/common/actors/cloud/grubenvtofile/tests/test_grubenvtofile.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest - -from leapp.libraries.actor import grubenvtofile -from leapp.libraries.common.testutils import logger_mocked -from leapp.libraries.stdlib import api, CalledProcessError -from leapp.models import HybridImage - - -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 run_mocked(object): - def __init__(self, raise_err=False): - self.called = 0 - self.args = [] - self.raise_err = raise_err - - def __call__(self, *args): - self.called += 1 - self.args.append(args) - if self.raise_err: - raise_call_error(args) - - -def test_grubenv_to_file(monkeypatch): - monkeypatch.setattr(api, 'consume', lambda x: iter([HybridImage()])) - monkeypatch.setattr(grubenvtofile, 'run', run_mocked()) - grubenvtofile.grubenv_to_file() - assert grubenvtofile.run.called == 2 - - -def test_fail_grubenv_to_file(monkeypatch): - monkeypatch.setattr(api, 'consume', lambda x: iter([HybridImage()])) - monkeypatch.setattr(grubenvtofile, 'run', run_mocked(raise_err=True)) - monkeypatch.setattr(api, 'current_logger', logger_mocked()) - grubenvtofile.grubenv_to_file() - assert grubenvtofile.run.called == 1 - assert api.current_logger.warnmsg[0].startswith('Could not unlink') diff --git a/repos/system_upgrade/common/actors/cloud/scanhybridimage/actor.py b/repos/system_upgrade/common/actors/cloud/scanhybridimage/actor.py new file mode 100644 index 0000000000..b1848141d0 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/scanhybridimage/actor.py @@ -0,0 +1,19 @@ +from leapp.actors import Actor +from leapp.libraries.actor.scanhybridimage import scan_hybrid_image +from leapp.models import FirmwareFacts, HybridImageAzure, InstalledRPM +from leapp.reporting import Report +from leapp.tags import FactsPhaseTag, IPUWorkflowTag + + +class ScanHybridImageAzure(Actor): + """ + Check if the system is using Azure hybrid image. + """ + + name = 'scan_hybrid_image_azure' + consumes = (InstalledRPM, FirmwareFacts) + produces = (HybridImageAzure, Report) + tags = (FactsPhaseTag, IPUWorkflowTag) + + def process(self): + scan_hybrid_image() diff --git a/repos/system_upgrade/common/actors/cloud/scanhybridimage/libraries/scanhybridimage.py b/repos/system_upgrade/common/actors/cloud/scanhybridimage/libraries/scanhybridimage.py new file mode 100644 index 0000000000..a37ab415b1 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/scanhybridimage/libraries/scanhybridimage.py @@ -0,0 +1,102 @@ +import os + +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, CalledProcessError, run +from leapp.models import FirmwareFacts, HybridImageAzure, InstalledRPM + +EFI_MOUNTPOINT = '/boot/efi/' +AZURE_HYPERVISOR_ID = 'microsoft' + +GRUBENV_BIOS_PATH = '/boot/grub2/grubenv' +GRUBENV_EFI_PATH = '/boot/efi/EFI/redhat/grubenv' + + +def scan_hybrid_image(): + """ + Check whether the system is using Azure hybrid image. + """ + + 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( + HybridImageAzure( + grubenv_is_symlink_to_efi=is_grubenv_symlink_to_efi() + ) + ) + + +def is_azure_agent_installed(): + """ + Check whether 'WALinuxAgent' package is installed. + """ + + src_ver_major = get_source_major_version() + + family = rhui.RHUIFamily(rhui.RHUIProvider.AZURE) + azure_setups = rhui.RHUI_SETUPS.get(family, []) + + agent_pkg = None + for setup in azure_setups: + setup_major_ver = str(setup.os_version[0]) + if setup_major_ver == src_ver_major: + agent_pkg = setup.extra_info.get('agent_pkg') + break + + if not agent_pkg: + return False + + 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 + """ + + ff = next(api.consume(FirmwareFacts), None) + return ff and ff.firmware == 'bios' + + +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! Error: {}'.format(e)) + return '' + + return result['stdout'] + + +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 diff --git a/repos/system_upgrade/common/actors/cloud/scanhybridimage/tests/test_scanhybridimage.py b/repos/system_upgrade/common/actors/cloud/scanhybridimage/tests/test_scanhybridimage.py new file mode 100644 index 0000000000..a0f6fd4c85 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/scanhybridimage/tests/test_scanhybridimage.py @@ -0,0 +1,124 @@ +import os + +import pytest + +from leapp import reporting +from leapp.libraries.actor import scanhybridimage +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, HybridImageAzure, InstalledRPM, RPM + +RH_PACKAGER = 'Red Hat, Inc. ' +WA_AGENT_RPM = RPM( + name='WALinuxAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch', + pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51' +) +NO_AGENT_RPM = RPM( + name='NoAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch', + pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51' +) + +INSTALLED_AGENT = InstalledRPM(items=[WA_AGENT_RPM]) +NOT_INSTALLED_AGENT = InstalledRPM(items=[NO_AGENT_RPM]) + +BIOS_FIRMWARE = FirmwareFacts(firmware='bios') +EFI_FIRMWARE = FirmwareFacts(firmware='efi') + +BIOS_PATH = '/boot/grub2/grubenv' +EFI_PATH = '/boot/efi/EFI/redhat/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'} + ) + + +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} + + raise AttributeError("Unexpected command supplied!") + + +@pytest.mark.parametrize('hypervisor, expected', [('none', False), ('microsoft', True)]) +def test_is_running_on_azure_hypervisor(monkeypatch, hypervisor, expected): + monkeypatch.setattr(scanhybridimage, 'run', run_mocked(hypervisor)) + + assert scanhybridimage.is_running_on_azure_hypervisor() == expected + + +def test_is_running_on_azure_hypervisor_error(monkeypatch): + monkeypatch.setattr(scanhybridimage, 'run', run_mocked('microsoft', raise_err=True)) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + + result = scanhybridimage.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_symlink', [True, False]) +@pytest.mark.parametrize('realpath_match', [True, False]) +def test_is_grubenv_symlink_to_efi(monkeypatch, is_symlink, realpath_match): + grubenv_efi_false = '/other/grub/grubenv' + + monkeypatch.setattr(scanhybridimage, 'GRUBENV_BIOS_PATH', BIOS_PATH) + monkeypatch.setattr(scanhybridimage, 'GRUBENV_EFI_PATH', EFI_PATH) + + monkeypatch.setattr(os.path, 'islink', lambda path: is_symlink) + + def mocked_realpath(path): + if realpath_match: + return EFI_PATH + + return grubenv_efi_false if path == EFI_PATH else EFI_PATH + + monkeypatch.setattr(os.path, 'realpath', mocked_realpath) + + result = scanhybridimage.is_grubenv_symlink_to_efi() + + assert result == (is_symlink and realpath_match) + + +@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]) +@pytest.mark.parametrize('is_symlink', [True, False]) +def test_hybrid_image(monkeypatch, tmpdir, is_bios, has_efi_partition, agent_installed, is_microsoft, is_symlink): + should_produce = (is_microsoft and is_bios and has_efi_partition) or (agent_installed and is_bios) + + monkeypatch.setattr(reporting, 'create_report', create_report_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(scanhybridimage, 'has_efi_partition', lambda: has_efi_partition) + monkeypatch.setattr(scanhybridimage, 'is_running_on_azure_hypervisor', lambda: is_microsoft) + monkeypatch.setattr(scanhybridimage, 'is_grubenv_symlink_to_efi', lambda: is_symlink) + + scanhybridimage.scan_hybrid_image() + + if should_produce: + assert api.produce.called == 1 + assert HybridImageAzure(grubenv_is_symlink_to_efi=is_symlink) in api.produce.model_instances + else: + assert not api.produce.called diff --git a/repos/system_upgrade/common/models/grubenv.py b/repos/system_upgrade/common/models/grubenv.py index be541131e8..c7f339f101 100644 --- a/repos/system_upgrade/common/models/grubenv.py +++ b/repos/system_upgrade/common/models/grubenv.py @@ -1,12 +1,11 @@ -from leapp.models import fields, Model +from leapp.models import Model from leapp.topics import SystemFactsTopic -class HybridImage(Model): +class ConvertGrubenvTask(Model): """ - Model used for instructing Leapp to convert "grubenv" symlink - into a regular file in case of hybrid (BIOS/EFI) images using BIOS - on Azure. + Model used for instructing Leapp to convert "grubenv" symlink into a + regular file. """ + topic = SystemFactsTopic - detected = fields.Boolean(default=False) diff --git a/repos/system_upgrade/common/models/hybridimage.py b/repos/system_upgrade/common/models/hybridimage.py new file mode 100644 index 0000000000..6cf860efcc --- /dev/null +++ b/repos/system_upgrade/common/models/hybridimage.py @@ -0,0 +1,12 @@ +from leapp.models import fields, Model +from leapp.topics import SystemFactsTopic + + +class HybridImageAzure(Model): + """ + Model used to signify that the system is using a hybrid (BIOS/EFI) images + using BIOS on Azure. + """ + + topic = SystemFactsTopic + grubenv_is_symlink_to_efi = fields.Boolean(default=False) diff --git a/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/actor.py b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/actor.py new file mode 100644 index 0000000000..14668e42f7 --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/actor.py @@ -0,0 +1,32 @@ +from leapp.actors import Actor +from leapp.libraries.actor import checkvalidgrubcfghybrid +from leapp.models import FirmwareFacts, HybridImageAzure +from leapp.reporting import Report +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckValidGrubConfigHybrid(Actor): + """ + Check potential for boot failures in Azure Gen1 VMs due to invalid grubcfg + + This actor addresses the issue where the `/boot/grub2/grub.cfg` file is + overwritten during the upgrade process by an old RHEL7 configuration + leftover on the system, causing the system to fail to boot. + + The problem occurs on hybrid Azure images, which support both UEFI and + Legacy systems. The issue is caused by one of the scriplets in `grub-efi` + which overwrites during the upgrade current configuration in + `/boot/grub2/grub.cfg` by an old configuration from + `/boot/efi/EFI/redhat/grub.cfg`. + + The issue is detected specifically to Azure hybrid cloud systems. + + """ + + name = 'check_valid_grubcfg_hybrid' + consumes = (FirmwareFacts, HybridImageAzure,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag) + + def process(self): + checkvalidgrubcfghybrid.process() diff --git a/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/libraries/checkvalidgrubcfghybrid.py b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/libraries/checkvalidgrubcfghybrid.py new file mode 100644 index 0000000000..374772f5ba --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/libraries/checkvalidgrubcfghybrid.py @@ -0,0 +1,30 @@ +from leapp import reporting +from leapp.libraries.stdlib import api +from leapp.models import HybridImageAzure + + +def process(): + hybrid_image = next(api.consume(HybridImageAzure), None) + + if hybrid_image: + reporting.create_report([ + reporting.Title( + 'Azure hybrid (BIOS/EFI) image detected. The GRUB configuration might be regenerated.' + ), + reporting.Summary( + 'Leapp detected that the system is running on Azure cloud and is booted using BIOS. ' + 'While upgrading from older systems (i.e. RHEL 7) on such systems' + 'it is possible that the system might end up with invalid GRUB configuration, ' + 'as `/boot/grub2/grub.cfg` might be overwritten by an old configuration from ' + '`/boot/efi/EFI/redhat/grub.cfg`, which might cause the system to fail to boot. ' + + 'Please ensure that the system is able to boot with both of these ' + 'configurations. If an invalid configuration is detected during upgrade, ' + 'it will be regenerated automatically using `grub2-mkconfig.`' + ), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([ + reporting.Groups.PUBLIC_CLOUD, + reporting.Groups.BOOT + ]), + ]) diff --git a/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/tests/test_checkvalidgrubcfghybrid.py b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/tests/test_checkvalidgrubcfghybrid.py new file mode 100644 index 0000000000..3fd9a53caa --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/tests/test_checkvalidgrubcfghybrid.py @@ -0,0 +1,25 @@ +import pytest + +from leapp import reporting +from leapp.libraries.actor import checkvalidgrubcfghybrid +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked +from leapp.libraries.stdlib import api +from leapp.models import HybridImageAzure + + +@pytest.mark.parametrize('is_hybrid', [True, False]) +def test_check_invalid_grubcfg_hybrid(monkeypatch, is_hybrid): + + monkeypatch.setattr(reporting, 'create_report', create_report_mocked()) + + msgs = [HybridImageAzure()] if is_hybrid else [] + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=msgs)) + monkeypatch.setattr(api, "produce", produce_mocked()) + + checkvalidgrubcfghybrid.process() + + if is_hybrid: + assert reporting.create_report.called == 1 + assert 'regenerated' in reporting.create_report.report_fields['title'] + else: + assert reporting.create_report.called == 0 diff --git a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py index 68de0433fc..a350c7a005 100644 --- a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py +++ b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py @@ -1,6 +1,6 @@ from leapp.actors import Actor from leapp.libraries.actor import ensurevalidgrubcfghybrid -from leapp.models import HybridImage +from leapp.models import HybridImageAzure from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag @@ -8,25 +8,21 @@ class EnsureValidGrubcfgHybrid(Actor): """ Resolve boot failures in Azure Gen1 VMs during upgrades from RHEL 7 to RHEL 8 to RHEL 9. - This actor addresses the issue where the `/boot/grub2/grub.cfg` file is - overwritten during the upgrade process by an old RHEL7 configuration - leftover on the system, causing the system to fail to boot. - - The problem occurs on hybrid Azure images, which support both UEFI and - Legacy systems and have both `grub-pc` and `grub-efi` packages installed. - It is caused by one of the scriplets in `grub-efi` which overwrites the old - configuration. - If old configuration is detected, this actor regenerates the grub configuration using `grub2-mkconfig -o /boot/grub2/grub.cfg` after installing rpms to ensure the correct boot configuration is in place. + Old configuration is detected by looking for a menuentry corresponding to a + kernel from RHEL 7 which should not be present on RHEL 8 systems. + The fix is applied specifically to Azure hybrid cloud systems. + See also CheckValidGrubConfigHybrid actor. + """ name = 'ensure_valid_grubcfg_hybrid' - consumes = (HybridImage,) + consumes = (HybridImageAzure,) produces = () tags = (ApplicationsPhaseTag, IPUWorkflowTag) diff --git a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/libraries/ensurevalidgrubcfghybrid.py b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/libraries/ensurevalidgrubcfghybrid.py index 127eccfc79..f94cf67b3b 100644 --- a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/libraries/ensurevalidgrubcfghybrid.py +++ b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/libraries/ensurevalidgrubcfghybrid.py @@ -3,7 +3,7 @@ from leapp.exceptions import StopActorExecutionError from leapp.libraries.common.config.architecture import ARCH_ACCEPTED from leapp.libraries.stdlib import api, CalledProcessError, run -from leapp.models import HybridImage +from leapp.models import HybridImageAzure GRUB_CFG_PATH = '/boot/grub2/grub.cfg' @@ -23,7 +23,7 @@ def process(): def _is_hybrid_image(): - return next(api.consume(HybridImage), None) is not None + return next(api.consume(HybridImageAzure), None) is not None def _read_grubcfg(): diff --git a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/tests/test_ensurevalidgrubcfghybrid.py b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/tests/test_ensurevalidgrubcfghybrid.py index c0fb0a0dfe..3ba46cb518 100644 --- a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/tests/test_ensurevalidgrubcfghybrid.py +++ b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/tests/test_ensurevalidgrubcfghybrid.py @@ -6,7 +6,7 @@ from leapp.libraries.actor import ensurevalidgrubcfghybrid from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked from leapp.libraries.stdlib import api, CalledProcessError -from leapp.models import HybridImage +from leapp.models import HybridImageAzure CUR_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -63,7 +63,7 @@ def test_valid_grubcfg(monkeypatch): Test valid configuration does not trigger grub2-mkconfig """ - monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImage()])) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImageAzure()])) monkeypatch.setattr(api, 'current_logger', logger_mocked()) monkeypatch.setattr(ensurevalidgrubcfghybrid, 'run', run_mocked(raise_err=False)) @@ -83,7 +83,7 @@ def test_invalid_grubcfg(monkeypatch): Test invalid configuration triggers grub2-mkconfig """ - monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImage()])) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImageAzure()])) monkeypatch.setattr(api, 'current_logger', logger_mocked()) monkeypatch.setattr(ensurevalidgrubcfghybrid, 'run', run_mocked(raise_err=False)) @@ -104,7 +104,7 @@ def test_run_error(monkeypatch): Test invalid configuration triggers grub2-mkconfig """ - monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImage()])) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[HybridImageAzure()])) monkeypatch.setattr(api, 'current_logger', logger_mocked()) monkeypatch.setattr(ensurevalidgrubcfghybrid, 'run', run_mocked(raise_err=True))