Files
admin 63aa63d0d6 agent v0.50.0: NAS network storage Part A1 (NFS/SMB automount foundation)
Host-side NFS/SMB automount of a bulk-media NAS share under /mnt/felhom-drives/<name>
(propagates into the guest via the existing shared bind), the +100000 uid recipe,
per-share liveness, and add/list/remove local-API endpoints. A NAS is a distinct class
that bypasses the drive enroll/eject/decommission/SMART/watchdog machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 11:12:27 +02:00

87 lines
3.4 KiB
Go

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