Skip to content

Commit 8f033e2

Browse files
committed
Implement the suspend and resume commands
For halting the emulation and putting the VM into paused run state. Also query the status of running instances, when showing in "list". * Use the new driver framework for pause * Add integration test for suspend/resume Signed-off-by: Anders F Björklund <anders.f.bjorklund@gmail.com>
1 parent 2cf4c26 commit 8f033e2

File tree

13 files changed

+371
-1
lines changed

13 files changed

+371
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ Use `<INSTANCE>:<FILENAME>` to specify a source or target inside an instance.
213213
#### `limactl stop`
214214
`limactl stop [--force] <INSTANCE>`: stop the instance
215215

216+
#### `limactl suspend`
217+
`limactl suspend <INSTANCE>`: pause the instance
218+
219+
#### `limactl resume`
220+
`limactl resume <INSTANCE>`: unpause the instance
221+
216222
#### `limactl delete`
217223
`limactl delete [--force] <INSTANCE>`: delete the instance
218224

cmd/limactl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ func newApp() *cobra.Command {
100100
newEditCommand(),
101101
newFactoryResetCommand(),
102102
newDiskCommand(),
103+
newSuspendCommand(),
104+
newResumeCommand(),
103105
)
104106
return rootCmd
105107
}

cmd/limactl/resume.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lima-vm/lima/pkg/pause"
7+
"github.com/lima-vm/lima/pkg/store"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newResumeCommand() *cobra.Command {
13+
var resumeCmd = &cobra.Command{
14+
Use: "resume INSTANCE",
15+
Short: "Resume (unpause) an instance",
16+
Aliases: []string{"unpause"},
17+
Args: cobra.MaximumNArgs(1),
18+
RunE: resumeAction,
19+
ValidArgsFunction: resumeBashComplete,
20+
}
21+
22+
return resumeCmd
23+
}
24+
25+
func resumeAction(cmd *cobra.Command, args []string) error {
26+
instName := DefaultInstanceName
27+
if len(args) > 0 {
28+
instName = args[0]
29+
}
30+
31+
inst, err := store.Inspect(instName)
32+
if err != nil {
33+
return err
34+
}
35+
36+
if inst.Status != store.StatusPaused {
37+
return fmt.Errorf("expected status %q, got %q", store.StatusPaused, inst.Status)
38+
}
39+
40+
ctx := cmd.Context()
41+
return pause.Resume(ctx, inst)
42+
}
43+
44+
func resumeBashComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
45+
return bashCompleteInstanceNames(cmd)
46+
}

cmd/limactl/suspend.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lima-vm/lima/pkg/pause"
7+
"github.com/lima-vm/lima/pkg/store"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newSuspendCommand() *cobra.Command {
13+
var suspendCmd = &cobra.Command{
14+
Use: "suspend INSTANCE",
15+
Short: "Suspend (pause) an instance",
16+
Aliases: []string{"pause"},
17+
Args: cobra.MaximumNArgs(1),
18+
RunE: suspendAction,
19+
ValidArgsFunction: suspendBashComplete,
20+
}
21+
22+
return suspendCmd
23+
}
24+
25+
func suspendAction(cmd *cobra.Command, args []string) error {
26+
instName := DefaultInstanceName
27+
if len(args) > 0 {
28+
instName = args[0]
29+
}
30+
31+
inst, err := store.Inspect(instName)
32+
if err != nil {
33+
return err
34+
}
35+
36+
if inst.Status != store.StatusRunning {
37+
return fmt.Errorf("expected status %q, got %q", store.StatusRunning, inst.Status)
38+
}
39+
40+
ctx := cmd.Context()
41+
return pause.Suspend(ctx, inst)
42+
}
43+
44+
func suspendBashComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
45+
return bashCompleteInstanceNames(cmd)
46+
}

hack/test-example.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ declare -A CHECKS=(
2121
["systemd-strict"]="1"
2222
["mount-home"]="1"
2323
["containerd-user"]="1"
24+
["pause"]="1"
2425
["restart"]="1"
2526
["port-forwards"]="1"
2627
["vmnet"]=""
@@ -263,6 +264,28 @@ if [[ -n ${CHECKS["disk"]} ]]; then
263264
set +x
264265
fi
265266

267+
if [[ -n ${CHECKS["pause"]} ]]; then
268+
INFO "Suspending \"$NAME\""
269+
limactl suspend "$NAME"
270+
271+
got=$(limactl ls --format '{{.Status}}' "$NAME")
272+
expected="Paused"
273+
if [ "$got" != "$expected" ]; then
274+
ERROR "suspend status: expected=${expected} got=${got}"
275+
exit 1
276+
fi
277+
278+
INFO "Resuming \"$NAME\""
279+
limactl resume "$NAME"
280+
281+
got=$(limactl ls --format '{{.Status}}' "$NAME")
282+
expected="Running"
283+
if [ "$got" != "$expected" ]; then
284+
ERROR "resume status: expected=${expected} got=${got}"
285+
exit 1
286+
fi
287+
fi
288+
266289
if [[ -n ${CHECKS["restart"]} ]]; then
267290
INFO "Create file in the guest home directory and verify that it still exists after a restart"
268291
# shellcheck disable=SC2016

pkg/driver/driver.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ 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"
89
)
910

