From 4af1b25f5ad12e1bbc68e843fee5db07285b05ac Mon Sep 17 00:00:00 2001 From: Gabriel Ionescu Date: Mon, 5 Jul 2021 16:44:10 +0300 Subject: [PATCH 1/5] add load snapshot implementation This commit adds implementation for the LoadSnapshot API call. Signed-off-by: Gabriel Ionescu Signed-off-by: Gabriel Ionescu --- firecracker.go | 17 +++++++++++++++++ machine.go | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) 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 506cf09d..30ea1ce9 100644 --- a/machine.go +++ b/machine.go @@ -1085,10 +1085,26 @@ func (m *Machine) CreateSnapshot(ctx context.Context, memFilePath, snapshotPath return nil } +// LoadSnapshot load a snapshot +func (m *Machine) LoadSnapshot(ctx context.Context, memFilePath, snapshotPath string, opts ...LoadSnapshotOpt) error { + snapshotParams := &models.SnapshotLoadParams{ + MemFilePath: String(memFilePath), + SnapshotPath: String(snapshotPath), + } + + if _, err := m.client.LoadSnapshot(ctx, snapshotParams, opts...); err != nil { + m.logger.Errorf("failed to load a snapshot for VM: %v", err) + return err + } + + m.logger.Debug("snapshot loaded successfully") + return nil +} + // CreateBalloon creates a balloon device if one does not exist func (m *Machine) CreateBalloon(ctx context.Context, amountMib int64, deflateOnOom bool, statsPollingIntervals int64, opts ...PutBalloonOpt) error { balloon := models.Balloon{ - AmountMib: &amountMib, + AmountMib: &amountMib, DeflateOnOom: &deflateOnOom, StatsPollingIntervals: statsPollingIntervals, } From 2b3103589123e9d7f5c827b774ff1da2474e7719 Mon Sep 17 00:00:00 2001 From: Gabriel Ionescu Date: Mon, 26 Jul 2021 07:09:46 +0300 Subject: [PATCH 2/5] extend machine start to support snapshots By adding functional options to Start(), we can call Start() with the WithSnapshot parameter to load a snapshot before starting. Signed-off-by: Gabriel Ionescu Signed-off-by: Gabriel Ionescu --- machine.go | 6 +++++- machineiface.go | 2 +- opts.go | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/machine.go b/machine.go index 30ea1ce9..cced7ea6 100644 --- a/machine.go +++ b/machine.go @@ -385,7 +385,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() { @@ -406,6 +406,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 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/opts.go b/opts.go index 842772c0..ba20d640 100644 --- a/opts.go +++ b/opts.go @@ -14,6 +14,7 @@ package firecracker import ( + "context" "os/exec" "github.com/sirupsen/logrus" @@ -47,3 +48,13 @@ func WithProcessRunner(cmd *exec.Cmd) Opt { machine.cmd = cmd } } + +// WithSnapshot will allow for the machine to start using a given snapshot. +func WithSnapshot(ctx context.Context, memFilePath, snapshotPath string) Opt { + return func(machine *Machine) { + err := machine.LoadSnapshot(ctx, memFilePath, snapshotPath) + if err != nil { + machine.logger.Errorf("LoadSnapshot failed with %s", err) + } + } +} From bf7ba8f02860f47569a12815dfc63256934132fa Mon Sep 17 00:00:00 2001 From: Gabriel Ionescu Date: Mon, 5 Jul 2021 22:19:46 +0300 Subject: [PATCH 3/5] test: add test for LoadSnapshot Test the LoadSnapshot functionality by creating a snapshot and loading it immediately. Signed-off-by: Gabriel Ionescu Signed-off-by: Gabriel Ionescu --- machine_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/machine_test.go b/machine_test.go index 52bfdf4d..db46b50a 100644 --- a/machine_test.go +++ b/machine_test.go @@ -72,6 +72,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 { @@ -1729,6 +1732,76 @@ 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 := 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(ctx, memPath, snapPath)) + 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) From ff57c092cacc00224199de31e9943eeb47b52404 Mon Sep 17 00:00:00 2001 From: Gabriel Ionescu Date: Mon, 5 Jul 2021 22:24:51 +0300 Subject: [PATCH 4/5] fix spacing in machine.go and machine_test.go Signed-off-by: Gabriel Ionescu Signed-off-by: Gabriel Ionescu --- machine_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine_test.go b/machine_test.go index db46b50a..1d242d05 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1810,7 +1810,7 @@ func testCreateBalloon(ctx context.Context, t *testing.T, m *Machine) { func testGetBalloonConfig(ctx context.Context, t *testing.T, m *Machine) { expectedBalloonConfig := models.Balloon{ - AmountMib: &testBalloonMemory, + AmountMib: &testBalloonMemory, DeflateOnOom: &testBalloonDeflateOnOom, StatsPollingIntervals: testStatsPollingIntervals, } From c7c01773a08ed361910924ff1322366acd4b5831 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Kato Date: Mon, 7 Mar 2022 18:01:32 +0000 Subject: [PATCH 5/5] Hide LoadSnapshot from Machine We should not expose LoadSnapshot, since it is allowed only before boot. Signed-off-by: Kazuyoshi Kato --- machine.go | 41 +++++++++++++++++++++++++---------------- machine_test.go | 8 ++++++-- opts.go | 11 ----------- snapshot.go | 21 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 snapshot.go diff --git a/machine.go b/machine.go index 14d7445f..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 @@ -722,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()) @@ -738,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, @@ -845,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, @@ -1096,22 +1121,6 @@ func (m *Machine) CreateSnapshot(ctx context.Context, memFilePath, snapshotPath return nil } -// LoadSnapshot load a snapshot -func (m *Machine) LoadSnapshot(ctx context.Context, memFilePath, snapshotPath string, opts ...LoadSnapshotOpt) error { - snapshotParams := &models.SnapshotLoadParams{ - MemFilePath: String(memFilePath), - SnapshotPath: String(snapshotPath), - } - - if _, err := m.client.LoadSnapshot(ctx, snapshotParams, opts...); err != nil { - m.logger.Errorf("failed to load a snapshot for VM: %v", err) - return err - } - - m.logger.Debug("snapshot loaded successfully") - return nil -} - // CreateBalloon creates a balloon device if one does not exist func (m *Machine) CreateBalloon(ctx context.Context, amountMib int64, deflateOnOom bool, statsPollingIntervals int64, opts ...PutBalloonOpt) error { balloon := models.Balloon{ diff --git a/machine_test.go b/machine_test.go index c7f17a0d..c9b3c8a2 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1780,7 +1780,11 @@ func TestLoadSnapshot(t *testing.T) { // Load a snapshot { - cfg := createValidConfig(t, socketPath+".load") + 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 @@ -1789,7 +1793,7 @@ func TestLoadSnapshot(t *testing.T) { }, WithLogger(logrus.NewEntry(machineLogger))) require.NoError(t, err) - err = m.Start(ctx, WithSnapshot(ctx, memPath, snapPath)) + err = m.Start(ctx) require.NoError(t, err) err = m.ResumeVM(ctx) diff --git a/opts.go b/opts.go index ba20d640..842772c0 100644 --- a/opts.go +++ b/opts.go @@ -14,7 +14,6 @@ package firecracker import ( - "context" "os/exec" "github.com/sirupsen/logrus" @@ -48,13 +47,3 @@ func WithProcessRunner(cmd *exec.Cmd) Opt { machine.cmd = cmd } } - -// WithSnapshot will allow for the machine to start using a given snapshot. -func WithSnapshot(ctx context.Context, memFilePath, snapshotPath string) Opt { - return func(machine *Machine) { - err := machine.LoadSnapshot(ctx, memFilePath, snapshotPath) - if err != nil { - machine.logger.Errorf("LoadSnapshot failed with %s", err) - } - } -} 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 +}