agent v0.37.0: re-assert mounted-but-DISABLED units (live felhom-usb fix)

The skip-if-mounted optimization defeated the actual root cause: felhom-usb is
mounted now but its unit is `disabled`, so a host reboot would not auto-mount
it. ReassertEnrolledMounts now skips ONLY the durable steady state (mounted AND
enabled) via the pure shouldReassertMount; a mounted-but-disabled unit is
re-asserted so enable --now re-creates the wants-symlink. Enabled-state read by
privilege-free Lstat of the multi-user.target.wants symlink (unitEnabled) — no
systemctl is-enabled subprocess, no new sudoers entry.

Tests: TestShouldReassertMount (4 combos), TestUnitEnabled (wants-symlink).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 17:55:48 +02:00
parent a621f4c5a0
commit 3e39dbb4f8
4 changed files with 96 additions and 12 deletions
+27 -3
View File
@@ -250,8 +250,15 @@ func (h *SudoHostOps) ReassertEnrolledMounts(ctx context.Context) {
continue
}
spec, ok := parseFelhomMountUnit(string(data))
if !ok || mounted[spec.Where] {
continue // not ours, or already mounted → nothing to do (no churn)
if !ok {
continue // not one of ours
}
// Re-assert unless the drive is BOTH mounted AND its unit enabled (the durable steady state).
// A mounted-but-DISABLED unit (the live felhom-usb bug: a prior detach left it disabled, so it
// would NOT auto-mount on the next host reboot) is re-asserted too — EnsureMount's enable --now
// re-creates the wants-symlink. Skipping a mounted-but-disabled unit would leave the reboot fragile.
if !shouldReassertMount(mounted[spec.Where], h.unitEnabled(e.Name())) {
continue // mounted + enabled → no churn
}
dev, derr := ResolveStorageDevice("uuid:" + spec.UUID)
if derr != nil {
@@ -262,10 +269,27 @@ func (h *SudoHostOps) ReassertEnrolledMounts(ctx context.Context) {
h.logger.Warn("storage: re-assert enrolled mount failed", "name", spec.Name, "where", spec.Where, "err", err)
continue
}
h.logger.Info("storage: re-asserted enrolled mount by UUID (enable --now)", "name", spec.Name, "where", spec.Where, "uuid", spec.UUID, "device", dev)
h.logger.Info("storage: re-asserted enrolled mount by UUID (enable --now)", "name", spec.Name, "where", spec.Where, "uuid", spec.UUID, "device", dev, "wasMounted", mounted[spec.Where])
}
}
// shouldReassertMount decides whether an enrolled mount needs re-asserting. Re-assert unless it is
// BOTH currently mounted AND its unit enabled — the durable steady state. The mounted-but-disabled
// case is the load-bearing one: the drive serves now but its unit has no wants-symlink, so a host
// reboot would not auto-mount it; re-asserting re-enables it. Pure → unit-tested.
func shouldReassertMount(mounted, enabled bool) bool {
return !(mounted && enabled)
}
// unitEnabled reports whether a WantedBy=multi-user.target unit is enabled, by checking for its
// wants-symlink. A privilege-free os.Lstat (the systemd dirs are world-readable) — no subprocess and
// no sudoers entry, consistent with the durable-id reads. A missing symlink (or any stat error) ==
// not enabled, so the re-assert errs toward re-enabling rather than leaving a reboot fragile.
func (h *SudoHostOps) unitEnabled(unitName string) bool {
_, err := os.Lstat(filepath.Join(h.unitDir, "multi-user.target.wants", unitName))
return err == nil
}
// mountedSet returns the set of currently-active mountpoints from /proc/mounts (mountpoint is field 2).
// Best-effort: a read failure yields an empty set (every enrolled drive is then considered for
// re-assert, which EnsureMount makes idempotent).