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/) 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 }