Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify supported target OS version in actors #1328

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions commands/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('.')


Expand Down Expand Up @@ -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()
Expand All @@ -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)


Expand Down
3 changes: 1 addition & 2 deletions commands/preupgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
3 changes: 1 addition & 2 deletions commands/upgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
22 changes: 22 additions & 0 deletions repos/system_upgrade/common/actors/checktargetversion/actor.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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
from leapp.utils.deprecation import suppress_deprecation

FMT_LIST_SEPARATOR = '\n - '


@suppress_deprecation(IPUPaths)
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'
)
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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
from leapp.utils.deprecation import suppress_deprecation
from leapp.utils.report import is_inhibitor


# It must be in a function so we can suppress the deprecation warning in tests.
@suppress_deprecation(IPUPaths)
def _get_upgrade_paths_data():
return 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=[_get_upgrade_paths_data()]
)
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 is_inhibitor(reporting.create_report.report_fields)


@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
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
31 changes: 31 additions & 0 deletions repos/system_upgrade/common/actors/scandefinedipupaths/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from leapp.actors import Actor
from leapp.libraries.actor import scandefinedipupaths
from leapp.models import IPUPaths
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanDefinedIPUPaths(Actor):
"""
Load defined IPU paths for the current major source system version
and defined upgrade flavour.

The upgrade paths are defined inside `files/upgrade_paths.json`.
Based on the defined upgrade flavour (default, saphana, ..) loads particular
definitions and filter out all upgrade paths from other system major versions.
I.e. for RHEL 8.10 system with the default upgrade flavour, load all upgrade
paths from any RHEL 8 system defined under the 'default' flavour.

The code is mostly taken from the CLI command_utils. The duplicate solution
is not so problematic now as it will be unified next time.

Note the deprecation suppression is expected here as this is considered as
temporary solution now.
"""

name = 'scan_defined_ipu_paths'
consumes = ()
produces = (IPUPaths,)
tags = (IPUWorkflowTag, FactsPhaseTag)

def process(self):
scandefinedipupaths.process()
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import json

from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api
from leapp.models import IPUPath, IPUPaths
from leapp.utils.deprecation import suppress_deprecation


def load_ipu_paths_for_flavour(flavour, _filename='upgrade_paths.json'):
"""
Load defined IPU paths from the upgrade_paths.json file for the specified
flavour.

Note the file is required to be always present, so skipping any test
for the missing file. Crash hard and terribly if the file is missing
or the content is invalid.

We expect the flavour to be always good as it is under our control
(already sanitized in IPUConfig), but return empty dict and log it if missing.
"""
with open(api.get_common_file_path(_filename)) as fp:
data = json.loads(fp.read())
if flavour not in data:
api.current_logger().warning(
'Cannot discover any upgrade paths for flavour: {}'
.format(flavour)
)
return data.get(flavour, {})


def get_filtered_ipu_paths(ipu_paths, src_major_version):
result = []
for src_version, tgt_versions in ipu_paths.items():
if src_version.split('.')[0] == src_major_version:
result.append(IPUPath(source_version=src_version, target_versions=tgt_versions))
return result


@suppress_deprecation(IPUPaths)
def process():
flavour = api.current_actor().configuration.flavour
ipu_paths = load_ipu_paths_for_flavour(flavour)
api.produce(IPUPaths(data=get_filtered_ipu_paths(ipu_paths, get_source_major_version())))
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"default": {
"8.10": ["9.4", "9.5", "9.6"],
"8.4": ["9.2"],
"9.6": ["10.0"],
"8": ["9.4", "9.5", "9.6"],
"9": ["10.0"]
},
"saphana": {
"8.10": ["9.6", "9.4"],
"8": ["9.6", "9.4"],
"9.6": ["10.0"],
"9": ["10.0"]
}
}
Loading