Skip to content

Commit 70e88c0

Browse files
committed
feat(agent): snapshot-based VM boot via cfg.Snapshot (node-local)
When ImpVM.Spec.SnapshotRef is set, applySnapshotBoot looks up the child ImpVMSnapshot, reads status.SnapshotPath, and populates cfg.Snapshot with SnapshotPath/MemFilePath/ResumeVM=true before firecracker.NewMachine is called. The SDK activates LoadSnapshotHandler automatically via cfg.hasSnapshot(). If the snapshot has no node-local path yet the VM cold-boots unchanged.
1 parent ca01c09 commit 70e88c0

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

internal/agent/firecracker_driver.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ func (d *FirecrackerDriver) Start(ctx context.Context, vm *impdevv1alpha1.ImpVM)
168168
// 5. Build Firecracker config.
169169
cfg := d.buildConfig(&class, rootfsPath, sockPath, netInfo, gaEnabled)
170170

171+
// 5a. Apply snapshot-based boot if requested.
172+
if vm.Spec.SnapshotRef != "" {
173+
if err := d.applySnapshotBoot(ctx, vm, &cfg); err != nil {
174+
return 0, fmt.Errorf("apply snapshot boot: %w", err)
175+
}
176+
}
177+
171178
// 6. Build the VMM command.
172179
cmd := exec.CommandContext(ctx, d.BinPath, "--api-sock", sockPath) //nolint:gosec // G204: BinPath validated in NewFirecrackerDriver
173180

@@ -532,5 +539,33 @@ func (d *FirecrackerDriver) Snapshot(ctx context.Context, vm *impdevv1alpha1.Imp
532539
return SnapshotResult{StatePath: statePath, MemPath: memPath}, nil
533540
}
534541

542+
// applySnapshotBoot looks up the ImpVMSnapshot named by vm.Spec.SnapshotRef,
543+
// reads its node-local SnapshotPath, and configures cfg.Snapshot for
544+
// snapshot-based boot. The Firecracker SDK activates LoadSnapshotHandler
545+
// automatically when cfg.hasSnapshot() returns true (i.e. SnapshotPath != "").
546+
// If the snapshot has no node-local path yet (e.g. still being created), the
547+
// function returns nil without modifying cfg — the VM will do a cold boot.
548+
func (d *FirecrackerDriver) applySnapshotBoot(ctx context.Context, vm *impdevv1alpha1.ImpVM, cfg *firecracker.Config) error {
549+
log := logf.FromContext(ctx)
550+
snap := &impdevv1alpha1.ImpVMSnapshot{}
551+
if err := d.Client.Get(ctx, ctrlclient.ObjectKey{
552+
Namespace: vm.Namespace,
553+
Name: vm.Spec.SnapshotRef,
554+
}, snap); err != nil {
555+
return fmt.Errorf("get snapshot %q: %w", vm.Spec.SnapshotRef, err)
556+
}
557+
if snap.Status.SnapshotPath == "" {
558+
log.Info("snapshot has no node-local path, skipping snapshot boot", "snapshot", snap.Name)
559+
return nil
560+
}
561+
cfg.Snapshot = firecracker.SnapshotConfig{
562+
SnapshotPath: filepath.Join(snap.Status.SnapshotPath, "vm.state"),
563+
MemFilePath: filepath.Join(snap.Status.SnapshotPath, "vm.mem"),
564+
ResumeVM: true,
565+
}
566+
log.Info("configured snapshot-based boot", "snapshotPath", cfg.Snapshot.SnapshotPath)
567+
return nil
568+
}
569+
535570
// compile-time interface check.
536571
var _ VMDriver = (*FirecrackerDriver)(nil)

internal/agent/firecracker_driver_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/exec"
99
"testing"
1010

11+
firecracker "github.com/firecracker-microvm/firecracker-go-sdk"
1112
"k8s.io/apimachinery/pkg/runtime"
1213
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1314

@@ -458,6 +459,72 @@ func TestFirecrackerDriver_Stop_callsRemoveNATOnLastVM(t *testing.T) {
458459
}
459460
}
460461

462+
func TestFirecrackerDriver_applySnapshotBoot_noPath(t *testing.T) {
463+
scheme := runtime.NewScheme()
464+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
465+
t.Fatalf("scheme: %v", err)
466+
}
467+
468+
snap := &impdevv1alpha1.ImpVMSnapshot{}
469+
snap.Namespace = "default"
470+
snap.Name = "snap-nopath"
471+
// snap.Status.SnapshotPath is empty — snapshot not yet written to disk.
472+
473+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(snap).Build()
474+
475+
d := &FirecrackerDriver{Client: fakeClient}
476+
477+
vm := &impdevv1alpha1.ImpVM{}
478+
vm.Namespace = "default"
479+
vm.Name = "vm-nopath"
480+
vm.Spec.SnapshotRef = "snap-nopath"
481+
482+
cfg := firecracker.Config{}
483+
if err := d.applySnapshotBoot(context.Background(), vm, &cfg); err != nil {
484+
t.Fatalf("unexpected error: %v", err)
485+
}
486+
if cfg.Snapshot.SnapshotPath != "" {
487+
t.Errorf("SnapshotPath = %q, want empty (cold boot)", cfg.Snapshot.SnapshotPath)
488+
}
489+
}
490+
491+
func TestFirecrackerDriver_applySnapshotBoot_withPath(t *testing.T) {
492+
scheme := runtime.NewScheme()
493+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
494+
t.Fatalf("scheme: %v", err)
495+
}
496+
497+
snap := &impdevv1alpha1.ImpVMSnapshot{}
498+
snap.Namespace = "default"
499+
snap.Name = "snap-ready"
500+
snap.Status.SnapshotPath = "/mnt/snaps/default/p/c"
501+
502+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(snap).Build()
503+
504+
d := &FirecrackerDriver{Client: fakeClient}
505+
506+
vm := &impdevv1alpha1.ImpVM{}
507+
vm.Namespace = "default"
508+
vm.Name = "vm-withpath"
509+
vm.Spec.SnapshotRef = "snap-ready"
510+
511+
cfg := firecracker.Config{}
512+
if err := d.applySnapshotBoot(context.Background(), vm, &cfg); err != nil {
513+
t.Fatalf("unexpected error: %v", err)
514+
}
515+
wantState := "/mnt/snaps/default/p/c/vm.state"
516+
if cfg.Snapshot.SnapshotPath != wantState {
517+
t.Errorf("SnapshotPath = %q, want %q", cfg.Snapshot.SnapshotPath, wantState)
518+
}
519+
wantMem := "/mnt/snaps/default/p/c/vm.mem"
520+
if cfg.Snapshot.MemFilePath != wantMem {
521+
t.Errorf("MemFilePath = %q, want %q", cfg.Snapshot.MemFilePath, wantMem)
522+
}
523+
if !cfg.Snapshot.ResumeVM {
524+
t.Error("ResumeVM = false, want true")
525+
}
526+
}
527+
461528
func TestFirecrackerDriver_Snapshot_noVM_returnsError(t *testing.T) {
462529
d := &FirecrackerDriver{procs: make(map[string]*fcProc)}
463530
vm := &impdevv1alpha1.ImpVM{}

0 commit comments

Comments
 (0)