Skip to content

Commit c07452a

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 <anders.f.bjorklund@gmail.com>
1 parent c494ea2 commit c07452a

File tree

8 files changed

+443
-1
lines changed

8 files changed

+443
-1
lines changed

README.md

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

242+
#### `limactl snapshot`
243+
`limactl snapshot <COMMAND> <INSTANCE>`: manage instance snapshots
244+
245+
Commands:
246+
`limactl snapshot create --tag TAG INSTANCE` : create (save) a snapshot
247+
`limactl snapshot apply --tag TAG INSTANCE` : apply (load) a snapshot
248+
`limactl snapshot delete --tag TAG INSTANCE` : delete (del) a snapshot
249+
`limactl snapshot list INSTANCE` : list existing snapshots in instance
250+
242251
#### `limactl completion`
243252
- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.
244253

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func newApp() *cobra.Command {
100100
newEditCommand(),
101101
newFactoryResetCommand(),
102102
newDiskCommand(),
103+
newSnapshotCommand(),
103104
)
104105
return rootCmd
105106
}

cmd/limactl/snapshot.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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 || line == "" {
167+
// skip header and empty line after using split
168+
continue
169+
}
170+
tag := fields[1]
171+
fmt.Printf("%s\n", tag)
172+
}
173+
return nil
174+
}
175+
fmt.Print(out)
176+
return nil
177+
}
178+
179+
func snapshotBashComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
180+
return bashCompleteInstanceNames(cmd)
181+
}

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"]=""
@@ -43,6 +45,9 @@ case "$NAME" in
4345
# ● run-r2b459797f5b04262bfa79984077a65c7.service loaded failed failed /usr/bin/systemctl start man-db-cache-update
4446
CHECKS["systemd-strict"]=
4547
;;
48+
"9p")
49+
CHECKS["snapshot-online"]=""
50+
;;
4651
"vmnet")
4752
CHECKS["vmnet"]=1
4853
;;
@@ -303,6 +308,46 @@ if [[ -n ${CHECKS["restart"]} ]]; then
303308
fi
304309
fi
305310

311+
if [[ -n ${CHECKS["snapshot-online"]} ]]; then
312+
INFO "Testing online snapshots"
313+
limactl shell "$NAME" sh -c 'echo foo > /tmp/test'
314+
limactl snapshot create "$NAME" --tag snap1
315+
got=$(limactl snapshot list "$NAME" --quiet)
316+
expected="snap1"
317+
INFO "snapshot list: expected=${expected} got=${got}"
318+
if [ "$got" != "$expected" ]; then
319+
ERROR "snapshot list did not return expected value"
320+
exit 1
321+
fi
322+
limactl shell "$NAME" sh -c 'echo bar > /tmp/test'
323+
limactl snapshot apply "$NAME" --tag snap1
324+
got=$(limactl shell "$NAME" cat /tmp/test)
325+
expected="foo"
326+
INFO "snapshot apply: expected=${expected} got=${got}"
327+
if [ "$got" != "$expected" ]; then
328+
ERROR "snapshot apply did not restore snapshot"
329+
exit 1
330+
fi
331+
limactl snapshot delete "$NAME" --tag snap1
332+
limactl shell "$NAME" rm /tmp/test
333+
fi
334+
if [[ -n ${CHECKS["snapshot-offline"]} ]]; then
335+
INFO "Testing offline snapshots"
336+
limactl stop "$NAME"
337+
sleep 3
338+
limactl snapshot create "$NAME" --tag snap2
339+
got=$(limactl snapshot list "$NAME" --quiet)
340+
expected="snap2"
341+
INFO "snapshot list: expected=${expected} got=${got}"
342+
if [ "$got" != "$expected" ]; then
343+
ERROR "snapshot list did not return expected value"
344+
exit 1
345+
fi
346+
limactl snapshot apply "$NAME" --tag snap2
347+
limactl snapshot delete "$NAME" --tag snap2
348+
limactl start "$NAME"
349+
fi
350+
306351
INFO "Stopping \"$NAME\""
307352
limactl stop "$NAME"
308353
sleep 3

pkg/driver/driver.go

Lines changed: 26 additions & 1 deletion
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 {
@@ -49,5 +58,21 @@ func (d *BaseDriver) ChangeDisplayPassword(_ context.Context, password string) e
4958
}
5059

5160
func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
52-
return "", nil
61+
return "", fmt.Errorf("unimplemented")
62+
}
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")
5378
}

0 commit comments

Comments
 (0)