Skip to content

Implement the suspend and resume commands #642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ func newApp() *cobra.Command {
newDiskCommand(),
newUsernetCommand(),
newGenDocCommand(),
newResumeCommand(),
newSuspendCommand(),
newSnapshotCommand(),
newProtectCommand(),
newUnprotectCommand(),
Expand Down
46 changes: 46 additions & 0 deletions cmd/limactl/resume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"

"github.com/lima-vm/lima/pkg/pause"
"github.com/lima-vm/lima/pkg/store"

"github.com/spf13/cobra"
)

func newResumeCommand() *cobra.Command {
resumeCmd := &cobra.Command{
Use: "resume INSTANCE",
Short: "Resume (unpause) an instance",
Aliases: []string{"unpause"},
Args: cobra.MaximumNArgs(1),
RunE: resumeAction,
ValidArgsFunction: resumeBashComplete,
}

return resumeCmd
}

func resumeAction(cmd *cobra.Command, args []string) error {
instName := DefaultInstanceName
if len(args) > 0 {
instName = args[0]
}

inst, err := store.Inspect(instName)
if err != nil {
return err
}

if inst.Status != store.StatusPaused {
return fmt.Errorf("expected status %q, got %q", store.StatusPaused, inst.Status)
}

ctx := cmd.Context()
return pause.Resume(ctx, inst)
}

func resumeBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
46 changes: 46 additions & 0 deletions cmd/limactl/suspend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"

"github.com/lima-vm/lima/pkg/pause"
"github.com/lima-vm/lima/pkg/store"

"github.com/spf13/cobra"
)

func newSuspendCommand() *cobra.Command {
suspendCmd := &cobra.Command{
Use: "suspend INSTANCE",
Short: "Suspend (pause) an instance",
Aliases: []string{"pause"},
Args: cobra.MaximumNArgs(1),
RunE: suspendAction,
ValidArgsFunction: suspendBashComplete,
}

return suspendCmd
}

func suspendAction(cmd *cobra.Command, args []string) error {
instName := DefaultInstanceName
if len(args) > 0 {
instName = args[0]
}

inst, err := store.Inspect(instName)
if err != nil {
return err
}

if inst.Status != store.StatusRunning {
return fmt.Errorf("expected status %q, got %q", store.StatusRunning, inst.Status)
}

ctx := cmd.Context()
return pause.Suspend(ctx, inst)
}

func suspendBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
23 changes: 23 additions & 0 deletions hack/test-templates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ declare -A CHECKS=(
["systemd-strict"]="1"
["mount-home"]="1"
["container-engine"]="1"
["pause"]="1"
["restart"]="1"
# snapshot tests are too flaky (especially with archlinux)
["snapshot-online"]=""
Expand Down Expand Up @@ -325,6 +326,28 @@ if [[ -n ${CHECKS["disk"]} ]]; then
set +x
fi

if [[ -n ${CHECKS["pause"]} ]]; then
INFO "Suspending \"$NAME\""
limactl suspend "$NAME"

got=$(limactl ls --format '{{.Status}}' "$NAME")
expected="Paused"
if [ "$got" != "$expected" ]; then
ERROR "suspend status: expected=${expected} got=${got}"
exit 1
fi

INFO "Resuming \"$NAME\""
limactl resume "$NAME"

got=$(limactl ls --format '{{.Status}}' "$NAME")
expected="Running"
if [ "$got" != "$expected" ]; then
ERROR "resume status: expected=${expected} got=${got}"
exit 1
fi
fi

if [[ -n ${CHECKS["restart"]} ]]; then
INFO "Create file in the guest home directory and verify that it still exists after a restart"
# shellcheck disable=SC2016
Expand Down
24 changes: 24 additions & 0 deletions pkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import (
"github.com/lima-vm/lima/pkg/store"
)

type BaseStatus struct {
Paused bool
Running bool
}

// Driver interface is used by hostagent for managing vm.
//
// This interface is extended by BaseDriver which provides default implementation.
Expand Down Expand Up @@ -55,6 +60,13 @@ type Driver interface {

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

Suspend(_ context.Context) error

Resume(_ context.Context) error

// Status returns the current status of the vm instance.
Status(_ context.Context) (*BaseStatus, error)

CreateSnapshot(_ context.Context, tag string) error

ApplySnapshot(_ context.Context, tag string) error
Expand Down Expand Up @@ -124,6 +136,18 @@ func (d *BaseDriver) GetDisplayConnection(_ context.Context) (string, error) {
return "", nil
}

func (d *BaseDriver) Suspend(_ context.Context) error {
return fmt.Errorf("unimplemented")
}

func (d *BaseDriver) Resume(_ context.Context) error {
return fmt.Errorf("unimplemented")
}

func (d *BaseDriver) Status(_ context.Context) (*BaseStatus, error) {
return &BaseStatus{Running: true, Paused: false}, nil
}

func (d *BaseDriver) CreateSnapshot(_ context.Context, _ string) error {
return fmt.Errorf("unimplemented")
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/hostagent/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@ package api
type Info struct {
SSHLocalPort int `json:"sshLocalPort,omitempty"`
}

type RunState = string

const (
StateRunning RunState = "running"
StatePaused RunState = "paused"
)

type Status struct {
State RunState `json:"state"`
}
16 changes: 16 additions & 0 deletions pkg/hostagent/api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type HostAgentClient interface {
HTTPClient() *http.Client
Info(context.Context) (*api.Info, error)
Status(context.Context) (*api.Status, error)
}

// NewHostAgentClient creates a client.
Expand Down Expand Up @@ -62,3 +63,18 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) {
}
return &info, nil
}

func (c *client) Status(ctx context.Context) (*api.Status, error) {
u := fmt.Sprintf("http://%s/%s/status", c.dummyHost, c.version)
resp, err := httpclientutil.Get(ctx, c.HTTPClient(), u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var status api.Status
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
22 changes: 22 additions & 0 deletions pkg/hostagent/api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(m)
}

// GetStatus is the handler for GET /v{N}/status.
func (b *Backend) GetStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

status, err := b.Agent.Status(ctx)
if err != nil {
b.onError(w, err, http.StatusInternalServerError)
return
}
m, err := json.Marshal(status)
if err != nil {
b.onError(w, err, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(m)
}

func AddRoutes(r *http.ServeMux, b *Backend) {
r.Handle("/v1/info", http.HandlerFunc(b.GetInfo))
r.Handle("/v1/status", http.HandlerFunc(b.GetStatus))
}
18 changes: 18 additions & 0 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,24 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
return info, nil
}

func (a *HostAgent) Status(ctx context.Context) (*hostagentapi.Status, error) {
driverStatus, err := a.driver.Status(ctx)
if err != nil {
return nil, err
}
var state hostagentapi.RunState
if driverStatus.Running {
state = hostagentapi.StateRunning
}
if driverStatus.Paused {
state = hostagentapi.StatePaused
}
status := &hostagentapi.Status{
State: state,
}
return status, nil
}

func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
if *a.instConfig.Plain {
logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.")
Expand Down
35 changes: 35 additions & 0 deletions pkg/pause/pause.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package pause

import (
"context"

"github.com/lima-vm/lima/pkg/driver"
"github.com/lima-vm/lima/pkg/driverutil"
"github.com/lima-vm/lima/pkg/store"
)

func Suspend(ctx context.Context, inst *store.Instance) error {
limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
Instance: inst,
})

if err := limaDriver.Suspend(ctx); err != nil {
return err
}

inst.Status = store.StatusPaused
return nil
}

func Resume(ctx context.Context, inst *store.Instance) error {
limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
Instance: inst,
})

if err := limaDriver.Resume(ctx); err != nil {
return err
}

inst.Status = store.StatusRunning
return nil
}
58 changes: 58 additions & 0 deletions pkg/qemu/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,64 @@ func execImgCommand(cfg Config, args ...string) (string, error) {
return string(b), err
}

func Stop(cfg Config) error {
qmpClient, err := newQmpClient(cfg)
if err != nil {
return err
}
if err := qmpClient.Connect(); err != nil {
return err
}
defer func() { _ = qmpClient.Disconnect() }()
rawClient := raw.NewMonitor(qmpClient)
logrus.Info("Sending QMP stop command")
return rawClient.Stop()
}

func Cont(cfg Config) error {
qmpClient, err := newQmpClient(cfg)
if err != nil {
return err
}
if err := qmpClient.Connect(); err != nil {
return err
}
defer func() { _ = qmpClient.Disconnect() }()
rawClient := raw.NewMonitor(qmpClient)
logrus.Info("Sending QMP cont command")
return rawClient.Cont()
}

type StatusInfo struct {
raw.StatusInfo
}

func QueryStatus(cfg Config) (*StatusInfo, error) {
qmpClient, err := newQmpClient(cfg)
if err != nil {
return nil, err
}
if err := qmpClient.Connect(); err != nil {
return nil, err
}
defer func() { _ = qmpClient.Disconnect() }()
rawClient := raw.NewMonitor(qmpClient)
logrus.Debug("Sending QMP query-status command")
status, err := rawClient.QueryStatus()
if err != nil {
return nil, err
}
return &StatusInfo{status}, nil
}

func IsPaused(info *StatusInfo) bool {
return info.Status == raw.RunStatePaused
}

func IsRunning(info *StatusInfo) bool {
return info.Status == raw.RunStateRunning
}

func Del(cfg Config, run bool, tag string) error {
if run {
out, err := sendHmpCommand(cfg, "delvm", tag)
Expand Down
Loading
Loading