Skip to content

Commit 207f7f7

Browse files
committed
fix(vm): retry reseed only on dial failure, not a live agent's rejection
Review follow-ups on the reseed signal: - reseedVM retried on ANY error, so a version-skew old agent (dial OK, reseed frame rejected) burned the full retry budget (~4s) on every clone during the rollout window. Retry only failed dials (the re-listen-after- resume case); once a live agent answers, its reply is final. Old-agent reseed now returns in ~2ms instead of ~4s, verified on bare metal. - fold refresh+signal into Handler.reseedAfterResume so a caller can't signal on a stale (pre-ToVM) VM; move refreshVM beside it and log the swallowed inspect error at Debug (was mislabeled 'vsock not configured'). - signalReseed reports whether a reseed was attempted, making the Windows/no-vsock skip guards assertable.
1 parent 168f8c9 commit 207f7f7

3 files changed

Lines changed: 58 additions & 40 deletions

File tree

cmd/vm/reseed.go

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"crypto/rand"
66
"fmt"
7-
"io"
87
"time"
98

109
"github.com/projecteru2/core/log"
@@ -36,8 +35,10 @@ func (h Handler) Reseed(cmd *cobra.Command, args []string) error {
3635
return reseedVM(ctx, vm, regenMachineID)
3736
}
3837

