Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R-669 live (controller v0.270.0, guest 9202, drill catalog): the recovery unit keeps the PINNED version's
|
||||
.felhom.yml, and a restore makes the restored file the applied record.
|
||||
|
||||
1. wishlist installed from the drill (probe port 3000 → applied-meta 3000).
|
||||
2. drill: a failing step (image v0.67.1 → latest + probe 8999). The sync flows 8999 into the stack dir's
|
||||
.felhom.yml; the applied record keeps 3000. Update → its own backup captures the unit → the step fails →
|
||||
undone. CHECK A: the unit's compose/.felhom.yml says 3000 (v0.269.1 captured the stack dir's 8999).
|
||||
3. drill: image back, probe still 8999 (the stack file keeps flowing 8999). The second drive's restore
|
||||
(`POST /backup/tier2/unit-restore`). CHECK B: applied-meta == the restored file == 3000.
|
||||
Evidence goes to felhom.eu/documentation/audits/r672-2026-09-24/D/.
|
||||
"""
|
||||
import json, re, sys, time
|
||||
sys.path.insert(0, ".")
|
||||
import walk as w
|
||||
import chaos as c
|
||||
|
||||
APP, SUB = "wishlist", "r669-wish"
|
||||
EV = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/r672-2026-09-24/D"
|
||||
out = {}
|
||||
w.login()
|
||||
|
||||
|
||||
def ports():
|
||||
r = w.guest("""cd /opt/docker/stacks/wishlist
|
||||
echo "stack: $(grep -m1 -A4 'type: api' .felhom.yml | grep -m1 port:)"
|
||||
echo "applied: $(grep -m1 -A4 'type: api' applied-meta/.felhom.yml 2>/dev/null | grep -m1 port:)"
|
||||
u=$(ls -d /mnt/sys_drive/felhom-data/backups/primary/wishlist/compose 2>/dev/null || ls -d /mnt/felhom-drives/*/backups/primary/wishlist/compose 2>/dev/null | head -1)
|
||||
echo "unit: $(grep -m1 -A4 'type: api' $u/.felhom.yml 2>/dev/null | grep -m1 port:) ($u)"
|
||||
""")
|
||||
return r.strip()
|
||||
|
||||
|
||||
print("deploy:", w.deploy(APP, SUB))
|
||||
out["1-installed"] = ports()
|
||||
w.say("[1] " + out["1-installed"].replace("\n", " | "))
|
||||
|
||||
c.break_step(APP) # image → :latest + probe 8999
|
||||
w.sync_rescan(expect_app=APP, expect_ref="ghcr.io/cmintey/wishlist:latest", tries=60, delay=5)
|
||||
out["2-before-update"] = ports()
|
||||
w.say("[2] " + out["2-before-update"].replace("\n", " | "))
|
||||
out["2-update"] = w.press_update(APP)
|
||||
out["2-after-update"] = ports()
|
||||
w.say("[2] after: " + out["2-after-update"].replace("\n", " | "))
|
||||
out["CHECK_A_unit_keeps_pinned_probe"] = "unit:" in out["2-after-update"] and "port: 3000" in out["2-after-update"].split("unit:")[1].split("(")[0]
|
||||
|
||||
# back to the head image, but leave the drill probe at 8999 so the stack file keeps the bad one
|
||||
comp = f"{w.DRILL}/templates/{APP}/docker-compose.yml"
|
||||
c.drill(lambda: open(comp, "w").write(open(comp).read().replace("image: ghcr.io/cmintey/wishlist:latest", "image: ghcr.io/cmintey/wishlist:v0.67.1", 1)),
|
||||
"wishlist image back to v0.67.1 (probe left at 8999)")
|
||||
w.sync_rescan()
|
||||
time.sleep(3)
|
||||
w.sync_rescan()
|
||||
out["3-before-restore"] = ports()
|
||||
w.say("[3] " + out["3-before-restore"].replace("\n", " | "))
|
||||
sess = open(f"{w.SC}/sess{__import__('os').getpid()}.txt").read().strip()
|
||||
csrf = open(f"{w.SC}/csrf{__import__('os').getpid()}.txt").read().strip()
|
||||
r = w.sh(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-X", "POST",
|
||||
"--data-urlencode", f"_csrf={csrf}", "--data-urlencode", f"stack_name={APP}", f"{w.BASE}/backup/tier2/unit-restore"], timeout=120)
|
||||
w.say(f"[3] POST /backup/tier2/unit-restore -> {r.stdout}")
|
||||
last = None
|
||||
for _ in range(300):
|
||||
code, dd = w.ctl("GET", "/api/backup/restore-status")
|
||||
d2 = (dd.get("data") or {}) if isinstance(dd, dict) else {}
|
||||
if not d2.get("running") and d2.get("last"):
|
||||
last = d2["last"]
|
||||
break
|
||||
time.sleep(2)
|
||||
out["3-restore"] = last
|
||||
w.say(f"[3] restore: {str(last)[:240]}")
|
||||
out["3-after-restore"] = ports()
|
||||
w.say("[3] after: " + out["3-after-restore"].replace("\n", " | "))
|
||||
a = out["3-after-restore"]
|
||||
out["CHECK_B_restore_resets_applied"] = "applied: " in a and "port: 3000" in a.split("applied:")[1].split("\n")[0]
|
||||
out["log"] = w.guest("docker logs --since 15m felhom-controller 2>&1 | grep -iE 'wishlist' | grep -iE 'applied|UNDO|undone|Restore-from-unit|pin ' | grep -v DEBUG | tail -10")
|
||||
w.say("CHECK A (unit keeps the pinned probe): %s CHECK B (restore resets the applied record): %s" % (out["CHECK_A_unit_keeps_pinned_probe"], out["CHECK_B_restore_resets_applied"]))
|
||||
json.dump(out, open(f"{EV}/30-r669-live.json", "w"), indent=2, ensure_ascii=False, default=str)
|
||||
open(f"{EV}/30-r669-live.log", "w").write("\n".join(w.LOG) + "\n")
|
||||
Reference in New Issue
Block a user