100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Drive one box's controller the way the dashboard does (restore-holes night, 2026-10-24).
|
|
|
|
Usage (the password comes ONLY from the environment, through `sec run`, and is never printed):
|
|
sec run TESTER1_PASSWORD -- python3 t1.py tester-1 login
|
|
python3 t1.py tester-1 api GET /api/stacks
|
|
python3 t1.py tester-1 form POST /backups/restore stack_name=metube snapshot_id=unit
|
|
python3 t1.py tester-1 guest '<bash script>' # inside the customer guest
|
|
python3 t1.py tester-1 host '<bash script>' # on the box's Proxmox host
|
|
Session cookie + CSRF token live in the session scratchpad (0600), never in the repo.
|
|
"""
|
|
import os, re, subprocess, sys, secrets as _s
|
|
|
|
SC = os.environ.get("T1_SCRATCH", "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/63b6c84e-0c09-4657-a160-855d2e4fe326/scratchpad/t1")
|
|
os.makedirs(SC, mode=0o700, exist_ok=True)
|
|
BOXES = {
|
|
"tester-1": {"ssh": ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "root@192.168.0.154"], "guest": "9201",
|
|
"domain": "enkicsifelhom.hu", "pwenv": "TESTER1_PASSWORD"},
|
|
"9202": {"ssh": ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "demo-hp"], "guest": "9202",
|
|
"domain": "enkisfelhom.hu", "pwenv": "REHEARSAL_2026_10_20_9202_PW"},
|
|
}
|
|
box = BOXES[sys.argv[1]]
|
|
cmd = sys.argv[2]
|
|
HOST = f"Host: felhom.{box['domain']}"
|
|
pre = f"{SC}/{sys.argv[1]}"
|
|
|
|
|
|
def run(args, inp=None, timeout=900):
|
|
return subprocess.run(args, input=inp, capture_output=True, text=True, errors="replace", timeout=timeout)
|
|
|
|
|
|
def host(script, timeout=900):
|
|
return run(box["ssh"] + ["bash -s"], inp=script, timeout=timeout)
|
|
|
|
|
|
def guest(script, timeout=900):
|
|
t = f"/tmp/rh-{os.getpid()}-{_s.token_hex(4)}.sh"
|
|
g = box["guest"]
|
|
return run(box["ssh"] + [f"cat > {t}; pct push {g} {t} {t}; pct exec {g} -- bash {t}; pct exec {g} -- rm -f {t}; rm -f {t}"],
|
|
inp=script, timeout=timeout)
|
|
|
|
|
|
def base():
|
|
r = guest("ip -4 -o addr show eth0")
|
|
m = re.search(r"inet (\d+\.\d+\.\d+\.\d+)/", r.stdout)
|
|
if not m:
|
|
sys.exit("no guest address: " + (r.stderr or r.stdout)[-200:])
|
|
return "https://" + m.group(1)
|
|
|
|
|
|
def login():
|
|
pw = os.environ.get(box["pwenv"])
|
|
if not pw:
|
|
sys.exit(f"{box['pwenv']} is not in the environment — run through `sec run {box['pwenv']} --`")
|
|
b = base()
|
|
hdr = f"{pre}.hdr"
|
|
run(["curl", "-sk", "-D", hdr, "-o", "/dev/null", "-H", HOST, "-X", "POST", "--data-urlencode", f"password={pw}", f"{b}/login"])
|
|
h = open(hdr).read()
|
|
os.remove(hdr)
|
|
m = re.search(r"felhom_session=[A-Za-z0-9._-]+", h, re.I)
|
|
if not m:
|
|
sys.exit("login failed: no session cookie (" + h.splitlines()[0] + ")")
|
|
r = run(["curl", "-sk", "-L", "-H", HOST, "-H", f"Cookie: {m.group(0)}", f"{b}/"])
|
|
c = re.search(r'<meta name="csrf-token" content="([^"]+)"', r.stdout or "")
|
|
if not c:
|
|
sys.exit("login failed: no csrf token")
|
|
for name, val in (("sess", m.group(0)), ("csrf", c.group(1)), ("base", b)):
|
|
fd = os.open(f"{pre}.{name}", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
os.write(fd, val.encode())
|
|
os.close(fd)
|
|
print("login ok", b)
|
|
|
|
|
|
def call(method, path, form=None, json_body=None):
|
|
sess, csrf, b = (open(f"{pre}.{n}").read().strip() for n in ("sess", "csrf", "base"))
|
|
a = ["curl", "-sk", "-H", HOST, "-H", f"Cookie: {sess}", "-w", "\n%{http_code}"]
|
|
if method != "GET":
|
|
a += ["-X", method, "-H", f"X-CSRF-Token: {csrf}"]
|
|
if form is not None:
|
|
a += ["--data-urlencode", f"_csrf={csrf}"]
|
|
for kv in form:
|
|
a += ["--data-urlencode", kv]
|
|
if json_body is not None:
|
|
a += ["-H", "Content-Type: application/json", "--data", json_body]
|
|
r = run(a + [b + path])
|
|
print(r.stdout)
|
|
|
|
|
|
if cmd == "login":
|
|
login()
|
|
elif cmd == "api":
|
|
call(sys.argv[3], sys.argv[4], json_body=sys.argv[5] if len(sys.argv) > 5 else None)
|
|
elif cmd == "form":
|
|
call(sys.argv[3], sys.argv[4], form=sys.argv[5:])
|
|
elif cmd in ("guest", "host"):
|
|
r = (guest if cmd == "guest" else host)(sys.argv[3])
|
|
sys.stdout.write(r.stdout)
|
|
sys.stderr.write(r.stderr[-2000:])
|
|
sys.exit(r.returncode)
|