#!/usr/bin/env python3 """Campaign 10 — deploy apps through the REAL endpoints. Mirrors the controller's own generator semantics (internal/stacks/deploy.go:870 generateValue): password:N -> N random alphanumerics hex:N -> N random BYTES, hex-encoded (2N chars) base64key:N-> "base64:" + base64(N random bytes) static:X -> X Runs on DooPlex; talks to the guest via ssh -J demo-hp + pct exec + /root/c10api.sh. Secrets are written to a 0600 manifest, never printed. """ import base64, json, os, secrets, string, subprocess, sys APPS = sys.argv[1:] or ["rallly", "homebox", "grafana", "papra"] DOMAIN = "c10.felhom.eu" HOME = os.path.expanduser("~/.config/campaign10") HOSTPW = json.load(open(os.path.join(HOME, "host-recovery.json")))["password"] def ssh(cmd, timeout=900): full = ["sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=25", "-J", "demo-hp", "root@192.168.0.105", "LC_ALL=C " + cmd] env = dict(os.environ, SSHPASS=HOSTPW, LC_ALL="C") return subprocess.run(full, capture_output=True, text=True, timeout=timeout, env=env).stdout def api(method, path, body=None): if body is None: return ssh(f"pct exec 9201 -- /root/c10api.sh {method} {path} 2>/dev/null") b64 = base64.b64encode(json.dumps(body).encode()).decode() # body travels base64 so no quoting/locale can mangle a secret return ssh("pct exec 9201 -- bash -c \"echo %s | base64 -d > /tmp/.b && " "/root/c10api.sh %s %s \\\"\\$(cat /tmp/.b)\\\" --json; shred -u /tmp/.b\" 2>/dev/null" % (b64, method, path)) def gen(spec): kind, _, param = spec.partition(":") if kind == "password": al = string.ascii_letters + string.digits return "".join(secrets.choice(al) for _ in range(int(param))) if kind == "hex": return secrets.token_bytes(int(param)).hex() if kind == "base64key": return "base64:" + base64.b64encode(secrets.token_bytes(int(param))).decode() if kind == "static": return param raise ValueError("unknown generator %r" % spec) manifest_path = os.path.join(HOME, "app-secrets.json") manifest = json.load(open(manifest_path)) if os.path.exists(manifest_path) else {} for app in APPS: raw = api("GET", f"/api/stacks/{app}/deploy-fields") try: meta = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])["data"]["metadata"] except Exception as e: print(f"{app:12s} FIELDS-FAIL {e} :: {raw[:160]!r}") continue values, classes = {}, {} for f in meta.get("deploy_fields", []): ev, typ, spec, dflt = f["env_var"], f.get("type", ""), f.get("generate", ""), f.get("default", "") if typ == "domain": values[ev] = DOMAIN elif spec: values[ev] = gen(spec) elif dflt: values[ev] = dflt elif f.get("required"): print(f"{app:12s} WARN required field {ev} has no default/generator") if typ in ("secret", "password"): classes[ev] = {"type": typ, "data_key": bool(f.get("data_key")), "label": f.get("label", "")} manifest[app] = {"values": values, "secret_classes": classes, "subdomain": values.get("SUBDOMAIN", ""), "domain": DOMAIN} out = api("POST", f"/api/stacks/{app}/deploy", {"values": values}) ok = '"ok":true' in out print(f"{app:12s} deploy -> {'OK' if ok else 'FAIL'} " f"secret={sum(1 for c in classes.values() if c['type']=='secret')} " f"password={sum(1 for c in classes.values() if c['type']=='password')} " f"data_key={sum(1 for c in classes.values() if c['data_key'])}") if not ok: print(" ", out.strip()[:300]) with open(manifest_path, "w") as fh: json.dump(manifest, fh, indent=1) os.chmod(manifest_path, 0o600) print("\nsecrets + secret-class map written to", manifest_path, "(0600, not printed)")