From 0ff1a94c554a25fb5ac21d9a8abed688433b32f9 Mon Sep 17 00:00:00 2001 From: David Son Date: Wed, 22 Jun 2022 17:35:51 +0000 Subject: [PATCH] Implemented snapshot loading capabilities Signed-off-by: David Son --- firecracker.go | 17 ++++++++ handlers.go | 2 +- machine.go | 37 ++++++++++++++++- machine_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ machineiface.go | 2 +- opts.go | 10 +++++ snapshot.go | 22 ++++++++++ 7 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 snapshot.go diff --git a/firecracker.go b/firecracker.go index 20ab74c3..d8e5e4bf 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/handlers.go b/handlers.go index 24013836..5526c066 100644 --- a/handlers.go +++ b/handlers.go @@ -34,7 +34,7 @@ const ( LinkFilesToRootFSHandlerName = "fcinit.LinkFilesToRootFS" SetupNetworkHandlerName = "fcinit.SetupNetwork" SetupKernelArgsHandlerName = "fcinit.SetupKernelArgs" - CreateBalloonHandlerName = "fcint.CreateBalloon" + CreateBalloonHandlerName = "fcinit.CreateBalloon" ValidateCfgHandlerName = "validate.Cfg" ValidateJailerCfgHandlerName = "validate.JailerCfg" diff --git a/machine.go b/machine.go index af55a2b9..58c6d614 100644 --- a/machine.go +++ b/machine.go @@ -151,6 +151,13 @@ 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 + + // Configuration for snapshot loading + 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 @@ -381,7 +388,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 ...StartOpt) error { m.logger.Debug("Called Machine.Start()") alreadyStarted := true m.startOnce.Do(func() { @@ -402,6 +409,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 @@ -719,6 +730,18 @@ 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, + }, ss.Opts...) + return err + } + resp, err := m.client.PutMachineConfiguration(ctx, &m.Cfg.MachineCfg) if err != nil { m.logger.Errorf("PutMachineConfiguration returned %s", resp.Error()) @@ -735,6 +758,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, @@ -814,6 +841,10 @@ func (m *Machine) UpdateGuestNetworkInterfaceRateLimit(ctx context.Context, ifac // attachDrive attaches a secondary block device func (m *Machine) attachDrive(ctx context.Context, dev models.Drive) error { + if m.Cfg.hasSnapshot() { + return nil + } + hostPath := StringValue(dev.PathOnHost) m.logger.Infof("Attaching drive %s, slot %s, root %t.", hostPath, StringValue(dev.DriveID), BoolValue(dev.IsRootDevice)) respNoContent, err := m.client.PutGuestDriveByID(ctx, StringValue(dev.DriveID), &dev) @@ -842,6 +873,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..ce310af4 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1728,6 +1728,111 @@ func TestCreateSnapshot(t *testing.T) { } } +func TestLoadSnapshot(t *testing.T) { + fctesting.RequiresKVM(t) + fctesting.RequiresRoot(t) + + dir, err := ioutil.TempDir("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + cases := []struct { + name string + createSnapshot func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) + loadSnapshot func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) + }{ + { + name: "TestLoadSnapshot", + createSnapshot: func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) { + // 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) + }, + + loadSnapshot: func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) { + cfg := createValidConfig(t, socketPath+".load") + 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, WithSnapshot(memPath, snapPath)) + require.NoError(t, err) + + err = m.ResumeVM(ctx) + require.NoError(t, err) + + err = m.StopVMM() + require.NoError(t, err) + }, + }, + { + name: "TestLoadSnapshot without create", + createSnapshot: func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) { + + }, + + loadSnapshot: func(ctx context.Context, machineLogger *logrus.Logger, socketPath, memPath, snapPath string) { + cfg := createValidConfig(t, socketPath+".load") + 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, WithSnapshot(memPath, snapPath)) + require.Error(t, err) + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + ctx := context.Background() + + // 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) + + c.createSnapshot(ctx, machineLogger, socketPath, snapPath, memPath) + c.loadSnapshot(ctx, machineLogger, socketPath, snapPath, memPath) + }) + } + +} + 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..a10a588c 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, ...StartOpt) error StopVMM() error Shutdown(context.Context) error Wait(context.Context) error diff --git a/opts.go b/opts.go index 842772c0..95ce540a 100644 --- a/opts.go +++ b/opts.go @@ -21,6 +21,7 @@ import ( // Opt represents a functional option to help modify functionality of a Machine. type Opt func(*Machine) +type StartOpt func(*Machine) // WithClient will use the client in place rather than the client constructed // during bootstrapping of the machine. This option is useful for mocking out @@ -47,3 +48,12 @@ func WithProcessRunner(cmd *exec.Cmd) Opt { machine.cmd = cmd } } + +// WithSnapshot will allow for the machine to start using a given snapshot. +func WithSnapshot(memFilePath, snapshotPath string, opts ...LoadSnapshotOpt) StartOpt { + return func(m *Machine) { + m.Cfg.Snapshot.MemFilePath = memFilePath + m.Cfg.Snapshot.SnapshotPath = snapshotPath + m.Cfg.Snapshot.Opts = opts + } +} diff --git a/snapshot.go b/snapshot.go new file mode 100644 index 00000000..f7f55762 --- /dev/null +++ b/snapshot.go @@ -0,0 +1,22 @@ +// 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 + Opts []LoadSnapshotOpt +}