[policies] Prefer most specific policy when multiple match - #4397
[policies] Prefer most specific policy when multiple match#4397asadawar wants to merge 1 commit into
Conversation
|
Congratulations! One of the builds has completed. 🍾 You can install the built RPMs by following these steps:
Please note that the RPMs should be used only in a testing environment. |
pmoravec
left a comment
There was a problem hiding this comment.
Nice idea, to pick us as-most-childish class as possible (well, written it this way, it sounds silly :) ).
|
For some reason, The reason is one "distro branch" (ubuntu/debian) is described in two different files. Maybe we need to merge ubuntu and debian distros into one file, or populate |
| for policy in import_policy(module): | ||
| if policy.check(remote=remote_check): | ||
| cache['policy'] = policy(sysroot=sysroot, init=init, | ||
| matches = [policy for policy in import_policy(module) |
There was a problem hiding this comment.
As described in a comment, debian module match skips ubuntu module completely, so we should move this outside for module .. loop. Like:
import sos.policies.distros
helper = ImporterHelper(sos.policies.distros)
matches = []
for module in helper.get_modules():
matches.extend([policy for policy in import_policy(module)
if policy.check(remote=remote_check)])
if matches:
matches.sort(key=lambda p: len(p.__mro__), reverse=True)
cache['policy'] = matches[0](sysroot=sysroot, init=init,
probe_runtime=probe_runtime,
remote_exec=remote_exec)
This works on Ubuntu, I havent tested it anywhere else.
This code assumes a system is detected to be applicable to a list of *Policy that constitute a chain, but not a tree with different branches. I.e. once the system is below, say, DebianPolicy, it can never ever be detected by RedHatPolicy or its children.
There was a problem hiding this comment.
Thanks @pmoravec, good catch on the Ubuntu/Debian cross-module case. Updated to collect matches across all modules before sorting, as you suggested.
When multiple policies within the same module return True from check(), the policy loader now selects the most specific one (deepest in the class hierarchy) instead of whichever happens to sort first alphabetically. Previously, import_policy() returned classes sorted by name via inspect.getmembers(), and load() picked the first match. This caused RHELPolicy to always win over RedHatCoreOSPolicy inside a toolbox container on RHCOS, because uppercase 'H' sorts before lowercase 'e' in ASCII. Both policies return True in that context (RHEL for the container's /etc/redhat-release, RHCOS for the host's /host/etc/os-release), but the more specific RHCOS policy was never reached. This has existed since RedHatCoreOSPolicy was introduced in 2019 (commit fa06bc0) but was never visible because RHCOS had no behavioral differences from RHEL until the archive naming change in commit 0e919b6. The fix collects all matching policies from a module and sorts by MRO depth (descending), so a subclass is always preferred over its parent. This is safe because a subclass that returns True from check() is by definition a more precise match than its parent. Assisted-by: Claude Code <https://claude.ai/code> Signed-off-by: asadawar <asadawar@users.noreply.github.com>
10c90ae to
0e3713a
Compare
| if policy.check(remote=remote_check)]) | ||
| if matches: | ||
| matches.sort(key=lambda p: len(p.__mro__), reverse=True) | ||
| cache['policy'] = matches[0](sysroot=sysroot, init=init, |
There was a problem hiding this comment.
This error is not a regression introduced by the patch, but a newly discovered issue that was present in previous code as well.
Learning more on this error, it really can cause issues in future(*) - e.g. in multi-threaded collector that collects sosreports from systems with different policies. Assume (very artificial, but very evident) use case sos collect is invoked for RHEL and Ubuntu system. Then:
- Thread 1 (RHEL node): checks cache, not found, starts loading
RHELPolicy - Thread 2 (Ubuntu node): checks cache, not found, starts loading
UbuntuPolicy - Thread 1:
cache['policy'] = RHELPolicy(...) - Thread 2:
cache['policy'] = UbuntuPolicy(...)← overwrites! - Thread 1: returns
UbuntuPolicyfor RHEL node ← wrong!
(*) Currently, we always call the load method either from single-threaded execution, or with explicit cache={}:
host = load(cache={}, sysroot=self.opts.sysroot, ...) # sosnode.py line 417
So the code is safe now. Still we should fix the (legitimate, though not applicable today) CodeQL error warning.
We can either:
- add some thread locking in place, to be really safe (imho redundantly complex for us)
- remove cache default value and compute it every time from scratch (adds a tiny runtime)
- document the current intentional usage properly - both in
loadmethod and insosnode.pymethod invocation, and suppress this CodeQL error
Any preference here?
Problem
When multiple policies within the same module both return
Truefromcheck(), the policy loader selects whichever class sorts first alphabetically viainspect.getmembers(). This causesRHELPolicyto always win overRedHatCoreOSPolicyinside a toolbox container on RHCOS, because uppercaseHsorts before lowercaseein ASCII.Both policies return
Truein that context: RHEL matches the container's/etc/redhat-release, and RHCOS matches the host's$HOST/etc/os-release. ButRHELPolicyis iterated first and the loader breaks on first match.This has existed since
RedHatCoreOSPolicywas introduced in 2019 (commit fa06bc0) but was never visible because RHCOS had no behavioral differences from RHEL. After the archive naming change in commit 0e919b6,RedHatCoreOSPolicy.get_archive_name()includes the OCP node role and cluster name, but it is never called because the RHEL policy is selected instead.Reproduction
Inside a toolbox container on an RHCOS node:
Output:
The loader picks
RHELPolicy(first match) and never reachesRedHatCoreOSPolicy.Fix
Collect all matching policies from a module and sort by MRO depth (descending), so a subclass is always preferred over its parent. A subclass that returns
Truefromcheck()is by definition a more precise match.This is safe for all existing policy pairs:
os_release_fileandos_release_idvalues, so they never both match alongside RHELTruefromcheck()when its parent should be preferred insteadAlternative considered
Move
RedHatCoreOSPolicyto a separate module file (e.g.coreos.py), similar to how Google Container-Optimized OS hascos.py. Module files are sorted alphabetically byImporterHelper.get_modules(), andcoreossorts beforeredhat, so RHCOS would be checked first. This avoids changing the loader but requires updating test imports (from sos.policies.distros.redhat import RedHatCoreOSPolicy) and could confuse the naming with the existingcos.py(Google COS).Test
Verified on OCP 4.22.4, RHCOS 9.8, toolbox with sos-4.11.2-2.el9_8:
RedHatCoreOSPolicy.check()returns True_get_node_role()returns correct role (e.g.worker)_get_cluster_name()returns correct cluster name (e.g.sharedocp422)sosreport-<hostname>-<date>-<rand>.tar.xzRelated: RHEL-188753
Assisted-by: Claude Code https://claude.ai/code
Signed-off-by: Abhijeet Sadawarte asadawar@redhat.com