v0.5.0: slice 5 Phase B — the host-root surface (mounts + SMART + grow + destructive gate)
The privileged write surface, isolated behind a narrow, arg-validated, adversarially- tested seam (HostOps), the same discipline as the slice-4 gate. Completes slice 5. - internal/storage: HostOps seam + SudoHostOps (systemd .mount units by fs-UUID, detach, SMART, lvs) via sudoers allowlist + fixed arg vectors, no shell; NoopHostOps fallback. - validate.go: strict UUID/mount-path/device/LVM validators + in-process systemd-escape. Headline test: adversarial matrix (metacharacters/traversal/malformed) refused with zero exec. - smart.go: smartctl SATA + NVMe parse, UNKNOWN-degrade; lvs thin-pool metadata fill. - observer enrichment (Observe only): fills smart + thin-pool metadata. - watchdog: benign re-mount response off the poll path (DevicePresent probe, rate-limited). - reconcile: ActionResize (benign, grow-only) + proxmox.ResizeLXC; destructive storage ops (ClassStorageWipe/Decommission) through the slice-4 gate, target-scoped; built+tested, inert live. - --selftest=storage [-watch] live harness; configs/felhom-agent.sudoers; privileged.* knobs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user