Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the snapshot commands #1054

Merged
merged 1 commit into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ $ limactl ls --format='{{.SSHConfigFile}}' default
$ ssh -F /Users/example/.lima/default/ssh.config lima-default
```

#### `limactl snapshot`
`limactl snapshot <COMMAND> <INSTANCE>`: manage instance snapshots

Commands:
`limactl snapshot create --tag TAG INSTANCE` : create (save) a snapshot
`limactl snapshot apply --tag TAG INSTANCE` : apply (load) a snapshot
`limactl snapshot delete --tag TAG INSTANCE` : delete (del) a snapshot
`limactl snapshot list INSTANCE` : list existing snapshots in instance

#### `limactl completion`
- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.

Expand Down
1 change: 1 addition & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func newApp() *cobra.Command {
newDiskCommand(),
newUsernetCommand(),
newGenManCommand(),
newSnapshotCommand(),
)
return rootCmd
}
Expand Down
185 changes: 185 additions & 0 deletions cmd/limactl/snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package main

import (
"fmt"
"strings"

"github.com/lima-vm/lima/pkg/snapshot"
"github.com/lima-vm/lima/pkg/store"

"github.com/spf13/cobra"
)

func newSnapshotCommand() *cobra.Command {
var snapshotCmd = &cobra.Command{
Use: "snapshot",
Short: "Manage instance snapshots",
}
snapshotCmd.AddCommand(newSnapshotApplyCommand())
snapshotCmd.AddCommand(newSnapshotCreateCommand())
snapshotCmd.AddCommand(newSnapshotDeleteCommand())
snapshotCmd.AddCommand(newSnapshotListCommand())

return snapshotCmd
}

func newSnapshotCreateCommand() *cobra.Command {
var createCmd = &cobra.Command{
Use: "create INSTANCE",
Aliases: []string{"save"},
Short: "Create (save) a snapshot",
Args: cobra.MinimumNArgs(1),
RunE: snapshotCreateAction,
ValidArgsFunction: snapshotBashComplete,
}
createCmd.Flags().String("tag", "", "name of the snapshot")

return createCmd
}

func snapshotCreateAction(cmd *cobra.Command, args []string) error {
instName := args[0]

inst, err := store.Inspect(instName)
if err != nil {
return err
}

tag, err := cmd.Flags().GetString("tag")
if err != nil {
return err
}

if tag == "" {
return fmt.Errorf("expected tag")
}

ctx := cmd.Context()
return snapshot.Save(ctx, inst, tag)
}

func newSnapshotDeleteCommand() *cobra.Command {
var deleteCmd = &cobra.Command{
Use: "delete INSTANCE",
Aliases: []string{"del"},
Short: "Delete (del) a snapshot",
Args: cobra.MinimumNArgs(1),
RunE: snapshotDeleteAction,
ValidArgsFunction: snapshotBashComplete,
}
deleteCmd.Flags().String("tag", "", "name of the snapshot")

return deleteCmd
}

func snapshotDeleteAction(cmd *cobra.Command, args []string) error {
instName := args[0]

inst, err := store.Inspect(instName)
if err != nil {
return err
}

tag, err := cmd.Flags().GetString("tag")
if err != nil {
return err
}

if tag == "" {
return fmt.Errorf("expected tag")
}

ctx := cmd.Context()
return snapshot.Del(ctx, inst, tag)
}

func newSnapshotApplyCommand() *cobra.Command {
var applyCmd = &cobra.Command{
Use: "apply INSTANCE",
Aliases: []string{"load"},
Short: "Apply (load) a snapshot",
Args: cobra.MinimumNArgs(1),
RunE: snapshotApplyAction,
ValidArgsFunction: snapshotBashComplete,
}
applyCmd.Flags().String("tag", "", "name of the snapshot")

return applyCmd
}

func snapshotApplyAction(cmd *cobra.Command, args []string) error {
instName := args[0]

inst, err := store.Inspect(instName)
if err != nil {
return err
}

tag, err := cmd.Flags().GetString("tag")
if err != nil {
return err
}

if tag == "" {
return fmt.Errorf("expected tag")
}

ctx := cmd.Context()
return snapshot.Load(ctx, inst, tag)
}

func newSnapshotListCommand() *cobra.Command {
var listCmd = &cobra.Command{
Use: "list INSTANCE",
Aliases: []string{"ls"},
Short: "List existing snapshots",
Args: cobra.MinimumNArgs(1),
RunE: snapshotListAction,
ValidArgsFunction: snapshotBashComplete,
}
listCmd.Flags().BoolP("quiet", "q", false, "Only show tags")

return listCmd
}

func snapshotListAction(cmd *cobra.Command, args []string) error {
instName := args[0]

inst, err := store.Inspect(instName)
if err != nil {
return err
}

quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
return err
}
ctx := cmd.Context()
out, err := snapshot.List(ctx, inst)
if err != nil {
return err
}
if quiet {
for i, line := range strings.Split(out, "\n") {
// "ID", "TAG", "VM SIZE", "DATE", "VM CLOCK", "ICOUNT"
fields := strings.Fields(line)
if i == 0 && len(fields) > 1 && fields[1] != "TAG" {
// make sure that output matches the expected
return fmt.Errorf("unknown header: %s", line)
}
if i == 0 || line == "" {
// skip header and empty line after using split
continue
}
tag := fields[1]
fmt.Printf("%s\n", tag)
}
return nil
}
fmt.Print(out)
return nil
}

func snapshotBashComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
45 changes: 45 additions & 0 deletions hack/test-example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ declare -A CHECKS=(
["mount-home"]="1"
["containerd-user"]="1"
["restart"]="1"
["snapshot-online"]="1"
["snapshot-offline"]="1"
["port-forwards"]="1"
["vmnet"]=""
["disk"]=""
Expand All @@ -44,6 +46,9 @@ case "$NAME" in
# ● run-r2b459797f5b04262bfa79984077a65c7.service loaded failed failed /usr/bin/systemctl start man-db-cache-update
CHECKS["systemd-strict"]=
;;
"9p")
CHECKS["snapshot-online"]=""
;;
"vmnet")
CHECKS["vmnet"]=1
;;
Expand All @@ -52,6 +57,7 @@ case "$NAME" in
;;
"net-user-v2")
CHECKS["port-forwards"]=""
CHECKS["snapshot-online"]=""
CHECKS["user-v2"]=1
;;
esac
Expand Down Expand Up @@ -329,6 +335,45 @@ if [[ -n ${CHECKS["user-v2"]} ]]; then
limactl delete "$secondvm"
set +x
fi
if [[ -n ${CHECKS["snapshot-online"]} ]]; then
INFO "Testing online snapshots"
limactl shell "$NAME" sh -c 'echo foo > /tmp/test'
limactl snapshot create "$NAME" --tag snap1
got=$(limactl snapshot list "$NAME" --quiet)
expected="snap1"
INFO "snapshot list: expected=${expected} got=${got}"
if [ "$got" != "$expected" ]; then
ERROR "snapshot list did not return expected value"
exit 1
fi
limactl shell "$NAME" sh -c 'echo bar > /tmp/test'
limactl snapshot apply "$NAME" --tag snap1
got=$(limactl shell "$NAME" cat /tmp/test)
expected="foo"
INFO "snapshot apply: expected=${expected} got=${got}"
if [ "$got" != "$expected" ]; then
ERROR "snapshot apply did not restore snapshot"
exit 1
fi
limactl snapshot delete "$NAME" --tag snap1
limactl shell "$NAME" rm /tmp/test
fi
if [[ -n ${CHECKS["snapshot-offline"]} ]]; then
INFO "Testing offline snapshots"
limactl stop "$NAME"
sleep 3
limactl snapshot create "$NAME" --tag snap2
got=$(limactl snapshot list "$NAME" --quiet)
expected="snap2"
INFO "snapshot list: expected=${expected} got=${got}"
if [ "$got" != "$expected" ]; then
ERROR "snapshot list did not return expected value"
exit 1
fi
limactl snapshot apply "$NAME" --tag snap2
limactl snapshot delete "$NAME" --tag snap2
limactl start "$NAME"
fi

INFO "Stopping \"$NAME\""
limactl stop "$NAME"
Expand Down
25 changes: 25 additions & 0 deletions pkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package driver

import (
"context"
"fmt"

"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/store"
Expand All @@ -19,6 +20,14 @@ type Driver interface {
ChangeDisplayPassword(_ context.Context, password string) error

GetDisplayConnection(_ context.Context) (string, error)

CreateSnapshot(_ context.Context, tag string) error

ApplySnapshot(_ context.Context, tag string) error

DeleteSnapshot(_ context.Context, tag string) error

ListSnapshots(_ context.Context) (string, error)
}

type BaseDriver struct {
Expand Down Expand Up @@ -51,3 +60,19 @@ func (d *BaseDriver) ChangeDisplayPassword(_ context.Context, password string) e
func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
return "", nil
}

func (d *BaseDriver) CreateSnapshot(_ context.Context, _ string) error {
return fmt.Errorf("unimplemented")
}

func (d *BaseDriver) ApplySnapshot(_ context.Context, _ string) error {
return fmt.Errorf("unimplemented")
}

func (d *BaseDriver) DeleteSnapshot(_ context.Context, _ string) error {
return fmt.Errorf("unimplemented")
}

func (d *BaseDriver) ListSnapshots(_ context.Context) (string, error) {
return "", fmt.Errorf("unimplemented")
}
Loading