Files
felhom.eu/documentation/audits/media-all-drives-2026-10-14/tools/deploy.py
T

74 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""deploy.py <app> [<app> ...] — Part B4: install an app on the Tester 1 box the way the deploy wizard does:
GET /api/stacks/<app>/deploy-fields, POST /api/stacks/<app>/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: ("<generated, kept in secrets.json>" 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"))