Files
felhom-agent/internal/guesthook/install.go
T
admin 44cdf82631 agent v0.33.0: C1 net — pre-start self-heal hook + decommission mp-delete
Pre-start PVE hookscript (internal/guesthook) creates host-root placeholders for
absent bind-mount sources so the guest always boots (fail-closed); decommission
now pct set --delete's the dead mp (GuestBinder.DetachBind) so a missing source
can't brick the next reboot (B3 C1 bug). Non-hollow tests + companions. Installed
+ registered per-guest by the provision back-half. Transitional ahead of the
intermediary-mount re-architecture which makes C1 structural.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:11:10 +02:00

60 lines
2.6 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.
const snippetBody = `#!/bin/sh
# felhom-agent guest pre-start self-heal hook (C1 net). PVE calls: <script> <vmid> <phase>.
exec ` + AgentBin + ` guest-hook "$1" "$2"
`
// 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.
func InstallSnippet(ctx context.Context, runner proxmox.Runner) error {
tmp := filepath.Join(os.TempDir(), "felhom-guest-hook.sh")
if err := os.WriteFile(tmp, []byte(snippetBody), 0o755); err != nil {
return fmt.Errorf("guesthook: write temp snippet: %w", err)
}
defer os.Remove(tmp)
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
}