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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user