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:
@@ -20,8 +20,18 @@ const (
|
||||
// ActionSetConfig applies benign config changes (cores/memory/description) in one
|
||||
// PUT (proxmox VM.Config.*). May return synchronously (empty UPID) — slice-4 proven.
|
||||
ActionSetConfig ActionKind = "set_config"
|
||||
// ActionResize GROWS the rootfs (proxmox `pct resize`, async). Grow-only — the planner
|
||||
// emits it only when desired DiskBytes > actual; a shrink is data-losing and is refused
|
||||
// (never silently applied as a grow). Slice 5 Phase B; unfed live until slice 10.
|
||||
ActionResize ActionKind = "resize"
|
||||
)
|
||||
|
||||
// growRoundMiB rounds a positive byte delta UP to whole MiB for the Proxmox `+<n>M` grow
|
||||
// size (Proxmox resizes in whole units; rounding up never under-provisions the desired size).
|
||||
func growRoundMiB(deltaBytes int64) int64 {
|
||||
return (deltaBytes + bytesPerMiB - 1) / bytesPerMiB
|
||||
}
|
||||
|
||||
// Action is one minimal mutation the engine will dispatch onto the per-guest queue.
|
||||
// In Phase A every Action is benign by construction (only the benign kinds exist).
|
||||
// Phase B's classifier/gate sits in front of the executor and may tag an action
|
||||
@@ -99,8 +109,27 @@ func Plan(desired DesiredState, actual ActualState, norm FieldNormalizers) []Act
|
||||
params["memory"] = strconv.FormatInt(want, 10)
|
||||
reasons = append(reasons, fmt.Sprintf("memory %dMiB->%dMiB", a.MemoryMiB, want))
|
||||
}
|
||||
// DiskBytes is intentionally NOT reconciled here (rootfs grow is
|
||||
// `pct resize`, grow-only and separate — a later slice).
|
||||
}
|
||||
// Rootfs GROW (slice 5 Phase B) — a separate async op from the config PUT, so
|
||||
// its own Action. GROW-ONLY: emit a resize only when desired > actual. A shrink
|
||||
// (desired < actual) is data-losing and is REFUSED here — we never silently
|
||||
// clamp it to a grow; it is simply not planned (a deliberate shrink would have
|
||||
// to come as a signed destructive op, slice 10). DiskBytes==0 means unmanaged.
|
||||
if d.Spec != nil && d.Spec.DiskBytes > 0 && a.DiskBytes > 0 {
|
||||
switch {
|
||||
case d.Spec.DiskBytes > a.DiskBytes:
|
||||
deltaMiB := growRoundMiB(d.Spec.DiskBytes - a.DiskBytes)
|
||||
actions = append(actions, Action{
|
||||
VMID: vmid,
|
||||
Kind: ActionResize,
|
||||
Params: map[string]string{"disk": "rootfs", "size": fmt.Sprintf("+%dM", deltaMiB)},
|
||||
Reason: fmt.Sprintf("disk grow %dB->%dB (+%dMiB)", a.DiskBytes, d.Spec.DiskBytes, deltaMiB),
|
||||
})
|
||||
case d.Spec.DiskBytes < a.DiskBytes:
|
||||
// Shrink refused by omission: emit NO action (a data-losing shrink is a
|
||||
// signed destructive op, slice 10 — never a benign reconcile grow). The
|
||||
// executor also guards (size must start with '+'). See the resize note above.
|
||||
}
|
||||
}
|
||||
if d.Description != nil && !norm.Equal("description", *d.Description, a.Description) {
|
||||
params["description"] = *d.Description
|
||||
|
||||
Reference in New Issue
Block a user