package storage import ( "fmt" "strings" ) // felhomUnitMarker is the header renderMountUnit writes; parseFelhomMountUnit uses it to tell our // units apart from any other .mount unit on the host. const felhomUnitMarker = "Managed by felhom-agent" // parseFelhomMountUnit is the inverse of renderMountUnit for the fields the host-reboot re-assert needs. // It returns the MountSpec (Name from Description, UUID from What=/dev/disk/by-uuid/, Where, Type, // Options), or ok=false when the content is not a felhom-rendered by-UUID mount unit. Pure → unit-tested. func parseFelhomMountUnit(content string) (MountSpec, bool) { if !strings.Contains(content, felhomUnitMarker) { return MountSpec{}, false } // GUARD (network-storage bypass, Scenario D): a NAS network mount carries the netUnitMarker, which // also contains felhomUnitMarker as a substring. It is NOT a drive — no durable-id, no device — and // MUST NOT be picked up by the host-reboot drive re-assert. Refuse it explicitly here so even a // (contrived) network unit shaped like a by-uuid drive unit can never be classified as a drive. if strings.Contains(content, netUnitMarker) { return MountSpec{}, false } var spec MountSpec for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "Description=Felhom storage mount "): spec.Name = strings.TrimPrefix(line, "Description=Felhom storage mount ") case strings.HasPrefix(line, "What="): if u, found := strings.CutPrefix(strings.TrimPrefix(line, "What="), byUUIDDir+"/"); found { spec.UUID = u } case strings.HasPrefix(line, "Where="): spec.Where = strings.TrimPrefix(line, "Where=") case strings.HasPrefix(line, "Type="): spec.FSType = strings.TrimPrefix(line, "Type=") case strings.HasPrefix(line, "Options="): spec.Options = strings.TrimPrefix(line, "Options=") } } if spec.UUID == "" || spec.Where == "" { // not a by-uuid mount we can re-resolve return MountSpec{}, false } return spec, true } // renderMountUnit builds the systemd .mount unit content for a (already-validated) spec. // Keyed by fs-UUID via What=/dev/disk/by-uuid/ so it survives /dev/sdX renumbering; // WantedBy=multi-user.target so `enable` makes it persist across reboot. // // All interpolated values are pre-validated by the caller (ValidateUUID / ValidateMountPath // / validateUnitOpt), so no value here can carry a newline or inject an extra directive. func renderMountUnit(spec MountSpec) string { what := byUUIDDir + "/" + spec.UUID var b strings.Builder b.WriteString("# Managed by felhom-agent — do not edit by hand.\n") b.WriteString("[Unit]\n") fmt.Fprintf(&b, "Description=Felhom storage mount %s\n", sanitizeDesc(spec.Name)) b.WriteString("After=local-fs-pre.target\n") b.WriteString("\n[Mount]\n") fmt.Fprintf(&b, "What=%s\n", what) fmt.Fprintf(&b, "Where=%s\n", spec.Where) if spec.FSType != "" { fmt.Fprintf(&b, "Type=%s\n", spec.FSType) } if spec.Options != "" { fmt.Fprintf(&b, "Options=%s\n", spec.Options) } b.WriteString("\n[Install]\n") b.WriteString("WantedBy=multi-user.target\n") return b.String() } // sanitizeDesc keeps the Description line single-line and harmless (it is cosmetic; the // name is already a Proxmox storage id, but be defensive against any newline). func sanitizeDesc(name string) string { name = strings.ReplaceAll(name, "\n", " ") name = strings.ReplaceAll(name, "\r", " ") if name == "" { return "(unnamed)" } return name }