Files
felhom.eu/documentation/audits/os-updates-spike-2026-10-04/scripts/sim-approved.py
T

62 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""sim-approved.py <approved.tsv> <layer> — READ-ONLY. Against throwaway apt indexes (no system list touched),
classify this machine's pending updates against an approved list: what the fast lane WOULD install (exact
approved version, downloadable now), what is approved but not downloadable, and what is NOT COVERED."""
import subprocess, sys, tempfile, os, re, shutil
approved_path, layer = sys.argv[1], sys.argv[2]
approved = {}
for l in open(approved_path):
if l.startswith('#') or not l.strip():
continue
lay, pkg, ver, origin = l.rstrip('\n').split('\t')
if lay == layer:
approved[pkg] = (ver, origin)
env = dict(os.environ, LC_ALL='C')
t = tempfile.mkdtemp(prefix='simapproved.')
os.makedirs(t + '/lists/partial'); os.makedirs(t + '/cache/archives/partial')
A = ['-o', 'Dir::State::Lists=' + t + '/lists', '-o', 'Dir::Cache=' + t + '/cache', '-o', 'Debug::NoLocking=1']
subprocess.run(['apt-get'] + A + ['-q', 'update'], env=env, capture_output=True)
sim = subprocess.run(['apt-get'] + A + ['-s', 'dist-upgrade'], env=env, capture_output=True, text=True).stdout
pending = {}
for l in sim.splitlines():
m = re.match(r'^Inst (\S+) (?:\[([^]]*)\] )?\((\S+) (.*?) \[[a-z0-9]+\]\)', l)
if m:
pending[m.group(1)] = (m.group(2) or '(new)', m.group(3), m.group(4))
def downloadable(pkg, ver):
out = subprocess.run(['apt-cache'] + A + ['madison', pkg], env=env, capture_output=True, text=True).stdout
return any(f.split('|')[1].strip() == ver for f in out.splitlines() if f.count('|') >= 2)
def installed(pkg):
r = subprocess.run(['dpkg-query', '-W', '-f=${Version}', pkg], env=env, capture_output=True, text=True)
return r.stdout if r.returncode == 0 else None
def newer(a, b): # a > b ?
return subprocess.run(['dpkg', '--compare-versions', a, 'gt', b]).returncode == 0
would, gone, already, notinst = [], [], [], []
for pkg, (ver, origin) in sorted(approved.items()):
inst = installed(pkg)
if inst is None:
notinst.append(pkg); continue
if not newer(ver, inst):
already.append(pkg); continue
(would if downloadable(pkg, ver) else gone).append((pkg, inst, ver))
notcov = [(p, v) for p, v in sorted(pending.items()) if p not in approved]
shutil.rmtree(t)
print(f"# layer {layer} on {os.uname().nodename}: {len(approved)} approved, {len(pending)} pending here")
print(f"WOULD INSTALL (exact approved version, downloadable now): {len(would)}")
for p, i, v in would: print(f" {p} {i} -> {v}")
print(f"APPROVED BUT NOT DOWNLOADABLE NOW: {len(gone)}")
for p, i, v in gone: print(f" {p} {i} -> {v} (gone)")
print(f"approved, already at or above: {len(already)} approved, not installed here: {len(notinst)} {' '.join(notinst)}")
by = {}
for p, (old, new, origin) in notcov:
by.setdefault(origin.split(',')[0].split(':')[0], []).append(f"{p} {old} -> {new}")
print(f"NOT COVERED (pending here, no approved entry): {len(notcov)}")
for o, ps in sorted(by.items()):
print(f" [{o}] {len(ps)}")
for x in ps: print(" " + x)