Skip to content

Commit

Permalink
Implemented snapshot loading capabilities
Browse files Browse the repository at this point in the history
Signed-off-by: David Son <davbson@amazon.com>
  • Loading branch information
sondavidb committed Jun 28, 2022
1 parent f0a967e commit 490d76c
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 3 deletions.
17 changes: 17 additions & 0 deletions firecracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const (
LinkFilesToRootFSHandlerName = "fcinit.LinkFilesToRootFS"
SetupNetworkHandlerName = "fcinit.SetupNetwork"
SetupKernelArgsHandlerName = "fcinit.SetupKernelArgs"
CreateBalloonHandlerName = "fcint.CreateBalloon"
CreateBalloonHandlerName = "fcinit.CreateBalloon"
LoadSnapshotHandlerName = "fcinit.LoadSnapshot"

ValidateCfgHandlerName = "validate.Cfg"
ValidateJailerCfgHandlerName = "validate.JailerCfg"
Expand Down Expand Up @@ -280,6 +281,17 @@ func NewCreateBalloonHandler(amountMib int64, deflateOnOom bool, StatsPollingInt
}
}

// NewLoadSnapshotHandler is a named handler that loads a snapshot
// from the specified filepath
func NewLoadSnapshotHandler(memFilePath, snapshotPath string, opts ...LoadSnapshotOpt) Handler {
return Handler{
Name: LoadSnapshotHandlerName,
Fn: func(ctx context.Context, m *Machine) error {
return m.loadSnapshot(ctx, memFilePath, snapshotPath, opts...)
},
}
}

var defaultFcInitHandlerList = HandlerList{}.Append(
SetupNetworkHandler,
SetupKernelArgsHandler,
Expand Down
44 changes: 43 additions & 1 deletion machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -719,6 +730,10 @@ func (m *Machine) captureFifoToFileWithChannel(ctx context.Context, logger *log.
}

func (m *Machine) createMachine(ctx context.Context) error {
if m.Cfg.hasSnapshot() {
return nil
}

resp, err := m.client.PutMachineConfiguration(ctx, &m.Cfg.MachineCfg)
if err != nil {
m.logger.Errorf("PutMachineConfiguration returned %s", resp.Error())
Expand All @@ -735,6 +750,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,
Expand Down Expand Up @@ -814,6 +833,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)
Expand Down Expand Up @@ -842,6 +865,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,
Expand Down Expand Up @@ -1093,6 +1120,21 @@ func (m *Machine) CreateSnapshot(ctx context.Context, memFilePath, snapshotPath
return nil
}

// loadSnapshot loads a snapshot of the VM
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 {
return fmt.Errorf("failed to load a snapshot for VM: %v", 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{
Expand Down
105 changes: 105 additions & 0 deletions machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion machineiface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,3 +48,15 @@ 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

m.Handlers.FcInit = m.Handlers.FcInit.AppendAfter("fcinit.StartVMM",
NewLoadSnapshotHandler(memFilePath, snapshotPath, opts...))
}
}
22 changes: 22 additions & 0 deletions snapshot.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 490d76c

Please sign in to comment.