Host power-off for the household (R-998): POST /host/poweroff
gates / gates (push) Successful in 34s

The guest's controller asks; the agent refuses in the first 10 min after boot, a second time in
one boot (persisted, keyed by /proc/stat btime), and while a backup, restore test, OS step,
controller update, drive format, NAS verify, escrow ceremony or vzdump runs (each a named 409
reason). On acceptance: the record first, a host report carrying deliberate_off accepted by the
hub (bounded 60 s), then pct shutdown --timeout 120, then systemctl poweroff.

Sudoers: two exact vectors (pct ^shutdown N --timeout N$, systemctl poweroff) in FELHOM_POWEROFF;
nine injection cases. New TestEveryAliasIsGranted: a defined alias missing from the grant line
passed the coverage check before. Every new test red-proofed.
This commit is contained in:
2026-10-13 19:29:52 +02:00
parent 86adbcaf8a
commit 3403c87c80
15 changed files with 792 additions and 2 deletions
+21
View File
@@ -1,6 +1,7 @@
package localapi
import (
"time"
"context"
"fmt"
"log/slog"
@@ -104,6 +105,26 @@ func (b *GuestBinder) RebootGuest(ctx context.Context, vmid int) error {
return nil
}
// ShutdownGuest stops the guest cleanly with a time limit (R-998 host power-off, step 2). `pct shutdown`
// blocks until the guest is down or the limit passes; then it fails and the guest is still running.
func (b *GuestBinder) ShutdownGuest(ctx context.Context, vmid int, timeout time.Duration) error {
secs := int(timeout / time.Second)
if secs < 1 {
secs = 1
}
b.logger.Warn("host-poweroff: shutting the guest down", "vmid", vmid, "timeout_s", secs)
if err := b.run(ctx, "pct", "shutdown", strconv.Itoa(vmid), "--timeout", strconv.Itoa(secs)); err != nil {
return fmt.Errorf("host-poweroff: pct shutdown %d: %w", vmid, err)
}
return nil
}
// PowerOffHost powers the Proxmox host off (R-998, step 3). The one fixed vector `/usr/bin/systemctl poweroff`.
func (b *GuestBinder) PowerOffHost(ctx context.Context) error {
b.logger.Warn("host-poweroff: powering the host off")
return b.run(ctx, "/usr/bin/systemctl", "poweroff")
}
func (b *GuestBinder) run(ctx context.Context, name string, args ...string) error {
_, stderr, err := b.runner.Run(ctx, name, args...)
if err != nil {