fix(localapi): F2 mount-role fallback — enrolled user-data drives ejectable again (v0.73.0)

roleForMountPath resolved role only from the PVE storage view; a bind-mounted
RAW enrolled user-data drive is not a PVE storage, so it fail-safe'd to system
and the eject/decommission gates 403'd EVERY user-data drive in the standard
topology (campaign F2, where=/mnt/teszt_enroll role=system). Add a mount-table
fallback mirroring durableIDForMount Impl-2b: device-keyed classification with a
whole-disk containment pass (new storage.SameWholeDisk) and the Observe-error
early return kept BEFORE the fallback (else a blind view -> permissive). Only
roleForMountPath touched. Tests A1/B1/B2/C1-C3 + 3 red-proofs; existing RoleGated
tests green unmodified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-06 20:07:45 +02:00
parent 26664d6d51
commit 7545af8a2c
4 changed files with 266 additions and 4 deletions
+40 -4
View File
@@ -1015,20 +1015,56 @@ func (s *Server) hostReader() storage.HostReader {
// roleForMountPath resolves the AUTHORITATIVE protection role of the storage mounted at `where`, from
// the agent's OWN storage view + host topology (never the caller's claim). It mirrors deviceRole but
// keys on the mount path (the eject input). It FAILS SAFE to system (most-protected) on any
// ambiguity — a view error, or no storage target found at `where` — so an unresolvable eject is
// refused rather than silently unmounted.
// ambiguity — a view error, an unrecognizable mount source, or `where` absent from both the storage
// view and the mount table — so an unresolvable eject is refused rather than silently unmounted.
func (s *Server) roleForMountPath(ctx context.Context, where string) storage.DeviceRole {
sysDisks, sysKnown := storage.SystemDisks(s.hostReader())
targets, err := s.storage.Observe(ctx)
if err != nil {
return storage.RoleSystem // can't read the view → treat as protected
// Can't read the view → protected. The mount-table fallback below must NOT run here: with
// the PVE view down its containment pass is blind, and falling through to raw classification
// could label a backup-backing device user-data — a PERMISSIVE regression.
return storage.RoleSystem
}
for _, t := range targets {
if t.MountPath == where {
return storage.RoleForStorage(t.Type, t.BackingDevice, sysDisks, sysKnown)
}
}
return storage.RoleSystem // no storage target at this mount → fail safe to protected
// Fallback (campaign F2, 2026-07-06 — mirrors durableIDForMount's Impl-2b): a RAW enrolled
// user-data drive is NOT a PVE storage, so it never appears in Observe — which made this
// function fail-safe every such mount to `system` and the eject/decommission gates 403 EVERY
// user-data drive in the standard topology (journal: where=/mnt/teszt_enroll role=system).
// Resolve the mount's backing device from the host mount table, then classify device-keyed:
// - non-/dev source (NAS "server:/export", tmpfs, …) → system (never drive-ejectable);
// - device on the same whole disk as a KNOWN storage target → THAT target's role (containment:
// felhom-usb/felhom-flash are dir storages with backup content — their disk must never
// become ejectable through this path);
// - otherwise RoleForRawDevice (system-disk membership; fail-safe to system).
mounts, merr := s.hostReader().Mounts()
if merr != nil {
return storage.RoleSystem // can't read the mount table → protected
}
for _, m := range mounts {
if m.MountPoint != where {
continue
}
if !strings.HasPrefix(m.Device, "/dev/") {
return storage.RoleSystem // network/virtual source — has its own lifecycle, protected here
}
// Containment pass over the ALREADY-FETCHED targets (never re-Observe: a racing error there
// would degrade to the permissive raw path). Compare at whole-disk granularity: targets carry
// the PARTITION as BackingDevice (a dir storage on /dev/sdb1) while a raw enrolled drive mounts
// the WHOLE disk (/dev/sdb) — SameWholeDisk normalizes both so a backup disk stays protected.
for _, t := range targets {
if t.BackingDevice != "" && storage.SameWholeDisk(t.BackingDevice, m.Device) {
return storage.RoleForStorage(t.Type, t.BackingDevice, sysDisks, sysKnown)
}
}
// No known target claims this device → classify the raw device by system-disk membership.
return storage.RoleForRawDevice(m.Device, sysDisks, sysKnown)
}
return storage.RoleSystem // no storage target and no mount-table entry → fail safe to protected
}
// deviceRole resolves a device's AUTHORITATIVE protection tier. It prefers a known storage target's