Skip to content

Commit 08e762d

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". Signed-off-by: Anders F Björklund <anders.f.bjorklund@gmail.com>
1 parent a550e52 commit 08e762d

File tree

13 files changed

+356
-1
lines changed

13 files changed

+356
-1
lines changed

cmd/limactl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ func newApp() *cobra.Command {
148148
newDiskCommand(),
149149
newUsernetCommand(),
150150
newGenDocCommand(),
151+
newResumeCommand(),
152+
newSuspendCommand(),
151153
newSnapshotCommand(),
152154
newProtectCommand(),
153155
newUnprotectCommand(),

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, _ []string, _ 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, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
45+
return bashCompleteInstanceNames(cmd)
46+
}

hack/test-templates.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ declare -A CHECKS=(
2626
["systemd-strict"]="1"
2727
["mount-home"]="1"
2828
["container-engine"]="1"
29+
["pause"]="1"
2930
["restart"]="1"
3031
# snapshot tests are too flaky (especially with archlinux)
3132
["snapshot-online"]=""
@@ -325,6 +326,28 @@ if [[ -n ${CHECKS["disk"]} ]]; then
325326
set +x
326327
fi
327328

329+
if [[ -n ${CHECKS["pause"]} ]]; then
330+
INFO "Suspending \"$NAME\""
331+
limactl suspend "$NAME"
332+
333+
got=$(limactl ls --format '{{.Status}}' "$NAME")
334+
expected="Paused"
335+
if [ "$got" != "$expected" ]; then
336+
ERROR "suspend status: expected=${expected} got=${got}"
337+
exit 1
338+
fi
339+
340+
INFO "Resuming \"$NAME\""
341+
limactl resume "$NAME"
342+
343+
got=$(limactl ls --format '{{.Status}}' "$NAME")
344+
expected="Running"
345+
if [ "$got" != "$expected" ]; then
346+
ERROR "resume status: expected=${expected} got=${got}"
347+
exit 1
348+
fi
349+
fi
350+
328351
if [[ -n ${CHECKS["restart"]} ]]; then
329352
INFO "Create file in the guest home directory and verify that it still exists after a restart"
330353
# shellcheck disable=SC2016

pkg/driver/driver.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
"github.com/lima-vm/lima/pkg/store"
1010
)
1111

12+
type BaseStatus struct {
13+
Paused bool
14+
Running bool
15+
}
16+
1217
// Driver interface is used by hostagent for managing vm.
1318
//
1419
// This interface is extended by BaseDriver which provides default implementation.
@@ -56,6 +61,13 @@ type Driver interface {
5661

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

64+
Suspend(_ context.Context) error
65+
66+
Resume(_ context.Context) error
67+
68+
// Status returns the current status of the vm instance.
69+
Status(_ context.Context) (*BaseStatus, error)
70+
5971
CreateSnapshot(_ context.Context, tag string) error
6072

6173
ApplySnapshot(_ context.Context, tag string) error
@@ -126,6 +138,18 @@ func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
126138
return "", nil
127139
}
128140

141+
func (d *BaseDriver) Suspend(_ context.Context) error {
142+
return fmt.Errorf("unimplemented")
143+
}
144+
145+
func (d *BaseDriver) Resume(_ context.Context) error {
146+
return fmt.Errorf("unimplemented")
147+
}
148+
149+
func (d *BaseDriver) Status(_ context.Context) (*BaseStatus, error) {
150+
return &BaseStatus{Running: true, Paused: false}, nil
151+
}
152+
129153
func (d *BaseDriver) CreateSnapshot(_ context.Context, _ string) error {
130154
return fmt.Errorf("unimplemented")
131155
}

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
@@ -50,6 +50,28 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) {
5050
_, _ = w.Write(m)
5151
}
5252

53+
// GetStatus is the handler for GET /v{N}/status
54+
func (b *Backend) GetStatus(w http.ResponseWriter, r *http.Request) {
55+
ctx := r.Context()
56+
ctx, cancel := context.WithCancel(ctx)
57+
defer cancel()
58+
59+
status, err := b.Agent.Status(ctx)
60+
if err != nil {
61+
b.onError(w, err, http.StatusInternalServerError)
62+
return
63+
}
64+
m, err := json.Marshal(status)
65+
if err != nil {
66+
b.onError(w, err, http.StatusInternalServerError)
67+
return
68+
}
69+
w.Header().Set("Content-Type", "application/json")
70+
w.WriteHeader(http.StatusOK)
71+
_, _ = w.Write(m)
72+
}
73+
5374
func AddRoutes(r *http.ServeMux, b *Backend) {
5475
r.Handle("/v1/info", http.HandlerFunc(b.GetInfo))
76+
r.Handle("/v1/status", http.HandlerFunc(b.GetStatus))
5577
}

pkg/hostagent/hostagent.go

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

469+
func (a *HostAgent) Status(ctx context.Context) (*hostagentapi.Status, error) {
470+
driverStatus, err := a.driver.Status(ctx)
471+
if err != nil {
472+
return nil, err
473+
}
474+
status := &hostagentapi.Status{
475+
Running: driverStatus.Running,
476+
Paused: driverStatus.Paused,
477+
}
478+
return status, nil
479+
}
480+
469481
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
470482
if *a.instConfig.Plain {
471483
logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.")

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+
InstConfig: 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+
InstConfig: 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+
}

pkg/qemu/qemu.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,64 @@ func execImgCommand(cfg Config, args ...string) (string, error) {
185185
return string(b), err
186186
}
187187

188+
func Stop(cfg Config) error {
189+
qmpClient, err := newQmpClient(cfg)
190+
if err != nil {
191+
return err
192+
}
193+
if err := qmpClient.Connect(); err != nil {
194+
return err
195+
}
196+
defer func() { _ = qmpClient.Disconnect() }()
197+
rawClient := raw.NewMonitor(qmpClient)
198+
logrus.Info("Sending QMP stop command")
199+
return rawClient.Stop()
200+
}
201+
202+
func Cont(cfg Config) error {
203+
qmpClient, err := newQmpClient(cfg)
204+
if err != nil {
205+
return err
206+
}
207+
if err := qmpClient.Connect(); err != nil {
208+
return err
209+
}
210+
defer func() { _ = qmpClient.Disconnect() }()
211+
rawClient := raw.NewMonitor(qmpClient)
212+
logrus.Info("Sending QMP cont command")
213+
return rawClient.Cont()
214+
}
215+
216+
type StatusInfo struct {
217+
raw.StatusInfo
218+
}
219+
220+
func QueryStatus(cfg Config) (*StatusInfo, error) {
221+
qmpClient, err := newQmpClient(cfg)
222+
if err != nil {
223+
return nil, err
224+
}
225+
if err := qmpClient.Connect(); err != nil {
226+
return nil, err
227+
}
228+
defer func() { _ = qmpClient.Disconnect() }()
229+
rawClient := raw.NewMonitor(qmpClient)
230+
logrus.Debug("Sending QMP query-status command")
231+
status, err := rawClient.QueryStatus()
232+
if err != nil {
233+
return nil, err
234+
}
235+
return &StatusInfo{status}, nil
236+
}
237+
238+
func IsPaused(info *StatusInfo) bool {
239+
return info.Status == raw.RunStatePaused
240+
}
241+
242+
func IsRunning(info *StatusInfo) bool {
243+
return info.Status == raw.RunStateRunning
244+
}
245+
188246
func Del(cfg Config, run bool, tag string) error {
189247
if run {
190248
out, err := sendHmpCommand(cfg, "delvm", tag)

0 commit comments

Comments
 (0)