diff --git a/commands/command_utils.py b/commands/command_utils.py index 190f5f0339..84b9de1b1e 100644 --- a/commands/command_utils.py +++ b/commands/command_utils.py @@ -28,7 +28,10 @@ def check_version(version): :return: release tuple """ if not re.match(VERSION_REGEX, version): - raise CommandError('Unexpected format of target version: {}'.format(version)) + raise CommandError( + "Unexpected format of target version: {}. " + "The required format is 'X.Y' (major and minor version).".format(version) + ) return version.split('.') @@ -126,7 +129,6 @@ def vet_upgrade_path(args): Make sure the user requested upgrade_path is a supported one. If LEAPP_DEVEL_TARGET_RELEASE is set then it's value is not vetted against upgrade_paths_map but used as is. - :raises: `CommandError` if the specified upgrade_path is not supported :return: `tuple` (target_release, flavor) """ flavor = get_upgrade_flavour() @@ -135,13 +137,6 @@ def vet_upgrade_path(args): check_version(env_version_override) return (env_version_override, flavor) target_release = args.target or get_target_version(flavor) - supported_target_versions = get_supported_target_versions(flavor) - if target_release not in supported_target_versions: - raise CommandError( - "Upgrade to {to} for {flavor} upgrade path is not supported, possible choices are {choices}".format( - to=target_release, - flavor=flavor, - choices=','.join(supported_target_versions))) return (target_release, flavor) diff --git a/commands/preupgrade/__init__.py b/commands/preupgrade/__init__.py index 631eca6bd1..c1fabbbd95 100644 --- a/commands/preupgrade/__init__.py +++ b/commands/preupgrade/__init__.py @@ -28,8 +28,7 @@ choices=['ga', 'e4s', 'eus', 'aus'], value_type=str.lower) # This allows the choices to be case insensitive @command_opt('iso', help='Use provided target RHEL installation image to perform the in-place upgrade.') -@command_opt('target', choices=command_utils.get_supported_target_versions(), - help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( +@command_opt('target', help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( command_utils.get_upgrade_flavour())) @command_opt('report-schema', help='Specify report schema version for leapp-report.json', choices=['1.0.0', '1.1.0', '1.2.0'], default=get_config().get('report', 'schema')) diff --git a/commands/upgrade/__init__.py b/commands/upgrade/__init__.py index 3dedd43802..608099acfd 100644 --- a/commands/upgrade/__init__.py +++ b/commands/upgrade/__init__.py @@ -34,8 +34,7 @@ choices=['ga', 'e4s', 'eus', 'aus'], value_type=str.lower) # This allows the choices to be case insensitive @command_opt('iso', help='Use provided target RHEL installation image to perform the in-place upgrade.') -@command_opt('target', choices=command_utils.get_supported_target_versions(), - help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( +@command_opt('target', help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( command_utils.get_upgrade_flavour())) @command_opt('report-schema', help='Specify report schema version for leapp-report.json', choices=['1.0.0', '1.1.0', '1.2.0'], default=get_config().get('report', 'schema')) diff --git a/repos/system_upgrade/common/actors/checktargetversion/actor.py b/repos/system_upgrade/common/actors/checktargetversion/actor.py new file mode 100644 index 0000000000..291ce3da3e --- /dev/null +++ b/repos/system_upgrade/common/actors/checktargetversion/actor.py @@ -0,0 +1,22 @@ +from leapp.actors import Actor +from leapp.libraries.actor import checktargetversion +from leapp.models import IPUPaths +from leapp.reporting import Report +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckTargetVersion(Actor): + """ + Check that the target system version is supported by the upgrade process. + + Invoke inhibitor if the target system is not supported. + Allow unsupported target if `LEAPP_UNSUPPORTED=1` is set. + """ + + name = 'check_target_version' + consumes = (IPUPaths,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag) + + def process(self): + checktargetversion.process() diff --git a/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py new file mode 100644 index 0000000000..e4f7847fad --- /dev/null +++ b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py @@ -0,0 +1,84 @@ +from leapp import reporting +from leapp.exceptions import StopActorExecutionError +from leapp.libraries.common.config import get_env, version +from leapp.libraries.stdlib import api +from leapp.models import IPUPaths + +FMT_LIST_SEPARATOR = '\n - ' + + +def get_supported_target_versions(): + ipu_paths = next(api.consume(IPUPaths), None) + src_version = version.get_source_version() + if not ipu_paths: + # NOTE: missing unit-tests. Unexpected situation and the solution + # is possibly temporary + raise StopActorExecutionError('Missing the IPUPaths message. Cannot determine defined upgrade paths.') + for ipu_path in ipu_paths.data: + if ipu_path.source_version == src_version: + return ipu_path.target_versions + + # Nothing discovered. Current src_version is not already supported or not yet. + # Problem of supported source versions is handled now separately in other + # actors. Fallbak from X.Y versioning to major version only. + api.current_logger().warning( + 'Cannot discover support upgrade path for this system release: {}' + .format(src_version) + ) + maj_version = version.get_source_major_version() + for ipu_path in ipu_paths.data: + if ipu_path.source_version == maj_version: + return ipu_path.target_versions + + # Completely unknown + api.current_logger().warning( + 'Cannot discover supported upgrade path for this system major version: {}' + .format(maj_version) + ) + return [] + + +def process(): + target_version = version.get_target_version() + supported_target_versions = get_supported_target_versions() + + if target_version in supported_target_versions: + api.current_logger().info('Target version is supported. Continue.') + return + + if get_env('LEAPP_UNSUPPORTED', '0') == '1': + api.current_logger().warning( + 'Upgrading to an unsupported version of the target system but LEAPP_UNSUPPORTED=1. Continue.' + ) + return + + # inhibit the upgrade - unsupported target and leapp running in production mode + hint = ( + 'Choose a supported version of the target OS for the upgrade.' + ' Alternatively, if you require to upgrade using an unsupported upgrade path,' + ' set the `LEAPP_UNSUPPORTED=1` environment variable to confirm you' + ' want to upgrade on your own risk.' + ) + + reporting.create_report([ + reporting.Title('Specified version of the target system is not supported'), + reporting.Summary( + 'The in-place upgrade to the specified version ({tgt_ver}) of the target system' + ' is not supported from the current system version. Follow the official' + ' documentation for up to date information about supported upgrade' + ' paths and future plans (see the attached link).' + ' The in-place upgrade is enabled to the following versions of the target system:{sep}{ver_list}' + .format( + sep=FMT_LIST_SEPARATOR, + ver_list=FMT_LIST_SEPARATOR.join(supported_target_versions), + tgt_ver=target_version + ) + ), + reporting.Groups([reporting.Groups.INHIBITOR]), + reporting.Severity(reporting.Severity.HIGH), + reporting.Remediation(hint=hint), + reporting.ExternalLink( + url='https://access.redhat.com/articles/4263361', + title='Supported in-place upgrade paths for Red Hat Enterprise Linux' + ) + ]) diff --git a/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py b/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py new file mode 100644 index 0000000000..8978251c4c --- /dev/null +++ b/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py @@ -0,0 +1,84 @@ +import os + +import pytest + +from leapp import reporting +from leapp.libraries.actor import checktargetversion +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api +from leapp.models import IPUPath, IPUPaths + +_UPGRADE_PATHS = IPUPaths(data=[ + IPUPath(source_version='7.9', target_versions=['8.10']), + IPUPath(source_version='8.10', target_versions=['9.4', '9.5', '9.6']), + IPUPath(source_version='9.6', target_versions=['10.0']), + IPUPath(source_version='7', target_versions=['8.10']), + IPUPath(source_version='8', target_versions=['9.4', '9.5', '9.6']), + IPUPath(source_version='9', target_versions=['10.0']) +]) + + +@pytest.fixture +def setup_monkeypatch(monkeypatch): + """Fixture to set up common monkeypatches.""" + + def _setup(source_version, target_version, leapp_unsupported='0'): + curr_actor_mocked = CurrentActorMocked( + src_ver=source_version, + dst_ver=target_version, + envars={'LEAPP_UNSUPPORTED': leapp_unsupported}, + msgs=[_UPGRADE_PATHS] + ) + monkeypatch.setattr(api, 'current_actor', curr_actor_mocked) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr(reporting, 'create_report', create_report_mocked()) + return _setup + + +@pytest.mark.parametrize(('source_version', 'target_version', 'leapp_unsupported'), [ + # LEAPP_UNSUPPORTED=0 + ('7.9', '9.0', '0'), + ('8.10', '9.0', '0'), + ('9.6', '10.1', '0'), + ('7', '9.0', '0'), + ('8', '9.0', '0'), + ('9', '10.1', '0'), + # LEAPP_UNSUPPORTED=1 + ('7.9', '9.0', '1'), + ('8.10', '9.0', '1'), + ('9.6', '10.1', '1'), + ('7', '9.0', '1'), + ('8', '9.0', '1'), + ('9', '10.1', '1'), +]) +def test_unsuppoted_paths(setup_monkeypatch, source_version, target_version, leapp_unsupported): + setup_monkeypatch(source_version, target_version, leapp_unsupported) + + if leapp_unsupported == '1': + checktargetversion.process() + assert reporting.create_report.called == 0 + assert api.current_logger.warnmsg + else: + checktargetversion.process() + assert reporting.create_report.called == 1 + assert 'inhibitor' in reporting.create_report.report_fields['groups'] + + +@pytest.mark.parametrize(('source_version', 'target_version'), [ + ('7.9', '8.10'), + ('8.10', '9.4'), + ('8.10', '9.5'), + ('8.10', '9.6'), + ('9.6', '10.0'), + ('7', '8.10'), + ('8', '9.4'), + ('8', '9.5'), + ('8', '9.6'), + ('9', '10.0'), +]) +def test_supported_paths(setup_monkeypatch, source_version, target_version): + setup_monkeypatch(source_version, target_version, leapp_unsupported='0') + + checktargetversion.process() + assert reporting.create_report.called == 0 + assert api.current_logger.infomsg diff --git a/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py b/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py index 9e213f644c..749b3347f5 100644 --- a/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py +++ b/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py @@ -64,17 +64,41 @@ def get_os_release(path): details={'details': str(e)}) +def check_target_major_version(curr_version, target_version): + required_major_version = int(curr_version.split('.')[0]) + 1 + specified_major_version = int(target_version.split('.')[0]) + if specified_major_version != required_major_version: + raise StopActorExecutionError( + message='Specified invalid major version of the target system', + details={ + 'Specified target major version': str(specified_major_version), + 'Required target major version': str(required_major_version), + 'hint': ( + 'The in-place upgrade is possible only to the next system' + ' major version: {ver}. Specify a valid version of the' + ' target system when running leapp.' + ' For more information about supported in-place upgrade paths' + ' follow: https://access.redhat.com/articles/4263361' + .format(ver=required_major_version) + ) + } + ) + + def produce_ipu_config(actor): flavour = os.environ.get('LEAPP_UPGRADE_PATH_FLAVOUR') target_version = os.environ.get('LEAPP_UPGRADE_PATH_TARGET_RELEASE') os_release = get_os_release('/etc/os-release') + source_version = os_release.version_id + + check_target_major_version(source_version, target_version) actor.produce(IPUConfig( leapp_env_vars=get_env_vars(), os_release=os_release, architecture=platform.machine(), version=Version( - source=os_release.version_id, + source=source_version, target=target_version ), kernel=get_booted_kernel(),