39-
// reseedVM pushes fresh entropy and a CRNG reseed order over vsock, retrying because the
40-
// guest agent re-listens shortly after a clone/restore resume.
38+
// reseedVM pushes fresh entropy and a CRNG reseed order over vsock. Only a failed
39+
// dial is retried — the guest agent re-listens shortly after a clone/restore resume;
40+
// once a live agent answers, its reply (success, version-skew rejection, or failure)
41+
// is final, so an old agent isn't billed the whole retry budget.
4142
func reseedVM(ctx context.Context, vm *types.VM, regenMachineID bool) error {
4243
if vm.VsockSocket == "" {
4344
return fmt.Errorf("reseed: %w (recreate the VM to enable agent reseed)", ErrVsockNotConfigured)
@@ -47,44 +48,66 @@ func reseedVM(ctx context.Context, vm *types.VM, regenMachineID bool) error {
4748
return fmt.Errorf("reseed: generate entropy: %w", err)
4849
}
4950

50-
var err error
51+
var dialErr error
5152
for attempt := 1; attempt <= reseedMaxAttempts; attempt++ {
52-
if err = ctx.Err(); err != nil {
53+
if err := ctx.Err(); err != nil {
5354
return err
5455
}
55-
var conn io.ReadWriteCloser
56-
if conn, err = dialHybridVsock(ctx, vm.VsockSocket, hypervisor.VsockAgentPort); err == nil {
57-
err = client.Reseed(ctx, conn, entropy, regenMachineID)
58-
conn.Close() //nolint:errcheck,gosec
59-
if err == nil {
60-
return nil
56+
conn, err := dialHybridVsock(ctx, vm.VsockSocket, hypervisor.VsockAgentPort)
57+
if err != nil {
58+
dialErr = err
59+
if attempt < reseedMaxAttempts {
60+
select {
61+
case <-ctx.Done():
62+
return ctx.Err()
63+
case <-time.After(reseedRetryDelay):
64+
}
6165
}
66+
continue
6267
}
63-
if attempt < reseedMaxAttempts {
64-
select {
65-
case <-ctx.Done():
66-
return ctx.Err()
67-
case <-time.After(reseedRetryDelay):
68-
}
68+
err = client.Reseed(ctx, conn, entropy, regenMachineID)
69+
conn.Close() //nolint:errcheck,gosec
70+
if err != nil {
71+
return fmt.Errorf("reseed: %w", err)
6972
}
73+
return nil
7074
}
71-
return fmt.Errorf("reseed: %w", err)
75+
return fmt.Errorf("reseed: dial agent: %w", dialErr)
76+
}
77+
78+
// reseedAfterResume re-inspects then fires the best-effort reseed. Clone/Restore return a
79+
// *types.VM built in-process that never passed through ToVM, so its VsockSocket is zero;
80+
// pairing refresh with signal keeps a caller from silently no-op-ing on a stale value.
81+
func (h Handler) reseedAfterResume(ctx context.Context, hyper hypervisor.Hypervisor, vm *types.VM, regenMachineID bool) {
82+
signalReseed(ctx, refreshVM(ctx, hyper, vm), regenMachineID)
7283
}
7384

74-
// signalReseed is the non-fatal wrapper called from clone/restore paths: skips silently on
75-
// Windows guests (agent verb is Linux-only) or legacy VMs without vsock, and never fails the
76-
// calling command on agent version skew.
77-
func signalReseed(ctx context.Context, vm *types.VM, regenMachineID bool) {
85+
// signalReseed is the non-fatal wrapper for clone/restore paths: it reports whether a reseed
86+
// was attempted (false = skipped for a Windows guest or a legacy VM without vsock) and never
87+
// fails the calling command on agent version skew.
88+
func signalReseed(ctx context.Context, vm *types.VM, regenMachineID bool) bool {
7889
logger := log.WithFunc("cmd.vm.reseed")
7990
if vm.Config.Windows {
8091
logger.Debug(ctx, "skip reseed signal: Windows guest")
81-
return
92+
return false
8293
}
8394
if vm.VsockSocket == "" {
8495
logger.Debug(ctx, "skip reseed signal: vsock not configured")
85-
return
96+
return false
8697
}
8798
if err := reseedVM(ctx, vm, regenMachineID); err != nil {
8899
logger.Warnf(ctx, "reseed signal failed (agent >= v0.1.6 required): %v; run 'cocoon vm reseed %s' after fixing", err, vm.ID)
89100
}
101+
return true
102+
}
103+
104+
// refreshVM re-inspects to recover runtime-only fields (VsockSocket, PID, SocketPath) that
105+
// ToVM sets but Clone/Restore's in-process return value lacks; keeps the original on error.
106+
func refreshVM(ctx context.Context, hyper hypervisor.Hypervisor, vm *types.VM) *types.VM {
107+
info, err := hyper.Inspect(ctx, vm.ID)
108+
if err != nil {
109+
log.WithFunc("cmd.vm.reseed").Debugf(ctx, "refresh VM %s before reseed: %v", vm.ID, err)
110+
return vm
111+
}
112+
return info
90113
}

cmd/vm/reseed_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ import (
99
)
1010

1111
func TestSignalReseed_SkipsWindows(t *testing.T) {
12+
// VsockSocket is set: a regressed Windows guard would fall through to a dial instead of skipping.
1213
vm := &types.VM{ID: "vm1", Config: types.VMConfig{Config: types.Config{Windows: true}}, VsockSocket: "/tmp/whatever.sock"}
13-
signalReseed(t.Context(), vm, true)
14+
if signalReseed(t.Context(), vm, true) {
15+
t.Error("attempted reseed on a Windows guest, want skip")
16+
}
1417
}
1518

1619
func TestSignalReseed_SkipsNoVsock(t *testing.T) {
1720
vm := &types.VM{ID: "vm1"}
18-
signalReseed(t.Context(), vm, true)
21+
if signalReseed(t.Context(), vm, true) {
22+
t.Error("attempted reseed with no vsock, want skip")
23+
}
1924
}
2025

2126
func TestReseedVM_NoVsock(t *testing.T) {

cmd/vm/run.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (h Handler) Clone(cmd *cobra.Command, args []string) error {
127127
rollbackNetwork(ctx, netProvider, vmID)
128128
return fmt.Errorf("clone VM: %w", cloneErr)
129129
}
130-
signalReseed(ctx, refreshVM(ctx, hyper, vm), true)
130+
h.reseedAfterResume(ctx, hyper, vm, true)
131131

132132
if done, jsonErr := cliutil.MaybeOutputJSON(cmd, vm); done {
133133
return jsonErr
@@ -197,7 +197,7 @@ func (h Handler) Restore(cmd *cobra.Command, args []string) error {
197197
if err != nil {
198198
return fmt.Errorf("restore: %w", err)
199199
}
200-
signalReseed(ctx, refreshVM(ctx, hyper, result), false)
200+
h.reseedAfterResume(ctx, hyper, result, false)
201201

202202
if done, jsonErr := cliutil.MaybeOutputJSON(cmd, result); done {
203203
return jsonErr
@@ -290,7 +290,7 @@ func (h Handler) cloneFromSrcDir(ctx context.Context, cmd *cobra.Command, conf *
290290
rollbackNetwork(ctx, netProvider, vmID)
291291
return fmt.Errorf("clone VM: %w", cloneErr)
292292
}
293-
signalReseed(ctx, refreshVM(ctx, hyper, vm), true)
293+
h.reseedAfterResume(ctx, hyper, vm, true)
294294

295295
if wantJSON {
296296
return cliutil.OutputJSON(vm)
@@ -364,7 +364,7 @@ func (h Handler) runDirectRestore(ctx context.Context, cmd *cobra.Command, hyper
364364
if err != nil {
365365
return fmt.Errorf("restore: %w", err)
366366
}
367-
signalReseed(ctx, refreshVM(ctx, hyper, result), false)
367+
h.reseedAfterResume(ctx, hyper, result, false)
368368
if wantJSON {
369369
return cliutil.OutputJSON(result)
370370
}
@@ -501,16 +501,6 @@ func rollbackNetwork(ctx context.Context, netProvider network.Network, vmID stri
501501
}
502502
}
503503

504-
// refreshVM re-inspects after clone/restore: the value Clone/Restore return is built in-process and
505-
// never passes through ToVM, so runtime-only fields (VsockSocket, PID, SocketPath) are still zero.
506-
// Falls back to the original value if inspect fails.
507-
func refreshVM(ctx context.Context, hyper hypervisor.Hypervisor, vm *types.VM) *types.VM {
508-
if info, err := hyper.Inspect(ctx, vm.ID); err == nil {
509-
return info
510-
}
511-
return vm
512-
}
513-
514504
func printPostCloneHints(vm *types.VM) {
515505
if vm.Config.Windows {
516506
fmt.Println()

0 commit comments

Comments
 (0)