v0.115.0 — R-116: the backup-target flag reaches the row the controller keys on
Session C measured it live: a drive whose device vanished raised the GENERIC storage_disconnected while its return raised the SPECIFIC backup_target_restored -- an alarm and an all-clear an operator cannot pair. backup_target_absent never fired at all. The mechanism is not what the Session-C audit first said, and the difference decides the fix. RoleForStorage returns RoleSystem whenever backingDevice == "" (internal/storage/role.go:180-181). When the device goes, exactMountDevice fails, BackingDevice becomes "", the target row's role flips to system and it loses its guest path -- but keeps its MountPath. The union loop skips any drive whose MountPath is already seen, so the registry row is DEDUPED AWAY ENTIRELY. /disks carries no row with that guest path, so isTarget[guestPath] is a MISSING KEY, not a false. Setting BackupTarget on the union row -- the obvious fix -- could not have worked, because that row is not emitted when the alarm is needed. The audit is corrected in the same push. Fix: on the Observe row only, carry the guest path when the row IS the backup target and its role flipped because the device vanished. Three gates, verified not assumed: - t.BackingDevice == "" restricts it to the vanished-device flip; a genuinely system-BACKED storage has a real device and is excluded, so a dir storage at /mnt/<name> on the root disk cannot acquire a guest path. - Case B, the common fresh-box shape, is safe twice over: its target is the builtin local on /var/lib/vz and StablePathForRaw returns "" for anything not exactly /mnt/<name>, so nothing is set even before the gates apply. - It cannot make the gate read an absent drive as PRESENT. BoundUnderParent is assigned at exactly two sites, both inside guest-path blocks a system-role row never enters, so it stays false and planDriveGates computes false || false. Pinned by TestAbsentTargetRowDoesNotRegisterPresence -- getting this backwards would have silenced the alarm the fix exists to raise. The :213-214 boundary stands: no system or backup mount gains a guest path. Tests +5, asserting the emitted /disks JSON through a faithful copy of the controller's driveTargetByPath, because the failure class is "the value is on the wrong row". Red-proof: removing the block fails with "isTarget[...] is a MISSING KEY"; reverted byte-identical. Filed not closed: the two-row shape that produced this survives.
This commit is contained in:
@@ -1,3 +1,53 @@
|
||||
## v0.115.0 — R-116: the backup-target flag reaches the row the controller keys on (2026-07-29)
|
||||
|
||||
**The defect, measured live in Session C.** A drive whose device vanished raised the **generic**
|
||||
`storage_disconnected`, while its return raised the **specific** `backup_target_restored` — an alarm
|
||||
and an all-clear an operator cannot pair. `backup_target_absent` never fired at all.
|
||||
|
||||
**The mechanism, and it is not what the Session-C audit first said.** `RoleForStorage` returns
|
||||
`RoleSystem` whenever `backingDevice == ""` (`internal/storage/role.go:180-181`). When the device goes,
|
||||
Observe's `exactMountDevice` fails, `t.BackingDevice` becomes `""`, the target row's role flips to
|
||||
system and it **loses its guest path** — but keeps its `MountPath`. The union loop skips any drive whose
|
||||
`MountPath` is already `seen`, so the registry row is **deduped away entirely**. `/disks` ends up with
|
||||
**no row carrying that guest path**, so the controller's `isTarget[guestPath]` is a **missing key**, not
|
||||
a `false`. The obvious fix — setting `BackupTarget` on the union row — **could not have worked**: that
|
||||
row is not emitted in the state where the alarm is needed. The audit has been corrected.
|
||||
|
||||
**The fix.** On the Observe row only, carry the guest path when the row **is** the backup target **and**
|
||||
its role flipped because the device vanished:
|
||||
|
||||
```go
|
||||
if di.GuestPath == "" && di.BackupTarget && t.BackingDevice == "" {
|
||||
di.GuestPath = StablePathForRaw(t.MountPath)
|
||||
}
|
||||
```
|
||||
|
||||
**Three gates, each verified rather than assumed:**
|
||||
|
||||
- `t.BackingDevice == ""` restricts this to the vanished-device flip. A storage that is `RoleSystem`
|
||||
because it is genuinely system-**backed** has a real device and is excluded — otherwise a dir storage
|
||||
at `/mnt/<name>` on the root disk would acquire a guest path.
|
||||
- **Case B, the common fresh-box shape, is safe twice over.** Its target is the builtin `local` on
|
||||
`/var/lib/vz`, and `StablePathForRaw` returns `""` for anything that is not exactly `/mnt/<name>`
|
||||
(`DriveNameFromRaw`, `intermediary.go:79-88`) — nothing is set even before the gates apply.
|
||||
- **It cannot make the gate read an absent drive as PRESENT.** `BoundUnderParent` is assigned at exactly
|
||||
two sites (`disks.go:222`, `:280`), both inside guest-path blocks a system-role row never enters, so
|
||||
it stays `false` and `planDriveGates` computes `present[gp] = false || false`. Inert by construction —
|
||||
pinned by `TestAbsentTargetRowDoesNotRegisterPresence`. Getting this wrong would have **silenced the
|
||||
alarm this fix exists to raise**.
|
||||
|
||||
**The `:213-214` boundary stands.** No system or backup mount gains a guest path; only the drive the
|
||||
alarm is *about* keeps its identity while it is missing, and only while its device is gone.
|
||||
|
||||
**Tests** +5 in `internal/localapi`, asserting the emitted `/disks` JSON through a faithful copy of the
|
||||
controller's `driveTargetByPath`, because the failure class is "the value is on the wrong row" and a
|
||||
hand-built fixture proves nothing about which row the handler emits. Red-proof: removing the block
|
||||
fails with *"isTarget[…] is a MISSING KEY"*; reverted, byte-identical.
|
||||
|
||||
**Known limitation, filed not closed:** the two-row shape that produced this survives. The flag and the
|
||||
guest path still live on different rows in the healthy state, and nothing prevents a future consumer
|
||||
keying on the wrong one.
|
||||
|
||||
## v0.114.0 — R-113: drive presence means the DEVICE, not the bind (2026-07-29)
|
||||
|
||||
**The bug, measured live in E-2d.** `BoundUnderParent` — the one field the controller's drive-absent
|
||||
|
||||
Reference in New Issue
Block a user