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:
2026-06-09 10:53:38 +02:00
parent 27b68f043b
commit 9d6e49236c
25 changed files with 2074 additions and 182 deletions
+15 -2
View File
@@ -18,8 +18,8 @@ type fakeAPI struct {
lxc []proxmox.Guest
cfg map[int]proxmox.GuestConfig
startUPID, stopUPID, setUPID string
startErr, stopErr, setErr error
startUPID, stopUPID, setUPID, resizeUPID string
startErr, stopErr, setErr, resizeErr error
// waitFunc maps a UPID to a (status, err); default = OK. Mirrors the real client,
// which errors on a non-OK exitstatus.
waitFunc func(upid string) (proxmox.TaskStatus, error)
@@ -29,10 +29,16 @@ type fakeAPI struct {
starts []int
stops []int
sets []setCall
resizes []resizeCall
waits []string
listErr error
}
type resizeCall struct {
vmid int
disk, size string
}
func (f *fakeAPI) TaskStatusOnce(_ context.Context, upid string) (proxmox.TaskStatus, error) {
if f.statusFunc != nil {
return f.statusFunc(upid)
@@ -81,6 +87,13 @@ func (f *fakeAPI) SetConfig(_ context.Context, vmid int, params map[string]strin
return f.setUPID, f.setErr
}
func (f *fakeAPI) ResizeLXC(_ context.Context, vmid int, disk, size string) (string, error) {
f.mu.Lock()
f.resizes = append(f.resizes, resizeCall{vmid, disk, size})
f.mu.Unlock()
return f.resizeUPID, f.resizeErr
}
func (f *fakeAPI) WaitTask(_ context.Context, upid string, _ proxmox.WaitOptions) (proxmox.TaskStatus, error) {
f.mu.Lock()
f.waits = append(f.waits, upid)