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:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -164,6 +165,15 @@ func (e *Engine) execute(ctx context.Context, act Action) error {
|
||||
upid, err = e.api.Stop(ctx, act.VMID)
|
||||
case ActionSetConfig:
|
||||
upid, err = e.api.SetConfig(ctx, act.VMID, act.Params)
|
||||
case ActionResize:
|
||||
// Defensive grow-only guard at the executor: a resize size MUST be a "+<n>" grow.
|
||||
// The planner only ever emits grows, but never let a shrink reach Proxmox here.
|
||||
disk, size := act.Params["disk"], act.Params["size"]
|
||||
if !strings.HasPrefix(size, "+") {
|
||||
err = fmt.Errorf("reconcile: refusing non-grow resize size %q (data-losing shrink is a signed op)", size)
|
||||
} else {
|
||||
upid, err = e.api.ResizeLXC(ctx, act.VMID, disk, size)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("reconcile: unknown action kind %q", act.Kind)
|
||||
}
|
||||
@@ -208,7 +218,9 @@ func (e *Engine) readActual(ctx context.Context) (ActualState, error) {
|
||||
}
|
||||
guests := make(map[int]ActualGuest, len(lxc))
|
||||
for _, g := range lxc {
|
||||
a := ActualGuest{VMID: g.VMID, Run: normRun(g.Status)}
|
||||
// MaxDisk (bytes) comes from the list entry and is reliable independent of the
|
||||
// per-guest config read — it is the actual side of the grow comparison.
|
||||
a := ActualGuest{VMID: g.VMID, Run: normRun(g.Status), DiskBytes: g.MaxDisk}
|
||||
cfg, err := e.api.GuestConfig(ctx, g.VMID)
|
||||
if err != nil {
|
||||
e.logger.Warn("reconcile: GuestConfig failed; spec unknown (run-state kept)",
|
||||
|
||||
Reference in New Issue
Block a user