bc4eda926b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
83 lines
3.9 KiB
Go
83 lines
3.9 KiB
Go
package guesthook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
|
)
|
|
|
|
// Install/registration of the pre-start hook. The wrapper lives in a PVE `snippets`-enabled storage dir
|
|
// (the `local` storage maps to /var/lib/vz/snippets) and is referenced per-guest by its volid.
|
|
const (
|
|
// SnippetDir is the local-storage snippets directory PVE serves hookscripts from.
|
|
SnippetDir = "/var/lib/vz/snippets"
|
|
// SnippetName is the wrapper filename.
|
|
SnippetName = "felhom-guest-hook.sh"
|
|
// HookVolID is the volid form `pct set --hookscript` expects.
|
|
HookVolID = "local:snippets/" + SnippetName
|
|
// AgentBin is the installed agent binary the wrapper delegates to.
|
|
AgentBin = "/usr/local/bin/felhom-agent"
|
|
)
|
|
|
|
// SnippetPath is the absolute path of the installed wrapper.
|
|
var SnippetPath = filepath.Join(SnippetDir, SnippetName)
|
|
|
|
// snippetBody is the tiny wrapper PVE execs as `<script> <vmid> <phase>`. It delegates to the agent
|
|
// binary so the heal LOGIC is the unit-tested Go, never duplicated (divergence-proof) shell. Executable.
|
|
// The wrapper NEVER exec's and ALWAYS exits 0 (CAMPAIGN-3 F10/rc255 belt): a hook that exits nonzero
|
|
// aborts the guest start. `exec` would surface the binary's exit code to PVE; instead we run it as a
|
|
// child, swallow any nonzero (missing/crashed binary, OOM-kill), and `exit 0` unconditionally. The Go
|
|
// side has its own recover + per-phase timeout — this is the second belt at the shell layer.
|
|
const snippetBody = `#!/bin/sh
|
|
# felhom-agent guest pre-start self-heal hook (C1 net). PVE calls: <script> <vmid> <phase>.
|
|
` + AgentBin + ` guest-hook "$1" "$2" || true
|
|
exit 0
|
|
`
|
|
|
|
// InstallSnippet writes the pre-start hook wrapper into the PVE snippets dir (idempotent, root-owned,
|
|
// executable). The agent runs as a non-root service user, so it writes an agent-writable temp file then
|
|
// `install`s it host-root (same pattern as the bootstrap mount + dnsmasq drop-ins). Safe to call repeatedly.
|
|
// The temp file is a RANDOM-named os.CreateTemp (audit B1): a fixed, predictable /tmp name could be
|
|
// pre-created by another local user and rewritten between our write and root's install (TOCTOU into a
|
|
// root-executed hookscript). The final mode comes from `install -m`, so the 0600 temp is fine.
|
|
func InstallSnippet(ctx context.Context, runner proxmox.Runner) error {
|
|
f, err := os.CreateTemp("", "felhom-guest-hook-*.sh")
|
|
if err != nil {
|
|
return fmt.Errorf("guesthook: create temp snippet: %w", err)
|
|
}
|
|
tmp := f.Name()
|
|
defer os.Remove(tmp)
|
|
if _, err := f.WriteString(snippetBody); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("guesthook: write temp snippet: %w", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return fmt.Errorf("guesthook: close temp snippet: %w", err)
|
|
}
|
|
// Ensure the snippets dir exists FIRST (B2, DRILL-day0-cleanroom-2026-07-03): a fresh PVE has
|
|
// no /var/lib/vz/snippets, and `install` (without -D) won't create the parent — the whole
|
|
// hook install silently failed on a freshly-bootstrapped box. Fenced root op like the install
|
|
// itself; idempotent.
|
|
if _, stderr, err := runner.Run(ctx, "mkdir", "-p", SnippetDir); err != nil {
|
|
return fmt.Errorf("guesthook: ensure snippets dir %s: %w: %s", SnippetDir, err, string(stderr))
|
|
}
|
|
if _, stderr, err := runner.Run(ctx, "install", "-m", "0755", "--", tmp, SnippetPath); err != nil {
|
|
return fmt.Errorf("guesthook: install snippet to %s: %w: %s", SnippetPath, err, string(stderr))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Register points a guest at the pre-start hook (`pct set <vmid> --hookscript <volid>`). Idempotent —
|
|
// re-setting the same hookscript is a no-op. Safe on a running guest (a config edit, not a start, so no
|
|
// start-lock contention).
|
|
func Register(ctx context.Context, runner proxmox.Runner, vmid int) error {
|
|
if _, stderr, err := runner.Run(ctx, "pct", "set", strconv.Itoa(vmid), "--hookscript", HookVolID); err != nil {
|
|
return fmt.Errorf("guesthook: register hookscript on %d: %w: %s", vmid, err, string(stderr))
|
|
}
|
|
return nil
|
|
}
|