Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions commands/upgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,44 @@
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:
# If you are adding new parameters please ensure that they are set in the upgrade function invocation in `rerun`
# otherwise there might be errors.


def command_opt_with_aliases(name, *aliases, **kwargs):
"""Like command_opt, but registers --<name> 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.<name>` 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)
Expand Down
13 changes: 11 additions & 2 deletions packaging/leapp-repository.spec
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,17 @@ Conflicts: leapp-upgrade-el7toel8

%endif

# Requires tools which allow switching between channels
Requires: cln-switch-channel = 2
# cln-switch-channel was provided by rhn-client-tools 2.x to support the
# CLN-side channel switch. rhn-client-tools 3.0+ removes both the binary and
# the Provide as part of the no-auth migration. The actor that invokes it
# (switchclnchannel) is gated on is_cln_package_channel_active() (CLOS-4056),
# so on no-auth systems it never runs and the missing binary is harmless. On
# CLN-active systems rhn-client-tools 2.x is installed, supplying the binary
# at runtime, so the install-time pin was redundant. Drop it to allow
# rhn-client-tools 3.0+ on the same system as leapp-upgrade-el8toel9
# (otherwise leapp_qa Run #54 dnf_transaction_check fails: rhn-client-tools
# 3.x cannot be installed alongside an RPM that requires
# cln-switch-channel = 2).

# IMPORTANT: every time the requirements are changed, increment number by one
# - same for Provides in deps subpackage
Expand Down
29 changes: 27 additions & 2 deletions repos/system_upgrade/cloudlinux/actors/checkcllicense/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.libraries.stdlib import CalledProcessError, run, api
from leapp.libraries.common.cllaunch import run_on_cloudlinux
from leapp.libraries.common.cln_detect import is_cln_package_channel_active

from leapp.models import (
TargetUserSpacePreupgradeTasks,
Expand All @@ -29,10 +30,34 @@ class CheckClLicense(Actor):

@run_on_cloudlinux
def process(self):
# CLOS-4056: the rhn_check XML-RPC call only verifies licenses on
# systems that use CLN as the package channel. Under no-auth (SWNG)
# the license is conveyed by other means (IP-based licensing,
# cloudlinux-release content) and the rhn_check round-trip is not a
# meaningful gate - on rhn-client-tools 3.0+ it fails outright with
# "Invalid System Credentials" against systemid files written by
# clnreg_ks. Skip the check under no-auth.
if not is_cln_package_channel_active():
api.current_logger().info(
"CLN is not the active package channel; skipping rhn_check"
" license verification (no-auth systems use IP licensing,"
" not the CLN XML-RPC roundtrip)."
)
return

res = None
if os.path.exists(self.system_id_path):
res = run([self.rhn_check_bin])
self.log.debug('rhn_check result: %s', res)
try:
res = run([self.rhn_check_bin])
self.log.debug('rhn_check result: %s', res)
except CalledProcessError as e:
# The original implementation assigned `res = run(...)`
# bare, but `run()` raises on non-zero exit codes - so
# the "produce an inhibitor on non-zero / non-empty stderr"
# branch below was dead code. Catch the failure and let
# the existing reporting path take over.
self.log.debug('rhn_check failed: %s', e)
res = None
if not res or res['exit_code'] != 0 or res['stderr']:
title = 'Server does not have an active CloudLinux license'
summary = 'Server does not have an active CloudLinux license. This renders key CloudLinux packages ' \
Expand Down
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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')
])
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
Expand Down Expand Up @@ -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."
Expand All @@ -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)
)
),
]
Expand Down
31 changes: 29 additions & 2 deletions repos/system_upgrade/cloudlinux/actors/copycllicense/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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_package_channel_active
from leapp.libraries.stdlib import api
from leapp.models import (
TargetUserSpacePreupgradeTasks,
Expand All @@ -11,7 +12,18 @@


RHN_CONFIG_DIR = '/etc/sysconfig/rhn'
REQUIRED_PKGS = ['dnf-plugin-spacewalk', 'rhn-client-tools']

# rhn-client-tools is the CLN identity / licensing client. Keep it on the
# target regardless of repo scheme - licensing does not go away under
# no-auth, only repo management does.
LICENSE_PKGS = ['rhn-client-tools']

# dnf-plugin-spacewalk is the DNF plugin that fetches packages from the
# CLN-side spacewalk channel. Pure repo-management plumbing. Under
# no-auth packages come from cl-channel via /etc/yum.repos.d/cl.repo and
# this plugin is unused; rhn-client-tools >= 3.0.1 even Obsoletes it on
# CL8/9.
SPACEWALK_PLUGIN_PKG = 'dnf-plugin-spacewalk'


class CopyClLicense(Actor):
Expand All @@ -38,7 +50,22 @@ def process(self):
if os.path.isfile(src_path):
files_to_copy.append(CopyFile(src=src_path))

# CLOS-4056: only the spacewalk plugin is repo-management and
# therefore conditional on the CLN package channel being active.
# Identity/licensing (rhn-client-tools, /etc/sysconfig/rhn) is
# unconditional - it stays even when we move the system off CLN as
# a package source.
install_rpms = list(LICENSE_PKGS)
if is_cln_package_channel_active():
install_rpms.append(SPACEWALK_PLUGIN_PKG)
else:
api.current_logger().info(
"CLN is not the active package channel; skipping %s in target"
" userspace install set",
SPACEWALK_PLUGIN_PKG,
)

api.produce(TargetUserSpacePreupgradeTasks(
install_rpms=REQUIRED_PKGS,
install_rpms=install_rpms,
copy_files=files_to_copy
))
Original file line number Diff line number Diff line change
Expand Up @@ -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])
])
Original file line number Diff line number Diff line change
Expand Up @@ -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'


Expand All @@ -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.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading