Newer
Older
openstack-caracal-ipv4 / scripts / d057-bundle-check.py
#!/usr/bin/env python3
"""
d057-bundle-check.py -- focused, fail-closed QA for the D-057 provider-vip split.

Asserts ONLY the D-057 end-state invariants on a Charmed-OpenStack bundle:
  1. exactly 11 API charms bind public -> provider-vip; none remain on provider-public
  2. every clustered VIP is a triple: provider-vip(10.12.8/22) admin(10.12.12/22)
     internal(10.12.16/22), all sharing one last octet in 50-60
  3. ovn-chassis bridge-interface-mappings has the 3 chassis MACs and NOT openstack0's

This does NOT re-review the whole bundle. scripts/review-bundle.py predates D-052
(it forbids the per-endpoint bindings D-052 added) and is not a current gate -- see
docs/design-decisions.md / the end-of-deployment review note. FAIL -> exit 1.
ASCII-only output.
"""
import sys, re, ipaddress
try:
    import yaml
except ImportError:
    sys.stderr.write("ERROR: PyYAML not installed (pip install pyyaml --break-system-packages)\n"); sys.exit(2)

PVIP     = ipaddress.ip_network("10.12.8.0/22")
ADMIN    = ipaddress.ip_network("10.12.12.0/22")
INTERNAL = ipaddress.ip_network("10.12.16.0/22")
OCTET_LO, OCTET_HI = 50, 60
EXPECT_PUBLIC_VIP  = 11
OPENSTACK0_MAC     = "52:54:00:3d:fd:54"
EXPECT_CHASSIS_MACS = {"52:54:00:9d:63:77", "52:54:00:89:7f:ce", "52:54:00:99:fc:c2"}
MAC_RE = re.compile(r"[0-9a-f]{2}(?::[0-9a-f]{2}){5}")

def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
    try:
        doc = yaml.safe_load(open(path, encoding="utf-8"))
    except Exception as e:
        sys.stderr.write("ERROR: cannot parse %s: %s\n" % (path, e)); return 2
    apps = (doc or {}).get("applications", {}) or {}
    fails, oks = [], []

    def pub(s): return ((s or {}).get("bindings", {}) or {}).get("public")
    vip_old = sorted(n for n, s in apps.items() if pub(s) == "provider-public")
    vip_new = sorted(n for n, s in apps.items() if pub(s) == "provider-vip")
    if vip_old:
        fails.append("public still on provider-public: %s" % ", ".join(vip_old))
    if len(vip_new) != EXPECT_PUBLIC_VIP:
        fails.append("public->provider-vip count=%d (expect %d): %s" % (len(vip_new), EXPECT_PUBLIC_VIP, ", ".join(vip_new)))
    else:
        oks.append("%d charms bind public->provider-vip; none on provider-public" % len(vip_new))

    vip_ok = 0
    for n, s in apps.items():
        vip = ((s or {}).get("options", {}) or {}).get("vip")
        if not vip:
            continue
        parts = str(vip).split()
        if len(parts) != 3:
            fails.append("%s vip not a triple: %r" % (n, vip)); continue
        prov, adm, intr = parts
        try:
            okp = ipaddress.ip_address(prov) in PVIP
            oka = ipaddress.ip_address(adm) in ADMIN
            oki = ipaddress.ip_address(intr) in INTERNAL
        except ValueError as e:
            fails.append("%s bad vip ip: %s" % (n, e)); continue
        if not okp: fails.append("%s provider leg %s not in %s" % (n, prov, PVIP)); continue
        if not oka: fails.append("%s admin leg %s not in %s" % (n, adm, ADMIN)); continue
        if not oki: fails.append("%s internal leg %s not in %s" % (n, intr, INTERNAL)); continue
        octs = {p.split(".")[-1] for p in parts}
        if len(octs) != 1:
            fails.append("%s vip octets differ: %r" % (n, vip)); continue
        o = int(octs.pop())
        if not (OCTET_LO <= o <= OCTET_HI):
            fails.append("%s vip octet .%d outside %d-%d" % (n, o, OCTET_LO, OCTET_HI)); continue
        vip_ok += 1
    if vip_ok:
        oks.append("%d clustered VIP(s) are provider-vip/admin/internal triples, octet 50-60" % vip_ok)

    for n, s in apps.items():
        if (s or {}).get("charm") != "ovn-chassis":
            continue
        bim = str(((s or {}).get("options", {}) or {}).get("bridge-interface-mappings", ""))
        if not bim:
            continue
        macs = set(MAC_RE.findall(bim.lower()))
        if OPENSTACK0_MAC in macs:
            fails.append("%s still maps openstack0 MAC %s (should be trimmed)" % (n, OPENSTACK0_MAC))
        missing = EXPECT_CHASSIS_MACS - macs
        if missing:
            fails.append("%s missing chassis MAC(s): %s" % (n, ", ".join(sorted(missing))))
        if OPENSTACK0_MAC not in macs and not missing:
            oks.append("%s bridge-interface-mappings: 3 chassis MACs present, openstack0 trimmed" % n)

    for o in oks:   print("  [ok]   %s" % o)
    for f in fails: print("  [FAIL] %s" % f)
    print("\n%s: D-057 bundle invariants (%s)" % ("PASS" if not fails else "FAIL", path))
    return 1 if fails else 0

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