Newer
Older
openstack-caracal-ipv4 / scripts / channel_assert.py
@JANeumatrix JANeumatrix 10 hours ago 2 KB Patches
#!/usr/bin/env python3
"""
channel_assert.py -- assert every charm channel pinned in the bundle EXISTS on
Charmhub (DOCFIX-073; fulfils the D-002 "verify against Charmhub before each
deploy" claim, which pre-flight never actually implemented).

For each unique charm->channel in bundle.yaml, runs `juju info <charm>` and
checks the pinned channel appears in the published channel map. Catches: typo'd
channels, tracks retired upstream, and pins drifting from what Charmhub serves.

Exit: 0 all pins exist | 1 a pinned channel is absent (do NOT deploy)
    | 2 charmhub/juju unreachable for one or more charms (verify manually).
Read-only. ASCII + LF. Usage: python3 scripts/channel_assert.py [bundle.yaml]
"""
import sys, subprocess, re
try:
    import yaml
except ImportError:
    sys.stderr.write("ERROR: PyYAML required\n"); sys.exit(2)

def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
    try:
        apps = (yaml.safe_load(open(path)) or {}).get("applications", {}) or {}
    except Exception as e:
        print("  [FAIL] cannot parse %s: %s" % (path, e)); return 1
    pins = {}
    for name, a in apps.items():
        ch = (a or {}).get("channel"); charm = (a or {}).get("charm")
        if ch and charm:
            pins.setdefault((str(charm), str(ch)), []).append(name)
    fails, warns = [], []
    for (charm, channel), users in sorted(pins.items()):
        # capture-then-test (house SIGPIPE rule); juju info output lists channels
        # as '  <track>/<risk>: <rev> ...' lines
        try:
            r = subprocess.run(["juju", "info", charm], capture_output=True,
                               text=True, timeout=60)
            out = r.stdout + r.stderr
        except Exception as e:
            warns.append("%s: juju info failed (%s) -- verify %s manually" % (charm, e, channel))
            continue
        if r.returncode != 0 or "channels:" not in out:
            warns.append("%s: no channel map returned -- verify %s manually (offline?)" % (charm, channel))
            continue
        if re.search(r"(?m)^\s+%s:\s" % re.escape(channel), out):
            print("  [ok]   %-22s %-16s (%s)" % (charm, channel, ", ".join(users)))
        else:
            fails.append("%s pinned to %s but Charmhub does not publish it (apps: %s)"
                         % (charm, channel, ", ".join(users)))
    for w in warns: print("  [WARN] %s" % w)
    for f in fails: print("  [FAIL] %s" % f)
    v = "FAIL" if fails else ("WARN" if warns else "PASS")
    print("\n%s: channel assert (%d pins, %d fail, %d warn)" % (v, len(pins), len(fails), len(warns)))
    return 1 if fails else (2 if warns else 0)

if __name__ == "__main__":
    sys.exit(main())