#!/usr/bin/env python3
"""Classify MAAS fabrics for safe pruning (pure; no mutation, no network).
Inputs are the three JSON blobs produced by:
maas <profile> fabrics read -> fabrics.json
maas <profile> subnets read -> subnets.json
maas <profile> machines read -> machines.json
SAFETY PREDICATE (delete iff ALL hold):
* fabric name matches ^fabric-[0-9]+$ (an auto-minted commissioning fabric), AND
* ZERO subnets are attached to the fabric, AND
* ZERO machine interfaces are attached to the fabric.
Everything else is kept:
* named / renamed fabrics (1_provider, 2_metal, f_oob, ...) -> never deleted;
* an auto-fabric that still carries a subnet (e.g. an LXD/substrate bridge:
10.37.x.0/24 + fd42::/64) -> never deleted (substrate, not a deploy artifact);
* an auto-fabric still holding an interface -> WAIT (a host NIC the carve has not
yet relocated); deletable only after the carve vacates it.
Output (stdout): deterministic JSON {"audit":[...], "delete_ids":[...]} sorted by
fabric id. delete_ids is the safe set.
Field shape (verified against MAAS 3.7 live output):
fabrics[].id, fabrics[].name
subnets[].vlan.fabric_id
machines[].interface_set[].vlan.fabric_id (present even for link_up-only NICs)
"""
import json
import re
import sys
AUTO = re.compile(r'^fabric-[0-9]+$')
def _count_by_fabric_subnets(subnets):
out = {}
for s in subnets:
fid = (s.get("vlan") or {}).get("fabric_id")
if fid is not None:
out[fid] = out.get(fid, 0) + 1
return out
def _count_by_fabric_ifaces(machines):
out = {}
for m in machines:
for i in (m.get("interface_set") or []):
fid = (i.get("vlan") or {}).get("fabric_id")
if fid is not None:
out[fid] = out.get(fid, 0) + 1
return out
def classify(fabrics, subnets, machines):
sub = _count_by_fabric_subnets(subnets)
iff = _count_by_fabric_ifaces(machines)
audit, delete_ids = [], []
for f in sorted(fabrics, key=lambda x: x["id"]):
fid = f["id"]
name = f.get("name", "")
ns = sub.get(fid, 0)
ni = iff.get(fid, 0)
auto = bool(AUTO.match(name))
if not auto:
verdict = "KEEP (named/default)"
elif ns > 0:
# auto-name but carries a subnet: substrate (e.g. LXD bridge) -- never delete
verdict = "KEEP auto-fabric HAS SUBNET(S) -- substrate/in-use, never delete"
elif ni > 0:
verdict = "WAIT auto-fabric in use by interface(s) -- vacate (carve) before prune"
else:
verdict = "ORPHAN -- delete"
delete_ids.append(fid)
audit.append({
"id": fid, "name": name, "subnets": ns, "ifaces": ni,
"auto": auto, "verdict": verdict,
})
return {"audit": audit, "delete_ids": delete_ids}
def _load(path):
with open(path) as fh:
return json.load(fh)
def main(argv):
if len(argv) != 4:
sys.stderr.write(
"usage: maas_fabric_classify.py <fabrics.json> <subnets.json> <machines.json>\n")
return 2
out = classify(_load(argv[1]), _load(argv[2]), _load(argv[3]))
print(json.dumps(out, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))