package storage import ( "context" "os" "path/filepath" "strings" ) // ForgetRetiredDrives (R-1124) removes the host mount unit of every DECOMMISSIONED drive that is physically gone, so a // later host boot no longer waits for it and the agent stops listing it (RegistryKnownTargets and // ReassertEnrolledMounts both enumerate the units — no unit, no entry, no „absent by UUID" line every 20 s). // // Only a drive that is BOTH decommissioned (the operator's or the customer's permanent removal) AND absent (its // /dev/disk/by-uuid link does not resolve) AND not mounted is forgotten. A decommissioned drive that is still plugged in // keeps its unit and its raw host mount — that is what makes the one-click re-enroll (H3) work, and decommission stays a // logical retire (`03` §storage). The intent record stays `decommissioned`: a forgotten drive that comes back is a new // candidate, never auto-mounted. The root half (`felhom-priv-apply unit-forget`) re-checks every one of these conditions // and refuses otherwise. Called at agent start, on the 20-second storage tick, and right after a decommission. Best // effort per unit; returns how many units it removed. Pinned by TestForgetRetiredDrives_*. func (h *SudoHostOps) ForgetRetiredDrives(ctx context.Context, intent IntentReader) int { if intent == nil { return 0 // no intent store → we cannot know a drive was retired; forget nothing } entries, err := os.ReadDir(h.unitDir) if err != nil { h.logger.Warn("storage: forget retired drives — cannot read unit dir", "dir", h.unitDir, "err", err) return 0 } mounted := map[string]bool{} if ms, merr := h.host.Mounts(); merr == nil { for _, m := range ms { mounted[m.MountPoint] = true } } else { h.logger.Warn("storage: forget retired drives — mount table unreadable; forgetting nothing", "err", merr) return 0 } removed := 0 for _, e := range entries { name := e.Name() if !strings.HasSuffix(name, ".mount") { continue } data, rerr := os.ReadFile(filepath.Join(h.unitDir, name)) if rerr != nil { continue } spec, ok := parseFelhomMountUnit(string(data)) if !ok { continue // not one of our by-UUID drive units (network units are refused by the parser) } durableID := "uuid:" + spec.UUID if intent.Get(durableID) != IntentDecommissioned { continue } if mounted[spec.Where] { h.logger.Debug("storage: decommissioned drive still mounted — keeping its unit", "where", spec.Where) continue } if _, derr := ResolveStorageDevice(durableID); derr == nil { h.logger.Debug("storage: decommissioned drive is present — keeping its unit (re-enroll stays one click)", "where", spec.Where) continue } if h.unitEnabled(name) { if err := h.run(ctx, h.bins.Systemctl, "disable", "--", name); err != nil { h.logger.Warn("storage: forget retired drive — disable failed", "where", spec.Where, "unit", name, "err", err) continue } } if err := h.run(ctx, h.bins.PrivApply, "unit-forget", name); err != nil { h.logger.Warn("storage: forget retired drive — unit removal refused or failed", "where", spec.Where, "unit", name, "err", err) continue } removed++ h.logger.Info("storage: forgot a decommissioned drive that is gone — its mount unit is removed", "name", spec.Name, "where", spec.Where, "uuid", spec.UUID, "unit", name) } if removed > 0 { if err := h.run(ctx, h.bins.Systemctl, "daemon-reload"); err != nil { h.logger.Warn("storage: daemon-reload after forgetting retired drives failed", "err", err) } } return removed }