#!/usr/bin/env python3 """Regenerate the container-VM portion of config/encapi/nodes.yaml from a checkout of terraform-incus. encapi is the single source of truth for Puppet ENC data, but the 130 container VMs are *defined* in terraform-incus (config/instances//config.yaml). This script lifts that classification into plain config data here: certname = . (cobbler_domain = main.unkin.net) role = cobbler_mgmt_classes[0] (default roles::base) environment = production It only rewrites the "container VMs" block; the prodnxsr physical nodes below the marker are maintained by hand and left untouched. Usage: python3 ci/extract_incus_nodes.py --incus-repo /path/to/terraform-incus """ import argparse import os import sys try: import yaml except ImportError: sys.exit("PyYAML required: pip install pyyaml") PHYS_MARKER = "# --- prodnxsr physical nodes (develop) ---" VM_MARKER = "# --- container VMs (production) ---" def main(): ap = argparse.ArgumentParser() ap.add_argument("--incus-repo", required=True, help="Path to a terraform-incus checkout") ap.add_argument("--domain", default="main.unkin.net") ap.add_argument("--nodes-file", default=os.path.join(os.path.dirname(__file__), "..", "config", "encapi", "nodes.yaml")) args = ap.parse_args() inst = os.path.join(args.incus_repo, "config", "instances") dirs = sorted(d for d in os.listdir(inst) if os.path.isdir(os.path.join(inst, d)) and os.path.exists(os.path.join(inst, d, "config.yaml"))) vm_lines = [] for d in dirs: with open(os.path.join(inst, d, "config.yaml")) as f: cfg = yaml.safe_load(f) or {} classes = cfg.get("cobbler_mgmt_classes") or [] role = classes[0] if classes else "roles::base" vm_lines.append(f"{d}.{args.domain}:") vm_lines.append(f" role: {role}") vm_lines.append(" environment: production") nodes_file = os.path.abspath(args.nodes_file) with open(nodes_file) as f: existing = f.read().splitlines() # Preserve everything from the physical-nodes marker onward. try: idx = existing.index(PHYS_MARKER) except ValueError: sys.exit(f"marker not found in {nodes_file}: {PHYS_MARKER!r}") tail = existing[idx:] # Preserve only the top-of-file comment block, stopping at the VM marker. header = [] for ln in existing[:idx]: if ln == VM_MARKER: break header.append(ln) while header and header[-1] == "": header.pop() out = header + ["", VM_MARKER] + vm_lines + ["", *tail] with open(nodes_file, "w") as f: f.write("\n".join(out).rstrip("\n") + "\n") print(f"wrote {len(dirs)} container VM records to {nodes_file}") if __name__ == "__main__": main()