v0.114.0 — R-113: drive presence means the DEVICE, not the bind
BoundUnderParent, the one field the controller's drive-absent gate keys on,
reported only "is this path a mount target in the guest's mountinfo". The
drive's raw mount at /mnt/<name> is a systemd mount unit bound to its device and
dies with it, but the agent's own bind of <raw>/felhom-data under the shared
parent is an ordinary bind: nothing ties it to the device, so its mountinfo
entry OUTLIVES the device as a stale shell. Presence read that survivor as true,
planDriveGates never produced a Stop action, and nothing fired on any channel --
not backup_target_absent, not the generic storage_disconnected. Measured live in
E-2d: detached at 10:58:37Z, silent for 4.5 minutes while the agent itself
logged "enrolled drive absent by UUID" every 20s (felhom.eu
audits/E2D-fresh-vm-2026-07-29.md §5.2).
The fix: BoundUnderParent becomes a CONJUNCTION -- bound under the parent AND
the drive's raw host mount still mounted (devicePresent, new deviceCheck seam
defaulting to isHostMountpoint). Applied at BOTH /disks construction sites. The
union path matters more, not less: it hardcodes State:"attached", so the
raw-mount check is the only device truth that row carries, and it is exactly the
shape E-2d detached.
Why a conjunction and not a replacement: half 2 alone would regress boot
ordering, where the raw drive mounts early and the bind lands ~18s later; the
gate depends on that window reading ABSENT. The conjunction leaves that
byte-identical and closes only the case the gate could never see.
Unknown is never absent: devicePresent("") returns TRUE. A false absent stops a
working customer's apps -- the failure mode of this fix, not of the bug.
Controller UNCHANGED, no MinAgent bump. BoundUnderParent has exactly one
functional consumer (planDriveGates, intermediary.go:226); every other mention
in both repos is a comment or a test, and boot convergence deliberately moved
off it to pollLiveBinds/driveBindLive. The alternative -- a new DevicePresent
bool the controller ANDs in -- was rejected as dangerous: a bool absent from an
older agent's JSON decodes to false, so every drive on a pre-0.114.0 agent would
have read ABSENT and stopped its apps.
Tests +6 in internal/localapi (208 -> 214): groups A-D plus a wire-contract test
asserting the ENCODED bound_under_parent, since that is what crosses to the
controller. Four red-proofs run and reverted (drop the conjunction on each path;
invert unknown; drop the bind half); disks.go verified byte-identical after.
NOT LIVE-VALIDATED. No drive was pulled. Leg awaiting Session C: device loss ->
gate Stop -> SetDisconnected -> backup_target_absent on the wire.
This commit is contained in:
@@ -152,9 +152,17 @@ type DiskInfo struct {
|
||||
// storage path. Set for /mnt/<name> drives; "" otherwise. Distinct from MountPath (the RAW host PVE
|
||||
// mount the agent ops on).
|
||||
GuestPath string `json:"guest_path,omitempty"`
|
||||
// BoundUnderParent reports whether the drive's felhom-data is currently bound under the shared parent
|
||||
// at GuestPath (a host mount-table check) — i.e. live + usable in the guest in the intermediary model.
|
||||
// BoundUnderParent reports whether the drive is live + usable in the guest in the intermediary model.
|
||||
// The controller's drive-absent gate + auto-restart key on this (and State).
|
||||
//
|
||||
// It is a CONJUNCTION of two facts, and both halves are load-bearing (R-113, v0.114.0):
|
||||
// 1. felhom-data is bound under the shared parent at GuestPath (the guest-visible mount check), and
|
||||
// 2. the drive's RAW host mount is still mounted — i.e. the DEVICE is still there.
|
||||
// Half 1 alone was the bug: the raw mount is device-bound and dies with its device, but the agent's
|
||||
// own bind is not, so half 1 stays true over a stale shell after the device is pulled. The controller
|
||||
// read that survivor as "present" and the drive-absent alarm could never fire — measured live in E-2d
|
||||
// (felhom.eu audits/E2D-fresh-vm-2026-07-29.md §5.2). Half 2 alone would regress boot ordering, where
|
||||
// the raw mounts early and the bind lands ~18s later; the conjunction keeps that window reading absent.
|
||||
BoundUnderParent bool `json:"bound_under_parent"`
|
||||
// Smart is the already-computed per-disk SMART health summary (v0.94.0), serialized here so the
|
||||
// controller can render a disk-health card + degradation alert WITHOUT any new smartctl load — the
|
||||
@@ -207,7 +215,12 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
if di.Role == string(storage.RoleUserData) {
|
||||
if gp := StablePathForRaw(t.MountPath); gp != "" {
|
||||
di.GuestPath = gp
|
||||
di.BoundUnderParent = s.boundUnderParent(r.Context(), vmid, gp)
|
||||
// R-113: AND in device presence. A conjunction, deliberately — it leaves the
|
||||
// boot-ordering behaviour the controller's gate depends on exactly as it was
|
||||
// (raw mounted early, bind not yet ⇒ still absent) while closing the case the
|
||||
// gate could never see (bind outlived the device ⇒ now absent).
|
||||
di.BoundUnderParent = s.boundUnderParent(r.Context(), vmid, gp) &&
|
||||
s.devicePresent(t.MountPath)
|
||||
}
|
||||
}
|
||||
// Inspect the backing device for the UI's data-bearing hint (the authoritative check
|
||||
@@ -260,7 +273,12 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
// exactly like the Observe path — else the controller reads a registry drive as "Leválasztva".
|
||||
if gp := StablePathForRaw(d.MountPath); gp != "" {
|
||||
di.GuestPath = gp
|
||||
di.BoundUnderParent = s.boundUnderParent(r.Context(), vmid, gp)
|
||||
// R-113: same conjunction as the Observe path. This path matters MORE, not less —
|
||||
// a registry drive with no PVE dir-storage is exactly the shape E-2d detached, and
|
||||
// its State is hardcoded "attached" below, so the raw-mount check is the only
|
||||
// device truth this row carries.
|
||||
di.BoundUnderParent = s.boundUnderParent(r.Context(), vmid, gp) &&
|
||||
s.devicePresent(d.MountPath)
|
||||
}
|
||||
// A registry drive has no PVE `pvesm status` snapshot, so fill backing device + capacity
|
||||
// from the host directly: resolve the device by fs-UUID, and statfs the mount for size —
|
||||
@@ -860,6 +878,29 @@ func (s *Server) boundUnderParent(ctx context.Context, vmid int, stablePath stri
|
||||
return s.guestAttach.GuestSeesMount(ctx, vmid, stablePath)
|
||||
}
|
||||
|
||||
// devicePresent reports whether the drive's BACKING DEVICE is still there, by asking whether its RAW
|
||||
// host mount is still a mountpoint (R-113).
|
||||
//
|
||||
// WHY THE RAW MOUNT AND NOT THE BIND. The raw mount at /mnt/<name> is a systemd mount unit bound to
|
||||
// its device: when the device goes, the unit stops and the mountpoint disappears. The agent's own bind
|
||||
// of <raw>/felhom-data under the shared parent is an ordinary bind — nothing ties it to the device, so
|
||||
// its mountinfo entry OUTLIVES the device as a stale shell. Measured live in E-2d with the device
|
||||
// pulled: `/mnt/mentes2` NOT mounted while `/mnt/felhom-drives/mentes2` still read
|
||||
// `/dev/sdb[/felhom-data]` (felhom.eu audits/E2D-fresh-vm-2026-07-29.md §5.2). Keying presence on the
|
||||
// survivor is exactly why the controller's drive-absent gate could never fire.
|
||||
//
|
||||
// An empty raw path means we have nothing to ask about — return TRUE (unknown), never false. Absent
|
||||
// stops a customer's apps, so "cannot tell" must never be reported as "gone".
|
||||
func (s *Server) devicePresent(rawMountPath string) bool {
|
||||
if rawMountPath == "" {
|
||||
return true // cannot tell → never claim absent
|
||||
}
|
||||
if s.deviceCheck != nil {
|
||||
return s.deviceCheck(rawMountPath)
|
||||
}
|
||||
return isHostMountpoint(rawMountPath)
|
||||
}
|
||||
|
||||
// guestBoundPaths returns the set of guest mountpoint paths (the `mp=` of each entry in the guest's
|
||||
// config) — i.e. the host drives actually BOUND into the guest. F9: this is the guest-attached signal
|
||||
// (`GuestAttached`) that distinguishes a guest-usable drive from one merely present on the host. A bind
|
||||
|
||||
Reference in New Issue
Block a user