diff --git a/cmd/api/api/fork_mailbox.go b/cmd/api/api/fork_mailbox.go new file mode 100644 index 00000000..b91560b8 --- /dev/null +++ b/cmd/api/api/fork_mailbox.go @@ -0,0 +1,37 @@ +package api + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/kernel/hypeman/lib/instances" + "github.com/kernel/hypeman/lib/oapi" +) + +func toDomainForkMailboxes(mailboxes *[]oapi.ForkMailboxPayload) ([]instances.ForkMailboxPayload, error) { + if mailboxes == nil || len(*mailboxes) == 0 { + return nil, nil + } + + out := make([]instances.ForkMailboxPayload, 0, len(*mailboxes)) + for _, mailbox := range *mailboxes { + payload, err := json.Marshal(mailbox.Payload) + if err != nil { + return nil, fmt.Errorf("marshal mailbox %q payload: %w", mailbox.Name, err) + } + waitForAck := mailbox.WaitForAck != nil && *mailbox.WaitForAck + var ackTimeout time.Duration + if mailbox.AckTimeoutMs != nil { + ackTimeout = time.Duration(*mailbox.AckTimeoutMs) * time.Millisecond + } + out = append(out, instances.ForkMailboxPayload{ + Name: mailbox.Name, + Token: mailbox.Token, + Payload: payload, + WaitForAck: waitForAck, + AckTimeout: ackTimeout, + }) + } + return out, nil +} diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index 2c85284b..33e0e918 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -655,11 +655,19 @@ func (s *ApiService) ForkInstance(ctx context.Context, request oapi.ForkInstance if request.Body.TargetState != nil { targetState = instances.State(*request.Body.TargetState) } + mailboxes, err := toDomainForkMailboxes(request.Body.Mailboxes) + if err != nil { + return oapi.ForkInstance400JSONResponse{ + Code: "invalid_request", + Message: err.Error(), + }, nil + } result, err := s.InstanceManager.ForkInstance(ctx, inst.Id, instances.ForkInstanceRequest{ Name: request.Body.Name, FromRunning: request.Body.FromRunning != nil && *request.Body.FromRunning, TargetState: targetState, + Mailboxes: mailboxes, }) if err != nil { switch { diff --git a/cmd/api/api/instances_test.go b/cmd/api/api/instances_test.go index 4e7863aa..a32cbd9a 100644 --- a/cmd/api/api/instances_test.go +++ b/cmd/api/api/instances_test.go @@ -1165,6 +1165,8 @@ func TestForkInstance_Success(t *testing.T) { result: forked, } svc.InstanceManager = mockMgr + waitForAck := true + ackTimeoutMS := 1500 resp, err := svc.ForkInstance( mw.WithResolvedInstance(ctx(), source.Id, source), @@ -1172,6 +1174,13 @@ func TestForkInstance_Success(t *testing.T) { Id: source.Id, Body: &oapi.ForkInstanceRequest{ Name: "forked-instance", + Mailboxes: &[]oapi.ForkMailboxPayload{{ + Name: "kernel.identity.v1", + Token: "template-token", + Payload: map[string]interface{}{"instance_name": "forked-instance"}, + WaitForAck: &waitForAck, + AckTimeoutMs: &ackTimeoutMS, + }}, }, }, ) @@ -1185,6 +1194,12 @@ func TestForkInstance_Success(t *testing.T) { assert.Equal(t, "forked-instance", mockMgr.lastReq.Name) assert.False(t, mockMgr.lastReq.FromRunning) assert.Equal(t, instances.State(""), mockMgr.lastReq.TargetState) + require.Len(t, mockMgr.lastReq.Mailboxes, 1) + assert.Equal(t, "kernel.identity.v1", mockMgr.lastReq.Mailboxes[0].Name) + assert.Equal(t, "template-token", mockMgr.lastReq.Mailboxes[0].Token) + assert.True(t, mockMgr.lastReq.Mailboxes[0].WaitForAck) + assert.Equal(t, 1500*time.Millisecond, mockMgr.lastReq.Mailboxes[0].AckTimeout) + assert.JSONEq(t, `{"instance_name":"forked-instance"}`, string(mockMgr.lastReq.Mailboxes[0].Payload)) } func TestForkInstance_NotSupported(t *testing.T) { diff --git a/cmd/api/api/snapshots.go b/cmd/api/api/snapshots.go index 00b6d06b..81a6eb99 100644 --- a/cmd/api/api/snapshots.go +++ b/cmd/api/api/snapshots.go @@ -178,6 +178,11 @@ func (s *ApiService) ForkSnapshot(ctx context.Context, request oapi.ForkSnapshot domainReq := instances.ForkSnapshotRequest{ Name: request.Body.Name, } + mailboxes, err := toDomainForkMailboxes(request.Body.Mailboxes) + if err != nil { + return oapi.ForkSnapshot400JSONResponse{Code: "invalid_request", Message: err.Error()}, nil + } + domainReq.Mailboxes = mailboxes if request.Body.TargetState != nil { domainReq.TargetState = instances.State(*request.Body.TargetState) } diff --git a/cmd/api/api/snapshots_test.go b/cmd/api/api/snapshots_test.go index 1cfaf1d8..4c7e3216 100644 --- a/cmd/api/api/snapshots_test.go +++ b/cmd/api/api/snapshots_test.go @@ -71,11 +71,20 @@ func TestForkSnapshotSuccess(t *testing.T) { result: forked, } svc.InstanceManager = mockMgr + waitForAck := true + ackTimeoutMS := 2500 resp, err := svc.ForkSnapshot(ctx(), oapi.ForkSnapshotRequestObject{ SnapshotId: "snap-123", Body: &oapi.ForkSnapshotRequest{ Name: "forked-instance", + Mailboxes: &[]oapi.ForkMailboxPayload{{ + Name: "kernel.identity.v1", + Token: "template-token", + Payload: map[string]interface{}{"instance_name": "forked-instance"}, + WaitForAck: &waitForAck, + AckTimeoutMs: &ackTimeoutMS, + }}, }, }) require.NoError(t, err) @@ -86,4 +95,10 @@ func TestForkSnapshotSuccess(t *testing.T) { assert.Equal(t, "snap-123", mockMgr.lastID) require.NotNil(t, mockMgr.lastReq) assert.Equal(t, "forked-instance", mockMgr.lastReq.Name) + require.Len(t, mockMgr.lastReq.Mailboxes, 1) + assert.Equal(t, "kernel.identity.v1", mockMgr.lastReq.Mailboxes[0].Name) + assert.Equal(t, "template-token", mockMgr.lastReq.Mailboxes[0].Token) + assert.True(t, mockMgr.lastReq.Mailboxes[0].WaitForAck) + assert.Equal(t, 2500*time.Millisecond, mockMgr.lastReq.Mailboxes[0].AckTimeout) + assert.JSONEq(t, `{"instance_name":"forked-instance"}`, string(mockMgr.lastReq.Mailboxes[0].Payload)) } diff --git a/lib/instances/fork.go b/lib/instances/fork.go index 1d4f72a8..bb3023aa 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -47,6 +47,17 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR if err != nil { return nil, "", err } + if len(req.Mailboxes) > 0 { + if source.State == StateStopped { + return nil, "", fmt.Errorf("%w: mailboxes require a standby snapshot fork", ErrInvalidRequest) + } + if targetState != StateRunning { + return nil, "", fmt.Errorf("%w: mailboxes require target_state %s", ErrInvalidRequest, StateRunning) + } + if err := validateForkMailboxHypervisor(source.HypervisorType); err != nil { + return nil, "", err + } + } switch source.State { case StateRunning: @@ -81,7 +92,9 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR // the source data directory. Restore the fork while source remains standby and // under lock, then restore the source. if forkErr == nil && targetState == StateRunning { - restoredFork, err := m.applyForkTargetState(ctx, forked.Id, StateRunning) + restoredFork, err := m.applyForkTargetState(ctx, forked.Id, StateRunning, restoreInstanceOptions{ + Mailboxes: req.Mailboxes, + }) if err != nil { forkErr = fmt.Errorf("restore forked instance before source restore: %w", err) if cleanupErr := m.cleanupForkInstanceOnError(ctx, forked.Id); cleanupErr != nil { @@ -377,6 +390,9 @@ func validateForkRequest(req ForkInstanceRequest) error { if req.TargetState != "" && req.TargetState != StateStopped && req.TargetState != StateStandby && req.TargetState != StateRunning { return fmt.Errorf("%w: invalid fork target state %q (must be one of %s, %s, %s)", ErrInvalidRequest, req.TargetState, StateStopped, StateStandby, StateRunning) } + if err := validateForkMailboxes(req.Mailboxes); err != nil { + return err + } return nil } @@ -401,7 +417,7 @@ func resolveForkTargetState(requested State, sourceState State) (State, error) { return requested, nil } -func (m *manager) applyForkTargetState(ctx context.Context, forkID string, target State) (*Instance, error) { +func (m *manager) applyForkTargetState(ctx context.Context, forkID string, target State, opts restoreInstanceOptions) (*Instance, error) { lock := m.getInstanceLock(forkID) lock.Lock() defer lock.Unlock() @@ -442,7 +458,7 @@ func (m *manager) applyForkTargetState(ctx context.Context, forkID string, targe case StateStandby: switch target { case StateRunning: - inst, err := m.restoreInstance(ctx, forkID) + inst, err := m.restoreInstanceWithOptions(ctx, forkID, opts) return returnWithReadiness(inst, err, current.NetworkEnabled && !current.SkipGuestAgent) case StateStopped: if err := os.RemoveAll(m.paths.InstanceSnapshotLatest(forkID)); err != nil { diff --git a/lib/instances/fork_mailbox.go b/lib/instances/fork_mailbox.go new file mode 100644 index 00000000..322fde9e --- /dev/null +++ b/lib/instances/fork_mailbox.go @@ -0,0 +1,284 @@ +package instances + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/logger" + mailboxpkg "github.com/kernel/hypeman/lib/mailbox" + "go.opentelemetry.io/otel/attribute" +) + +var forkMailboxOffsetByMark sync.Map + +type patchedForkMailbox struct { + name string + waiter *guestResumeNetworkUDPWaiter + timeout time.Duration +} + +type forkMailboxPatch struct { + name string + token string + payload []byte +} + +type forkMailboxHandoff struct { + manager *manager + stored *StoredMetadata + patched []patchedForkMailbox +} + +func (m *manager) prepareForkMailboxHandoff(ctx context.Context, stored *StoredMetadata, snapshotDir string, mailboxes []ForkMailboxPayload) (*forkMailboxHandoff, error) { + patched, err := m.patchForkMailboxes(ctx, stored, snapshotDir, mailboxes) + if err != nil { + return nil, err + } + return &forkMailboxHandoff{ + manager: m, + stored: stored, + patched: patched, + }, nil +} + +func (h *forkMailboxHandoff) AfterResume(ctx context.Context) error { + if h == nil { + return nil + } + return h.manager.waitForForkMailboxAcks(ctx, h.stored, h.patched) +} + +func (h *forkMailboxHandoff) Close() { + if h == nil { + return + } + closePatchedForkMailboxes(h.patched) +} + +func validateForkMailboxes(mailboxes []ForkMailboxPayload) error { + if len(mailboxes) > 16 { + return fmt.Errorf("%w: at most 16 mailboxes can be patched for a fork", ErrInvalidRequest) + } + seen := make(map[string]struct{}, len(mailboxes)) + for _, mailbox := range mailboxes { + name := mailbox.Name + if !mailboxpkg.ValidForkMailboxName(name) { + return fmt.Errorf("%w: invalid mailbox name %q", ErrInvalidRequest, mailbox.Name) + } + if _, ok := seen[name]; ok { + return fmt.Errorf("%w: duplicate mailbox name %q", ErrInvalidRequest, name) + } + seen[name] = struct{}{} + + if strings.TrimSpace(mailbox.Token) == "" { + return fmt.Errorf("%w: mailbox %q token is required", ErrInvalidRequest, name) + } + if !mailboxpkg.ValidForkMailboxToken(mailbox.Token) { + return fmt.Errorf("%w: mailbox %q token is too long", ErrInvalidRequest, name) + } + if _, err := mailboxpkg.ForkMailboxMarker(name, mailbox.Token); err != nil { + return fmt.Errorf("%w: mailbox %q marker is too long", ErrInvalidRequest, name) + } + if len(mailbox.Payload) == 0 { + return fmt.Errorf("%w: mailbox %q payload is required", ErrInvalidRequest, name) + } + if len(mailbox.Payload) > mailboxpkg.ForkMailboxPayloadSize { + return fmt.Errorf("%w: mailbox %q payload is too large", ErrInvalidRequest, name) + } + var payload map[string]any + if err := json.Unmarshal(mailbox.Payload, &payload); err != nil || payload == nil { + return fmt.Errorf("%w: mailbox %q payload must be a JSON object", ErrInvalidRequest, name) + } + if mailbox.WaitForAck { + if mailbox.AckTimeout < 0 { + return fmt.Errorf("%w: mailbox %q ack_timeout_ms must not be negative", ErrInvalidRequest, name) + } + if mailbox.AckTimeout > 30*time.Second { + return fmt.Errorf("%w: mailbox %q ack_timeout_ms must be 30000 or less", ErrInvalidRequest, name) + } + } + } + return nil +} + +func validateForkMailboxHypervisor(hvType hypervisor.Type) error { + if hvType != hypervisor.TypeFirecracker { + return fmt.Errorf("%w: mailboxes are only supported for %s standby forks", ErrNotSupported, hypervisor.TypeFirecracker) + } + return nil +} + +func (m *manager) patchForkMailboxes(ctx context.Context, stored *StoredMetadata, snapshotDir string, mailboxes []ForkMailboxPayload) ([]patchedForkMailbox, error) { + if len(mailboxes) == 0 { + return nil, nil + } + + patched := make([]patchedForkMailbox, 0, len(mailboxes)) + patches := make([]forkMailboxPatch, 0, len(mailboxes)) + for _, mailbox := range mailboxes { + payload := mailbox.Payload + var waiter *guestResumeNetworkUDPWaiter + if mailbox.WaitForAck { + var err error + waiter, err = startGuestResumeNetworkUDPWaiter() + if err != nil { + closePatchedForkMailboxes(patched) + return nil, fmt.Errorf("start mailbox %q UDP ack waiter: %w", mailbox.Name, err) + } + payload, err = forkMailboxPayloadWithAckPort(payload, waiter.Port()) + if err != nil { + waiter.Close() + closePatchedForkMailboxes(patched) + return nil, err + } + } + + patches = append(patches, forkMailboxPatch{ + name: mailbox.Name, + token: mailbox.Token, + payload: payload, + }) + patched = append(patched, patchedForkMailbox{ + name: mailbox.Name, + waiter: waiter, + timeout: forkMailboxAckTimeout(mailbox.AckTimeout), + }) + } + if err := m.patchForkMailboxPayloads(ctx, stored, snapshotDir, patches); err != nil { + closePatchedForkMailboxes(patched) + return nil, err + } + return patched, nil +} + +func forkMailboxAckTimeout(timeout time.Duration) time.Duration { + if timeout > 0 { + return timeout + } + return 5 * time.Second +} + +func closePatchedForkMailboxes(patched []patchedForkMailbox) { + for _, mailbox := range patched { + if mailbox.waiter != nil { + mailbox.waiter.Close() + } + } +} + +func (m *manager) waitForForkMailboxAcks(ctx context.Context, stored *StoredMetadata, patched []patchedForkMailbox) error { + log := logger.FromContext(ctx) + for _, mailbox := range patched { + if mailbox.waiter == nil { + continue + } + + waitCtx, waitSpanEnd := m.startLifecycleStep(ctx, "guest.fork_mailbox.udp_ack_wait", + attribute.String("instance_id", stored.Id), + attribute.String("mailbox", mailbox.name), + attribute.String("operation", "guest_fork_mailbox_udp_ack_wait"), + ) + waitCtx, cancel := context.WithTimeout(waitCtx, mailbox.timeout) + elapsed, ack, err := mailbox.waiter.WaitMailboxApplied(waitCtx, mailbox.name) + cancel() + waitSpanEnd(err) + if err != nil { + return fmt.Errorf("wait for mailbox %q ack: %w", mailbox.name, err) + } + log.InfoContext(ctx, "guest fork mailbox UDP ack received", "instance_id", stored.Id, "mailbox", mailbox.name, "elapsed", elapsed, "ack", ack) + } + return nil +} + +func forkMailboxPayloadWithAckPort(payload json.RawMessage, port uint32) (json.RawMessage, error) { + var obj map[string]any + if err := json.Unmarshal(payload, &obj); err != nil { + return nil, fmt.Errorf("unmarshal mailbox payload for ack injection: %w", err) + } + if obj == nil { + return nil, fmt.Errorf("%w: mailbox payload must be a JSON object when wait_for_ack is true", ErrInvalidRequest) + } + obj["ack_port"] = port + out, err := json.Marshal(obj) + if err != nil { + return nil, fmt.Errorf("marshal mailbox payload with ack port: %w", err) + } + if len(out) > mailboxpkg.ForkMailboxPayloadSize { + return nil, fmt.Errorf("%w: mailbox payload is too large after ack_port injection", ErrInvalidRequest) + } + return out, nil +} + +func (m *manager) patchForkMailboxPayloads(ctx context.Context, stored *StoredMetadata, snapshotDir string, patches []forkMailboxPatch) error { + file, err := os.OpenFile(filepath.Join(snapshotDir, firecrackerSnapshotMemoryFile), os.O_RDWR, 0) + if err != nil { + return fmt.Errorf("open snapshot memory for fork mailboxes: %w", err) + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return fmt.Errorf("stat snapshot memory for fork mailboxes: %w", err) + } + if info.Size() <= 0 { + return fmt.Errorf("fork mailbox memory file is empty") + } + + type preparedForkMailboxPatch struct { + patch forkMailboxPatch + offset int64 + finish func(error) + } + prepared := make([]preparedForkMailboxPatch, 0, len(patches)) + finishPrepared := func(err error) { + for _, item := range prepared { + item.finish(err) + } + } + + for _, patch := range patches { + _, patchSpanEnd := m.startLifecycleStep(ctx, "guest.fork_mailbox.patch", + attribute.String("instance_id", stored.Id), + attribute.String("mailbox", patch.name), + attribute.String("operation", "guest_fork_mailbox_patch"), + ) + marker, err := mailboxpkg.ForkMailboxMarker(patch.name, patch.token) + if err == nil { + var offset int64 + offset, err = mailboxpkg.FindMarker(file, info.Size(), marker, &forkMailboxOffsetByMark) + if err == nil { + err = mailboxpkg.EnsurePayloadFits(mailboxpkg.ForkLayout, info.Size(), offset, len(patch.payload)) + } + if err == nil { + prepared = append(prepared, preparedForkMailboxPatch{ + patch: patch, + offset: offset, + finish: patchSpanEnd, + }) + continue + } + } + patchSpanEnd(err) + finishPrepared(err) + return fmt.Errorf("preflight mailbox %q: %w", patch.name, err) + } + + for i, item := range prepared { + err := mailboxpkg.WritePayloadAt(file, mailboxpkg.ForkLayout, item.offset, item.patch.payload) + item.finish(err) + if err != nil { + for _, pending := range prepared[i+1:] { + pending.finish(err) + } + return fmt.Errorf("write mailbox %q frame: %w", item.patch.name, err) + } + } + return nil +} diff --git a/lib/instances/fork_mailbox_test.go b/lib/instances/fork_mailbox_test.go new file mode 100644 index 00000000..d00962ae --- /dev/null +++ b/lib/instances/fork_mailbox_test.go @@ -0,0 +1,193 @@ +package instances + +import ( + "context" + "encoding/binary" + "os" + "path/filepath" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/mailbox" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPatchForkMailbox(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + memoryPath := filepath.Join(dir, firecrackerSnapshotMemoryFile) + memory := make([]byte, 8192) + marker, err := mailbox.ForkMailboxMarker("kernel.identity.v1", "template-token") + require.NoError(t, err) + const offset = 1024 + copy(memory[offset:], marker) + require.NoError(t, os.WriteFile(memoryPath, memory, 0600)) + + mgr := &manager{} + stored := &StoredMetadata{Id: "forked-instance"} + require.NoError(t, mgr.patchForkMailboxPayloads(context.Background(), stored, dir, []forkMailboxPatch{{ + name: "kernel.identity.v1", + token: "template-token", + payload: []byte(`{"instance_name":"forked"}`), + }})) + + updated, err := os.ReadFile(memoryPath) + require.NoError(t, err) + assert.Equal(t, uint32(1), binary.LittleEndian.Uint32(updated[offset+mailbox.ForkMailboxSeqOffset:])) + payloadLen := binary.LittleEndian.Uint32(updated[offset+mailbox.ForkMailboxLengthOffset:]) + assert.Equal(t, uint32(len(`{"instance_name":"forked"}`)), payloadLen) + assert.Equal(t, `{"instance_name":"forked"}`, string(updated[offset+mailbox.ForkMailboxPayloadOffset:offset+mailbox.ForkMailboxPayloadOffset+int(payloadLen)])) +} + +func TestPatchForkMailboxPreflightsAllMarkers(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + memoryPath := filepath.Join(dir, firecrackerSnapshotMemoryFile) + memory := make([]byte, 8192) + marker, err := mailbox.ForkMailboxMarker("kernel.identity.v1", "template-token") + require.NoError(t, err) + const offset = 1024 + copy(memory[offset:], marker) + require.NoError(t, os.WriteFile(memoryPath, memory, 0600)) + + mgr := &manager{} + stored := &StoredMetadata{Id: "forked-instance"} + err = mgr.patchForkMailboxPayloads(context.Background(), stored, dir, []forkMailboxPatch{ + { + name: "kernel.identity.v1", + token: "template-token", + payload: []byte(`{"instance_name":"forked"}`), + }, + { + name: "kernel.other.v1", + token: "other-token", + payload: []byte(`{"value":true}`), + }, + }) + require.Error(t, err) + + updated, err := os.ReadFile(memoryPath) + require.NoError(t, err) + assert.Equal(t, uint32(0), binary.LittleEndian.Uint32(updated[offset+mailbox.ForkMailboxSeqOffset:])) + assert.Equal(t, uint32(0), binary.LittleEndian.Uint32(updated[offset+mailbox.ForkMailboxLengthOffset:])) +} + +func TestForkMailboxPayloadWithAckPort(t *testing.T) { + t.Parallel() + + payload, err := forkMailboxPayloadWithAckPort([]byte(`{"instance_name":"forked"}`), 12345) + require.NoError(t, err) + assert.JSONEq(t, `{"instance_name":"forked","ack_port":12345}`, string(payload)) +} + +func TestWaitMailboxAppliedRequiresExactFields(t *testing.T) { + t.Parallel() + + waiter := &guestResumeNetworkUDPWaiter{ch: make(chan guestResumeNetworkUDPAck, 2)} + now := time.Now() + waiter.ch <- guestResumeNetworkUDPAck{ + received: now, + text: "stage=applied mailbox=kernel.identity.v10", + } + waiter.ch <- guestResumeNetworkUDPAck{ + received: now.Add(time.Millisecond), + text: "mailbox=kernel.identity.v1 stage=applied", + } + + _, ack, err := waiter.WaitMailboxApplied(context.Background(), "kernel.identity.v1") + require.NoError(t, err) + assert.Equal(t, "mailbox=kernel.identity.v1 stage=applied", ack) +} + +func TestWaitMailboxAppliedIgnoresMalformedAck(t *testing.T) { + t.Parallel() + + waiter := &guestResumeNetworkUDPWaiter{ch: make(chan guestResumeNetworkUDPAck, 1)} + waiter.ch <- guestResumeNetworkUDPAck{ + received: time.Now(), + text: "stage=applied mailbox=kernel.identity.v1-extra freeform", + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + _, _, err := waiter.WaitMailboxApplied(ctx, "kernel.identity.v1") + require.Error(t, err) + assert.ErrorIs(t, err, context.DeadlineExceeded) +} + +func TestWaitAppliedRequiresExactFields(t *testing.T) { + t.Parallel() + + waiter := &guestResumeNetworkUDPWaiter{ch: make(chan guestResumeNetworkUDPAck, 2)} + now := time.Now() + waiter.ch <- guestResumeNetworkUDPAck{ + received: now, + text: "stage=applied mac=02:00:00:85:17:c8:ff ip=10.102.146.62", + } + waiter.ch <- guestResumeNetworkUDPAck{ + received: now.Add(time.Millisecond), + text: "ip=10.102.146.62 stage=applied mac=02:00:00:85:17:C8", + } + + _, ack, err := waiter.WaitApplied(context.Background(), "02:00:00:85:17:c8", "10.102.146.62") + require.NoError(t, err) + assert.Equal(t, "ip=10.102.146.62 stage=applied mac=02:00:00:85:17:C8", ack) +} + +func TestValidateForkMailboxesRejectsPaddedName(t *testing.T) { + t.Parallel() + + err := validateForkMailboxes([]ForkMailboxPayload{{ + Name: " kernel.identity.v1 ", + Token: "template-token", + Payload: []byte(`{"instance_name":"forked"}`), + }}) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidRequest) +} + +func TestValidateForkMailboxesAckTimeout(t *testing.T) { + t.Parallel() + + base := ForkMailboxPayload{ + Name: "kernel.identity.v1", + Token: "template-token", + Payload: []byte(`{"instance_name":"forked"}`), + } + + withDefaultTimeout := base + withDefaultTimeout.WaitForAck = true + require.NoError(t, validateForkMailboxes([]ForkMailboxPayload{withDefaultTimeout})) + + withNegativeTimeout := base + withNegativeTimeout.WaitForAck = true + withNegativeTimeout.AckTimeout = -time.Millisecond + err := validateForkMailboxes([]ForkMailboxPayload{withNegativeTimeout}) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidRequest) + + ignoredWithoutAck := base + ignoredWithoutAck.AckTimeout = 31 * time.Second + require.NoError(t, validateForkMailboxes([]ForkMailboxPayload{ignoredWithoutAck})) +} + +func TestForkMailboxAckTimeoutDefault(t *testing.T) { + t.Parallel() + + assert.Equal(t, 5*time.Second, forkMailboxAckTimeout(0)) + assert.Equal(t, 1500*time.Millisecond, forkMailboxAckTimeout(1500*time.Millisecond)) +} + +func TestValidateForkMailboxHypervisor(t *testing.T) { + t.Parallel() + + require.NoError(t, validateForkMailboxHypervisor(hypervisor.TypeFirecracker)) + + err := validateForkMailboxHypervisor(hypervisor.TypeCloudHypervisor) + require.Error(t, err) + assert.ErrorIs(t, err, ErrNotSupported) +} diff --git a/lib/instances/fork_test.go b/lib/instances/fork_test.go index 1268d90c..83137c4a 100644 --- a/lib/instances/fork_test.go +++ b/lib/instances/fork_test.go @@ -308,7 +308,7 @@ func TestApplyForkTargetStateStoppedRefreshesSnapshotForkCID(t *testing.T) { meta.StoredMetadata.Phases.Record(phasetracking.PhaseStandby, time.Now()) require.NoError(t, manager.saveMetadata(meta)) - inst, err := manager.applyForkTargetState(ctx, forkID, StateStopped) + inst, err := manager.applyForkTargetState(ctx, forkID, StateStopped, restoreInstanceOptions{}) require.NoError(t, err) require.Equal(t, StateStopped, inst.State) require.Equal(t, generateVsockCID(forkID), inst.VsockCID) diff --git a/lib/instances/guest_resume_network.go b/lib/instances/guest_resume_network.go index 0d880eb6..7468fefa 100644 --- a/lib/instances/guest_resume_network.go +++ b/lib/instances/guest_resume_network.go @@ -1,9 +1,7 @@ package instances import ( - "bytes" "context" - "encoding/binary" "fmt" stdnet "net" "os" @@ -17,7 +15,6 @@ import ( "github.com/kernel/hypeman/lib/mailbox" "github.com/nrednav/cuid2" "go.opentelemetry.io/otel/attribute" - "golang.org/x/sys/unix" ) const firecrackerSnapshotMemoryFile = "memory" @@ -120,18 +117,31 @@ func (w *guestResumeNetworkUDPWaiter) readLoop() { } func (w *guestResumeNetworkUDPWaiter) WaitApplied(ctx context.Context, mac, ip string) (time.Duration, string, error) { + wantMAC := strings.ToLower(mac) + return w.wait(ctx, func(fields map[string]string) bool { + return strings.EqualFold(fields["stage"], "applied") && + strings.EqualFold(fields["mac"], wantMAC) && + fields["ip"] == ip + }) +} + +func (w *guestResumeNetworkUDPWaiter) WaitMailboxApplied(ctx context.Context, name string) (time.Duration, string, error) { + return w.wait(ctx, func(fields map[string]string) bool { + return strings.EqualFold(fields["stage"], "applied") && fields["mailbox"] == name + }) +} + +func (w *guestResumeNetworkUDPWaiter) wait(ctx context.Context, match func(map[string]string) bool) (time.Duration, string, error) { if w == nil { return 0, "", fmt.Errorf("guest resume network UDP waiter is nil") } start := time.Now() - wantMAC := "mac=" + strings.ToLower(mac) - wantIP := "ip=" + ip for { select { case ack := <-w.ch: - text := strings.ToLower(ack.text) - if strings.Contains(text, "stage=applied") && strings.Contains(text, wantMAC) && strings.Contains(text, wantIP) { + fields := parseUDPAckFields(ack.text) + if match(fields) { return ack.received.Sub(start), ack.text, nil } case <-ctx.Done(): @@ -140,6 +150,18 @@ func (w *guestResumeNetworkUDPWaiter) WaitApplied(ctx context.Context, mac, ip s } } +func parseUDPAckFields(text string) map[string]string { + fields := make(map[string]string) + for _, part := range strings.Fields(text) { + key, value, ok := strings.Cut(part, "=") + if !ok || key == "" { + continue + } + fields[strings.ToLower(key)] = value + } + return fields +} + func (m *manager) waitForGuestResumeNetworkUDPAck(ctx context.Context, waiter *guestResumeNetworkUDPWaiter, stored *StoredMetadata, cfg *guestNetworkConfig) error { if waiter == nil || cfg == nil { return nil @@ -191,45 +213,19 @@ func patchGuestResumeNetworkMailbox(snapshotDir, token string, payload *mailbox. if err != nil { return err } - if idx+int64(mailbox.MailboxPayloadOffset)+int64(len(payloadBytes)) > info.Size() { - return fmt.Errorf("resume network mailbox marker is too close to end of memory file") + if err := mailbox.EnsurePayloadFits(mailbox.ResumeNetworkLayout, info.Size(), idx, len(payloadBytes)); err != nil { + return fmt.Errorf("resume network mailbox payload does not fit: %w", err) } - - if _, err := file.WriteAt(payloadBytes, idx+int64(mailbox.MailboxPayloadOffset)); err != nil { - return fmt.Errorf("write resume network mailbox payload: %w", err) - } - var u32 [4]byte - binary.LittleEndian.PutUint32(u32[:], uint32(len(payloadBytes))) - if _, err := file.WriteAt(u32[:], idx+int64(mailbox.MailboxLengthOffset)); err != nil { - return fmt.Errorf("write resume network mailbox payload length: %w", err) - } - binary.LittleEndian.PutUint32(u32[:], 1) - if _, err := file.WriteAt(u32[:], idx+int64(mailbox.MailboxSeqOffset)); err != nil { - return fmt.Errorf("write resume network mailbox sequence: %w", err) + if err := mailbox.WritePayloadAt(file, mailbox.ResumeNetworkLayout, idx, payloadBytes); err != nil { + return fmt.Errorf("write resume network mailbox frame: %w", err) } return nil } func findGuestResumeNetworkMailbox(file *os.File, size int64, marker []byte, token string) (int64, error) { - if cached, ok := guestResumeNetworkMailboxOffsets.Load(token); ok { - if offset, ok := cached.(int64); ok && offset >= 0 && offset+int64(len(marker)) <= size { - buf := make([]byte, len(marker)) - if _, err := file.ReadAt(buf, offset); err == nil && bytes.Equal(buf, marker) { - return offset, nil - } - } - } - - data, err := unix.Mmap(int(file.Fd()), 0, int(size), unix.PROT_READ, unix.MAP_SHARED) + idx, err := mailbox.FindMarker(file, size, marker, &guestResumeNetworkMailboxOffsets) if err != nil { - return 0, fmt.Errorf("mmap snapshot memory for resume network mailbox: %w", err) - } - defer unix.Munmap(data) - - idx := bytes.Index(data, marker) - if idx < 0 { - return 0, fmt.Errorf("resume network mailbox marker not found") + return 0, fmt.Errorf("find resume network mailbox marker for token %q: %w", token, err) } - guestResumeNetworkMailboxOffsets.Store(token, int64(idx)) - return int64(idx), nil + return idx, nil } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 0ded26ea..2fa0eeeb 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -391,7 +391,9 @@ func (m *manager) ForkInstance(ctx context.Context, id string, req ForkInstanceR inst := forked if !forkTargetStateAlreadyApplied(inst, targetState) { - inst, err = m.applyForkTargetState(ctx, forked.Id, targetState) + inst, err = m.applyForkTargetState(ctx, forked.Id, targetState, restoreInstanceOptions{ + Mailboxes: req.Mailboxes, + }) if err != nil { if cleanupErr := m.cleanupForkInstanceOnError(ctx, forked.Id); cleanupErr != nil { return nil, fmt.Errorf("apply fork target state: %w; additionally failed to cleanup forked instance %s: %v", err, forked.Id, cleanupErr) diff --git a/lib/instances/restore.go b/lib/instances/restore.go index c8645e0c..565a7b93 100644 --- a/lib/instances/restore.go +++ b/lib/instances/restore.go @@ -20,11 +20,28 @@ import ( "google.golang.org/grpc/status" ) +type restoreInstanceOptions struct { + Mailboxes []ForkMailboxPayload +} + +type restoreHandoff interface { + AfterResume(ctx context.Context) error + Close() +} + // RestoreInstance restores an instance from standby. // Multi-hop orchestration: Standby → Paused → Running. func (m *manager) restoreInstance( ctx context.Context, id string, +) (_ *Instance, retErr error) { + return m.restoreInstanceWithOptions(ctx, id, restoreInstanceOptions{}) +} + +func (m *manager) restoreInstanceWithOptions( + ctx context.Context, + id string, + opts restoreInstanceOptions, ) (_ *Instance, retErr error) { start := time.Now() log := logger.FromContext(ctx) @@ -248,13 +265,25 @@ func (m *manager) restoreInstance( proxyRegistered = true } + var handoffs []restoreHandoff + defer func() { + closeRestoreHandoffs(handoffs) + }() + resumeNetworkHandoff, err := m.prepareResumeNetworkHandoff(ctx, stored, allocatedNet, snapshotDir) if err != nil { log.ErrorContext(ctx, "failed to prepare guest resume network handoff", "instance_id", id, "error", err) releaseNetwork() return nil, fmt.Errorf("prepare guest resume network handoff: %w", err) } - defer resumeNetworkHandoff.Close() + handoffs = append(handoffs, resumeNetworkHandoff) + + forkMailboxHandoff, err := m.prepareForkMailboxHandoff(ctx, stored, snapshotDir, opts.Mailboxes) + if err != nil { + releaseNetwork() + return nil, fmt.Errorf("prepare fork mailbox handoff: %w", err) + } + handoffs = append(handoffs, forkMailboxHandoff) // 5. Transition: Standby → Paused (start hypervisor + restore) restoreCtx, restoreSpanEnd := m.startLifecycleStep(ctx, "restore_from_snapshot", @@ -301,24 +330,11 @@ func (m *manager) restoreInstance( reservedResources = false } - // Forked standby restores may allocate a fresh identity while the guest memory snapshot - // still has the source VM's old IP configuration. Reconfigure guest networking after - // resume so host ingress to the new private IP works reliably. - if allocatedNet != nil && !stored.SkipGuestAgent { - reconfigureCtx, reconfigureSpanEnd := m.startLifecycleStep(ctx, "reconfigure_guest_network", - attribute.String("instance_id", id), - attribute.String("hypervisor", string(stored.HypervisorType)), - attribute.String("operation", "reconfigure_guest_network"), - ) - reconfigureErr := resumeNetworkHandoff.AfterResume(reconfigureCtx) - reconfigureSpanEnd(reconfigureErr) - if reconfigureErr != nil { - log.ErrorContext(ctx, "failed to configure guest network after restore", "instance_id", id, "error", reconfigureErr) - _ = hv.Shutdown(ctx) - m.rollbackAdmissionAllocationActive(stored) - releaseNetwork() - return nil, fmt.Errorf("configure guest network after restore: %w", reconfigureErr) - } + if err := afterRestoreHandoffs(ctx, stored, handoffs); err != nil { + _ = hv.Shutdown(ctx) + m.rollbackAdmissionAllocationActive(stored) + releaseNetwork() + return nil, err } // 8. Delete snapshot after successful restore unless the hypervisor is keeping it @@ -358,6 +374,22 @@ func (m *manager) restoreInstance( return &finalInst, nil } +func closeRestoreHandoffs(handoffs []restoreHandoff) { + for _, handoff := range handoffs { + handoff.Close() + } +} + +func afterRestoreHandoffs(ctx context.Context, stored *StoredMetadata, handoffs []restoreHandoff) error { + for _, handoff := range handoffs { + if err := handoff.AfterResume(ctx); err != nil { + logger.FromContext(ctx).ErrorContext(ctx, "failed after restore handoff", "instance_id", stored.Id, "error", err) + return fmt.Errorf("after restore handoff: %w", err) + } + } + return nil +} + func (m *manager) restoreCompressionConfigForMetrics(stored *StoredMetadata, snapshotDir string) *snapshotstore.SnapshotCompressionConfig { _, algorithm, ok := findCompressedSnapshotMemoryFile(snapshotDir) if !ok { diff --git a/lib/instances/resume_network_handoff.go b/lib/instances/resume_network_handoff.go index 073fc92e..9136a35f 100644 --- a/lib/instances/resume_network_handoff.go +++ b/lib/instances/resume_network_handoff.go @@ -71,12 +71,22 @@ func (h *resumeNetworkHandoff) AfterResume(ctx context.Context) error { if h == nil || h.allocatedNet == nil || h.stored.SkipGuestAgent { return nil } + + ctx, spanEnd := h.manager.startLifecycleStep(ctx, "reconfigure_guest_network", + attribute.String("instance_id", h.stored.Id), + attribute.String("hypervisor", string(h.stored.HypervisorType)), + attribute.String("operation", "reconfigure_guest_network"), + ) + var err error + defer func() { spanEnd(err) }() + if h.patched { - err := h.manager.waitForGuestResumeNetworkUDPAck(ctx, h.ackWaiter, h.stored, h.ackCfg) + err = h.manager.waitForGuestResumeNetworkUDPAck(ctx, h.ackWaiter, h.stored, h.ackCfg) if err == nil { return nil } logger.FromContext(ctx).ErrorContext(ctx, "guest resume network UDP ack wait failed; falling back to host-initiated reconfigure", "instance_id", h.stored.Id, "error", err) } - return reconfigureGuestNetwork(ctx, h.stored, h.allocatedNet) + err = reconfigureGuestNetwork(ctx, h.stored, h.allocatedNet) + return err } diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go index 05d8084e..fb3724c0 100644 --- a/lib/instances/snapshot.go +++ b/lib/instances/snapshot.go @@ -387,10 +387,23 @@ func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkS if err != nil { return nil, err } + if len(req.Mailboxes) > 0 { + if rec.Snapshot.Kind != SnapshotKindStandby { + return nil, fmt.Errorf("%w: mailboxes require a standby snapshot fork", ErrInvalidRequest) + } + if targetState != StateRunning { + return nil, fmt.Errorf("%w: mailboxes require target_state %s", ErrInvalidRequest, StateRunning) + } + } targetHypervisor, err := m.resolveSnapshotTargetHypervisor(rec, req.TargetHypervisor) if err != nil { return nil, err } + if len(req.Mailboxes) > 0 { + if err := validateForkMailboxHypervisor(targetHypervisor); err != nil { + return nil, err + } + } forkID := cuid2.Generate() if _, err := m.loadMetadata(forkID); err == nil { @@ -486,7 +499,9 @@ func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkS } cu.Release() - inst, err := m.applyForkTargetState(ctx, forkID, targetState) + inst, err := m.applyForkTargetState(ctx, forkID, targetState, restoreInstanceOptions{ + Mailboxes: req.Mailboxes, + }) if err != nil { if cleanupErr := m.cleanupForkInstanceOnError(ctx, forkID); cleanupErr != nil { return nil, fmt.Errorf("apply snapshot fork target state: %w; additionally failed to cleanup forked instance %s: %v", err, forkID, cleanupErr) @@ -569,6 +584,9 @@ func validateForkSnapshotRequest(req ForkSnapshotRequest) error { if req.TargetState != "" && req.TargetState != StateStopped && req.TargetState != StateStandby && req.TargetState != StateRunning { return fmt.Errorf("%w: invalid target_state %q", ErrInvalidRequest, req.TargetState) } + if err := validateForkMailboxes(req.Mailboxes); err != nil { + return err + } return nil } diff --git a/lib/instances/types.go b/lib/instances/types.go index 8c031fc9..fe92a0f7 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -1,6 +1,7 @@ package instances import ( + "encoding/json" "time" "github.com/kernel/hypeman/lib/autostandby" @@ -271,6 +272,17 @@ type ForkInstanceRequest struct { Name string // Required: name for the new forked instance FromRunning bool // Optional: allow forking from Running by auto standby/fork/restore TargetState State // Optional: desired final state of forked instance (Stopped, Standby, Running). Empty means inherit source state. + Mailboxes []ForkMailboxPayload +} + +// ForkMailboxPayload is a caller-provided JSON payload patched into guest memory +// before a forked standby snapshot is resumed. +type ForkMailboxPayload struct { + Name string + Token string + Payload json.RawMessage + WaitForAck bool + AckTimeout time.Duration } // SnapshotKind determines how snapshot data is captured and restored. @@ -314,6 +326,7 @@ type ForkSnapshotRequest struct { Name string // Required: name for the new instance TargetState State // Optional TargetHypervisor hypervisor.Type // Optional, allowed only for Stopped snapshots + Mailboxes []ForkMailboxPayload } // SnapshotPolicy defines default snapshot behavior for an instance. diff --git a/lib/mailbox/README.md b/lib/mailbox/README.md index 3e834b7c..00b5bc68 100644 --- a/lib/mailbox/README.md +++ b/lib/mailbox/README.md @@ -3,3 +3,5 @@ The resume network handoff uses a small pre-armed mailbox in guest memory to avoid making the first post-resume operation a host-initiated guest RPC. When a Firecracker guest boots with mailbox env enabled, the guest agent allocates and locks a fixed-size buffer containing a magic marker and token. That buffer is captured in standby snapshots. Before restore, the host finds the marker in the snapshot memory file, writes a JSON network payload into the buffer, and flips the sequence field. After resume, VMGenID wakes the guest watcher, which reads the payload, applies the new network identity locally, and sends a UDP applied ack to the host. If the mailbox is missing, cannot be patched, or does not ack in time, restore falls back to the host-initiated network reconfigure path. + +Fork payload mailboxes use the same snapshot-memory handoff pattern for caller-provided JSON payloads. A guest-side component places a named marker plus token in memory before the standby snapshot is captured; Hypeman does not create, return, or enumerate those tokens. When a fork is restored from that snapshot, Hypeman finds the named marker, patches the JSON payload, and optionally waits for the guest to send a UDP `stage=applied` acknowledgement for that mailbox name. diff --git a/lib/mailbox/mailbox.go b/lib/mailbox/mailbox.go index 832f852f..b2bb362c 100644 --- a/lib/mailbox/mailbox.go +++ b/lib/mailbox/mailbox.go @@ -1,8 +1,16 @@ package mailbox import ( + "bytes" + "encoding/binary" "encoding/json" "fmt" + "io" + "os" + "regexp" + "sync" + + "golang.org/x/sys/unix" ) const MailboxEnv = "HYPEMAN_RESUME_NETWORK_MAILBOX" @@ -15,6 +23,41 @@ const MailboxLengthOffset = 68 const MailboxPayloadOffset = 72 const MailboxTokenMaxLen = MailboxSeqOffset - len(MailboxMagic) +const ForkMailboxSize = 4096 +const ForkMailboxMagic = "HYPEMAN_FORK_MAILBOX_V1\x00" +const ForkMailboxSeqOffset = 256 +const ForkMailboxLengthOffset = 260 +const ForkMailboxPayloadOffset = 264 +const ForkMailboxPayloadSize = ForkMailboxSize - ForkMailboxPayloadOffset +const ForkMailboxTokenMaxLen = 128 + +var ForkMailboxNamePattern = regexp.MustCompile(`^[A-Za-z0-9._:-]{1,64}$`) + +type Layout struct { + Size int + SeqOffset int + LengthOffset int + PayloadOffset int +} + +var ResumeNetworkLayout = Layout{ + Size: MailboxSize, + SeqOffset: MailboxSeqOffset, + LengthOffset: MailboxLengthOffset, + PayloadOffset: MailboxPayloadOffset, +} + +var ForkLayout = Layout{ + Size: ForkMailboxSize, + SeqOffset: ForkMailboxSeqOffset, + LengthOffset: ForkMailboxLengthOffset, + PayloadOffset: ForkMailboxPayloadOffset, +} + +func (l Layout) PayloadSize() int { + return l.Size - l.PayloadOffset +} + type Payload struct { InterfaceName string `json:"interface_name"` MAC string `json:"mac"` @@ -38,6 +81,90 @@ func Marker(token string) ([]byte, error) { return marker, nil } +func ValidForkMailboxName(name string) bool { + return ForkMailboxNamePattern.MatchString(name) +} + +func ValidForkMailboxToken(token string) bool { + return token != "" && len(token) <= ForkMailboxTokenMaxLen +} + +func ForkMailboxMarker(name, token string) ([]byte, error) { + if !ValidForkMailboxName(name) { + return nil, fmt.Errorf("fork mailbox name is invalid") + } + if !ValidForkMailboxToken(token) { + return nil, fmt.Errorf("fork mailbox token is invalid") + } + marker := make([]byte, 0, len(ForkMailboxMagic)+len(name)+1+len(token)) + marker = append(marker, ForkMailboxMagic...) + marker = append(marker, name...) + marker = append(marker, 0) + marker = append(marker, token...) + if len(marker) > ForkMailboxSeqOffset { + return nil, fmt.Errorf("fork mailbox marker is too long") + } + return marker, nil +} + +func FindMarker(file *os.File, size int64, marker []byte, cache *sync.Map) (int64, error) { + cacheKey := string(marker) + if cache != nil { + if cached, ok := cache.Load(cacheKey); ok { + if offset, ok := cached.(int64); ok && offset >= 0 && offset+int64(len(marker)) <= size { + buf := make([]byte, len(marker)) + if _, err := file.ReadAt(buf, offset); err == nil && bytes.Equal(buf, marker) { + return offset, nil + } + } + } + } + + data, err := unix.Mmap(int(file.Fd()), 0, int(size), unix.PROT_READ, unix.MAP_SHARED) + if err != nil { + return 0, fmt.Errorf("mmap snapshot memory: %w", err) + } + defer unix.Munmap(data) + + idx := bytes.Index(data, marker) + if idx < 0 { + return 0, fmt.Errorf("marker not found") + } + if cache != nil { + cache.Store(cacheKey, int64(idx)) + } + return int64(idx), nil +} + +func EnsurePayloadFits(layout Layout, memorySize int64, offset int64, payloadLen int) error { + if payloadLen > layout.PayloadSize() { + return fmt.Errorf("mailbox payload too large: %d bytes", payloadLen) + } + if offset+int64(layout.PayloadOffset)+int64(payloadLen) > memorySize { + return fmt.Errorf("mailbox marker is too close to end of memory file") + } + return nil +} + +func WritePayloadAt(w io.WriterAt, layout Layout, offset int64, payload []byte) error { + if len(payload) > layout.PayloadSize() { + return fmt.Errorf("mailbox payload too large: %d bytes", len(payload)) + } + if _, err := w.WriteAt(payload, offset+int64(layout.PayloadOffset)); err != nil { + return fmt.Errorf("write mailbox payload: %w", err) + } + var u32 [4]byte + binary.LittleEndian.PutUint32(u32[:], uint32(len(payload))) + if _, err := w.WriteAt(u32[:], offset+int64(layout.LengthOffset)); err != nil { + return fmt.Errorf("write mailbox payload length: %w", err) + } + binary.LittleEndian.PutUint32(u32[:], 1) + if _, err := w.WriteAt(u32[:], offset+int64(layout.SeqOffset)); err != nil { + return fmt.Errorf("write mailbox sequence: %w", err) + } + return nil +} + func MarshalPayload(payload *Payload) ([]byte, error) { if payload == nil { return nil, fmt.Errorf("resume network mailbox payload is nil") diff --git a/lib/mailbox/mailbox_test.go b/lib/mailbox/mailbox_test.go index db0b5c2b..4f0590bb 100644 --- a/lib/mailbox/mailbox_test.go +++ b/lib/mailbox/mailbox_test.go @@ -9,6 +9,13 @@ import ( "github.com/stretchr/testify/require" ) +type sliceWriterAt []byte + +func (w sliceWriterAt) WriteAt(p []byte, off int64) (int, error) { + copy(w[off:], p) + return len(p), nil +} + func TestMarker(t *testing.T) { t.Parallel() @@ -45,3 +52,30 @@ func TestPayloadRoundTrip(t *testing.T) { require.NoError(t, err) assert.Equal(t, *want, got) } + +func TestForkMailboxMarker(t *testing.T) { + t.Parallel() + + marker, err := ForkMailboxMarker("kernel.identity.v1", "template-token") + require.NoError(t, err) + assert.Equal(t, ForkMailboxMagic+"kernel.identity.v1\x00template-token", string(marker)) + + _, err = ForkMailboxMarker("", "template-token") + require.Error(t, err) + + _, err = ForkMailboxMarker("kernel.identity.v1", "") + require.Error(t, err) +} + +func TestWriteForkMailboxPayloadFrame(t *testing.T) { + t.Parallel() + + buf := make([]byte, ForkMailboxSize) + payload := []byte(`{"instance_name":"forked"}`) + require.NoError(t, WritePayloadAt(sliceWriterAt(buf), ForkLayout, 512, payload)) + + assert.Equal(t, uint32(1), binary.LittleEndian.Uint32(buf[512+ForkMailboxSeqOffset:])) + payloadLen := binary.LittleEndian.Uint32(buf[512+ForkMailboxLengthOffset:]) + assert.Equal(t, uint32(len(payload)), payloadLen) + assert.Equal(t, string(payload), string(buf[512+ForkMailboxPayloadOffset:512+ForkMailboxPayloadOffset+int(payloadLen)])) +} diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 1c053f62..8b4a6781 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -727,6 +727,13 @@ type ForkInstanceRequest struct { // When true and source is Running, the source is put into standby, forked, then restored back to Running. FromRunning *bool `json:"from_running,omitempty"` + // Mailboxes Optional JSON mailbox payloads to patch into a standby snapshot before resuming the fork. + // Each mailbox must correspond to a guest-side mailbox marker that was present when the + // source snapshot was captured. Guest software writes the marker before standby capture; + // Hypeman does not create, return, or enumerate mailbox tokens. Mailboxes are only supported + // for forks that restore from a standby snapshot into Running state. + Mailboxes *[]ForkMailboxPayload `json:"mailboxes,omitempty"` + // Name Name for the forked instance (lowercase letters, digits, and dashes only; cannot start or end with a dash) Name string `json:"name"` @@ -734,8 +741,35 @@ type ForkInstanceRequest struct { TargetState *ForkTargetState `json:"target_state,omitempty"` } +// ForkMailboxPayload defines model for ForkMailboxPayload. +type ForkMailboxPayload struct { + // AckTimeoutMs Timeout for wait_for_ack. Omit or set 0 to use the default 5000ms. + AckTimeoutMs *int `json:"ack_timeout_ms,omitempty"` + + // Name Guest mailbox name. + Name string `json:"name"` + + // Payload JSON object patched into the mailbox before the fork resumes. + Payload map[string]interface{} `json:"payload"` + + // Token Per-template mailbox token used to identify the guest memory marker. + Token string `json:"token"` + + // WaitForAck If true, Hypeman injects ack_port into the payload and waits after resume for a UDP + // acknowledgement containing the mailbox name and stage=applied. If the acknowledgement + // does not arrive before ack_timeout_ms, the fork restore fails. + WaitForAck *bool `json:"wait_for_ack,omitempty"` +} + // ForkSnapshotRequest defines model for ForkSnapshotRequest. type ForkSnapshotRequest struct { + // Mailboxes Optional JSON mailbox payloads to patch into a standby snapshot before resuming the fork. + // Each mailbox must correspond to a guest-side mailbox marker that was present when the + // source snapshot was captured. Guest software writes the marker before standby capture; + // Hypeman does not create, return, or enumerate mailbox tokens. Mailboxes are only supported + // for forks that restore from a standby snapshot into Running state. + Mailboxes *[]ForkMailboxPayload `json:"mailboxes,omitempty"` + // Name Name for the new instance (lowercase letters, digits, and dashes only; cannot start or end with a dash) Name string `json:"name"` @@ -15867,299 +15901,308 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "BZVdlgEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index c6bce872..4747ac99 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -690,6 +690,50 @@ components: Optional final state for the forked instance. Default is the source instance state at fork time. For example, forking from Running defaults the fork result to Running. + mailboxes: + type: array + description: | + Optional JSON mailbox payloads to patch into a standby snapshot before resuming the fork. + Each mailbox must correspond to a guest-side mailbox marker that was present when the + source snapshot was captured. Guest software writes the marker before standby capture; + Hypeman does not create, return, or enumerate mailbox tokens. Mailboxes are only supported + for forks that restore from a standby snapshot into Running state. + items: + $ref: "#/components/schemas/ForkMailboxPayload" + + ForkMailboxPayload: + type: object + required: [name, token, payload] + properties: + name: + type: string + description: Guest mailbox name. + pattern: ^[A-Za-z0-9._:-]+$ + minLength: 1 + maxLength: 64 + example: kernel.identity.v1 + token: + type: string + description: Per-template mailbox token used to identify the guest memory marker. + minLength: 1 + maxLength: 128 + payload: + type: object + description: JSON object patched into the mailbox before the fork resumes. + additionalProperties: true + wait_for_ack: + type: boolean + description: | + If true, Hypeman injects ack_port into the payload and waits after resume for a UDP + acknowledgement containing the mailbox name and stage=applied. If the acknowledgement + does not arrive before ack_timeout_ms, the fork restore fails. + default: false + ack_timeout_ms: + type: integer + description: Timeout for wait_for_ack. Omit or set 0 to use the default 5000ms. + minimum: 0 + maximum: 30000 + default: 5000 ForkTargetState: type: string @@ -875,6 +919,16 @@ components: Optional hypervisor override. Allowed only when forking from a Stopped snapshot. Standby snapshots must fork with their original hypervisor. example: cloud-hypervisor + mailboxes: + type: array + description: | + Optional JSON mailbox payloads to patch into a standby snapshot before resuming the fork. + Each mailbox must correspond to a guest-side mailbox marker that was present when the + source snapshot was captured. Guest software writes the marker before standby capture; + Hypeman does not create, return, or enumerate mailbox tokens. Mailboxes are only supported + for forks that restore from a standby snapshot into Running state. + items: + $ref: "#/components/schemas/ForkMailboxPayload" SnapshotScheduleRetention: type: object