v0.41.0: provision back-half sets onboot:1 so customer guests auto-start after host reboot (F3)

The golden bakes --onboot 0 (template safety) and the back-half never overrode it, so every
provisioned customer guest was onboot:0 -> after a host reboot/power-cut the customer's whole
home-server stayed stopped until a manual pct start. Add a fatal 'pct set <vmid> -onboot 1' step
to BackHalf.Provision (right after the config-mount attach), mirroring the existing pct set ops.
No startup/boot-order: the v0.75 mountpoint-gate covers the drive-bind race at boot.

Golden build-golden.sh unchanged (templates must not auto-start). Unit-tested
(TestProvision_SetsOnbootOne + red-proof). RUNBOOK note added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpBYrZCt9sFDqLgbG5GRGD
This commit is contained in:
2026-06-24 11:20:45 +02:00
parent db95d5106e
commit 166a1c8bcb
6 changed files with 119 additions and 105 deletions
+10
View File
@@ -153,6 +153,16 @@ func (b *BackHalf) Provision(ctx context.Context, in Input) (Result, error) {
return Result{}, fmt.Errorf("provision: attach config mount: %w", err)
}
// 5b. Ensure the customer guest auto-starts after a host reboot / power-cut (F3). The golden bakes
// onboot:0 (template safety — the build guest must not auto-start); the provisioned customer
// guest must come back on its own. No startup order/delay: the v0.75 mountpoint-gate covers the
// drive-bind race at boot, so the controller won't write app data onto the rootfs while the agent
// re-binds drives. Fatal like the config-mount attach above — a guest that won't auto-recover is
// a provisioning defect, not a soft warning.
if err := b.run(ctx, "pct", "set", strconv.Itoa(in.VMID), "-onboot", "1"); err != nil {
return Result{}, fmt.Errorf("provision: set onboot: %w", err)
}
// 6. Install + register the pre-start self-heal hook (C1 net): if a data drive is absent at a future
// boot, the hook creates a placeholder for its missing bind source so the guest still starts.
// Best-effort + non-fatal — it's defense-in-depth; a provision must not fail over the hook.
+40
View File
@@ -41,6 +41,29 @@ func (r *recRunner) find(name string) []string {
return nil
}
// hasExact reports whether any recorded command matches the given args exactly (name + all args).
// Needed because several `pct` invocations are recorded; find() only returns the first.
func (r *recRunner) hasExact(want ...string) bool {
r.mu.Lock()
defer r.mu.Unlock()
for _, c := range r.cmds {
if len(c) != len(want) {
continue
}
match := true
for i := range c {
if c[i] != want[i] {
match = false
break
}
}
if match {
return true
}
}
return false
}
// mintMinter returns a fixed token and records the vmid it was minted for.
type mintMinter struct {
token string
@@ -125,6 +148,23 @@ func TestProvision_WritesChownsAndAttaches(t *testing.T) {
}
}
// F3: the provisioned customer guest must be set onboot:1 so it auto-starts after a host
// reboot/power-cut (the golden bakes onboot:0 as a template). Assert the exact pct invocation.
// Companion red-proof: removing the `b.run(... -onboot 1)` call in Provision makes this FAIL
// (no such invocation recorded) — re-applying the call turns it green.
func TestProvision_SetsOnbootOne(t *testing.T) {
dir := t.TempDir()
runner := &recRunner{}
bh := NewBackHalf(&mintMinter{token: "t"}, runner, dir, testLogger())
if _, err := bh.Provision(context.Background(), newInput()); err != nil {
t.Fatalf("provision: %v", err)
}
if !runner.hasExact("pct", "set", "8200", "-onboot", "1") {
t.Fatalf("expected `pct set 8200 -onboot 1` to be issued; recorded: %v", runner.cmds)
}
}
// The Result must never carry the token, and the token must not appear in any field returned to
// the caller (secret discipline — only the 0600 file + the store hash hold it).
func TestProvision_ResultHasNoToken(t *testing.T) {