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) 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/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) ) ), ] 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]) ]) 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 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'): 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