Skip to content

Commit 89281e7

Browse files
committed
Implement the snapshot command and subcommands
For handling qcow2 snapshots of the running virtual machine. It is possible to make multiple snapshots, in the diffdisk. * Use the new driver framework for snapshot * Add integration test for limactl snapshot Signed-off-by: Anders F Björklund <[email protected]>
1 parent 5d87656 commit 89281e7

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)