v0.89.0: pbsdr self-grant (R-22) + escrow config live-reload + agent-plane poke listener (Direction-2a)

- pbsdr: on a 403 pre-check (non-default storage id, no ACL yet) self-grant via the root wrapper then re-read, instead of aborting before the grant — closes F4/R-22. Red-proof TestSelfGrant_PreCheck403DoesNotAbortBeforeGrant.
- escrow preflight: late-bound CurrentPBSStorageID re-reads agent.json so a pbsdr-seeded pbs_storage_id flips the row green in-process (no restart). Red-proof TestEscrowPreflight_PBSStorageIDLiveReload.
- internal/poke: contentless UDP poke listener bound exclusively to the box WG /32 (port 51822), leading-edge debounced, fires the hub-loop out-of-band trigger for an immediate desired-state cycle. First slice of R-13. Red-proofs TestBindConfinement + TestDebounceCoalescesBurst.
This commit is contained in:
2026-07-16 22:47:22 +02:00
parent c040c180e9
commit a659e5dc09
11 changed files with 643 additions and 15 deletions
+31 -2
View File
@@ -239,8 +239,37 @@ func (m *Manager) Apply(ctx context.Context, fetched bool, block *hub.WirePBSDR)
entry, found, err := m.px.StorageEntry(ctx, block.StorageID)
if err != nil {
m.logger.Warn("pbsdr: storage-entry read failed (transient; retrying next tick)", "err", err)
return
// R-22 self-grant (F4, tests/VALIDATION-n100-baremetal): on a NON-DEFAULT storage id the
// agent token holds no ACL on /storage/<id> yet, so this token-auth pre-check
// (GET /storage/<id>) 403s. Aborting here would deadlock permanently — the root-run wrapper
// `grant` that CREATES that very ACL is only reached further down (adoption / create paths).
// So on a 403 ONLY, run the grant now (root, no secret, no pre-existing entry required —
// `pveum acl modify` on a path is unconditional) and re-read once; the retry then flows the
// normal adoption/create path. Every OTHER error stays transient (retry next tick). The
// pre-check itself is KEPT: once the ACL exists the read succeeds and short-circuits the
// happy path cheaply — we only stop the 403 from being a first-contact dead-end.
var ae *proxmox.APIError
if !errors.As(err, &ae) || !ae.IsForbidden() {
m.logger.Warn("pbsdr: storage-entry read failed (transient; retrying next tick)", "err", err)
return
}
m.logger.Info("pbsdr: pre-check 403 (token has no ACL on this storage id yet) — self-granting via the root wrapper, then re-reading (R-22)",
"storage_id", block.StorageID)
if _, errOut, gerr := m.runner.Run(ctx, WrapperPath, "grant", block.StorageID); gerr != nil {
m.logger.Warn("pbsdr: self-grant failed (retrying next tick)", "err", gerr, "stderr", tail(errOut))
m.setStatus(&hub.PBSDRStatus{State: "verify_failed", StorageID: block.StorageID, Namespace: block.Namespace,
Message: "pre-check 403 and self-grant failed: " + gerr.Error()})
return
}
entry, found, err = m.px.StorageEntry(ctx, block.StorageID)
if err != nil {
// Grant succeeded but the read STILL fails → not the ACL bootstrap after all; surface it
// loudly rather than looping silently.
m.logger.Warn("pbsdr: storage-entry read still failing after self-grant (retrying next tick)", "err", err)
m.setStatus(&hub.PBSDRStatus{State: "verify_failed", StorageID: block.StorageID, Namespace: block.Namespace,
Message: "storage read failed even after self-grant: " + err.Error()})
return
}
}
if found && entry.Type != "pbs" {
msg := fmt.Sprintf("storage id %s exists with type %q (not pbs) — refusing to touch it", block.StorageID, entry.Type)