diff --git a/firecracker.go b/firecracker.go index 3f15c352..855c8995 100644 --- a/firecracker.go +++ b/firecracker.go @@ -265,6 +265,23 @@ func (f *Client) CreateSnapshot(ctx context.Context, snapshotParams *models.Snap return f.client.Operations.CreateSnapshot(params) } +// LoadSnapshotOpt is a functional option to be used for the +// LoadSnapshot API in setting any additional optional fields. +type LoadSnapshotOpt func(*ops.LoadSnapshotParams) + +// LoadSnapshot is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) LoadSnapshot(ctx context.Context, snapshotParams *models.SnapshotLoadParams, opts ...LoadSnapshotOpt) (*ops.LoadSnapshotNoContent, error) { + params := ops.NewLoadSnapshotParamsWithContext(ctx) + params.SetBody(snapshotParams) + + for _, opt := range opts { + opt(params) + } + + return f.client.Operations.LoadSnapshot(params) +} + // CreateSyncActionOpt is a functional option to be used for the // CreateSyncAction API in setting any additional optional fields. type CreateSyncActionOpt func(*ops.CreateSyncActionParams) diff --git a/machine.go b/machine.go index 5bdadc92..a4619e31 100644 --- a/machine.go +++ b/machine.go @@ -150,6 +150,12 @@ type Config struct { // It is possible to use a valid IPv4 link-local address (169.254.0.0/16). // If not provided, the default address (169.254.169.254) will be used. MmdsAddress net.IP + + Snapshot SnapshotConfig +} + +func (cfg *Config) hasSnapshot() bool { + return cfg.Snapshot.MemFilePath != "" || cfg.Snapshot.SnapshotPath != "" } // Validate will ensure that the required fields are set and that @@ -380,7 +386,7 @@ func NewMachine(ctx context.Context, cfg Config, opts ...Opt) (*Machine, error) // handlers succeed, then this will start the VMM instance. // Start may only be called once per Machine. Subsequent calls will return // ErrAlreadyStarted. -func (m *Machine) Start(ctx context.Context) error { +func (m *Machine) Start(ctx context.Context, opts ...Opt) error { m.logger.Debug("Called Machine.Start()") alreadyStarted := true m.startOnce.Do(func() { @@ -401,6 +407,10 @@ func (m *Machine) Start(ctx context.Context) error { } }() + for _, opt := range opts { + opt(m) + } + err = m.Handlers.Run(ctx, m) if err != nil { return err @@ -718,6 +728,17 @@ func (m *Machine) captureFifoToFileWithChannel(ctx context.Context, logger *log. } func (m *Machine) createMachine(ctx context.Context) error { + ss := m.Cfg.Snapshot + if ss.SnapshotPath != "" || ss.MemFilePath != "" { + _, err := m.client.LoadSnapshot(ctx, &models.SnapshotLoadParams{ + SnapshotPath: String(ss.SnapshotPath), + MemFilePath: String(ss.MemFilePath), + EnableDiffSnapshots: ss.EnableDiffSnapshots, + ResumeVM: ss.ResumeVM, + }) + return err + } + resp, err := m.client.PutMachineConfiguration(ctx, &m.Cfg.MachineCfg) if err != nil { m.logger.Errorf("PutMachineConfiguration returned %s", resp.Error()) @@ -734,6 +755,10 @@ func (m *Machine) createMachine(ctx context.Context) error { } func (m *Machine) createBootSource(ctx context.Context, imagePath, initrdPath, kernelArgs string) error { + if m.Cfg.hasSnapshot() { + return nil + } + bsrc := models.BootSource{ KernelImagePath: &imagePath, InitrdPath: initrdPath, @@ -841,6 +866,10 @@ func (m *Machine) addVsock(ctx context.Context, dev VsockDevice) error { } func (m *Machine) startInstance(ctx context.Context) error { + if m.Cfg.hasSnapshot() { + return nil + } + action := models.InstanceActionInfoActionTypeInstanceStart info := models.InstanceActionInfo{ ActionType: &action, diff --git a/machine_test.go b/machine_test.go index 4f9779bf..c9b3c8a2 100644 --- a/machine_test.go +++ b/machine_test.go @@ -71,6 +71,9 @@ var ( testBalloonDeflateOnOom = true testStatsPollingIntervals = int64(1) testNewStatsPollingIntervals = int64(6) + + // How long to wait for the socket to appear. + firecrackerSocketWait = int64(10) ) func envOrDefault(k, empty string) string { @@ -1728,6 +1731,80 @@ func TestCreateSnapshot(t *testing.T) { } } +func TestLoadSnapshot(t *testing.T) { + fctesting.RequiresKVM(t) + fctesting.RequiresRoot(t) + + ctx := context.Background() + + dir, err := ioutil.TempDir("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + // Set snap and mem paths + socketPath := filepath.Join(dir, fsSafeTestName.Replace(t.Name())) + snapPath := socketPath + "SnapFile" + memPath := socketPath + "MemFile" + defer os.Remove(socketPath) + defer os.Remove(snapPath) + defer os.Remove(memPath) + + // Tee logs for validation: + var logBuffer bytes.Buffer + machineLogger := logrus.New() + machineLogger.Out = io.MultiWriter(os.Stderr, &logBuffer) + + // Create a snapshot + { + cfg := createValidConfig(t, socketPath+".create") + m, err := NewMachine(ctx, cfg, func(m *Machine) { + // Rewriting m.cmd partially wouldn't work since Cmd has + // some unexported members + args := m.cmd.Args[1:] + m.cmd = exec.Command(getFirecrackerBinaryPath(), args...) + }, WithLogger(logrus.NewEntry(machineLogger))) + require.NoError(t, err) + + err = m.Start(ctx) + require.NoError(t, err) + + err = m.PauseVM(ctx) + require.NoError(t, err) + + err = m.CreateSnapshot(ctx, memPath, snapPath) + require.NoError(t, err) + + err = m.StopVMM() + require.NoError(t, err) + } + + // Load a snapshot + { + cfg := Config{ + DisableValidation: true, + SocketPath: socketPath + ".load", + Snapshot: SnapshotConfig{SnapshotPath: snapPath, MemFilePath: memPath}, + } + m, err := NewMachine(ctx, cfg, func(m *Machine) { + // Rewriting m.cmd partially wouldn't work since Cmd has + // some unexported members + args := m.cmd.Args[1:] + m.cmd = exec.Command(getFirecrackerBinaryPath(), args...) + }, WithLogger(logrus.NewEntry(machineLogger))) + require.NoError(t, err) + + err = m.Start(ctx) + require.NoError(t, err) + + err = m.ResumeVM(ctx) + require.NoError(t, err) + + err = m.StopVMM() + require.NoError(t, err) + } + +} + func testCreateBalloon(ctx context.Context, t *testing.T, m *Machine) { if err := m.CreateBalloon(ctx, testBalloonMemory, testBalloonDeflateOnOom, testStatsPollingIntervals); err != nil { t.Errorf("Create balloon device failed from testAttachBalloon: %s", err) diff --git a/machineiface.go b/machineiface.go index 79e0a46e..ac55b8b6 100644 --- a/machineiface.go +++ b/machineiface.go @@ -23,7 +23,7 @@ var _ MachineIface = (*Machine)(nil) // MachineIface can be used for mocking and testing of the Machine. The Machine // is subject to change, meaning this interface would change. type MachineIface interface { - Start(context.Context) error + Start(context.Context, ...Opt) error StopVMM() error Shutdown(context.Context) error Wait(context.Context) error diff --git a/snapshot.go b/snapshot.go new file mode 100644 index 00000000..eefdc47b --- /dev/null +++ b/snapshot.go @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package firecracker + +type SnapshotConfig struct { + MemFilePath string + SnapshotPath string + EnableDiffSnapshots bool + ResumeVM bool +}