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:
@@ -0,0 +1,117 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Network-mount guest-reboot reassert (RCA AUDIT-nas-cwa-rca-2026-07-11 fix 1).
|
||||
//
|
||||
// THE BUG IT FIXES: a fresh guest namespace inherits REAL submounts of the shared parent (ext4, or
|
||||
// an actively-mounted nfs4) but NOT an idle autofs trigger — so after any guest reboot an idle NAS
|
||||
// share silently degrades to a local stub directory inside the guest. The heal is host-side and
|
||||
// host-global: re-creating the .automount unit emits a FRESH trigger-mount event, which propagates
|
||||
// live into every running guest's slave bind (live-proven in the RCA remediation, 2026-07-11).
|
||||
//
|
||||
// The action uses the sudoers-granted verbs only (`systemctl stop -- *.automount` +
|
||||
// `systemctl enable --now -- *.automount`; there is NO restart grant). Idempotent: re-arming an
|
||||
// already-armed trigger just recreates it — same end state, and the fresh mount event is harmless.
|
||||
// An ACTIVE real mount is never touched (stopping the automount of a live mount would churn it).
|
||||
|
||||
// Reassert actions (the §8 decision table, encoded).
|
||||
const (
|
||||
// NetReassertRearmed: the trigger was re-created (stop + enable --now) — the propagation heal.
|
||||
NetReassertRearmed = "rearmed"
|
||||
// NetReassertSkipActive: a real network fs is mounted at the path — inherited by fresh
|
||||
// namespaces, nothing to do (verify only).
|
||||
NetReassertSkipActive = "skip-active"
|
||||
// NetReassertSkipNone: neither a real mount nor an armed trigger at the path — a removed or
|
||||
// orphan state owned by the add/remove flows, not this reconcile.
|
||||
NetReassertSkipNone = "skip-none"
|
||||
)
|
||||
|
||||
// NetReassertResult is one share's outcome in a reassert pass.
|
||||
type NetReassertResult struct {
|
||||
Name string // share name (mount dir basename)
|
||||
Where string // guest-visible mountpoint (/mnt/felhom-drives/<name>)
|
||||
Action string // NetReassert* constant
|
||||
Err error // set when the rearm action failed (skip rows never error)
|
||||
}
|
||||
|
||||
// netReassertAction is the pure §8 decision: the /proc/mounts fstype at the share's mountpoint
|
||||
// ("" = nothing mounted there) → the action to take.
|
||||
func netReassertAction(fstype string) string {
|
||||
switch {
|
||||
case isNetworkMounted(fstype):
|
||||
return NetReassertSkipActive
|
||||
case fstype == "autofs":
|
||||
return NetReassertRearmed
|
||||
default:
|
||||
return NetReassertSkipNone
|
||||
}
|
||||
}
|
||||
|
||||
// ReassertNetworkAutomounts runs the reassert pass over every configured network mount: for each
|
||||
// installed pair, decide per netReassertAction and re-arm idle triggers. Returns one result per
|
||||
// share so callers (agent startup / guest-hook post-start) can verify guest visibility. Errors on
|
||||
// one share never stop the pass. Callers MUST NOT invoke this from periodic health paths — an idle
|
||||
// trigger is healthy, and the pass is only needed after a guest (re)start or at agent startup.
|
||||
func (h *SudoHostOps) ReassertNetworkAutomounts(ctx context.Context) []NetReassertResult {
|
||||
entries, err := h.networkUnitEntries()
|
||||
if err != nil {
|
||||
h.logger.Warn("netreassert: unit enumeration failed", "err", err)
|
||||
return nil
|
||||
}
|
||||
fstypes := map[string]string{}
|
||||
if h.host != nil {
|
||||
if mounts, merr := h.host.Mounts(); merr == nil {
|
||||
for _, m := range mounts {
|
||||
fstypes[m.MountPoint] = m.FSType
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("netreassert: mount-table read failed — treating all shares as unmounted", "err", merr)
|
||||
}
|
||||
}
|
||||
var out []NetReassertResult
|
||||
for _, e := range entries {
|
||||
res := NetReassertResult{Name: e.name, Where: e.where, Action: netReassertAction(fstypes[e.where])}
|
||||
switch res.Action {
|
||||
case NetReassertSkipActive:
|
||||
h.logger.Debug("netreassert: share actively mounted — skip (fresh namespaces inherit real mounts)",
|
||||
"name", e.name, "where", e.where)
|
||||
case NetReassertSkipNone:
|
||||
h.logger.Debug("netreassert: no mount and no armed trigger — skip (removed/orphan state owned elsewhere)",
|
||||
"name", e.name, "where", e.where)
|
||||
case NetReassertRearmed:
|
||||
if err := h.rearmNetworkAutomount(ctx, e.where); err != nil {
|
||||
res.Err = err
|
||||
h.logger.Warn("netreassert: trigger re-arm failed", "name", e.name, "where", e.where, "err", err)
|
||||
} else {
|
||||
h.logger.Info("netreassert: automount trigger re-armed (fresh mount event propagates into running guests)",
|
||||
"name", e.name, "where", e.where)
|
||||
}
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// rearmNetworkAutomount stops then re-enables+starts the .automount for a mountpoint. The stop is
|
||||
// tolerated failing (unit not loaded); the enable --now is the action that must succeed. Both verbs
|
||||
// are the existing FELHOM_NETMOUNT sudoers grants.
|
||||
func (h *SudoHostOps) rearmNetworkAutomount(ctx context.Context, where string) error {
|
||||
mountUnit, err := UnitNameForMount(where)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netreassert: unit name for %s: %w", where, err)
|
||||
}
|
||||
automountUnit := strings.TrimSuffix(mountUnit, ".mount") + ".automount"
|
||||
if err := h.run(ctx, h.bins.Systemctl, "stop", "--", automountUnit); err != nil {
|
||||
// Tolerated: a not-loaded unit still enables cleanly below.
|
||||
h.logger.Debug("netreassert: automount stop tolerated failure", "unit", automountUnit, "err", err)
|
||||
}
|
||||
if err := h.run(ctx, h.bins.Systemctl, "enable", "--now", "--", automountUnit); err != nil {
|
||||
return fmt.Errorf("netreassert: enabling automount %s: %w", automountUnit, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user