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..4b8ce82504 --- /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, 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() 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..c9fc2ed1ba --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/libraries/checkgrubenvtofile.py @@ -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 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..27201747c9 --- /dev/null +++ b/repos/system_upgrade/common/actors/cloud/checkgrubenvtofile/tests/test_checkgrubenvtofile.py @@ -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 diff --git a/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py b/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py index 3cd2d8645f..53b1f41a23 100644 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py +++ b/repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py @@ -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) diff --git a/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py b/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py index a4eb6fa19a..61d0cc9ffa 100644 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py +++ b/repos/system_upgrade/common/actors/cloud/checkhybridimage/libraries/checkhybridimage.py @@ -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) @@ -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'] 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 index 16fbb44c37..f9bb54c89a 100644 --- a/repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py +++ b/repos/system_upgrade/common/actors/cloud/checkhybridimage/tests/test_checkhybridimage.py @@ -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. ' WA_AGENT_RPM = RPM( @@ -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 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/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..d00bc44966 --- /dev/null +++ b/repos/system_upgrade/common/models/hybridimage.py @@ -0,0 +1,11 @@ +from leapp.models import Model +from leapp.topics import SystemFactsTopic + + +class HybridImage(Model): + """ + Model used to signify that the system is using a hybrid (BIOS/EFI) images + using BIOS on Azure. + """ + + topic = SystemFactsTopic 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..00406581f5 --- /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, HybridImage +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, HybridImage,) + 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..796c608fae --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/cloud/checkvalidgrubcfghybrid/libraries/checkvalidgrubcfghybrid.py @@ -0,0 +1,27 @@ +from leapp import reporting +from leapp.libraries.stdlib import api +from leapp.models import HybridImage + + +def process(): + hybrid_image = next(api.consume(HybridImage), 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 the system is running on Azure cloud, booted using BIOS In case ' + 'of such a hybrid image scenario and upgrading from older systems (i.e. RHEL 7) ' + '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]), + ]) 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..f98d9be89d --- /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 HybridImage + + +@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 = [HybridImage()] 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..8115198fcd 100644 --- a/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py +++ b/repos/system_upgrade/el8toel9/actors/cloud/ensurevalidgrubcfghybrid/actor.py @@ -8,21 +8,17 @@ 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'