v0.84.0: ReassertNetworkMounts — NAS automount survives guest reboots (RCA fix 1)

Storage §8 decision table (stop + enable --now on idle triggers; active mounts untouched),
daemon leg at startup with per-running-guest visibility verify, guest-hook post-start leg
(root, direct systemctl, non-fatal). Red-proofs: always-rearm table FAIL; unwired hook FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 20:46:59 +02:00
parent 0df72ea643
commit 474b858c0b
12 changed files with 715 additions and 27 deletions
+40 -14
View File
@@ -450,12 +450,43 @@ func (h *SudoHostOps) RemoveNetworkMount(ctx context.Context, name string) error
// timeout — it NEVER stat()s the (possibly EIO/D-state) mountpoint, so a black-holed NAS cannot wedge
// this call. Best-effort: an unreadable unit dir yields an empty list.
func (h *SudoHostOps) ListNetworkMounts(ctx context.Context) ([]NetworkMountStatus, error) {
entries, err := h.networkUnitEntries()
if err != nil {
return nil, err
}
mounts := h.mountedFSTypes()
var out []NetworkMountStatus
for _, e := range entries {
st := NetworkMountStatus{
Name: e.name,
Protocol: e.proto,
Server: e.server,
Export: e.export,
Where: e.where,
Configured: true,
Mounted: isNetworkMounted(mounts[e.where]),
Reachable: endpointReachable(netEndpoint(e.proto, e.server)),
}
st.Health = networkHealth(st.Reachable, st.Mounted)
out = append(out, st)
}
return out, nil
}
// networkUnitEntry is one installed network-storage unit pair, parsed from its .mount file.
type networkUnitEntry struct {
name, proto, server, export, where string
}
// networkUnitEntries enumerates the installed network-storage unit pairs from the (world-readable)
// unit dir — the shared core of ListNetworkMounts and ReassertNetworkAutomounts. No probing, no
// mountpoint access. Best-effort per file; an unreadable unit dir is the only error.
func (h *SudoHostOps) networkUnitEntries() ([]networkUnitEntry, error) {
entries, err := os.ReadDir(h.unitDir)
if err != nil {
return nil, fmt.Errorf("netmount: reading unit dir: %w", err)
}
mounts := h.mountedFSTypes()
var out []NetworkMountStatus
var out []networkUnitEntry
for _, e := range entries {
if !strings.HasSuffix(e.Name(), ".mount") {
continue // the .mount unit carries the What/Type; the .automount mirrors Where only
@@ -468,18 +499,13 @@ func (h *SudoHostOps) ListNetworkMounts(ctx context.Context) ([]NetworkMountStat
if !ok {
continue // not one of ours (or a drive by-uuid mount)
}
st := NetworkMountStatus{
Name: strings.TrimPrefix(where, NetworkMountRoot+"/"),
Protocol: proto,
Server: server,
Export: export,
Where: where,
Configured: true,
Mounted: isNetworkMounted(mounts[where]),
Reachable: endpointReachable(netEndpoint(proto, server)),
}
st.Health = networkHealth(st.Reachable, st.Mounted)
out = append(out, st)
out = append(out, networkUnitEntry{
name: strings.TrimPrefix(where, NetworkMountRoot+"/"),
proto: proto,
server: server,
export: export,
where: where,
})
}
return out, nil
}