v0.128.0 — R-221: the escrow seed is asserted every tick, not remembered once
gates / gates (push) Successful in 20s
gates / gates (push) Successful in 20s
A rebuilt box could not run the escrow ceremony AT ALL, with no way forward from inside the product.
This was the only open item blocking a customer from something we promise them.
MECHANISM, established at file:line rather than assumed. The preflight refuses on
escrow.pbs_storage_id; the pbsdr bridge writes that key; and it wrote it from exactly one place —
finishConverged, reached only on the paths that actually converge.
The marker and the key live in different places and die at different times. The marker is host-side
(<agent-state>/pbsdr/marker.json). The key is in agent.json, which step_agent_config renders from
`base = {}` unless an explicit --preserve-from is given (felhom.eu/scripts/felhom-host-install.sh:
2396 the step, :2449 the render, :2579 the O_TRUNC write; the flag :1246, defaulting empty at :256)
— AND THE RENDER NEVER WRITES AN escrow SECTION AT ALL (grep over the whole heredoc: zero hits). So
a rebuild keeps the marker and takes the key: same descriptor, same hash, early return, and the seed
never runs again into a config that no longer has it.
A rebuild is only the case that was measured. The same hole opens for a hand-edited or restored
config, which is the honest reason this is a seam fix rather than an installer fix: the seed must be
a thing the loop ASSERTS, not a thing it did once.
Apply now re-asserts the seed BEFORE the idempotent early return. seedEscrowStorageID is unchanged
and still never clobbers a different existing value — an operator's own choice outranks the
descriptor's, with a warning naming both.
THE EARLY RETURN IS KEPT. It stops a converged box re-running Proxmox operations every 60s, and
TestSeedReasserted_OnConvergedTick_WithZeroProxmoxCalls asserts ZERO recorded runner calls on that
tick, so a "fix" that simply deleted the return would fail. Cost: one small file read plus a JSON
parse per tick, no exec, no network, early-returning once the value matches.
A seed failure can never un-converge the box: Warn plus a message on the published status, exactly
as finishConverged does it — no marker write, no state change.
Tests drive the REAL Apply with a real temp-dir agent.json and a call-recording runner; calling
seedEscrowStorageID directly cannot see the early return, which IS the defect. Production wiring
(pbsdr.NewManager(..., cfg.SourcePath, ...)) is asserted by walking main.go's AST, not by
strings.Contains, which a commented-out call also satisfies.
Red-proofs, each with the mutation asserted applied: removing the new call makes Scenario A fail on
today's tree (it did, with the intended message); removing the early return makes the
zero-Proxmox-calls assertion fail (it did).
go build / go vet / go test ./... green (29 packages), run separately from this commit.
This commit is contained in:
@@ -283,7 +283,36 @@ func (m *Manager) Apply(ctx context.Context, fetched bool, block *hub.WirePBSDR)
|
||||
h := descriptorHash(block)
|
||||
cf := m.loadConsumedFailed()
|
||||
if mk := m.loadMarker(); mk != nil && mk.Hash == h && (cf == nil || cf.Hash != h) {
|
||||
m.setStatus(&hub.PBSDRStatus{State: mk.State, StorageID: block.StorageID, Namespace: block.Namespace, AppliedAt: mk.AppliedAt})
|
||||
// R-221: RE-ASSERT THE SEED, DO NOT REMEMBER IT. The marker records that this descriptor
|
||||
// converged; it says nothing about whether the file the seed writes still exists.
|
||||
//
|
||||
// The two live in different places and die at different times. The marker is host-side
|
||||
// (`<agent-state>/pbsdr/`, markerPath above); the seed's target is `agent.json`, and the
|
||||
// installer's `step_agent_config` renders that file from `base = {}` unless an explicit
|
||||
// `--preserve-from` is passed — it NEVER writes an `escrow` section — then replaces it with
|
||||
// O_TRUNC (felhom-host-install.sh:2396, :2449, :2579; the flag is :1246, defaulting empty at
|
||||
// :256). So a rebuild leaves the marker and takes the seed, the hash still matches, this
|
||||
// branch returns, and `escrow.pbs_storage_id` is never written again. The customer then
|
||||
// cannot run the escrow ceremony AT ALL: handleEscrowPreflight fails the `pbs_storage_id`
|
||||
// row and the wizard refuses, with no way forward from inside the product.
|
||||
//
|
||||
// A rebuild is only the case that was measured. The same hole opens for a hand-edited or
|
||||
// restored config, which is the honest reason this is a seam fix rather than an installer
|
||||
// fix — the seed must be a thing the loop asserts, not a thing it did once.
|
||||
//
|
||||
// COST: this runs on the converged path, i.e. every tick (60 s) forever. It is one small
|
||||
// file read plus a JSON parse — no exec, no network, no Proxmox call — and seedEscrowStorageID
|
||||
// returns early once the value matches. That is the whole reason it is affordable here.
|
||||
//
|
||||
// IT MUST NEVER UN-CONVERGE THE BOX: a failure is a Warn plus a message on the published
|
||||
// status, exactly as finishConverged does it. No marker write, no state change, no retry
|
||||
// storm — the early return below still happens either way.
|
||||
msg := ""
|
||||
if err := m.seedEscrowStorageID(block.StorageID); err != nil {
|
||||
msg = "escrow.pbs_storage_id seed failed: " + err.Error() + " (set it manually before the ceremony)"
|
||||
m.logger.Warn("pbsdr: " + msg)
|
||||
}
|
||||
m.setStatus(&hub.PBSDRStatus{State: mk.State, StorageID: block.StorageID, Namespace: block.Namespace, AppliedAt: mk.AppliedAt, Message: msg})
|
||||
return // idempotent: this exact descriptor already converged
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user