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:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user