Skip to content

Commit a235d10

Browse files
authored
Merge pull request #1054 from afbjorklund/snapshot
Implement the snapshot commands
2 parents 72a9725 + 89281e7 commit a235d10

File tree

8 files changed

+461
-0
lines changed

8 files changed

+461
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@ $ limactl ls --format='{{.SSHConfigFile}}' default
244244
$ ssh -F /Users/example/.lima/default/ssh.config lima-default
245245
```
246246

247+
#### `limactl snapshot`
248+
`limactl snapshot <COMMAND> <INSTANCE>`: manage instance snapshots
249+
250+
Commands:
251+
`limactl snapshot create --tag TAG INSTANCE` : create (save) a snapshot
252+
`limactl snapshot apply --tag TAG INSTANCE` : apply (load) a snapshot
253+
`limactl snapshot delete --tag TAG INSTANCE` : delete (del) a snapshot
254+
`limactl snapshot list INSTANCE` : list existing snapshots in instance
255+
247256
#### `limactl completion`
248257
- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.
249258

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func newApp() *cobra.Command {
112112
newDiskCommand(),
113113
newUsernetCommand(),
114114
newGenManCommand(),
115+
newSnapshotCommand(),
115116
)
116117
return rootCmd
117118
}

cmd/limactl/snapshot.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/lima-vm/lima/pkg/snapshot"
8+
"github.com/lima-vm/lima/pkg/store"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func newSnapshotCommand() *cobra.Command {
14+
var snapshotCmd = &cobra.Command{
15+
Use: "snapshot",
16+
Short: "Manage instance snapshots",
17+
}
18+
snapshotCmd.AddCommand(newSnapshotApplyCommand())
19+
snapshotCmd.AddCommand(newSnapshotCreateCommand())
20+
snapshotCmd.AddCommand(newSnapshotDeleteCommand())
21+
snapshotCmd.AddCommand(newSnapshotListCommand())
22+
23+
return snapshotCmd
24+
}
25+
26+
func newSnapshotCreateCommand() *cobra.Command {
27+
var createCmd = &cobra.Command{
28+
Use: "create INSTANCE",
29+
Aliases: []string{"save"},
30+
Short: "Create (save) a snapshot",
31+
Args: cobra.MinimumNArgs(1),
32+
RunE: snapshotCreateAction,
33+
ValidArgsFunction: snapshotBashComplete,
34+
}
35+
createCmd.Flags().String("tag", "", "name of the snapshot")
36+
37+
return createCmd
38+
}
39+
40+
func snapshotCreateAction(cmd *cobra.Command, args []string) error {
41+
instName := args[0]
42+
43+
inst, err := store.Inspect(instName)
44+
if err != nil {
45+
return err
46+
}
47+
48+
tag, err := cmd.Flags().GetString("tag")
49+
if err != nil {
50+
return err
51+
}
52+
53+
if tag == "" {
54+
return fmt.Errorf("expected tag")
55+
}
56+
57+
ctx := cmd.Context()
58+
return snapshot.Save(ctx, inst, tag)
59+
}
60+
61+
func newSnapshotDeleteCommand() *cobra.Command {
62+
var deleteCmd = &cobra.Command{
63+
Use: "delete INSTANCE",
64+
Aliases: []string{"del"},
65+
Short: "Delete (del) a snapshot",
66+
Args: cobra.MinimumNArgs(1),
67+
RunE: snapshotDeleteAction,
68+
ValidArgsFunction: snapshotBashComplete,
69+
}
70+
deleteCmd.Flags().String("tag", "", "name of the snapshot")
71+
72+
return deleteCmd
73+
}
74+
75+
func snapshotDeleteAction(cmd *cobra.Command, args []string) error {
76+
instName := args[0]
77+
78+
inst, err := store.Inspect(instName)
79+
if err != nil {
80+
return err
81+
}
82+
83+
tag, err := cmd.Flags().GetString("tag")
84+
if err != nil {
85+
return err
86+
}
87+
88+
if tag == "" {
89+
return fmt.Errorf("expected tag")
90+
}
91+
92+
ctx := cmd.Context()
93+
return snapshot.Del(ctx, inst, tag)
94+
}
95+
96+
func newSnapshotApplyCommand() *cobra.Command {
97+
var applyCmd = &cobra.Command{
98+
Use: "apply INSTANCE",
99+
Aliases: []string{"load"},
100+
Short: "Apply (load) a snapshot",
101+
Args: cobra.MinimumNArgs(1),
102+
RunE: snapshotApplyAction,
103+
ValidArgsFunction: snapshotBashComplete,
104+
}
105+
applyCmd.Flags().String("tag", "", "name of the snapshot")
106+
107+
return applyCmd
108+
}
109+
110+
func snapshotApplyAction(cmd *cobra.Command, args []string) error {
111+
instName := args[0]
112+
113+
inst, err := store.Inspect(instName)
114+
if err != nil {
115+
return err
116+
}
117+
118+
tag, err := cmd.Flags().GetString("tag")
119+
if err != nil {
120+
return err
121+
}
122+
123+
if tag == "" {
124+
return fmt.Errorf("expected tag")
125+
}
126+
127+
ctx := cmd.Context()
128+
return snapshot.Load(ctx, inst, tag)
129+
}
130+
131+
func newSnapshotListCommand() *cobra.Command {
132+
var listCmd = &cobra.Command{
133+
Use: "list INSTANCE",
134+
Aliases: []string{"ls"},
135+
Short: "List existing snapshots",
136+
Args: cobra.MinimumNArgs(1),
137+
RunE: snapshotListAction,
138+
ValidArgsFunction: snapshotBashComplete,
139+
}
140+
listCmd.Flags().BoolP("quiet", "q", false, "Only show tags")
141+
142+
return listCmd
143+
}
144+
145+
func snapshotListAction(cmd *cobra.Command, args []string) error {
146+
instName := args[0]
147+
148+
inst, err := store.Inspect(instName)
149+
if err != nil {
150+
return err
151+
}
152+
153+
quiet, err := cmd.Flags().GetBool("quiet")
154+
if err != nil {
155+
return err
156+
}
157+
ctx := cmd.Context()
158+
out, err := snapshot.List(ctx, inst)
159+
if err != nil {
160+
return err
161+
}
162+
if quiet {
163+
for i, line := range strings.Split(out, "\n") {
164+
// "ID", "TAG", "VM SIZE", "DATE", "VM CLOCK", "ICOUNT"
165+
fields := strings.Fields(line)
166+
if i == 0 && len(fields) > 1 && fields[1] != "TAG" {
167+
// make sure that output matches the expected
168+
return fmt.Errorf("unknown header: %s", line)
169+
}
170+
if i == 0 || line == "" {
171+
// skip header and empty line after using split
172+
continue
173+
}
174+
tag := fields[1]
175+
fmt.Printf("%s\n", tag)
176+
}
177+
return nil
178+
}
179+
fmt.Print(out)
180+
return nil
181+
}
182+
183+
func snapshotBashComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
184+
return bashCompleteInstanceNames(cmd)
185+
}

hack/test-example.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ declare -A CHECKS=(
2222
["mount-home"]="1"
2323
["containerd-user"]="1"
2424
["restart"]="1"
25+
["snapshot-online"]="1"
26+
["snapshot-offline"]="1"
2527
["port-forwards"]="1"
2628
["vmnet"]=""
2729
["disk"]=""
@@ -44,6 +46,9 @@ case "$NAME" in
4446
# ● run-r2b459797f5b04262bfa79984077a65c7.service loaded failed failed /usr/bin/systemctl start man-db-cache-update
4547
CHECKS["systemd-strict"]=
4648
;;
49+
"9p")
50+
CHECKS["snapshot-online"]=""
51+
;;
4752
"vmnet")
4853
CHECKS["vmnet"]=1
4954
;;
@@ -52,6 +57,7 @@ case "$NAME" in
5257
;;
5358
"net-user-v2")
5459
CHECKS["port-forwards"]=""
60+
CHECKS["snapshot-online"]=""
5561
CHECKS["user-v2"]=1
5662
;;
5763
esac
@@ -329,6 +335,45 @@ if [[ -n ${CHECKS["user-v2"]} ]]; then
329335
limactl delete "$secondvm"
330336
set +x
331337
fi
338+
if [[ -n ${CHECKS["snapshot-online"]} ]]; then
339+
INFO "Testing online snapshots"
340+
limactl shell "$NAME" sh -c 'echo foo > /tmp/test'
341+
limactl snapshot create "$NAME" --tag snap1
342+
got=$(limactl snapshot list "$NAME" --quiet)
343+
expected="snap1"
344+
INFO "snapshot list: expected=${expected} got=${got}"
345+
if [ "$got" != "$expected" ]; then
346+
ERROR "snapshot list did not return expected value"
347+
exit 1
348+
fi
349+
limactl shell "$NAME" sh -c 'echo bar > /tmp/test'
350+
limactl snapshot apply "$NAME" --tag snap1
351+
got=$(limactl shell "$NAME" cat /tmp/test)
352+
expected="foo"
353+
INFO "snapshot apply: expected=${expected} got=${got}"
354+
if [ "$got" != "$expected" ]; then
355+
ERROR "snapshot apply did not restore snapshot"
356+
exit 1
357+
fi
358+
limactl snapshot delete "$NAME" --tag snap1
359+
limactl shell "$NAME" rm /tmp/test
360+
fi
361+
if [[ -n ${CHECKS["snapshot-offline"]} ]]; then
362+
INFO "Testing offline snapshots"
363+
limactl stop "$NAME"
364+
sleep 3
365+
limactl snapshot create "$NAME" --tag snap2
366+
got=$(limactl snapshot list "$NAME" --quiet)
367+
expected="snap2"
368+
INFO "snapshot list: expected=${expected} got=${got}"
369+
if [ "$got" != "$expected" ]; then
370+
ERROR "snapshot list did not return expected value"
371+
exit 1
372+
fi
373+
limactl snapshot apply "$NAME" --tag snap2
374+
limactl snapshot delete "$NAME" --tag snap2
375+
limactl start "$NAME"
376+
fi
332377

333378
INFO "Stopping \"$NAME\""
334379
limactl stop "$NAME"

pkg/driver/driver.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package driver
22

33
import (
44
"context"
5+
"fmt"
56

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

2122
GetDisplayConnection(_ context.Context) (string, error)
23+
24+
CreateSnapshot(_ context.Context, tag string) error
25+
26+
ApplySnapshot(_ context.Context, tag string) error
27+
28+
DeleteSnapshot(_ context.Context, tag string) error
29+
30+
ListSnapshots(_ context.Context) (string, error)
2231
}
2332

2433
type BaseDriver struct {
@@ -51,3 +60,19 @@ func (d *BaseDriver) ChangeDisplayPassword(_ context.Context, password string) e
5160
func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
5261
return "", nil
5362
}
63+
64+
func (d *BaseDriver) CreateSnapshot(_ context.Context, _ string) error {
65+
return fmt.Errorf("unimplemented")
66+
}
67+
68+
func (d *BaseDriver) ApplySnapshot(_ context.Context, _ string) error {
69+
return fmt.Errorf("unimplemented")
70+
}
71+
72+
func (d *BaseDriver) DeleteSnapshot(_ context.Context, _ string) error {
73+
return fmt.Errorf("unimplemented")
74+
}
75+
76+
func (d *BaseDriver) ListSnapshots(_ context.Context) (string, error) {
77+
return "", fmt.Errorf("unimplemented")
78+
}

0 commit comments

Comments
 (0)