diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py index 6a21e10b6e..3b37334ccc 100644 --- a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py @@ -1,8 +1,10 @@ from leapp.actors import Actor from leapp import reporting +from leapp.libraries.stdlib import api from leapp.reporting import Report from leapp.tags import ChecksPhaseTag, IPUWorkflowTag from leapp.libraries.common.cllaunch import run_on_cloudlinux +from leapp.libraries.common.cln_detect import is_cln_package_channel_active class CheckRhnVersionOverride(Actor): @@ -17,23 +19,37 @@ class CheckRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): + if not is_cln_package_channel_active(): + # CLOS-4056: versionOverride only matters when CLN is delivering packages, + # since the upgrade rewrites it to drive channel selection. + # On no-auth systems this does not apply. + return + up2date_config = '/etc/sysconfig/rhn/up2date' - with open(up2date_config, 'r') as f: - config_data = f.readlines() - for line in config_data: - if line.startswith('versionOverride='): - stripped_line = line.strip().split("=") - versionOverrideValue = stripped_line[1] - # If the version is being overriden to 8, we can continue as is. - if versionOverrideValue not in ['', '8']: - title = 'RHN up2date: versionOverride overwritten by the upgrade' - summary = ("The RHN config file up2date has a set value of the versionOverride option: {}." - " This value will get overwritten by the upgrade process, and reset to an empty" - " value once it's complete.".format(versionOverrideValue)) - reporting.create_report([ - reporting.Title(title), - reporting.Summary(summary), - reporting.Severity(reporting.Severity.MEDIUM), - reporting.Groups([reporting.Groups.OS_FACTS]), - reporting.RelatedResource('file', '/etc/sysconfig/rhn/up2date') - ]) + try: + with open(up2date_config, 'r') as f: + config_data = f.readlines() + except (OSError, IOError): + api.current_logger().info( + "RHN up2date config %s not present; skipping versionOverride check", + up2date_config, + ) + return + + for line in config_data: + if line.startswith('versionOverride='): + stripped_line = line.strip().split("=") + versionOverrideValue = stripped_line[1] + # If the version is being overriden to 8, we can continue as is. + if versionOverrideValue not in ['', '8']: + title = 'RHN up2date: versionOverride overwritten by the upgrade' + summary = ("The RHN config file up2date has a set value of the versionOverride option: {}." + " This value will get overwritten by the upgrade process, and reset to an empty" + " value once it's complete.".format(versionOverrideValue)) + reporting.create_report([ + reporting.Title(title), + reporting.Summary(summary), + reporting.Severity(reporting.Severity.MEDIUM), + reporting.Groups([reporting.Groups.OS_FACTS]), + reporting.RelatedResource('file', '/etc/sysconfig/rhn/up2date') + ]) diff --git a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/libraries/enableyumspacewalkplugin.py b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/libraries/enableyumspacewalkplugin.py index 32b75fbc98..2dd7b4a5cb 100644 --- a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/libraries/enableyumspacewalkplugin.py +++ b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/libraries/enableyumspacewalkplugin.py @@ -10,10 +10,11 @@ ParserClass = configparser.ConfigParser -# DNF plugin config path on the target system (CL8). FirstBoot runs after the -# target OS is already in place; on CL8 the plugin package is -# dnf-plugin-spacewalk (PES replacement for CL7's yum-rhn-plugin, -# pes-events id=1586) and its config ships with enabled=0. +# DNF plugin config path on the target system (CL8). +# FirstBoot runs after the target OS is already in place; +# on CL8 the plugin package is dnf-plugin-spacewalk +# (PES replacement for CL7's yum-rhn-plugin, pes-events id=1586) +# and its config ships with enabled=0. DEFAULT_CONFIG_PATH = '/etc/dnf/plugins/spacewalk.conf' @@ -24,8 +25,8 @@ def _enable_plugin(config_path, parser_cls=ParserClass, log=None): when the plugin is not installed, and otherwise a human-readable problem description suitable for a Leapp report. - Absence of `config_path` is treated as a silent skip: on no-auth / - SWNG systems (CLOS-4056) `rhn-client-tools >= 3.0.1` Obsoletes the + Absence of `config_path` is treated as a silent skip: on no-auth + systems (CLOS-4056) `rhn-client-tools >= 3.0.1` Obsoletes the `dnf-plugin-spacewalk` package, so the config file is either gone by then, or doesn't do anything. """ diff --git a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/tests/test_enableyumspacewalkplugin.py b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/tests/test_enableyumspacewalkplugin.py index 7e7cba2a79..d74da391e4 100644 --- a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/tests/test_enableyumspacewalkplugin.py +++ b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/tests/test_enableyumspacewalkplugin.py @@ -17,10 +17,9 @@ def _write(tmp_path, body): def test_missing_config_is_silent_skip(tmp_path): """Config file absent -> silent skip: no change, no title, no report. - On no-auth / SWNG systems (CLOS-4056) the dnf-plugin-spacewalk - package is Obsoleted by rhn-client-tools >= 3.0.1 and the config - file is absent by design. Emitting a 'not found' report there - would be noise. + On no-auth systems (CLOS-4056) the dnf-plugin-spacewalk + package is Obsoleted by rhn-client-tools >= 3.0.1. + Emitting a 'not found' report there would be noise. """ changed, title = _enable_plugin(str(tmp_path / "absent.conf"), ParserClass) assert changed is False diff --git a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py index bc1686233f..bc19301a03 100644 --- a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py @@ -4,6 +4,7 @@ from leapp.actors import Actor from leapp.libraries.stdlib import api from leapp.libraries.common.cllaunch import run_on_cloudlinux +from leapp.libraries.common.cln_detect import is_cln_package_channel_active from leapp.libraries.common.cln_switch import get_target_userspace_path from leapp.tags import DownloadPhaseTag, IPUWorkflowTag from leapp.libraries.common.config.version import get_target_major_version @@ -25,6 +26,14 @@ class PinClnMirror(Actor): @run_on_cloudlinux def process(self): """Pin CLN mirror""" + if not is_cln_package_channel_active(): + # CLOS-4056: pinning the CLN mirror is only meaningful when CLN is delivering packages. + # With the no-auth repo scheme active, there's no point in doing so. + api.current_logger().info( + "CLN is not the active package channel; skipping mirror pinning" + ) + return + target_userspace = get_target_userspace_path() api.current_logger().info("Pin CLN mirror: target userspace=%s", target_userspace) @@ -54,6 +63,11 @@ def process(self): api.current_logger().info("Pin CLN mirror %s in %s", mirror_url, mirrorlist_path) up2date_path = os.path.join(target_userspace, 'etc/sysconfig/rhn/up2date') - with open(up2date_path, 'a+') as file: - file.write('\nmirrorURL[comment]=Set mirror URL to /etc/mirrorlist\nmirrorURL=file:///etc/mirrorlist\n') - api.current_logger().info("Updated up2date_path %s", up2date_path) + try: + with open(up2date_path, 'a+') as file: + file.write('\nmirrorURL[comment]=Set mirror URL to /etc/mirrorlist\nmirrorURL=file:///etc/mirrorlist\n') + api.current_logger().info("Updated up2date_path %s", up2date_path) + except (OSError, IOError) as e: + api.current_logger().info( + "Could not update %s: %s", up2date_path, e, + ) diff --git a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py index 21b2164cb0..32109201a3 100644 --- a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py @@ -1,6 +1,8 @@ from leapp.actors import Actor +from leapp.libraries.stdlib import api from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag from leapp.libraries.common.cllaunch import run_on_cloudlinux +from leapp.libraries.common.cln_detect import is_cln_package_channel_active class ResetRhnVersionOverride(Actor): @@ -15,11 +17,28 @@ class ResetRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): + if not is_cln_package_channel_active(): + # CLOS-4056: versionOverride only matters when CLN is delivering packages, + # since the upgrade rewrites it to drive channel selection. + # On no-auth systems this does not apply. + return + up2date_config = '/etc/sysconfig/rhn/up2date' - with open(up2date_config, 'r') as f: - config_data = f.readlines() - for line in config_data: - if line.startswith('versionOverride='): - line = 'versionOverride=' + try: + with open(up2date_config, 'r') as f: + config_data = f.readlines() + except (OSError, IOError): + api.current_logger().info( + "RHN up2date config %s not present; skipping versionOverride reset", + up2date_config, + ) + return + + new_data = [] + for line in config_data: + if line.startswith('versionOverride='): + new_data.append('versionOverride=\n') + else: + new_data.append(line) with open(up2date_config, 'w') as f: - f.writelines(config_data) + f.writelines(new_data) diff --git a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py index 86421856b2..dc0ac24317 100644 --- a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py @@ -3,7 +3,8 @@ from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag from leapp.libraries.stdlib import CalledProcessError from leapp.libraries.common.cllaunch import run_on_cloudlinux -from leapp.libraries.common.cln_switch import cln_switch, get_target_userspace_path +from leapp.libraries.common.cln_detect import is_cln_package_channel_active +from leapp.libraries.common.cln_switch import cln_switch from leapp import reporting from leapp.reporting import Report from leapp.libraries.common.config.version import get_target_major_version @@ -22,9 +23,21 @@ class SwitchClnChannel(Actor): @run_on_cloudlinux def process(self): + if not is_cln_package_channel_active(): + # CLOS-4056: CLN package channel is inactive, so skipping the channel switch + # is correct - packages come from standard repositories instead. + # Leapp manages those without custom actions through repomaps. + api.current_logger().info( + "CLN is not the active package channel; skipping channel switch" + ) + return + try: cln_switch(target=int(get_target_major_version())) except CalledProcessError as e: + # Do not inhibit. Even on systems that ARE using CLN as the package channel, + # a transient CLN-server reachability problem at FirstBoot + # shouldn't block the upgrade. reporting.create_report( [ reporting.Title( @@ -33,17 +46,20 @@ def process(self): reporting.Summary( "Command {} failed with exit code {}." " The most probable cause of that is a problem with this system's" - " CloudLinux Network registration.".format(e.command, e.exit_code) + " CloudLinux Network registration. If this system now uses the" + " no-auth (SWNG) repository scheme, this failure is harmless -" + " CL9 packages come from cl-channel / cloudlinux9-baseos instead" + " of CLN.".format(e.command, e.exit_code) ), reporting.Remediation( - hint="Check the state of this system's registration with \'rhn_check\'." - " Attempt to re-register the system with \'rhnreg_ks --force\'." + hint="If you rely on CLN: check registration with 'rhn_check' and" + " re-register with 'rhnreg_ks --force'. If you have migrated to" + " no-auth repos, this message can be ignored." ), - reporting.Severity(reporting.Severity.HIGH), + reporting.Severity(reporting.Severity.MEDIUM), reporting.Groups( [reporting.Groups.OS_FACTS, reporting.Groups.AUTHENTICATION] ), - reporting.Groups([reporting.Groups.INHIBITOR]), ] ) except OSError as e: diff --git a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py index 8e7ffc93a0..7343117018 100644 --- a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py @@ -2,6 +2,7 @@ from leapp.actors import Actor from leapp.libraries.common.cllaunch import run_on_cloudlinux +from leapp.libraries.common.cln_detect import is_cln_package_channel_active from leapp.libraries.common.cln_switch import get_target_userspace_path from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag @@ -19,6 +20,11 @@ class UnpinClnMirror(Actor): @run_on_cloudlinux def process(self): + if not is_cln_package_channel_active(): + # CLOS-4056: pinclnmirror skipped its work for the same reason + # (CLN package channel inactive), so there is nothing for us to unpin. + return + target_userspace = get_target_userspace_path() mirrorlist_path = os.path.join(target_userspace, 'etc/mirrorlist') diff --git a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py new file mode 100644 index 0000000000..17ad307f07 --- /dev/null +++ b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py @@ -0,0 +1,75 @@ +"""Detection helpers for the CloudLinux Network (CLN) package channel. + +CLN has historically combined two concerns: + +1. *Registration / identity* - the system is registered with the CLN +server (`/etc/sysconfig/rhn/systemid`, JWT token), used for licensing +regardless of how packages are delivered. + +2. *Package delivery* - the system pulls CloudLinux packages +through the spacewalk DNF/YUM plugin against the +CLN-side channel (`cloudlinux-x86_64-server-N`). + +The no-auth repository transition decouples these. +New CL8 and CL9 systems keep CLN *registration*, +but no longer use CLN as the *package channel* - packages come from the SWNG mirrorlist +via `/etc/yum.repos.d/cl.repo` (`cl-channel`) instead. +`rhn-client-tools >= 3.0.1` disables the spacewalk plugin to enforce this. + +The CLN-touching actors in this repo only care about the second concern: +they exist to make the CLN package channel work during ELevate. +On systems where the channel has been switched off they should stand down, +regardless of registration state. + +CLOS-4056: gate those actors on `is_cln_package_channel_active()`. +""" + +import os + + +RHN_SYSTEMID = '/etc/sysconfig/rhn/systemid' +SPACEWALK_DNF_CONF = '/etc/dnf/plugins/spacewalk.conf' +SPACEWALK_YUM_CONF = '/etc/yum/pluginconf.d/spacewalk.conf' + + +def _plugin_explicitly_disabled(conf_path): + try: + with open(conf_path) as f: + for line in f: + stripped = line.strip().lower() + if not stripped or stripped.startswith('#') or stripped.startswith('['): + continue + if stripped.startswith('enabled') and '=' in stripped: + value = stripped.split('=', 1)[1].strip() + return value == '0' + except (OSError, IOError): + pass + return False + + +def is_cln_package_channel_active(): + """Return True when CLN is the active package channel for this system. + + A True result means the spacewalk DNF/YUM plugin is installed, not + explicitly disabled, and the system has CLN registration state for + the plugin to authenticate with. A False result means the system is + either deregistered or has been moved to the no-auth (SWNG) scheme, + so CLN-targeting actions (channel switch, mirror pinning, version + overrides) are not meaningful and should be skipped. + + This is a deliberately heuristic check - it asks "is CLN going to + serve packages here", not "is the system registered with CLN" (the + two were the same thing pre-no-auth and have since diverged). + """ + if not os.path.exists(RHN_SYSTEMID): + return False + + configs = [p for p in (SPACEWALK_DNF_CONF, SPACEWALK_YUM_CONF) if os.path.exists(p)] + if not configs: + return False + + for conf in configs: + if _plugin_explicitly_disabled(conf): + return False + + return True diff --git a/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py new file mode 100644 index 0000000000..a048537029 --- /dev/null +++ b/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py @@ -0,0 +1,75 @@ +import os + +import pytest + +from leapp.libraries.common import cln_detect + + +@pytest.fixture +def clean_paths(monkeypatch, tmp_path): + """Point cln_detect at a clean tmp dir so each test starts from no state.""" + systemid = tmp_path / "systemid" + dnf_conf = tmp_path / "dnf_spacewalk.conf" + yum_conf = tmp_path / "yum_spacewalk.conf" + monkeypatch.setattr(cln_detect, "RHN_SYSTEMID", str(systemid)) + monkeypatch.setattr(cln_detect, "SPACEWALK_DNF_CONF", str(dnf_conf)) + monkeypatch.setattr(cln_detect, "SPACEWALK_YUM_CONF", str(yum_conf)) + return {"systemid": systemid, "dnf_conf": dnf_conf, "yum_conf": yum_conf} + + +def _touch(path, content=""): + path.write_text(content) + + +def test_no_systemid_means_channel_inactive(clean_paths): + # Without registration the spacewalk plugin can't authenticate, so even + # if the plugin is installed it is not the active package channel. + assert cln_detect.is_cln_package_channel_active() is False + + +def test_systemid_but_no_plugin_means_channel_inactive(clean_paths): + _touch(clean_paths["systemid"]) + assert cln_detect.is_cln_package_channel_active() is False + + +def test_systemid_and_enabled_dnf_plugin_means_channel_active(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\nenabled = 1\n") + assert cln_detect.is_cln_package_channel_active() is True + + +def test_explicit_disabled_dnf_plugin_means_channel_inactive(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\nenabled = 0\n") + assert cln_detect.is_cln_package_channel_active() is False + + +def test_explicit_disabled_yum_plugin_means_channel_inactive(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["yum_conf"], "[main]\nenabled=0\n") + assert cln_detect.is_cln_package_channel_active() is False + + +def test_one_plugin_disabled_one_not_means_channel_inactive(clean_paths): + # If either plugin config disables the plugin, treat the channel as off. + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\nenabled = 1\n") + _touch(clean_paths["yum_conf"], "[main]\nenabled = 0\n") + assert cln_detect.is_cln_package_channel_active() is False + + +def test_plugin_conf_without_enabled_key_means_channel_active(clean_paths): + # A plugin config that does not mention `enabled` defaults to enabled + # upstream, so we must treat the channel as active. + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\ntimeout = 120\n") + assert cln_detect.is_cln_package_channel_active() is True + + +def test_comments_and_blank_lines_ignored(clean_paths): + _touch(clean_paths["systemid"]) + _touch( + clean_paths["dnf_conf"], + "# some comment\n\n[main]\n# enabled = 0\nenabled = 1\n", + ) + assert cln_detect.is_cln_package_channel_active() is True