ep0-copy-gc: read ep0's real {"data": [...]} namespace answer; any other shape aborts (found on the first live dry run, guard held)
gates / gates (push) Successful in 4m31s
gates / gates (push) Successful in 4m31s
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,5 @@
|
||||
2026-10-09T05:25:03Z
|
||||
sync=Synced health=Healthy rev=702e19ee582f1883b3622388cef3717c4128eaf4
|
||||
image=gitea.dooplex.hu/admin/felhom-hub:0.144.0
|
||||
hub-6b44759c56-cvh6c gitea.dooplex.hu/admin/felhom-hub:0.144.0 ready=true
|
||||
2026/10/09 07:24:22 [INFO] felhom-hub 0.144.0 starting
|
||||
@@ -0,0 +1,10 @@
|
||||
== agent_config_update 0.154.0 (bundle 93487989…) signed with felhom-op-1, ttl 45m, 2026-10-09T05:17:56Z
|
||||
-- demo-hp-bb76ea
|
||||
signed: op=agent_config_update host=demo-hp-bb76ea guest="" key_id=felhom-op-1 nonce=4dd19f519d257d7b994015ddd9418138 expires=2026-10-09T06:02:56Z
|
||||
uploaded signed op to the hub jobs queue
|
||||
-- demo-felhom-8363b5
|
||||
signed: op=agent_config_update host=demo-felhom-8363b5 guest="" key_id=felhom-op-1 nonce=535a6c27b1161d683462a17a893d03ab expires=2026-10-09T06:02:56Z
|
||||
uploaded signed op to the hub jobs queue
|
||||
-- tester-1-d70be4 2026-10-09T05:25:17Z
|
||||
signed: op=agent_config_update host=tester-1-d70be4 guest="" key_id=felhom-op-1 nonce=375fee9b045ba506074be0951d11f6ae expires=2026-10-09T06:10:17Z
|
||||
uploaded signed op to the hub jobs queue
|
||||
@@ -0,0 +1,6 @@
|
||||
== DRY RUN 2026-10-09T05:26:53Z (installed from felhom.eu 702e19ee)
|
||||
ep0-copy-gc: ABORT — 4 namespaces absent on ep0 at once (limit 2): ep0's list looks partial (a rebuild?), not like customer deletions; nothing recorded, nothing deleted. Absent: Tester-2, demo-felhom, demo-hp, tester-1
|
||||
rc=2
|
||||
-- copy namespaces on disk: Tester-2 demo-felhom demo-hp operator tester-1
|
||||
-- state file:
|
||||
cat: /var/lib/felhom-ep0-copy-gc/absent.json: No such file or directory
|
||||
@@ -1,3 +1,13 @@
|
||||
## 2026-10-09 — ep0-copy-gc reads ep0's real namespace answer (found on the first live dry run)
|
||||
|
||||
- `scripts/ep0-copy-gc/felhom-ep0-copy-gc`: `proxmox-backup-client namespace list --output-format json` answers
|
||||
`{"data": [{"ns": ""}, {"ns": "demo-hp"}, …]}`; the job iterated the wrapper object and read `data` as ep0's only
|
||||
namespace. On DooPlex's first dry run (operator present) all four customer namespaces read „absent" and the
|
||||
mass-absence guard stopped it — nothing recorded, nothing deleted. With two or fewer customers it would have started
|
||||
their 7-day clock. Now any shape but `{"data": [{"ns": str}, …]}` aborts as „could not tell". The test fake printed a
|
||||
bare list (a model of the tool, not the tool) and now prints the measured shape; `test_ep0_unknown_shape_aborts`
|
||||
(red-proved: 4 failures with the real shape before the fix).
|
||||
|
||||
## facebook — fb_probe: a deleted post answers (#10) „Object does not exist" (2026-10-08, fixed without a row)
|
||||
|
||||
- The first write run proved a text post gone only on code 100; Meta v26.0 answers a deleted scheduled post with
|
||||
|
||||
@@ -76,10 +76,17 @@ def ep0_namespaces():
|
||||
r = pbc(["namespace", "list", "--repository", EP0_REPO, "--output-format", "json"], EP0_TOKEN_FILE, EP0_FINGERPRINT_FILE)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError("ep0 namespace list failed (rc %d): %s" % (r.returncode, r.stderr.strip()[-200:]))
|
||||
# The real answer is {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} (measured on DooPlex 2026-10-09). Any other
|
||||
# shape is „could not tell": the first version iterated the wrapper object, read „data" as ep0's only namespace,
|
||||
# and was stopped on its first dry run only by the mass-absence guard. Pinned by test_ep0_unknown_shape_aborts and
|
||||
# by the fake, which now prints the real shape.
|
||||
doc = json.loads(r.stdout or "null")
|
||||
items = doc.get("data") if isinstance(doc, dict) else None
|
||||
if not isinstance(items, list) or not all(isinstance(i, dict) and isinstance(i.get("ns"), str) for i in items):
|
||||
raise RuntimeError("ep0 namespace list has an unexpected shape: %s" % (r.stdout or "").strip()[:120])
|
||||
out = set()
|
||||
for item in json.loads(r.stdout or "[]"):
|
||||
name = item.get("ns", "") if isinstance(item, dict) else str(item)
|
||||
top = name.split("/")[0]
|
||||
for item in items:
|
||||
top = item["ns"].split("/")[0]
|
||||
if top:
|
||||
out.add(top)
|
||||
return out
|
||||
|
||||
@@ -24,7 +24,12 @@ with open(os.environ["FAKE_LOG"], "a") as f:
|
||||
if a[:2] == ["namespace", "list"]:
|
||||
if os.environ.get("FAKE_EP0_FAIL"):
|
||||
sys.stderr.write("connection refused\n"); sys.exit(255)
|
||||
print(json.dumps([{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])]))
|
||||
if os.environ.get("FAKE_EP0_RAW"):
|
||||
print(os.environ["FAKE_EP0_RAW"]); sys.exit(0)
|
||||
# the REAL shape, measured on DooPlex 2026-10-09: {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} -- a root entry
|
||||
# and a wrapper object. The first fake printed a bare list, so the tests passed while the job read „data" as
|
||||
# ep0's only namespace on the real tool (stopped live only by the mass-absence guard).
|
||||
print(json.dumps({"data": [{"ns": ""}] + [{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])]}))
|
||||
sys.exit(0)
|
||||
if a[:2] == ["namespace", "delete"]:
|
||||
sys.exit(0)
|
||||
@@ -112,6 +117,14 @@ class EndToEnd(unittest.TestCase):
|
||||
self.assertEqual(r.returncode, 2)
|
||||
self.assertEqual(self.deletes(), [])
|
||||
|
||||
def test_ep0_unknown_shape_aborts(self):
|
||||
# an answer the job cannot read is „could not tell", never „these customers are gone"
|
||||
self.seed("gone-cust", "2026-01-01")
|
||||
for raw in ('{"result": [{"ns": "demo-hp"}]}', '["demo-hp"]', '{"data": "x"}'):
|
||||
r = self.run_gc("--apply", FAKE_EP0_RAW=raw)
|
||||
self.assertEqual(r.returncode, 2, raw)
|
||||
self.assertEqual(self.deletes(), [], raw)
|
||||
|
||||
def test_ep0_unreachable_aborts(self):
|
||||
self.seed("gone-cust", "2026-01-01")
|
||||
r = self.run_gc("--apply", FAKE_EP0_FAIL="1")
|
||||
|
||||
Reference in New Issue
Block a user