11+
type BaseStatus struct {
12+
Paused bool
13+
Running bool
14+
}
15+
1016
type Driver interface {
1117
Validate() error
1218

@@ -19,6 +25,12 @@ type Driver interface {
1925
ChangeDisplayPassword(_ context.Context, password string) error
2026

2127
GetDisplayConnection(_ context.Context) (string, error)
28+
29+
Suspend(_ context.Context) error
30+
31+
Resume(_ context.Context) error
32+
33+
QueryStatus(_ context.Context) (*BaseStatus, error)
2234
}
2335

2436
type BaseDriver struct {
@@ -51,3 +63,15 @@ func (d *BaseDriver) ChangeDisplayPassword(_ context.Context, password string) e
5163
func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
5264
return "", nil
5365
}
66+
67+
func (d *BaseDriver) Suspend(_ context.Context) error {
68+
return fmt.Errorf("unimplemented")
69+
}
70+
71+
func (d *BaseDriver) Resume(_ context.Context) error {
72+
return fmt.Errorf("unimplemented")
73+
}
74+
75+
func (d *BaseDriver) QueryStatus(_ context.Context) (*BaseStatus, error) {
76+
return &BaseStatus{Paused: false, Running: true}, nil
77+
}

pkg/hostagent/api/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ package api
33
type Info struct {
44
SSHLocalPort int `json:"sshLocalPort,omitempty"`
55
}
6+
7+
type Status struct {
8+
Running bool `json:"running"`
9+
Paused bool `json:"paused"`
10+
}

pkg/hostagent/api/client/client.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
type HostAgentClient interface {
1717
HTTPClient() *http.Client
1818
Info(context.Context) (*api.Info, error)
19+
Status(context.Context) (*api.Status, error)
1920
}
2021

2122
// NewHostAgentClient creates a client.
@@ -62,3 +63,18 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) {
6263
}
6364
return &info, nil
6465
}
66+
67+
func (c *client) Status(ctx context.Context) (*api.Status, error) {
68+
u := fmt.Sprintf("http://%s/%s/status", c.dummyHost, c.version)
69+
resp, err := httpclientutil.Get(ctx, c.HTTPClient(), u)
70+
if err != nil {
71+
return nil, err
72+
}
73+
defer resp.Body.Close()
74+
var status api.Status
75+
dec := json.NewDecoder(resp.Body)
76+
if err := dec.Decode(&status); err != nil {
77+
return nil, err
78+
}
79+
return &status, nil
80+
}

pkg/hostagent/api/server/server.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,29 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) {
4646
_, _ = w.Write(m)
4747
}
4848

49+
// GetStatus is the handler for GET /v{N}/status
50+
func (b *Backend) GetStatus(w http.ResponseWriter, r *http.Request) {
51+
ctx := r.Context()
52+
ctx, cancel := context.WithCancel(ctx)
53+
defer cancel()
54+
55+
status, err := b.Agent.Status(ctx)
56+
if err != nil {
57+
b.onError(w, r, err, http.StatusInternalServerError)
58+
return
59+
}
60+
m, err := json.Marshal(status)
61+
if err != nil {
62+
b.onError(w, r, err, http.StatusInternalServerError)
63+
return
64+
}
65+
w.Header().Set("Content-Type", "application/json")
66+
w.WriteHeader(http.StatusOK)
67+
_, _ = w.Write(m)
68+
}
69+
4970
func AddRoutes(r *mux.Router, b *Backend) {
5071
v1 := r.PathPrefix("/v1").Subrouter()
5172
v1.Path("/info").Methods("GET").HandlerFunc(b.GetInfo)
73+
v1.Path("/status").Methods("GET").HandlerFunc(b.GetStatus)
5274
}

pkg/hostagent/hostagent.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,18 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
381381
return info, nil
382382
}
383383

384+
func (a *HostAgent) Status(ctx context.Context) (*hostagentapi.Status, error) {
385+
driverStatus, err := a.driver.QueryStatus(ctx)
386+
if err != nil {
387+
return nil, err
388+
}
389+
status := &hostagentapi.Status{
390+
Running: driverStatus.Running,
391+
Paused: driverStatus.Paused,
392+
}
393+
return status, nil
394+
}
395+
384396
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
385397
a.onClose = append(a.onClose, func() error {
386398
logrus.Debugf("shutting down the SSH master")

pkg/pause/pause.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package pause
2+
3+
import (
4+
"context"
5+
6+
"github.com/lima-vm/lima/pkg/driver"
7+
"github.com/lima-vm/lima/pkg/driverutil"
8+
"github.com/lima-vm/lima/pkg/store"
9+
)
10+
11+
func Suspend(ctx context.Context, inst *store.Instance) error {
12+
y, err := inst.LoadYAML()
13+
if err != nil {
14+
return err
15+
}
16+
17+
limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
18+
Instance: inst,
19+
Yaml: y,
20+
})
21+
22+
if err := limaDriver.Suspend(ctx); err != nil {
23+
return err
24+
}
25+
26+
inst.Status = store.StatusPaused
27+
return nil
28+
}
29+
30+
func Resume(ctx context.Context, inst *store.Instance) error {
31+
y, err := inst.LoadYAML()
32+
if err != nil {
33+
return err
34+
}
35+
36+
limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
37+
Instance: inst,
38+
Yaml: y,
39+
})
40+
41+
if err := limaDriver.Resume(ctx); err != nil {
42+
return err
43+
}
44+
45+
inst.Status = store.StatusRunning
46+
return nil
47+
}

0 commit comments

Comments
 (0)