Skip to content

Commit cf98c73

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 9b1e811 commit cf98c73

File tree

12 files changed

+325
-1
lines changed

12 files changed

+325
-1
lines changed

README.md

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

218+
#### `limactl suspend`
219+
`limactl suspend <INSTANCE>`: pause the instance
220+
221+
#### `limactl resume`
222+
`limactl resume <INSTANCE>`: unpause the instance
223+
218224
#### `limactl delete`
219225
`limactl delete [--force] <INSTANCE>`: delete the instance
220226

cmd/limactl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ func newApp() *cobra.Command {
9393
newEditCommand(),
9494
newFactoryResetCommand(),
9595
newDiskCommand(),
96+
newSuspendCommand(),
97+
newResumeCommand(),
9698
)
9799
return rootCmd
98100
}

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

pkg/driver/driver.go

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

10+
type BaseStatus struct {
11+
Paused bool
12+
Running bool
13+
}
14+
1015
type Driver interface {
1116
Validate() error
1217

@@ -15,6 +20,8 @@ type Driver interface {
1520
Start(_ context.Context) (chan error, error)
1621

1722
Stop(_ context.Context) error
23+
24+
QueryStatus(_ context.Context) (*BaseStatus, error)
1825
}
1926

2027
type BaseDriver struct {
@@ -39,3 +46,7 @@ func (d *BaseDriver) Start(_ context.Context) (chan error, error) {
3946
func (d *BaseDriver) Stop(_ context.Context) error {
4047
return nil
4148
}
49+
50+
func (d *BaseDriver) QueryStatus(_ context.Context) (*BaseStatus, error) {
51+
return &BaseStatus{Paused: false, Running: true}, nil
52+
}

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
@@ -305,6 +305,18 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
305305
return info, nil
306306
}
307307

308+
func (a *HostAgent) Status(ctx context.Context) (*hostagentapi.Status, error) {
309+
driverStatus, err := a.driver.QueryStatus(ctx)
310+
if err != nil {
311+
return nil, err
312+
}
313+
status := &hostagentapi.Status{
314+
Running: driverStatus.Running,
315+
Paused: driverStatus.Paused,
316+
}
317+
return status, nil
318+
}
319+
308320
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
309321
a.onClose = append(a.onClose, func() error {
310322
logrus.Debugf("shutting down the SSH master")

pkg/pause/pause.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package pause
2+
3+
import (
4+
"context"
5+
6+
"github.com/lima-vm/lima/pkg/limayaml"
7+
"github.com/lima-vm/lima/pkg/qemu"
8+
"github.com/lima-vm/lima/pkg/store"
9+
)
10+
11+
func stop(ctx context.Context, instName, instDir string, y *limayaml.LimaYAML) error {
12+
qCfg := qemu.Config{
13+
Name: instName,
14+
InstanceDir: instDir,
15+
LimaYAML: y,
16+
}
17+
return qemu.Stop(qCfg)
18+
}
19+
20+
func Suspend(ctx context.Context, inst *store.Instance) error {
21+
y, err := inst.LoadYAML()
22+
if err != nil {
23+
return err
24+
}
25+
26+
if err := stop(ctx, inst.Name, inst.Dir, y); err != nil {
27+
return err
28+
}
29+
30+
inst.Status = store.StatusPaused
31+
return nil
32+
}
33+
34+
func cont(ctx context.Context, instName, instDir string, y *limayaml.LimaYAML) error {
35+
qCfg := qemu.Config{
36+
Name: instName,
37+
InstanceDir: instDir,
38+
LimaYAML: y,
39+
}
40+
return qemu.Cont(qCfg)
41+
}
42+
43+
func Resume(ctx context.Context, inst *store.Instance) error {
44+
y, err := inst.LoadYAML()
45+
if err != nil {
46+
return err
47+
}
48+
49+
if err := cont(ctx, inst.Name, inst.Dir, y); err != nil {
50+
return err
51+
}
52+
53+
inst.Status = store.StatusRunning
54+
return nil
55+
}

pkg/qemu/qemu_driver.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,83 @@ func logPipeRoutine(r io.Reader, header string) {
154154
logrus.Debugf("%s: %s", header, line)
155155
}
156156
}
157+
func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {
158+
qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
159+
qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second)
160+
if err != nil {
161+
return nil, err
162+
}
163+
return qmpClient, nil
164+
}
165+
166+
func Stop(cfg Config) error {
167+
qmpClient, err := newQmpClient(cfg)
168+
if err != nil {
169+
return err
170+
}
171+
if err := qmpClient.Connect(); err != nil {
172+
return err
173+
}
174+
defer func() { _ = qmpClient.Disconnect() }()
175+
rawClient := raw.NewMonitor(qmpClient)
176+
logrus.Info("Sending QMP stop command")
177+
return rawClient.Stop()
178+
}
179+
180+
func Cont(cfg Config) error {
181+
qmpClient, err := newQmpClient(cfg)
182+
if err != nil {
183+
return err
184+
}
185+
if err := qmpClient.Connect(); err != nil {
186+
return err
187+
}
188+
defer func() { _ = qmpClient.Disconnect() }()
189+
rawClient := raw.NewMonitor(qmpClient)
190+
logrus.Info("Sending QMP cont command")
191+
return rawClient.Cont()
192+
}
193+
194+
func (l *LimaQemuDriver) QueryStatus(ctx context.Context) (*driver.BaseStatus, error) {
195+
qCfg := Config{
196+
Name: l.Instance.Name,
197+
InstanceDir: l.Instance.Dir,
198+
LimaYAML: l.Yaml,
199+
SSHLocalPort: l.SSHLocalPort,
200+
}
201+
status, err := queryStatus(qCfg)
202+
return &driver.BaseStatus{Paused: IsPaused(status), Running: IsRunning(status)}, err
203+
}
204+
205+
type statusInfo struct {
206+
raw.StatusInfo
207+
}
208+
209+
func queryStatus(cfg Config) (*statusInfo, error) {
210+
qmpClient, err := newQmpClient(cfg)
211+
if err != nil {
212+
return nil, err
213+
}
214+
if err := qmpClient.Connect(); err != nil {
215+
return nil, err
216+
}
217+
defer func() { _ = qmpClient.Disconnect() }()
218+
rawClient := raw.NewMonitor(qmpClient)
219+
logrus.Debug("Sending QMP query-status command")
220+
status, err := rawClient.QueryStatus()
221+
if err != nil {
222+
return nil, err
223+
}
224+
return &statusInfo{status}, nil
225+
}
226+
227+
func IsPaused(info *statusInfo) bool {
228+
return info.Status == raw.RunStatePaused
229+
}
230+
231+
func IsRunning(info *statusInfo) bool {
232+
return info.Status == raw.RunStateRunning
233+
}
157234

158235
type qArgTemplateApplier struct {
159236
files []*os.File

0 commit comments

Comments
 (0)