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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user