#!/usr/bin/env python3 """deploy.py [ ...] — Part B4: install an app on the Tester 1 box the way the deploy wizard does: GET /api/stacks//deploy-fields, POST /api/stacks//deploy, then wait for the deploy's own completion mark (the pin in app.yaml), as box_walk.deploy does. Values: DOMAIN, the app's default SUBDOMAIN, HDD_PATH = drive A (the drive the household would pick; the drill templates bind both drives by plain path and do not read it), and a generated value for each password field — kept ONLY in the 0600 file ~/.felhom-tester1/secrets.json (never printed, never in evidence). box_walk.deploy is not used because its deploy_values makes a folder on the scratch box's drive path, which does not exist here. Live-probes rule: never send a deploy for an app that is not meant to be installed. This script refuses an app that is already installed and installs only the apps named on the command line. """ import json, os, secrets, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import box_walk as w HDD = os.environ.get("HDD", "/mnt/felhom-drives/media_a") SEC = os.path.join(os.environ.get("SC", os.path.expanduser("~/.felhom-tester1")), "secrets.json") def load(): try: return json.load(open(SEC)) except FileNotFoundError: return {} def save(d): fd = os.open(SEC, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w") as f: json.dump(d, f) w.login() for app in sys.argv[1:]: st = w.stack(app) if st.get("deployed"): w.say(f"{app}: already installed — not touched") continue c, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields") fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or [] vals, gen = {"DOMAIN": w.DOMAIN}, [] for f in fields: ev, ty = f.get("env_var"), f.get("type") if ev == "DOMAIN": continue if ev == "SUBDOMAIN": vals[ev] = f.get("default") or st.get("meta", {}).get("subdomain") or app elif ev == "HDD_PATH": vals[ev] = HDD elif ty == "password": vals[ev] = "T1-" + secrets.token_urlsafe(15) gen.append(ev) elif f.get("required"): sys.exit(w.say(f"{app}: required field {ev} ({ty}) has no rule here — STOP, not guessing") or 1) s = load(); s[app] = {k: vals[k] for k in gen}; save(s) shown = {k: ("" if k in gen else v) for k, v in vals.items()} c, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": vals}) w.say(f"{app}: POST deploy {json.dumps(shown)} -> {c} {json.dumps(d, ensure_ascii=False)[:200]}") if c != "202": continue for _ in range(120): time.sleep(5) st = w.stack(app) pins = (st.get("app_config") or {}).get("pinned_images") if st.get("deployed") and pins and st.get("state") in ("running", "unhealthy", "degraded"): w.say(f"{app}: deployed, state={st.get('state')}, pinned={pins}") break else: w.say(f"{app}: never became deployed (state={st.get('state')})") print(w.guest(f"docker inspect {app} -f '{{{{.State.Status}}}} {{{{range .Mounts}}}}{{{{.Source}}}}->{{{{.Destination}}}}:{{{{.RW}}}} {{{{end}}}}'; " f"docker logs --since 10m felhom-controller 2>&1 | grep -i '{app}' | grep -v DEBUG | grep -iE 'after_install|deploy|error|warn' | tail -8"))