-
Notifications
You must be signed in to change notification settings - Fork 615
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
vz: implement auto save/restore #2900
base: master
Are you sure you want to change the base?
Changes from all commits
4fc95cd
349ccd1
929c7e5
c5305c0
1d533a7
de78015
7f4df6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/lima-vm/lima/pkg/instance" | ||
networks "github.com/lima-vm/lima/pkg/networks/reconcile" | ||
"github.com/lima-vm/lima/pkg/store" | ||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func newSaveCommand() *cobra.Command { | ||
saveCmd := &cobra.Command{ | ||
Use: "save INSTANCE", | ||
Short: "Save an instance", | ||
PersistentPreRun: func(*cobra.Command, []string) { | ||
logrus.Warn("`limactl save` is experimental") | ||
}, | ||
Args: WrapArgsError(cobra.MaximumNArgs(1)), | ||
RunE: saveAction, | ||
ValidArgsFunction: saveBashComplete, | ||
GroupID: basicCommand, | ||
} | ||
|
||
return saveCmd | ||
} | ||
|
||
func saveAction(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 | ||
} | ||
|
||
err = instance.StopGracefully(inst, true) | ||
// TODO: should we also reconcile networks if graceful save returned an error? | ||
if err == nil { | ||
err = networks.Reconcile(cmd.Context(), "") | ||
} | ||
return err | ||
} | ||
|
||
func saveBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { | ||
return bashCompleteInstanceNames(cmd) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,11 @@ package client | |
// Apache License 2.0 | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/lima-vm/lima/pkg/hostagent/api" | ||
|
@@ -16,6 +18,7 @@ import ( | |
type HostAgentClient interface { | ||
HTTPClient() *http.Client | ||
Info(context.Context) (*api.Info, error) | ||
DriverConfig(context.Context, interface{}) (interface{}, error) | ||
} | ||
|
||
// NewHostAgentClient creates a client. | ||
|
@@ -62,3 +65,35 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) { | |
} | ||
return &info, nil | ||
} | ||
|
||
func (c *client) DriverConfig(ctx context.Context, config interface{}) (interface{}, error) { | ||
u := fmt.Sprintf("http://%s/%s/driver/config", c.dummyHost, c.version) | ||
method := "GET" | ||
var body io.Reader | ||
if config != nil { | ||
method = "PATCH" | ||
b, err := json.Marshal(config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
body = bytes.NewBuffer(b) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing both GET and PATCH in the same function is confusing, both for understanding this code, and both for code using this function. If we want to avoid duplicate code for sending a request and getting a response, move the common code to a helper. |
||
req, err := http.NewRequestWithContext(ctx, method, u, body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
resp, err := c.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
if err := httpclientutil.Successful(resp); err != nil { | ||
return nil, err | ||
} | ||
dec := json.NewDecoder(resp.Body) | ||
if err := dec.Decode(&config); err != nil { | ||
return nil, err | ||
} | ||
return &config, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,36 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) { | |
_, _ = w.Write(m) | ||
} | ||
|
||
// DriverConfig is the handler for GET /v1/driver/config and PATCH /v1/driver/config. | ||
func (b *Backend) DriverConfig(w http.ResponseWriter, r *http.Request) { | ||
ctx := r.Context() | ||
ctx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
var config interface{} | ||
if r.Method == http.MethodPatch { | ||
defer r.Body.Close() | ||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil { | ||
b.onError(w, err, http.StatusBadRequest) | ||
return | ||
} | ||
} | ||
config, err := b.Agent.DriverRuntimeConfig(ctx, config) | ||
if err != nil { | ||
b.onError(w, err, http.StatusInternalServerError) | ||
return | ||
} | ||
m, err := json.Marshal(config) | ||
if err != nil { | ||
b.onError(w, err, http.StatusInternalServerError) | ||
return | ||
} | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't OK the default? |
||
_, _ = w.Write(m) | ||
} | ||
|
||
func AddRoutes(r *http.ServeMux, b *Backend) { | ||
r.Handle("/v1/info", http.HandlerFunc(b.GetInfo)) | ||
r.Handle("GET /v1/driver/config", http.HandlerFunc(b.DriverConfig)) | ||
r.Handle("PATCH /v1/driver/config", http.HandlerFunc(b.DriverConfig)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name of the new handler is not consistent with existing GetInfo. GetConfig and PatchConfig would be consistent. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,18 +9,42 @@ import ( | |
"strings" | ||
"time" | ||
|
||
hostagentclient "github.com/lima-vm/lima/pkg/hostagent/api/client" | ||
hostagentevents "github.com/lima-vm/lima/pkg/hostagent/events" | ||
"github.com/lima-vm/lima/pkg/limayaml" | ||
"github.com/lima-vm/lima/pkg/osutil" | ||
"github.com/lima-vm/lima/pkg/store" | ||
"github.com/lima-vm/lima/pkg/store/filenames" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
func StopGracefully(inst *store.Instance) error { | ||
func StopGracefully(inst *store.Instance, saveOnStop bool) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
if inst.Status != store.StatusRunning { | ||
return fmt.Errorf("expected status %q, got %q (maybe use `limactl stop -f`?)", store.StatusRunning, inst.Status) | ||
} | ||
|
||
if saveOnStop && inst.Saved { | ||
logrus.Warn("saved VZ machine state is found. It will be overwritten by the new one.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why warning? this is expected condition, nothing to warn about. Use Debug? |
||
} | ||
|
||
if inst.VMType == limayaml.VZ { | ||
haSock := filepath.Join(inst.Dir, filenames.HostAgentSock) | ||
haClient, err := hostagentclient.NewHostAgentClient(haSock) | ||
if err != nil { | ||
logrus.WithError(err).Error("Failed to create a host agent client") | ||
} | ||
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) | ||
defer cancel() | ||
disableSaveOnStopConfig := struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this "disableSaveOnStopConfig"? why not patch? |
||
SaveOnStop bool `json:"saveOnStop"` | ||
}{SaveOnStop: saveOnStop} | ||
_, err = haClient.DriverConfig(ctx, disableSaveOnStopConfig) | ||
if err != nil { | ||
return fmt.Errorf("failed to disable saveOnStop: %w", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we treating this as disable when the value can be true? |
||
} | ||
} else if saveOnStop { | ||
return fmt.Errorf("save is not supported for %q", inst.VMType) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing input validation first is better and more clear, something like:
This keeps the happy path very clear, and it is easy to very that we did not forget to handle all abnormal cases. |
||
} | ||
begin := time.Now() // used for logrus propagation | ||
logrus.Infof("Sending SIGINT to hostagent process %d", inst.HostAgentPID) | ||
if err := osutil.SysKill(inst.HostAgentPID, osutil.SigInt); err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -855,6 +855,32 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) { | |
y.NestedVirtualization = ptr.Of(false) | ||
} | ||
|
||
if y.SaveOnStop == nil { | ||
y.SaveOnStop = d.SaveOnStop | ||
} | ||
if o.SaveOnStop != nil { | ||
y.SaveOnStop = o.SaveOnStop | ||
} | ||
if y.SaveOnStop == nil { | ||
y.SaveOnStop = ptr.Of(false) | ||
} | ||
if *y.SaveOnStop { | ||
y.SaveOnStop = ptr.Of(false) | ||
if *y.VMType != VZ { | ||
logrus.Warn("saveOnStop is only supported for VM type vz") | ||
} else if runtime.GOARCH != "arm64" { | ||
logrus.Warn("saveOnStop is only supported for arm64 VM type vz") | ||
} else if runtime.GOOS != "darwin" { | ||
logrus.Warn("saveOnStop is only supported on macOS") | ||
} else if macOSProductVersion, err := osutil.ProductVersion(); err != nil { | ||
logrus.WithError(err).Warn("Failed to get macOS product version") | ||
} else if macOSProductVersion.LessThan(*semver.New("14.0.0")) { | ||
logrus.Warn("saveOnStop is not supported on macOS prior to 14.0") | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will more clear if we extract all the checks to a helper like:
In the helper can do something like:
|
||
y.SaveOnStop = ptr.Of(true) | ||
} | ||
} | ||
|
||
if y.Plain == nil { | ||
y.Plain = d.Plain | ||
} | ||
|
@@ -878,6 +904,7 @@ func fixUpForPlainMode(y *LimaYAML) { | |
y.Containerd.User = ptr.Of(false) | ||
y.Rosetta.BinFmt = ptr.Of(false) | ||
y.Rosetta.Enabled = ptr.Of(false) | ||
y.SaveOnStop = ptr.Of(false) | ||
y.TimeZone = ptr.Of("") | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -523,4 +523,7 @@ func warnExperimental(y *LimaYAML) { | |
if y.MountInotify != nil && *y.MountInotify { | ||
logrus.Warn("`mountInotify` is experimental") | ||
} | ||
if y.SaveOnStop != nil && *y.SaveOnStop { | ||
logrus.Warn("`saveOnStop` is experimental") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense but we should add it to the commit adding the option. Do yo plan to squash all the commits to one commit later? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,7 @@ type Instance struct { | |
// Hostname, not HostName (corresponds to SSH's naming convention) | ||
Hostname string `json:"hostname"` | ||
Status Status `json:"status"` | ||
Saved bool `json:"saved"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can document here what saved mean? |
||
Dir string `json:"dir"` | ||
VMType limayaml.VMType `json:"vmType"` | ||
Arch limayaml.Arch `json:"arch"` | ||
|
@@ -147,6 +148,14 @@ func Inspect(instName string) (*Instance, error) { | |
} | ||
|
||
inspectStatus(instDir, inst, y) | ||
_, err = os.Stat(filepath.Join(instDir, filenames.VzMachineState)) | ||
if err == nil { | ||
inst.Saved = true | ||
} else if errors.Is(err, os.ErrNotExist) { | ||
inst.Saved = false | ||
} else { | ||
inst.Errors = append(inst.Errors, fmt.Errorf("cannot determine whether the instance is saved: %w", err)) | ||
} | ||
|
||
tmpl, err := template.New("format").Parse(y.Message) | ||
if err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
//go:build darwin && !arm64 && !no_vz | ||
|
||
package vz | ||
|
||
import ( | ||
"fmt" | ||
"runtime" | ||
|
||
"github.com/Code-Hex/vz/v3" | ||
) | ||
|
||
func saveVM(vm *vz.VirtualMachine, machineStatePath string) error { | ||
return fmt.Errorf("save is not supported on the vz driver for the architecture %s", runtime.GOARCH) | ||
} | ||
|
||
func restoreVM(vm *vz.VirtualMachine, machineStatePath string) error { | ||
return fmt.Errorf("restore is not supported on the vz driver for the architecture %s", runtime.GOARCH) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would more clear as: