d4eb259da2
v0.116.0 validated on a fresh box: real day-0 from the v1.25.0 ISO on a nested PVE on demo-hp (per runbooks/target-selection.md), agent installed unaided from the vouched Day-0 manifest, drives enrolled through the real endpoints, device loss a real hot-detach. 07:20:04 backup_target_absent (error) Cel meghajto <- TARGET, specific 07:22:34 backup_target_restored (info) Cel meghajto <- its matching pair 07:24:04 storage_disconnected (error) Adat meghajto <- NON-target, generic 07:25:34 storage_reconnected (info) Adat meghajto All four reached the hub; gate fired in 3 s. Discrimination is proven NON-trivially for the first time -- both prior runs had the target itself emit the generic event, so their mirror proved nothing. Over-correction passes on a POSITIVE observable: 0 ABSENT lines and 0 drive events over 2m14s with both drives present, while two RETURNED lines prove the gate was ticking rather than dead. Caveat recorded, not a regression: the drill's controller was 0.185.1 from the golden, which predates R-114, so its absent-state banner showed the old false "backup is on the system disk" copy. The R-114 guard is pinned by unit test and by the payload, but could not be confirmed on that box. Filed as R-120 -- the golden is a release behind the deployed controller, which is R-115's class one layer up. Teardown all three layers, including the hub: VM purged, storage removed with the space measured back, hub records gate-blocked on ONLINE with the command recorded. Suite rc=0 read separately from this commit.
144 lines
9.6 KiB
Markdown
144 lines
9.6 KiB
Markdown
# REPORT — v0.116.0: R-116, the flag and the key share a row (2026-07-30)
|
|
|
|
**Overwritten** per the standing rule. Agent-only fix: the controller was **not** touched (one fix, one
|
|
place) and `v0.115.0` was **not** reverted. Mechanism evidence: `felhom.eu`
|
|
`audits/DIAG-r116-disks-payload-2026-07-30.md`.
|
|
|
|
## Baselines as actually running (not as on `main`)
|
|
|
|
| | `main` | running | a FRESH box gets (hub Day-0 manifest) |
|
|
|---|---|---|---|
|
|
| agent | 0.115.0 → **0.116.0** | felhom-pve **0.115.0**, demo-hp **0.113.0** | **0.115.0** |
|
|
| controller | 0.186.0 | felhom-pve **0.186.0**, demo-hp **0.185.1** | golden bakes **0.185.1** |
|
|
| hub | 0.81.0 | 0.81.0 | `min_agent` 0.113.0, `min_controller` 0.156.0 |
|
|
| `felhom.eu` | `1aa1bd1` | — | — |
|
|
|
|
**The fleet is not uniform on either component.** The golden baking controller **0.185.1** while
|
|
demo-felhom runs **0.186.0** is load-bearing for the ruling below: a controller-side fix would not reach
|
|
a fresh box without a re-bake, whereas the agent channel already serves the newest published version.
|
|
|
|
## The ruling: neither (a) nor (b) — a hybrid, because both regress R-114
|
|
|
|
`primaryTargetID` is a PVE storage name (`felhom-backup`); the registry row's `Name` is the drive UUID.
|
|
With the device gone the two records of one drive share **no runtime field** — no mount, no backing
|
|
device, and the Observe row's `DurableID` has degraded off the fs-UUID to `path:/mnt/cel`.
|
|
|
|
**Rejected (a) — back-fill `MountPath` on the Observe row.** The spec's lean, and it does re-activate
|
|
v0.115.0. Rejected on source evidence: `backup_target_offer.go:79` reads
|
|
`d.BackupTarget && d.MountPath != ""` as *"a real drive with its own mountpoint — healthy"* and returns
|
|
**before** its `TargetAbsent` branch. Option (a) manufactures exactly that row while the drive is
|
|
missing, so it would have silently flipped **R-114** — shipped 2026-07-29, proven live — back to a false
|
|
*healthy* banner. Also `MountPath`'s own contract is "the RAW host PVE mount" (`disks.go:152-153`); a path
|
|
that is not mounted is not that, so the downstream reading is correct and the change would be the bug.
|
|
|
|
**Rejected (b) — carry `BackupTarget` on the registry row.** A join **does** exist (below), so this was
|
|
available — but the registry row's `MountPath` is non-empty, read from the by-then-stale `.mount` unit
|
|
file. It trips `:79` identically. Same regression, same reason.
|
|
|
|
**Rejected (c) — resolve controller-side.** The mapping is not there to find, and the *one state, one
|
|
owner* ruling stands: the agent owns drive identity.
|
|
|
|
**Taken (d) — give the Observe row the guest path (and NO MountPath), then dedup the registry row on
|
|
guest path.** The gate keys on **GuestPath**; R-114 keys on **MountPath**. Separating them satisfies both:
|
|
the surviving row carries the flag and the guest path with `mount_path: ""`, so `isTarget[guestPath]` is
|
|
`true` **and** `:79` does not match, leaving R-114's `TargetAbsent` branch reachable.
|
|
|
|
**The join:** with the device gone the shared identity is **configuration**, not state — the storage's
|
|
`path` from `storage.cfg` (Observe side) and the `.mount` unit's `Where` (registry side) are the same
|
|
path, so both derive the same stable guest path. That is the key both sides can still compute.
|
|
|
|
**What (d)'s dedup removes in the absent state**, enumerated: the registry row's `uuid:` `durable_id`, its
|
|
hardcoded and **false** `state: "attached"`, its `role: user-data`, and its `total_bytes`/`used_bytes`
|
|
copied from the **root** filesystem. No consumer found — wipe/decommission require the drive present;
|
|
`firstOfferableDrive` (`:138`) already requires `MountPath != ""` **and** `Role == "user-data"`; `diskKey`
|
|
(`disk_health.go:130`) changes key transiently but the health check excludes UNKNOWN verdicts, so no false
|
|
degradation. **R-118's symptom disappears incidentally — R-118 is NOT fixed and stays open.**
|
|
|
|
**Deliberately unchanged:** `Role` (the absent target still reads `system`), the `BoundUnderParent`
|
|
conjunction, and every wire field.
|
|
|
|
## Files
|
|
|
|
`internal/hub/report.go` (`ConfigPath`, **`json:"-"`** — that struct is a cross-repo contract pinned by
|
|
`testdata/host-report.golden.json` + `contract_test.go`'s key-set comparison, so a wire-visible field
|
|
would have needed a matching `felhom.eu/hub` change; nothing off-box needs the value) ·
|
|
`internal/storage/observe.go` (set from `s.Path`) · `internal/localapi/disks.go` (ConfigPath fallback +
|
|
guest-path dedup) · `internal/localapi/disks_backup_target_row_test.go` (fixture corrected + 4 tests) ·
|
|
`CHANGELOG.md`.
|
|
|
|
`observe.go`'s change is **not** the fallthrough its `:176-183` comment forbids: that prohibition is about
|
|
resolving a *device or UUID* from the **containing** filesystem, which would hand back root's identity and
|
|
mis-target a DR re-attach. `s.Path` is the storage's own declaration, identifies nothing but itself, and is
|
|
used for no resolution. `MountPath` stays empty, which is the truth.
|
|
|
|
## §2 reconfirmation
|
|
|
|
| Cited | Line now | Holds |
|
|
|---|---|---|
|
|
| `mountPath` assigned only on exactMount success | `observe.go:184-190` | **HOLDS** verbatim |
|
|
| empty `backingDevice` ⇒ `RoleSystem` | `role.go:180-181` | **HOLDS** verbatim |
|
|
| guest-path guard skipped for non-user-data | `disks.go:215-217` | **HOLDS** |
|
|
| `Known()` reads unit FILES, never the mount table | `registry_known.go:40-75`, `MountPath: spec.Where` at `:70` | **HOLDS** |
|
|
| `driveTargetByPath` keys only from non-empty fields | `intermediary.go:602-618` (`:611`, `:614`) | **HOLDS** |
|
|
| the dedup keys on `d.MountPath` at `:263` | `:263` is `if t.BackingDevice != ""`. Actual: `seen` built `:290-295`, dedup `:298` | **HOLDS; the line number in the spec is wrong** |
|
|
| "`seen[""]` is set, so the union row is not skipped" | **DOES NOT HOLD as stated** — `seen[d.MountPath] = true` is guarded by `if d.MountPath != ""`, so `seen` never gains `""`. The union row survives because `/mnt/<name>` is *absent* from `seen`, not because `""` is present. Same outcome, different reason | corrected |
|
|
|
|
## Tests — 845 → 849, suite green (`go build ./... && go vet ./... && go test ./...` rc=0)
|
|
|
|
All PASS: `TestAbsentBackupTargetIsResolvableByGuestPath` (rewritten — real shape **and** the union loop),
|
|
`TestAbsentTargetAppearsExactlyOnce` (new, the join), `TestAbsentTargetKeepsR114DegradedSignal` (new, the
|
|
regression guard), `TestPresentTargetPayloadUnchanged` (new, fleet-wide parity),
|
|
`TestAbsentNonTargetKeepsItsRegistryRowAndNoFlag` (new, negative + over-suppression),
|
|
`TestAbsentTargetRowDoesNotRegisterPresence`, `TestCaseBLocalTargetGetsNoGuestPath`,
|
|
`TestSystemBackedTargetUnderMntGetsNoGuestPath`, `TestNonTargetDriveNeverCarriesTheFlag`.
|
|
|
|
**Red-proofs — each mutation asserted to have landed before the run:**
|
|
|
|
1. v0.115.0's MountPath-only fallback → `isTarget[…] = FALSE … rows=2`. **The empirical proof v0.115.0
|
|
was inert**, rather than an argument for it.
|
|
2. Drop the guest-path dedup → `the absent drive is carried by 2 rows, want exactly 1`.
|
|
3. Give the absent row a `MountPath` (what option (a) produces) → the R-114 guard fires.
|
|
4. Over-broad dedup (`gp != ""` without `seenGuest`) → the non-target drive loses its own row, and two
|
|
pre-existing R-113 union tests fail as well.
|
|
|
|
**The corrected fixture is the headline test change.** `theAbsentTarget` supplied
|
|
`MountPath: "/mnt/mentes"`, which production never supplies, and `targetRowServer` left `DriveTargets`
|
|
**nil**, so the union loop never ran and the two-row shape was invisible to the entire suite. Both fixed —
|
|
that combination is why an inert fix shipped green.
|
|
|
|
## Process note
|
|
|
|
One red-proof was restored with `git checkout <file>`, which discarded the whole implementation rather
|
|
than the one-line mutation. Re-applied and re-verified; later red-proofs restored from pristine copies
|
|
kept outside the repo. `git checkout` is not an undo for a mutation on a work-in-progress file.
|
|
|
|
## LIVE VALIDATION — all claims PASS (`felhom.eu` `audits/R116-v0116-2026-07-30.md`)
|
|
|
|
Fresh box, real day-0 from the v1.25.0 ISO on a nested PVE on **demo-hp** (per `runbooks/target-selection.md`),
|
|
agent **installed unaided from the vouched Day-0 manifest**, both drives enrolled through the real endpoints,
|
|
device loss a real hot-detach.
|
|
|
|
```
|
|
07:20:04 backup_target_absent (error) Cel meghajto ← TARGET, specific (was generic through v0.115.0)
|
|
07:22:34 backup_target_restored (info) Cel meghajto ← its matching pair
|
|
07:24:04 storage_disconnected (error) Adat meghajto ← NON-target, generic, same box 4 min later
|
|
07:25:34 storage_reconnected (info) Adat meghajto
|
|
```
|
|
|
|
All four at the hub; gate fired in 3 s. **Discrimination proven non-trivially for the first time.**
|
|
Over-correction passes on a positive observable (0 ABSENT lines / 0 drive events over 2m14s with both drives
|
|
present, target `degraded:false`, while two `RETURNED` lines prove the gate ticked). Publish observables:
|
|
registry GET of the published bytes matched `b47c5c4d…`, the manifest was read back showing 0.116.0, and the
|
|
box reported it running.
|
|
|
|
**One caveat, not a regression:** the drill's controller was **0.185.1** from the golden, which predates
|
|
R-114, so its absent-state banner showed the old false "the backup is on the system disk" copy. The R-114
|
|
guard is pinned by unit test and by the payload (no row combines the flag with a mount path) but could not be
|
|
confirmed on that box. Filed as **R-120** — the golden is a release behind, which is R-115's class one layer up.
|
|
|
|
## Teardown — all three §13 layers
|
|
|
|
VM 9401 destroyed `--purge`; `r116-images` removed and the space measured back (`felhom-backup` 928787076 KiB
|
|
available vs 928787080 before); hub records **gate-blocked** — the cascade was attempted and correctly refused
|
|
`HTTP 409 host … is ONLINE`, with the command recorded for once it ages to DOWN. Secrets shredded.
|