Skip to content

Commit e1619a2

Browse files
authored
Merge pull request #66 from prilr/CLOS-3716-elevate-installed-wrong-kernel-for-cln-9-r2
CLOS-3716: inhibit elevate when target kernel minor outruns userland
2 parents 86f6da3 + d970498 commit e1619a2

3 files changed

Lines changed: 404 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor import checktargetkernelminor
3+
from leapp.libraries.common.cllaunch import run_on_cloudlinux
4+
from leapp.libraries.stdlib import api
5+
from leapp.models import TargetUserSpaceInfo
6+
from leapp.reporting import Report
7+
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag
8+
9+
10+
class CheckTargetKernelMinor(Actor):
11+
"""
12+
Inhibit the upgrade when target CloudLinux repositories would deliver
13+
a kernel from a newer minor than the rest of the CloudLinux userland.
14+
15+
Detects the gradual-rollout-leak behind CLOS-3716 (ZD 268790): the CLN
16+
package channel serves the latest-minor kernel ahead of the matching
17+
userland during a staged minor rollout. Installing such a kernel on
18+
the older-minor userland breaks per-kernel-minor modules like kmodlve,
19+
so we refuse the upgrade until the rollout is complete or repos are
20+
pinned to one minor.
21+
22+
See the `checktargetkernelminor` library docstring for the detection
23+
details.
24+
"""
25+
26+
name = 'check_target_kernel_minor'
27+
consumes = (TargetUserSpaceInfo,)
28+
produces = (Report,)
29+
tags = (IPUWorkflowTag, TargetTransactionChecksPhaseTag)
30+
31+
@run_on_cloudlinux
32+
def process(self):
33+
info = next(api.consume(TargetUserSpaceInfo), None)
34+
if info is None:
35+
self.log.info(
36+
'No TargetUserSpaceInfo available; skipping kernel-minor check.'
37+
)
38+
return
39+
checktargetkernelminor.process(installroot=info.path)
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""
2+
Detect a target-repository state where the available kernel's minor version
3+
is greater than the available `cloudlinux-release`'s minor version.
4+
5+
This is the gradual-rollout-leak shape behind CLOS-3716 (ZD 268790): the CLN
6+
package channel `cloudlinux-x86_64-server-9` resolves to the latest minor
7+
available to a system's gradual-rollout slot, and a new minor's kernel is
8+
typically promoted ahead of the rest of the userland. During an in-flight
9+
rollout, elevate can therefore pull a 9.7 kernel onto a 9.6 userland; the
10+
matching kmod-lve is built per kernel minor, so the new default-boot kernel
11+
fails to load kmodlve and breaks LVE.
12+
13+
The actor consuming this library inhibits at Checks phase whenever the
14+
target repositories expose this mismatch, so the upgrade refuses to proceed
15+
while rollout content is in flight. This is defense in depth - it catches
16+
the leak regardless of which CL repo (CLN channel, no-auth fallback, etc.)
17+
ends up serving the newer-minor kernel.
18+
"""
19+
20+
import re
21+
22+
from leapp import reporting
23+
from leapp.libraries.common.config.version import get_target_major_version
24+
from leapp.libraries.stdlib import CalledProcessError, api, run
25+
26+
27+
# CloudLinux/RHEL dist-tag with minor version: e.g.
28+
# 611.5.1.el9_7 -> (9, 7)
29+
# 570.12.1.el9_6.x86_64 -> (9, 6)
30+
# 1.0-1.el8_10 -> (8, 10)
31+
# 1.0-1.el8_6.cloudlinux -> (8, 6)
32+
_DIST_MINOR_RE = re.compile(r'\.el(\d+)_(\d+)(?:\.|$)')
33+
34+
# cloudlinux-release versions are "<major>.<minor>" - e.g. "9.6", "8.10".
35+
_RELEASE_MINOR_RE = re.compile(r'^(\d+)\.(\d+)(?:\.|$)')
36+
37+
38+
def parse_kernel_minor(release_str, target_major):
39+
"""Return the minor version embedded in a kernel RPM release string,
40+
or None if not present or not for the target major.
41+
42+
The target-major filter is critical: the target userspace's repoquery
43+
returns packages from both source (e.g. CL8) and target (e.g. CL9) repos,
44+
and dist-tagged minors must not be conflated across majors
45+
(e.g. `el8_10` minor 10 is meaningless when comparing against an
46+
`el9_*` kernel).
47+
"""
48+
if not release_str:
49+
return None
50+
m = _DIST_MINOR_RE.search(release_str)
51+
if not m:
52+
return None
53+
if int(m.group(1)) != int(target_major):
54+
return None
55+
return int(m.group(2))
56+
57+
58+
def parse_release_minor(version_str, target_major):
59+
"""Return the minor version from a cloudlinux-release RPM version field,
60+
or None if not present or not for the target major.
61+
62+
See `parse_kernel_minor` for why the major-filter matters.
63+
"""
64+
if not version_str:
65+
return None
66+
m = _RELEASE_MINOR_RE.match(version_str)
67+
if not m:
68+
return None
69+
if int(m.group(1)) != int(target_major):
70+
return None
71+
return int(m.group(2))
72+
73+
74+
def _repoquery(installroot, pkg):
75+
"""Query the target userspace container for available versions of `pkg`.
76+
77+
Returns a list of (version, release) tuples. Empty list on error or
78+
nothing available - callers treat absence as "cannot determine" rather
79+
than as evidence of safety.
80+
"""
81+
cmd = [
82+
'dnf', '-q', 'repoquery',
83+
'--installroot={}'.format(installroot),
84+
'--available',
85+
'--queryformat=%{version}|%{release}\n',
86+
pkg,
87+
]
88+
try:
89+
result = run(cmd, split=False)
90+
except (OSError, CalledProcessError) as exc:
91+
api.current_logger().warning(
92+
'repoquery for %s in %s failed: %s', pkg, installroot, exc
93+
)
94+
return []
95+
rows = []
96+
for line in (result.get('stdout') or '').splitlines():
97+
line = line.strip()
98+
if not line or '|' not in line:
99+
continue
100+
version, release = line.split('|', 1)
101+
rows.append((version, release))
102+
return rows
103+
104+
105+
def _max_kernel_minor(rows, target_major):
106+
"""Highest minor across kernel rows for the target major; None if no row matches."""
107+
minors = [parse_kernel_minor(release, target_major) for _, release in rows]
108+
minors = [m for m in minors if m is not None]
109+
return max(minors) if minors else None
110+
111+
112+
def _max_release_minor(rows, target_major):
113+
"""Highest minor across cloudlinux-release rows for the target major; None if no row matches."""
114+
minors = [parse_release_minor(version, target_major) for version, _ in rows]
115+
minors = [m for m in minors if m is not None]
116+
return max(minors) if minors else None
117+
118+
119+
def _report_inhibitor(target_major, kernel_minor, release_minor):
120+
reporting.create_report([
121+
reporting.Title(
122+
'CloudLinux target repositories are mid gradual-rollout: kernel newer than userland'
123+
),
124+
reporting.Summary(
125+
'The target CloudLinux repositories currently offer a kernel from minor el{major}_{kminor}'
126+
' while the newest available cloudlinux-release is for minor {major}.{rminor}.'
127+
' CloudLinux gradual-rollouts typically promote the kernel ahead of the rest of the'
128+
' userland, and running the upgrade now would install a kernel newer than the matching'
129+
' kmod-lve and other per-kernel-minor packages. On reboot the new default-boot kernel'
130+
' would fail to load kmodlve, breaking LVE.'.format(
131+
major=target_major, kminor=kernel_minor, rminor=release_minor,
132+
)
133+
),
134+
reporting.Remediation(
135+
hint='Re-run the upgrade once the new CloudLinux minor has finished its gradual rollout'
136+
' (target repositories should then offer a cloudlinux-release matching the kernel'
137+
' minor). Alternatively, pin the target repositories to a single, fully-released minor.'
138+
),
139+
reporting.Severity(reporting.Severity.HIGH),
140+
reporting.Groups([
141+
reporting.Groups.INHIBITOR,
142+
reporting.Groups.KERNEL,
143+
reporting.Groups.REPOSITORY,
144+
reporting.Groups.UPGRADE_PROCESS,
145+
]),
146+
])
147+
148+
149+
def process(installroot, query_fn=None, target_major=None):
150+
"""Inhibit when newest available kernel's minor exceeds newest available
151+
cloudlinux-release's minor in the target userspace's repositories.
152+
"""
153+
if query_fn is None:
154+
query_fn = _repoquery
155+
if target_major is None:
156+
target_major = get_target_major_version()
157+
158+
kernel_rows = query_fn(installroot, 'kernel-core')
159+
release_rows = query_fn(installroot, 'cloudlinux-release')
160+
161+
k_minor = _max_kernel_minor(kernel_rows, target_major)
162+
r_minor = _max_release_minor(release_rows, target_major)
163+
164+
if k_minor is None or r_minor is None:
165+
api.current_logger().info(
166+
'Skipping kernel-minor check: could not determine both minors'
167+
' (kernel=%s, release=%s)', k_minor, r_minor,
168+
)
169+
return
170+
171+
api.current_logger().info(
172+
'Target userspace: newest kernel-core minor=el%s_%s, newest cloudlinux-release minor=%s.%s',
173+
target_major, k_minor, target_major, r_minor,
174+
)
175+
if k_minor > r_minor:
176+
_report_inhibitor(target_major, k_minor, r_minor)

0 commit comments

Comments
 (0)