From 1aafb7317df1f18934aeafc2222296c037ee2e43 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Mon, 27 Apr 2026 12:06:10 -0400 Subject: [PATCH 1/9] CLOS-4056: gate CLN-assuming actors on is_cln_configured() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systems migrated to the no-auth (SWNG) scheme no longer have CLN as a package source. Several CL-specific actors assumed CLN was always active and either crashed on missing files or produced spurious inhibitors. Add cln_detect.is_cln_configured() — True when the CLN plumbing is present and not explicitly disabled (registration file exists + spacewalk plugin installed + plugin enabled). Gate these actors on it: - switch_cln_channel: skip the cln-switch-channel call on no-auth systems. Also downgrade the failed-switch inhibitor to a MEDIUM report, since a failure on a transitional system where CLN plumbing lingers but is no longer usable should not block the upgrade — CL9 packages come from cl-channel / cloudlinux9-baseos instead. - pin_cln_mirror / unpin_cln_mirror: no-op on no-auth systems; also wrap the up2date update in try/except in pin_cln_mirror in case the file was not shipped on the target. - check_rhn_version_override / reset_rhn_version_override: skip on no-auth systems and fall back cleanly when /etc/sysconfig/rhn/up2date is missing. reset_rhn_version_override: also fix a pre-existing bug where rebinding inside the loop did not update config_data, so the reset was silently a no-op. enable_yum_spacewalk_plugin is not touched here — CLOS-3960 has concurrent work on it. --- .../actors/checkrhnversionoverride/actor.py | 52 ++++++++----- .../cloudlinux/actors/pinclnmirror/actor.py | 19 ++++- .../actors/resetrhnversionoverride/actor.py | 29 ++++++-- .../actors/switchclnchannel/actor.py | 29 ++++++-- .../cloudlinux/actors/unpinclnmirror/actor.py | 5 ++ .../cloudlinux/libraries/cln_detect.py | 55 ++++++++++++++ .../libraries/tests/test_cln_detect.py | 74 +++++++++++++++++++ 7 files changed, 229 insertions(+), 34 deletions(-) create mode 100644 repos/system_upgrade/cloudlinux/libraries/cln_detect.py create mode 100644 repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py index 6a21e10b6e..a9786d143e 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_configured class CheckRhnVersionOverride(Actor): @@ -17,23 +19,35 @@ class CheckRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): + if not is_cln_configured(): + # CLOS-4056: no-auth systems have no CLN config to inspect. + 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/pinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py index bc1686233f..162dbba96d 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_configured 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,13 @@ class PinClnMirror(Actor): @run_on_cloudlinux def process(self): """Pin CLN mirror""" + if not is_cln_configured(): + # CLOS-4056: no-auth systems don't use CLN mirrors; skip cleanly. + api.current_logger().info( + "CLN is not configured on this system; skipping mirror pinning" + ) + return + target_userspace = get_target_userspace_path() api.current_logger().info("Pin CLN mirror: target userspace=%s", target_userspace) @@ -54,6 +62,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..7b5f5d0f51 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_configured class ResetRhnVersionOverride(Actor): @@ -15,11 +17,26 @@ class ResetRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): + if not is_cln_configured(): + # CLOS-4056: no-auth systems have no CLN config to reset. + 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..e80f0e8fa6 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_configured +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,22 @@ class SwitchClnChannel(Actor): @run_on_cloudlinux def process(self): + if not is_cln_configured(): + # CLOS-4056: No-auth (SWNG) systems have no CLN plumbing. Skipping + # the channel switch here is correct — the system receives CL9 + # packages via cl-channel / cloudlinux9-baseos instead. + api.current_logger().info( + "CLN is not configured on this system; skipping channel switch" + ) + return + try: cln_switch(target=int(get_target_major_version())) except CalledProcessError as e: + # CLOS-4056: Do not inhibit. CLN may be partially configured (legacy + # registration files present but no working registration) on systems + # transitioning to the no-auth scheme, and a failed channel switch + # there is expected — the no-auth repos still serve CL9 packages. reporting.create_report( [ reporting.Title( @@ -33,17 +47,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..cc2247d91e 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_configured from leapp.libraries.common.cln_switch import get_target_userspace_path from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag @@ -19,6 +20,10 @@ class UnpinClnMirror(Actor): @run_on_cloudlinux def process(self): + if not is_cln_configured(): + # CLOS-4056: pinclnmirror was skipped on no-auth systems, nothing 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..623f36a71a --- /dev/null +++ b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py @@ -0,0 +1,55 @@ +"""Detection helpers for CLN (CloudLinux Network / Spacewalk) state. + +A system is considered to have CLN *configured* when it has registration +state plus the spacewalk DNF/YUM plugin installed and not explicitly +disabled. Systems that have been migrated to the no-auth (SWNG mirrorlist) +scheme have either: + + - no `/etc/sysconfig/rhn/systemid` (never registered or deregistered), + - no spacewalk plugin installed (rhn-client-tools >= 3.0.1 removes it), or + - the plugin's `enabled = 0` in its config. + +CLOS-4056: several CloudLinux-specific actors were written when CLN was the +only scheme and assume it is always active. They need to gate their +behavior on `is_cln_configured()` so systems on the no-auth scheme pass +through without bogus inhibitors or crashes. +""" + +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_configured(): + """Return True if CLN plumbing is present and not disabled on this system.""" + 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 + + # If any plugin config explicitly disables the plugin, treat as no-auth. + 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..ee2bbae411 --- /dev/null +++ b/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py @@ -0,0 +1,74 @@ +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_no_cln(clean_paths): + # Without /etc/sysconfig/rhn/systemid the system is not registered with CLN. + assert cln_detect.is_cln_configured() is False + + +def test_systemid_but_no_plugin_means_no_cln(clean_paths): + _touch(clean_paths["systemid"]) + assert cln_detect.is_cln_configured() is False + + +def test_systemid_and_enabled_dnf_plugin_means_cln(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\nenabled = 1\n") + assert cln_detect.is_cln_configured() is True + + +def test_explicit_disabled_dnf_plugin_means_no_cln(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\nenabled = 0\n") + assert cln_detect.is_cln_configured() is False + + +def test_explicit_disabled_yum_plugin_means_no_cln(clean_paths): + _touch(clean_paths["systemid"]) + _touch(clean_paths["yum_conf"], "[main]\nenabled=0\n") + assert cln_detect.is_cln_configured() is False + + +def test_one_plugin_disabled_one_not_means_no_cln(clean_paths): + # If either plugin config disables it, CLN is not usable. + _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_configured() is False + + +def test_plugin_conf_without_enabled_key_means_cln(clean_paths): + # A plugin config that doesn't mention `enabled` defaults to enabled upstream, + # so we must treat it as CLN active. + _touch(clean_paths["systemid"]) + _touch(clean_paths["dnf_conf"], "[main]\ntimeout = 120\n") + assert cln_detect.is_cln_configured() 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_configured() is True From d7eb9963402bbf02ef41516b3d7d57b5e05539ab Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Mon, 27 Apr 2026 12:06:10 -0400 Subject: [PATCH 2/9] =?UTF-8?q?CLOS-4056:=20clarify=20CLN=20detection=20?= =?UTF-8?q?=E2=80=94=20package=20channel=20vs=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first commit on this branch named the helper is_cln_configured() and the actor comments said things like 'CLN is not configured here'. That conflated two separate concerns and was misleading: CLN registration is still in use on no-auth (SWNG) systems for licensing and inventory — what the no-auth migration changes is only the package channel (the spacewalk DNF plugin no longer delivers packages from a CLN channel). Rename: is_cln_configured() -> is_cln_package_channel_active() so the function name reflects what the actors actually need to gate on, and rewrite its docstring + module header to spell out the package-channel vs registration distinction. Update each of the five actor comments accordingly. The detection logic itself is unchanged — registration state plus a non-disabled spacewalk plugin remains the right heuristic for 'CLN is delivering packages'. Tests updated to match the new name and to phrase scenarios in terms of the channel rather than 'CLN configured'. --- .../actors/checkrhnversionoverride/actor.py | 10 ++-- .../cloudlinux/actors/pinclnmirror/actor.py | 11 ++-- .../actors/resetrhnversionoverride/actor.py | 9 ++-- .../actors/switchclnchannel/actor.py | 22 ++++---- .../cloudlinux/actors/unpinclnmirror/actor.py | 8 +-- .../cloudlinux/libraries/cln_detect.py | 50 +++++++++++++------ .../libraries/tests/test_cln_detect.py | 39 ++++++++------- 7 files changed, 92 insertions(+), 57 deletions(-) diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py index a9786d143e..21a8b3ee13 100644 --- a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py @@ -4,7 +4,7 @@ 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_configured +from leapp.libraries.common.cln_detect import is_cln_package_channel_active class CheckRhnVersionOverride(Actor): @@ -19,8 +19,12 @@ class CheckRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): - if not is_cln_configured(): - # CLOS-4056: no-auth systems have no CLN config to inspect. + if not is_cln_package_channel_active(): + # CLOS-4056: versionOverride only matters when CLN is delivering + # packages — the upgrade rewrites it to drive channel selection. + # On no-auth (SWNG) systems the package channel is cl-channel, + # not CLN, so there is nothing to inspect or warn about even if + # /etc/sysconfig/rhn/up2date is still present from registration. return up2date_config = '/etc/sysconfig/rhn/up2date' diff --git a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py index 162dbba96d..1bdcb42d9e 100644 --- a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py @@ -4,7 +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_configured +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 @@ -26,10 +26,13 @@ class PinClnMirror(Actor): @run_on_cloudlinux def process(self): """Pin CLN mirror""" - if not is_cln_configured(): - # CLOS-4056: no-auth systems don't use CLN mirrors; skip cleanly. + if not is_cln_package_channel_active(): + # CLOS-4056: pinning the CLN mirror is only meaningful when CLN + # is delivering packages. On no-auth (SWNG) systems packages come + # from cl-channel via mirrorlist, so there is nothing to pin — + # registration may still be in place but is irrelevant here. api.current_logger().info( - "CLN is not configured on this system; skipping mirror pinning" + "CLN is not the active package channel; skipping mirror pinning" ) return diff --git a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py index 7b5f5d0f51..ff446c77a2 100644 --- a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py @@ -2,7 +2,7 @@ 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_configured +from leapp.libraries.common.cln_detect import is_cln_package_channel_active class ResetRhnVersionOverride(Actor): @@ -17,8 +17,11 @@ class ResetRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): - if not is_cln_configured(): - # CLOS-4056: no-auth systems have no CLN config to reset. + if not is_cln_package_channel_active(): + # CLOS-4056: versionOverride is only set/used by the CLN package + # channel flow. If the system isn't on CLN for packages, leave + # /etc/sysconfig/rhn/up2date alone — registration metadata there + # is not ours to touch. return up2date_config = '/etc/sysconfig/rhn/up2date' diff --git a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py index e80f0e8fa6..f7c5675c82 100644 --- a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py @@ -3,7 +3,7 @@ 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_detect import is_cln_configured +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 @@ -23,22 +23,24 @@ class SwitchClnChannel(Actor): @run_on_cloudlinux def process(self): - if not is_cln_configured(): - # CLOS-4056: No-auth (SWNG) systems have no CLN plumbing. Skipping - # the channel switch here is correct — the system receives CL9 - # packages via cl-channel / cloudlinux9-baseos instead. + if not is_cln_package_channel_active(): + # CLOS-4056: CLN is no longer the package channel here (no-auth / + # SWNG mode). Skipping the channel switch is correct — packages + # come from cl-channel / cloudlinux9-baseos instead. The system + # may still be CLN-registered for licensing; that is a separate + # concern this actor does not need to manage. api.current_logger().info( - "CLN is not configured on this system; skipping channel switch" + "CLN is not the active package channel; skipping channel switch" ) return try: cln_switch(target=int(get_target_major_version())) except CalledProcessError as e: - # CLOS-4056: Do not inhibit. CLN may be partially configured (legacy - # registration files present but no working registration) on systems - # transitioning to the no-auth scheme, and a failed channel switch - # there is expected — the no-auth repos still serve CL9 packages. + # CLOS-4056: Do not inhibit. Even on systems that ARE using CLN + # as the package channel, a transient CLN-server reachability + # problem at FirstBoot (DNS/network not up yet) shouldn't block + # the upgrade — the no-auth fallback repos still serve packages. reporting.create_report( [ reporting.Title( diff --git a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py index cc2247d91e..01ddafadf4 100644 --- a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py @@ -2,7 +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_configured +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 @@ -20,8 +20,10 @@ class UnpinClnMirror(Actor): @run_on_cloudlinux def process(self): - if not is_cln_configured(): - # CLOS-4056: pinclnmirror was skipped on no-auth systems, nothing to unpin. + if not is_cln_package_channel_active(): + # CLOS-4056: pinclnmirror skipped its work for the same reason + # (CLN is not the package channel here), so there is nothing + # for us to unpin. return target_userspace = get_target_userspace_path() diff --git a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py index 623f36a71a..f5c6532f87 100644 --- a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py +++ b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py @@ -1,18 +1,27 @@ -"""Detection helpers for CLN (CloudLinux Network / Spacewalk) state. +"""Detection helpers for the CloudLinux Network (CLN) package channel. -A system is considered to have CLN *configured* when it has registration -state plus the spacewalk DNF/YUM plugin installed and not explicitly -disabled. Systems that have been migrated to the no-auth (SWNG mirrorlist) -scheme have either: +CLN has historically combined two concerns: - - no `/etc/sysconfig/rhn/systemid` (never registered or deregistered), - - no spacewalk plugin installed (rhn-client-tools >= 3.0.1 removes it), or - - the plugin's `enabled = 0` in its config. + 1. **Registration / identity** — the system is registered with the CLN + server (`/etc/sysconfig/rhn/systemid`, JWT token), used for licensing + and inventory regardless of how packages are delivered. -CLOS-4056: several CloudLinux-specific actors were written when CLN was the -only scheme and assume it is always active. They need to gate their -behavior on `is_cln_configured()` so systems on the no-auth scheme pass -through without bogus inhibitors or crashes. + 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 (SWNG) 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 +even though registration may still be present and valid. + +CLOS-4056: gate those actors on `is_cln_package_channel_active()`. """ import os @@ -38,8 +47,20 @@ def _plugin_explicitly_disabled(conf_path): return False -def is_cln_configured(): - """Return True if CLN plumbing is present and not disabled on this system.""" +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 @@ -47,7 +68,6 @@ def is_cln_configured(): if not configs: return False - # If any plugin config explicitly disables the plugin, treat as no-auth. for conf in configs: if _plugin_explicitly_disabled(conf): return False diff --git a/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py index ee2bbae411..a048537029 100644 --- a/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py +++ b/repos/system_upgrade/cloudlinux/libraries/tests/test_cln_detect.py @@ -21,48 +21,49 @@ def _touch(path, content=""): path.write_text(content) -def test_no_systemid_means_no_cln(clean_paths): - # Without /etc/sysconfig/rhn/systemid the system is not registered with CLN. - assert cln_detect.is_cln_configured() is False +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_no_cln(clean_paths): +def test_systemid_but_no_plugin_means_channel_inactive(clean_paths): _touch(clean_paths["systemid"]) - assert cln_detect.is_cln_configured() is False + assert cln_detect.is_cln_package_channel_active() is False -def test_systemid_and_enabled_dnf_plugin_means_cln(clean_paths): +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_configured() is True + assert cln_detect.is_cln_package_channel_active() is True -def test_explicit_disabled_dnf_plugin_means_no_cln(clean_paths): +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_configured() is False + assert cln_detect.is_cln_package_channel_active() is False -def test_explicit_disabled_yum_plugin_means_no_cln(clean_paths): +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_configured() is False + assert cln_detect.is_cln_package_channel_active() is False -def test_one_plugin_disabled_one_not_means_no_cln(clean_paths): - # If either plugin config disables it, CLN is not usable. +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_configured() is False + assert cln_detect.is_cln_package_channel_active() is False -def test_plugin_conf_without_enabled_key_means_cln(clean_paths): - # A plugin config that doesn't mention `enabled` defaults to enabled upstream, - # so we must treat it as CLN active. +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_configured() is True + assert cln_detect.is_cln_package_channel_active() is True def test_comments_and_blank_lines_ignored(clean_paths): @@ -71,4 +72,4 @@ def test_comments_and_blank_lines_ignored(clean_paths): clean_paths["dnf_conf"], "# some comment\n\n[main]\n# enabled = 0\nenabled = 1\n", ) - assert cln_detect.is_cln_configured() is True + assert cln_detect.is_cln_package_channel_active() is True From 0a31d423b27c4ac8f6443d6a67d4deb0f418ed94 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Mon, 27 Apr 2026 12:06:10 -0400 Subject: [PATCH 3/9] CLOS-4056: replace em-dashes with ASCII for Python 2.7 / lint compat The previous two commits introduced em-dashes (U+2014) in docstrings and inline comments. The upstream make lint target now greps for any non-ASCII byte (commit 92aee84b) because Python 2.7 source files reject non-ASCII without an encoding declaration, and the leapp framework still supports running on 2.7 in places. Replace each em-dash with an ASCII hyphen so the files lint clean against that gate. --- .../cloudlinux/actors/checkrhnversionoverride/actor.py | 2 +- .../cloudlinux/actors/pinclnmirror/actor.py | 2 +- .../cloudlinux/actors/resetrhnversionoverride/actor.py | 2 +- .../cloudlinux/actors/switchclnchannel/actor.py | 6 +++--- repos/system_upgrade/cloudlinux/libraries/cln_detect.py | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py index 21a8b3ee13..41cdd8843f 100644 --- a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py @@ -21,7 +21,7 @@ class CheckRhnVersionOverride(Actor): def process(self): if not is_cln_package_channel_active(): # CLOS-4056: versionOverride only matters when CLN is delivering - # packages — the upgrade rewrites it to drive channel selection. + # packages - the upgrade rewrites it to drive channel selection. # On no-auth (SWNG) systems the package channel is cl-channel, # not CLN, so there is nothing to inspect or warn about even if # /etc/sysconfig/rhn/up2date is still present from registration. diff --git a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py index 1bdcb42d9e..478609ce02 100644 --- a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py @@ -29,7 +29,7 @@ def process(self): if not is_cln_package_channel_active(): # CLOS-4056: pinning the CLN mirror is only meaningful when CLN # is delivering packages. On no-auth (SWNG) systems packages come - # from cl-channel via mirrorlist, so there is nothing to pin — + # from cl-channel via mirrorlist, so there is nothing to pin - # registration may still be in place but is irrelevant here. api.current_logger().info( "CLN is not the active package channel; skipping mirror pinning" diff --git a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py index ff446c77a2..408a77f85f 100644 --- a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py @@ -20,7 +20,7 @@ def process(self): if not is_cln_package_channel_active(): # CLOS-4056: versionOverride is only set/used by the CLN package # channel flow. If the system isn't on CLN for packages, leave - # /etc/sysconfig/rhn/up2date alone — registration metadata there + # /etc/sysconfig/rhn/up2date alone - registration metadata there # is not ours to touch. return diff --git a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py index f7c5675c82..4a77da4740 100644 --- a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py @@ -25,7 +25,7 @@ class SwitchClnChannel(Actor): def process(self): if not is_cln_package_channel_active(): # CLOS-4056: CLN is no longer the package channel here (no-auth / - # SWNG mode). Skipping the channel switch is correct — packages + # SWNG mode). Skipping the channel switch is correct - packages # come from cl-channel / cloudlinux9-baseos instead. The system # may still be CLN-registered for licensing; that is a separate # concern this actor does not need to manage. @@ -40,7 +40,7 @@ def process(self): # CLOS-4056: Do not inhibit. Even on systems that ARE using CLN # as the package channel, a transient CLN-server reachability # problem at FirstBoot (DNS/network not up yet) shouldn't block - # the upgrade — the no-auth fallback repos still serve packages. + # the upgrade - the no-auth fallback repos still serve packages. reporting.create_report( [ reporting.Title( @@ -50,7 +50,7 @@ def process(self): "Command {} failed with exit code {}." " The most probable cause of that is a problem with this system's" " CloudLinux Network registration. If this system now uses the" - " no-auth (SWNG) repository scheme, this failure is harmless —" + " 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) ), diff --git a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py index f5c6532f87..8bb4bb2a2b 100644 --- a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py +++ b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py @@ -2,17 +2,17 @@ CLN has historically combined two concerns: - 1. **Registration / identity** — the system is registered with the CLN + 1. **Registration / identity** - the system is registered with the CLN server (`/etc/sysconfig/rhn/systemid`, JWT token), used for licensing and inventory regardless of how packages are delivered. - 2. **Package delivery** — the system pulls CloudLinux packages through + 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 (SWNG) 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 +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. @@ -57,7 +57,7 @@ def is_cln_package_channel_active(): 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 + 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). """ From bb4a76aa77ef4951aa5de736276680791539fe85 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Thu, 30 Apr 2026 21:42:44 +0200 Subject: [PATCH 4/9] CLOS-4056: Tighten CLN-related actor comments and clarifications --- .../actors/checkrhnversionoverride/actor.py | 8 ++---- .../libraries/enableyumspacewalkplugin.py | 13 +++++---- .../tests/test_enableyumspacewalkplugin.py | 7 ++--- .../cloudlinux/actors/pinclnmirror/actor.py | 6 ++-- .../actors/resetrhnversionoverride/actor.py | 7 ++--- .../actors/switchclnchannel/actor.py | 15 ++++------ .../cloudlinux/actors/unpinclnmirror/actor.py | 3 +- .../cloudlinux/libraries/cln_detect.py | 28 +++++++++---------- 8 files changed, 39 insertions(+), 48 deletions(-) diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py index 41cdd8843f..3b37334ccc 100644 --- a/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/checkrhnversionoverride/actor.py @@ -20,11 +20,9 @@ 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 - the upgrade rewrites it to drive channel selection. - # On no-auth (SWNG) systems the package channel is cl-channel, - # not CLN, so there is nothing to inspect or warn about even if - # /etc/sysconfig/rhn/up2date is still present from registration. + # 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' 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 478609ce02..bc19301a03 100644 --- a/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/pinclnmirror/actor.py @@ -27,10 +27,8 @@ class PinClnMirror(Actor): 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. On no-auth (SWNG) systems packages come - # from cl-channel via mirrorlist, so there is nothing to pin - - # registration may still be in place but is irrelevant here. + # 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" ) diff --git a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py index 408a77f85f..32109201a3 100644 --- a/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/resetrhnversionoverride/actor.py @@ -18,10 +18,9 @@ class ResetRhnVersionOverride(Actor): @run_on_cloudlinux def process(self): if not is_cln_package_channel_active(): - # CLOS-4056: versionOverride is only set/used by the CLN package - # channel flow. If the system isn't on CLN for packages, leave - # /etc/sysconfig/rhn/up2date alone - registration metadata there - # is not ours to touch. + # 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' diff --git a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py index 4a77da4740..dc0ac24317 100644 --- a/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/switchclnchannel/actor.py @@ -24,11 +24,9 @@ class SwitchClnChannel(Actor): @run_on_cloudlinux def process(self): if not is_cln_package_channel_active(): - # CLOS-4056: CLN is no longer the package channel here (no-auth / - # SWNG mode). Skipping the channel switch is correct - packages - # come from cl-channel / cloudlinux9-baseos instead. The system - # may still be CLN-registered for licensing; that is a separate - # concern this actor does not need to manage. + # 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" ) @@ -37,10 +35,9 @@ def process(self): try: cln_switch(target=int(get_target_major_version())) except CalledProcessError as e: - # CLOS-4056: Do not inhibit. Even on systems that ARE using CLN - # as the package channel, a transient CLN-server reachability - # problem at FirstBoot (DNS/network not up yet) shouldn't block - # the upgrade - the no-auth fallback repos still serve packages. + # 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( diff --git a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py index 01ddafadf4..7343117018 100644 --- a/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/unpinclnmirror/actor.py @@ -22,8 +22,7 @@ class UnpinClnMirror(Actor): def process(self): if not is_cln_package_channel_active(): # CLOS-4056: pinclnmirror skipped its work for the same reason - # (CLN is not the package channel here), so there is nothing - # for us to unpin. + # (CLN package channel inactive), so there is nothing for us to unpin. return target_userspace = get_target_userspace_path() diff --git a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py index 8bb4bb2a2b..17ad307f07 100644 --- a/repos/system_upgrade/cloudlinux/libraries/cln_detect.py +++ b/repos/system_upgrade/cloudlinux/libraries/cln_detect.py @@ -2,24 +2,24 @@ 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 - and inventory regardless of how packages are delivered. +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`). +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 (SWNG) 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 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 -even though registration may still be present and valid. +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()`. """ From e8944e5298204a09143f7ed131c600d3b4b68336 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Thu, 30 Apr 2026 21:42:57 +0200 Subject: [PATCH 5/9] upgrade cli: register --non-interactive alias compatible with leapp 0.18 The earlier patch passed `aliases=['non-interactive']` to @command_opt, which threads through to leapp framework's `add_option`. In 0.18.0 that function does not accept an `aliases` kwarg, so leapp loading failed with `add_option() got an unexpected keyword argument 'aliases'` on every CLI invocation - including `leapp preupgrade` and `leapp upgrade`. argparse natively supports multiple long names per option; we just need to get them past add_option. Add a local `_command_opt_with_aliases` helper that wraps the framework's `_add_opt` directly, so both --nowarn and --non-interactive land on the same argparse argument (dest=nowarn). No framework change required, no consumer changes (args.nowarn keeps working). Verified on a CL8 source VM with leapp-0.18.0-2.el8: `leapp upgrade --help` now prints both names as a single option and parses --non-interactive. Co-Authored-By: Claude Opus 4.7 (1M context) --- commands/upgrade/__init__.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/commands/upgrade/__init__.py b/commands/upgrade/__init__.py index 7528297d82..57b4240ea8 100644 --- a/commands/upgrade/__init__.py +++ b/commands/upgrade/__init__.py @@ -8,7 +8,7 @@ from leapp.exceptions import CommandError, LeappError from leapp.logger import configure_logger from leapp.utils.audit import Execution -from leapp.utils.clicmd import command, command_opt +from leapp.utils.clicmd import command, command_opt, _ensure_command from leapp.utils.output import beautify_actor_exception, report_errors, report_info # NOTE: @@ -16,10 +16,36 @@ # otherwise there might be errors. +def command_opt_with_aliases(name, *aliases, **kwargs): + """Like command_opt, but registers -- AND extra long-form aliases. + + leapp framework's add_option (as of 0.18.0) accepts only one long name, + so a plain `aliases=` kwarg trips on `add_option() got an unexpected + keyword argument 'aliases'`. argparse, however, supports multiple long + forms natively when add_argument is called with several name strings. + We bypass add_option and call the lower-level _add_opt directly. + + `dest` is derived by argparse from the first long form (here `name`), + so existing consumers reading `args.` keep working unchanged. + """ + is_flag = kwargs.pop('is_flag', False) + help_text = kwargs.pop('help', '') + action = kwargs.pop('action', 'store_true' if is_flag else 'store') + inherit = kwargs.pop('inherit', False) + + @_ensure_command + def wrapper(f): + names = ['--' + n.lstrip('-') for n in (name,) + aliases] + f.command._add_opt(*names, action=action, help=help_text, + internal={'wrapped': f, 'inherit': inherit}, + **kwargs) + return f + return wrapper + + @command('upgrade', help='Upgrade the current system to the next available major version.') @command_opt('resume', is_flag=True, help='Continue the last execution after it was stopped (e.g. after reboot)') -@command_opt('nowarn', is_flag=True, help='Do not display interactive warnings', - aliases=['non-interactive']) +@command_opt_with_aliases('nowarn', 'non-interactive', is_flag=True, help='Do not display interactive warnings') @command_opt('reboot', is_flag=True, help='Automatically performs reboot when requested.') @command_opt('whitelist-experimental', action='append', metavar='ActorName', help='Enable experimental actors') @command_opt('debug', is_flag=True, help='Enable debug mode', inherit=False) From c77a9c28099b3dcf4dd86138aa39a0a290c2b82d Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Thu, 30 Apr 2026 21:42:57 +0200 Subject: [PATCH 6/9] Revert "Rebuild rpmdb before dnf transactions targeting el9 to silence bdb_ro warnings" This reverts commit be4ece2deeb4d4b05e070b555d1ac412f48e02c8. The new call site sat inside _prepare_perform, which mounts an overlayfs at /installroot. rpmdb --rebuilddb does an atomic directory rename to swap the old database with the freshly-converted (bdb -> sqlite) one. Across overlay layers that rename returns EXDEV, so rpm prints error: failed to replace old database with new database! error: replace files in /var/lib/rpm with files from /var/lib/rpmrebuilddb.1 to recover and the dnf_transaction_check actor crashes with exit code 1, blocking every CL8 -> CL9 preupgrade. The two existing _rebuild_rpm_db call sites (perform_transaction_install, install_initramdisk_requirements) use _prepare_transaction with binds=['/:/installroot'], so the rename stays on a single filesystem and works. Those are the load-bearing rebuilds for the actual upgrade. Dropping the new preupgrade-time call only restores the harmless bdb_ro warnings; the upgrade transaction itself is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- repos/system_upgrade/common/libraries/dnfplugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/repos/system_upgrade/common/libraries/dnfplugin.py b/repos/system_upgrade/common/libraries/dnfplugin.py index 0fc011ab61..2d7c3fbd50 100644 --- a/repos/system_upgrade/common/libraries/dnfplugin.py +++ b/repos/system_upgrade/common/libraries/dnfplugin.py @@ -483,8 +483,6 @@ def _prepare_perform(used_repos, target_userspace_info, xfs_info, storage_info, mount_target=os.path.join(context.base_dir, 'installroot'), scratch_reserve=reserve_space) as overlay: with mounting.mount_upgrade_iso_to_root_dir(target_userspace_info.path, target_iso): - if get_target_major_version() == '9': - _rebuild_rpm_db(context, root='/installroot') yield context, overlay, target_repoids From 5335b7458da4b47684619529248e5ab073251c41 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Thu, 30 Apr 2026 21:42:57 +0200 Subject: [PATCH 7/9] EnableYumSpacewalkPlugin: rename `config` -> CONFIG_PATH (reserved name) The Actor base class in leapp framework 0.18+ runs self.config = retrieve_config(self.config_schemas) in __init__, which clobbers any class-level `config` attribute the actor defined. Our class-level `config = '/etc/dnf/plugins/spacewalk.conf'` was silently turned into the runtime config dict, so the FirstBoot phase crashed with `TypeError: stat: path should be string, ..., not dict` on every CL upgrade (CLOS-3960 introduced the assignment, but the rename to DEFAULT_CONFIG_PATH in CLOS-3960's library didn't touch the actor's class attribute name). Rename to CONFIG_PATH so the framework leaves it alone. _enable_plugin() already silently no-ops when the file is missing, so no_auth systems (where the spacewalk plugin is removed by rhn-client-tools >= 3.0.1) keep skipping cleanly. Verified live on a CL9 machine: patched actor loads, process() reaches _enable_plugin with the string path, and silently returns False/None when the plugin config is absent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cloudlinux/actors/enableyumspacewalkplugin/actor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py index 4856c02894..e370791410 100644 --- a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py +++ b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py @@ -11,17 +11,18 @@ class EnableYumSpacewalkPlugin(Actor): consumes = () produces = (Report,) tags = (FirstBootPhaseTag, IPUWorkflowTag) - config = enableyumspacewalkplugin.DEFAULT_CONFIG_PATH + + CONFIG_PATH = enableyumspacewalkplugin.DEFAULT_CONFIG_PATH @run_on_cloudlinux def process(self): _, title = enableyumspacewalkplugin._enable_plugin( - self.config, enableyumspacewalkplugin.ParserClass, self.log + self.CONFIG_PATH, enableyumspacewalkplugin.ParserClass, self.log ) if title: reporting.create_report([ reporting.Title(title), - reporting.Summary("DNF spacewalk plugin must be enabled for CLN channels. Config path: " + self.config), + reporting.Summary("DNF spacewalk plugin must be enabled for CLN channels. Config path: " + self.CONFIG_PATH), reporting.Severity(reporting.Severity.MEDIUM), reporting.Groups([reporting.Groups.SANITY]) ]) From 02cd38ab98cf332a3967141c0fe2ba0e41c6672b Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Thu, 30 Apr 2026 21:42:58 +0200 Subject: [PATCH 8/9] Update Governor-related messages and hints in cl-MySQL actor with more details --- .../libraries/clmysql_cloudlinux.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/repos/system_upgrade/cloudlinux/actors/clmysqlrepositorysetup/libraries/clmysql_cloudlinux.py b/repos/system_upgrade/cloudlinux/actors/clmysqlrepositorysetup/libraries/clmysql_cloudlinux.py index 840ff5a43c..14a4760609 100644 --- a/repos/system_upgrade/cloudlinux/actors/clmysqlrepositorysetup/libraries/clmysql_cloudlinux.py +++ b/repos/system_upgrade/cloudlinux/actors/clmysqlrepositorysetup/libraries/clmysql_cloudlinux.py @@ -41,7 +41,7 @@ def clmysql_process(lib, repofile_name, repofile_data): reporting.Summary( "MySQL Governor records the installed database type as '{governor}', " "but the mysqld binary on disk belongs to '{rpm}'. " - "This usually means 'mysqlgovernor.py --mysql-version' was run " + "This usually means '/usr/share/lve/dbgovernor/mysqlgovernor.py --mysql-version' was run " "without a follow-up '--install', or packages were changed manually. " "Proceeding could enable the wrong DNF module stream and break the upgrade.".format( governor=detected.governor_type, rpm=detected.pkg_type @@ -56,11 +56,11 @@ def clmysql_process(lib, repofile_name, repofile_data): hint=( "Examine the current state of the system's DB packages." "Complete the pending Governor install:\n" - " mysqlgovernor.py --mysql-version={governor}\n" - " mysqlgovernor.py --install --yes\n" + " /usr/share/lve/dbgovernor/mysqlgovernor.py --mysql-version={governor}\n" + " /usr/share/lve/dbgovernor/mysqlgovernor.py --install --yes\n" "Or reset Governor to match the actual packages:\n" - " mysqlgovernor.py --mysql-version={rpm}\n" - " mysqlgovernor.py --install --yes\n" + " /usr/share/lve/dbgovernor/mysqlgovernor.py --mysql-version={rpm}\n" + " /usr/share/lve/dbgovernor/mysqlgovernor.py --install --yes\n" "Then restart the upgrade process.".format( governor=detected.governor_type, rpm=detected.pkg_type ) @@ -109,7 +109,7 @@ def clmysql_process(lib, repofile_name, repofile_data): "The detected database type is '{}', but the cl-mysql-meta " "repo URL points to '{}'. " "This may happen when the database version was changed " - "without a follow-up 'mysqlgovernor.py --install', or the " + "without a follow-up '/usr/share/lve/dbgovernor/mysqlgovernor.py --install', or the " "cl-mysql.repo file was manually edited. " "Proceeding with the wrong repository would result in " "an incorrect upgrade operation." @@ -125,13 +125,16 @@ def clmysql_process(lib, repofile_name, repofile_data): reporting.Groups([reporting.Groups.INHIBITOR]), reporting.Remediation( hint=( - "Re-run MySQL Governor to regenerate the repository file: " - "mysqlgovernor.py --install --yes, " - "then restart the upgrade process. " - "Alternatively, if the repository file was manually edited, " - "either correct the baseurl to match the installed DB type or " - "set the desired DB type in Governor and re-run --install " - "to have it write the correct URL." + "Download the correct repository file for the installed " + "database type: " + "curl -o /etc/yum.repos.d/cl-mysql.repo " + "http://repo.cloudlinux.com/other/" + "cl${{releasever}}/mysqlmeta/{expected}-common.repo\n" + "Or re-run MySQL Governor to regenerate it " + "(this reinstalls the full DB stack): " + "/usr/share/lve/dbgovernor/mysqlgovernor.py --install --yes\n" + "Then restart the upgrade process." + .format(expected=expected_fragment) ) ), ] From 2488be977212615b85aea831680d3a847910e0a7 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Fri, 1 May 2026 18:38:56 -0400 Subject: [PATCH 9/9] target_userspace_creator: tolerate missing dnf-plugin-spacewalk config When the target userspace is built from no-auth-aware repos (rhn-client-tools >= 3.0.1, which Obsoletes dnf-plugin-spacewalk on CL8/9 per CLOS-4056), /etc/dnf/plugins/spacewalk.conf is absent in /var/lib/leapp/elNuserspace/. The unconditional `open(spacewalk_conf, 'r')` inside `if os.path.isdir('/etc/sysconfig/rhn')` then raises IOError: [Errno 2] No such file or directory: '/var/lib/leapp/el8userspace/etc/dnf/plugins/spacewalk.conf' and target_userspace_creator crashes with exit code 1, blocking preupgrade on every CL7+CLN -> CL8+no_auth transition where the new RC packages reach the target. Surfaced during CLOS-4056 verification (elevate-qa Run #3 build #40 with BS_BUILDS_TARGET delivery wired up correctly via leapp_upgrade_repositories.repo late-append). The outer /etc/sysconfig/rhn check is correct (CLN registration may persist on no-auth for licensing), but the inner file presence is no longer guaranteed - guard it with an explicit os.path.isfile check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../libraries/userspacegen.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py index 8e4579a7ec..2d40d19a8e 100644 --- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py +++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py @@ -681,16 +681,26 @@ def _prep_repository_access(context, target_userspace): run(['rm', '-rf', os.path.join(target_etc, 'rhsm')]) context.copytree_from('/etc/rhsm', os.path.join(target_etc, 'rhsm')) - if os.path.isdir('/etc/sysconfig/rhn'): + # Set up spacewalk plugin config in the target chroot only if the plugin's + # config file actually exists there. Under the no-auth migration (CLOS-4056) + # rhn-client-tools >= 3.0.1 Obsoletes dnf-plugin-spacewalk on CL8/9, so + # the target userspace built from the no-auth-aware repos has no + # /etc/dnf/plugins/spacewalk.conf - the original unconditional open + # raised IOError [Errno 2] and crashed target_userspace_creator. The + # outer /etc/sysconfig/rhn directory check is on the source side and + # remains valid (CLN registration may persist for licensing/inventory), + # but the inner file presence is no longer guaranteed. + spacewalk_conf = os.path.join(target_etc, 'dnf/plugins/spacewalk.conf') + if os.path.isdir('/etc/sysconfig/rhn') and os.path.isfile(spacewalk_conf): # Set up spacewalk plugin config - with open(os.path.join(target_etc, 'dnf/plugins/spacewalk.conf'), 'r') as f: + with open(spacewalk_conf, 'r') as f: lines = f.readlines() new_lines = [] for line in lines: if 'enabled' in line: line = 'enabled = 1\n' new_lines.append(line) - with open(os.path.join(target_etc, 'dnf/plugins/spacewalk.conf'), 'w') as f: + with open(spacewalk_conf, 'w') as f: f.writelines(new_lines) if os.path.isfile('/etc/mirrorlist'):