diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9baa7458e73..808d5ce4170 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,8 @@ jobs: Terminal 1: \`\`\`console [macOS]$ limactl start + ... + INFO[0029] READY. Run `lima` to open the shell. \`\`\` Terminal 2: diff --git a/README.md b/README.md index 21f898271e4..0fa340cb6e6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[[⬇️ **Download]**](https://github.com/AkihiroSuda/lima/releases) +[[📖**Getting started]**](#getting-started) +[[❓**FAQs & Troubleshooting]**](#faqs--troubleshooting) + # Lima: Linux virtual machines (on macOS, in most cases) Lima launches Linux virtual machines with automatic file sharing, port forwarding, and [containerd](https://containerd.io). @@ -17,9 +21,9 @@ It may work on NetBSD and Windows hosts as well. ✅ ARM on Intel -✅ ARM on ARM (untested) +✅ ARM on ARM -✅ Intel on ARM (untested) +✅ Intel on ARM ✅ Ubuntu guest @@ -77,49 +81,12 @@ For the usage of containerd and nerdctl (contaiNERD ctl), visit https://github.c ### Requirements (Intel Mac) - QEMU v6.0.0 or later (`brew install qemu`) - -
- -Signing the binary (not needed for recent version of QEMU and macOS, in most cases) - - -

- -If you have installed QEMU v6.0.0 or later on macOS 11, your binary should have been already automatically signed to enable HVF acceleration. - -However, if you see `HV_ERROR`, you might need to sign the binary manually. - -```bash -cat >entitlements.xml < - - - - com.apple.security.hypervisor - - - -EOF - -codesign -s - --entitlements entitlements.xml --force /usr/local/bin/qemu-system-x86_64 -``` - -Note: **Only** on macOS versions **before** 10.15.7 you might need to add this entitlement in addition: - -``` - com.apple.vm.hypervisor - -``` - -

-
- ### Requirements (ARM Mac) -- QEMU with `--accel=hvf` support, see https://gist.github.com/citruz/9896cd6fb63288ac95f81716756cb9aa +- QEMU with `--accel=hvf` support, see https://gist.github.com/nrjdalal/e70249bb5d2e9d844cc203fd11f74c55 > **NOTE** -> Lima is not tested on ARM Mac. +> Lima is not regularly tested on ARM Mac. ### Install @@ -129,6 +96,22 @@ and extract it under `/usr/local` (or somewhere else). To install from the source, run `make && make install`. ### Usage + +Terminal 1: +```console +[macOS]$ limactl start +... +INFO[0029] READY. Run `lima` to open the shell. +``` + +Terminal 2: +```console +[macOS]$ lima uname +Linux +``` + +Detailed usage: + - Run `limactl start ` to start the Linux instance. The default instance name is "default". Lima automatically opens an editor (`vi`) for reviewing and modifying the configuration. @@ -156,7 +139,7 @@ Especially, the following data might be easily lost: ### Configuration -See [`./pkg/limayaml/default.TEMPLATE.yaml`](./pkg/limayaml/default.TEMPLATE.yaml). +See [`./pkg/limayaml/default.yaml`](./pkg/limayaml/default.yaml). The current default spec: - OS: Ubuntu 21.04 (Hirsute Hippo) @@ -183,16 +166,41 @@ The current default spec: ### Help wanted :pray: -- Test on ARM Mac +- [Test on ARM Mac](https://github.com/AkihiroSuda/lima/issues/42) - Performance optimization -- Homebrew +- [Homebrew](https://github.com/AkihiroSuda/lima/issues/37) - More guest distros - Windows hosts - GUI with system tray icon (Qt or Electron, for portability) -- VirtFS to replace the current reverse sshfs (work has to be done on QEMU repo) +- [VirtFS to replace the current reverse sshfs (work has to be done on QEMU repo)](https://github.com/NixOS/nixpkgs/pull/122420) - [vsock](https://github.com/apple/darwin-xnu/blob/xnu-7195.81.3/bsd/man/man4/vsock.4) to replace SSH (work has to be done on QEMU repo) ## FAQs & Troubleshooting + + + + +### Generic + +- [Generic](#generic) + - ["What's my login password?"](#whats-my-login-password) + - ["Does Lima work on ARM Mac?"](#does-lima-work-on-arm-mac) + - ["Can I run non-Ubuntu guests?"](#can-i-run-non-ubuntu-guests) + - ["Can I run other container engines such as Podman?"](#can-i-run-other-container-engines-such-as-podman) + - ["Can I run Lima with a remote Linux machine?"](#can-i-run-lima-with-a-remote-linux-machine) + - ["Advantages compared to Docker for Mac?"](#advantages-compared-to-docker-for-mac) +- [QEMU](#qemu) + - ["QEMU crashes with `HV_ERROR`"](#qemu-crashes-with-hv_error) + - ["QEMU is slow"](#qemu-is-slow) + - [error "killed -9"](#error-killed--9) +- [SSH](#ssh) + - ["Port forwarding does not work"](#port-forwarding-does-not-work) + - [error "field SSHPubKeys must be set"](#error-field-sshpubkeys-must-be-set) + - [error "hostkeys_foreach failed: No such file or directory"](#error-hostkeys_foreach-failed-no-such-file-or-directory) + - [error "failed to execute script ssh: [...] Permission denied (publickey)"](#error-failed-to-execute-script-ssh--permission-denied-publickey) +- ["Hints for debugging other problems?"](#hints-for-debugging-other-problems) + + ### Generic #### "What's my login password?" Password is disabled and locked by default. @@ -201,7 +209,7 @@ You have to use `limactl shell bash` (or `lima bash`) to open a shell. Alternatively, you may also directly ssh into the guest: `ssh -p 60022 -o NoHostAuthenticationForLocalhost=yes 127.0.0.1`. #### "Does Lima work on ARM Mac?" -Yes, it should work, but not tested on ARM. +Yes, it should work, but not regularly tested on ARM. #### "Can I run non-Ubuntu guests?" Fedora is also known to work, see [`./examples/fedora.yaml`](./examples/fedora.yaml). @@ -211,7 +219,6 @@ An image has to satisfy the following requirements: - systemd - cloud-init - The following binaries to be preinstalled: - - `curl` - `sudo` - The following binaries to be preinstalled, or installable via the package manager: - `sshfs` @@ -230,17 +237,49 @@ the predecessor or Lima, provides similar features for remote Linux machines. e.g., run `sshocker -v /Users/foo:/home/foo/mnt -p 8080:80 @` to expose `/Users/foo` to the remote machine as `/home/foo/mnt`, and forward `localhost:8080` to the port 80 of the remote machine. +#### "Advantages compared to Docker for Mac?" +Lima is free software (Apache License 2.0), while Docker for Mac is not. +Their [EULA](https://www.docker.com/legal/docker-software-end-user-license-agreement) even prohibits disclosure of benchmarking result. + +On the other hand, [Moby](https://github.com/moby/moby), aka Docker for Linux, is free software, but Moby/Docker lacks several novel features of containerd, such as: +- [On-demand image pulling (aka lazy-pulling, eStargz)](https://github.com/containerd/nerdctl/blob/master/docs/stargz.md) +- [Running an encrypted container](https://github.com/containerd/nerdctl/blob/master/docs/ocicrypt.md) +- Importing and exporting [local OCI archives](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) + ### QEMU #### "QEMU crashes with `HV_ERROR`" -You have to add `com.apple.security.hypervisor` entitlement to `qemu-system-x86_64` binary. -See [Getting started](#getting-started). +If you have installed QEMU v6.0.0 or later on macOS 11 via homebrew, your QEMU binary should have been already automatically signed to enable HVF acceleration. + +However, if you see `HV_ERROR`, you might need to sign the binary manually. + +```bash +cat >entitlements.xml < + + + + com.apple.security.hypervisor + + + +EOF + +codesign -s - --entitlements entitlements.xml --force /usr/local/bin/qemu-system-x86_64 +``` + +Note: **Only** on macOS versions **before** 10.15.7 you might need to add this entitlement in addition: + +``` + com.apple.vm.hypervisor + +``` #### "QEMU is slow" -- Make sure that HVF is enabled with `com.apple.security.hypervisor` entitlement. See [Getting started](#getting-started). +- Make sure that HVF is enabled with `com.apple.security.hypervisor` entitlement. See ["QEMU crashes with `HV_ERROR`"](#qemu-crashes-with-hv_error). - Emulating non-native machines (ARM-on-Intel, Intel-on-ARM) is slow by design. #### error "killed -9" -- make sure qemu is codesigned, see [Getting started](#getting-started). +- make sure qemu is codesigned, See ["QEMU crashes with `HV_ERROR`"](#qemu-crashes-with-hv_error). - if you are on macOS 10.15.7 or 11.0 or later make sure the entitlement `com.apple.vm.hypervisor` is **not** added. It only works on older macOS versions. You can clear the codesigning with `codesign --remove-signature /usr/local/bin/qemu-system-x86_64` and [start over](#getting-started). diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index 248102cdca8..aebfdbf9999 100644 --- a/cmd/lima-guestagent/daemon_linux.go +++ b/cmd/lima-guestagent/daemon_linux.go @@ -16,9 +16,8 @@ import ( ) var daemonCommand = &cli.Command{ - Name: "daemon", - Usage: "run the daemon", - ArgsUsage: "[SOCKET]", + Name: "daemon", + Usage: "run the daemon", Flags: []cli.Flag{ &cli.StringFlag{ Name: "socket", diff --git a/docs/internal.md b/docs/internal.md index ac36490efa5..9b0d512a821 100644 --- a/docs/internal.md +++ b/docs/internal.md @@ -8,7 +8,7 @@ An instance directory contains the following files: - `cidata.iso`: cloud-init ISO9660 image. (`user-data`, `meta-data`, `lima-guestagent.Linux-`) - `basedisk`: the base image - `diffdisk`: the diff image (QCOW2) -- `qemu-pid`: PID of the QEMU +- `qemu.pid`: PID of the QEMU - `ssh.sock`: SSH control master socket - `ga.sock`: Forwarded to `/run/user/$UID/lima-guestagent.sock` - `serial.log`: QEMU serial log, for debugging diff --git a/examples/default.TEMPLATE.yaml b/examples/default.TEMPLATE.yaml deleted file mode 120000 index 9cfda050cac..00000000000 --- a/examples/default.TEMPLATE.yaml +++ /dev/null @@ -1 +0,0 @@ -../pkg/limayaml/default.TEMPLATE.yaml \ No newline at end of file diff --git a/examples/default.yaml b/examples/default.yaml new file mode 120000 index 00000000000..cfc34e263f1 --- /dev/null +++ b/examples/default.yaml @@ -0,0 +1 @@ +../pkg/limayaml/default.yaml \ No newline at end of file diff --git a/pkg/guestagent/api/client/client.go b/pkg/guestagent/api/client/client.go index 10687b5a738..7ae59375630 100644 --- a/pkg/guestagent/api/client/client.go +++ b/pkg/guestagent/api/client/client.go @@ -7,14 +7,10 @@ import ( "context" "encoding/json" "fmt" - "io" - "io/ioutil" - "net" "net/http" - "os" "github.com/AkihiroSuda/lima/pkg/guestagent/api" - "github.com/pkg/errors" + "github.com/AkihiroSuda/lima/pkg/httpclientutil" ) type GuestAgentClient interface { @@ -26,17 +22,10 @@ type GuestAgentClient interface { // NewGuestAgentClient creates a client. // socketPath is a path to the UNIX socket, without unix:// prefix. func NewGuestAgentClient(socketPath string) (GuestAgentClient, error) { - if _, err := os.Stat(socketPath); err != nil { + hc, err := httpclientutil.NewHTTPClientWithSocketPath(socketPath) + if err != nil { return nil, err } - hc := &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, "unix", socketPath) - }, - }, - } return NewGuestAgentClientWithHTTPClient(hc), nil } @@ -62,19 +51,11 @@ func (c *client) HTTPClient() *http.Client { func (c *client) Info(ctx context.Context) (*api.Info, error) { u := fmt.Sprintf("http://%s/%s/info", c.dummyHost, c.version) - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - resp, err := c.HTTPClient().Do(req) + resp, err := httpclientutil.Get(ctx, c.HTTPClient(), u) if err != nil { return nil, err } defer resp.Body.Close() - if err := successful(resp); err != nil { - return nil, err - } var info api.Info dec := json.NewDecoder(resp.Body) if err := dec.Decode(&info); err != nil { @@ -85,19 +66,11 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) { func (c *client) Events(ctx context.Context, onEvent func(api.Event)) error { u := fmt.Sprintf("http://%s/%s/events", c.dummyHost, c.version) - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return err - } - req = req.WithContext(ctx) - resp, err := c.HTTPClient().Do(req) + resp, err := httpclientutil.Get(ctx, c.HTTPClient(), u) if err != nil { return err } defer resp.Body.Close() - if err := successful(resp); err != nil { - return err - } var ev api.Event dec := json.NewDecoder(resp.Body) for { @@ -107,56 +80,3 @@ func (c *client) Events(ctx context.Context, onEvent func(api.Event)) error { onEvent(ev) } } - -func readAtMost(r io.Reader, maxBytes int) ([]byte, error) { - lr := &io.LimitedReader{ - R: r, - N: int64(maxBytes), - } - b, err := ioutil.ReadAll(lr) - if err != nil { - return b, err - } - if lr.N == 0 { - return b, errors.Errorf("expected at most %d bytes, got more", maxBytes) - } - return b, nil -} - -// HTTPStatusErrorBodyMaxLength specifies the maximum length of HTTPStatusError.Body -const HTTPStatusErrorBodyMaxLength = 64 * 1024 - -// HTTPStatusError is created from non-2XX HTTP response -type HTTPStatusError struct { - // StatusCode is non-2XX status code - StatusCode int - // Body is at most HTTPStatusErrorBodyMaxLength - Body string -} - -// Error implements error. -// If e.Body is a marshalled string of api.ErrorJSON, Error returns ErrorJSON.Message . -// Otherwise Error returns a human-readable string that contains e.StatusCode and e.Body. -func (e *HTTPStatusError) Error() string { - if e.Body != "" && len(e.Body) < HTTPStatusErrorBodyMaxLength { - var ej api.ErrorJSON - if json.Unmarshal([]byte(e.Body), &ej) == nil { - return ej.Message - } - } - return fmt.Sprintf("unexpected HTTP status %s, body=%q", http.StatusText(e.StatusCode), e.Body) -} - -func successful(resp *http.Response) error { - if resp == nil { - return errors.New("nil response") - } - if resp.StatusCode/100 != 2 { - b, _ := readAtMost(resp.Body, HTTPStatusErrorBodyMaxLength) - return &HTTPStatusError{ - StatusCode: resp.StatusCode, - Body: string(b), - } - } - return nil -} diff --git a/pkg/httpclientutil/httpclientutil.go b/pkg/httpclientutil/httpclientutil.go new file mode 100644 index 00000000000..31c7022e9f6 --- /dev/null +++ b/pkg/httpclientutil/httpclientutil.go @@ -0,0 +1,105 @@ +package httpclientutil + +// Forked from https://github.com/rootless-containers/rootlesskit/blob/v0.14.2/pkg/api/client/client.go +// Apache License 2.0 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "os" + + "github.com/AkihiroSuda/lima/pkg/guestagent/api" + "github.com/pkg/errors" +) + +// NewHTTPClientWithSocketPath creates a client. +// socketPath is a path to the UNIX socket, without unix:// prefix. +func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { + if _, err := os.Stat(socketPath); err != nil { + return nil, err + } + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + } + return hc, nil +} + +// Get calls HTTP GET and verifies that the status code is 2XX . +func Get(ctx context.Context, c *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if err := Successful(resp); err != nil { + resp.Body.Close() + return nil, err + } + return resp, nil +} + +func readAtMost(r io.Reader, maxBytes int) ([]byte, error) { + lr := &io.LimitedReader{ + R: r, + N: int64(maxBytes), + } + b, err := ioutil.ReadAll(lr) + if err != nil { + return b, err + } + if lr.N == 0 { + return b, errors.Errorf("expected at most %d bytes, got more", maxBytes) + } + return b, nil +} + +// HTTPStatusErrorBodyMaxLength specifies the maximum length of HTTPStatusError.Body +const HTTPStatusErrorBodyMaxLength = 64 * 1024 + +// HTTPStatusError is created from non-2XX HTTP response +type HTTPStatusError struct { + // StatusCode is non-2XX status code + StatusCode int + // Body is at most HTTPStatusErrorBodyMaxLength + Body string +} + +// Error implements error. +// If e.Body is a marshalled string of api.ErrorJSON, Error returns ErrorJSON.Message . +// Otherwise Error returns a human-readable string that contains e.StatusCode and e.Body. +func (e *HTTPStatusError) Error() string { + if e.Body != "" && len(e.Body) < HTTPStatusErrorBodyMaxLength { + var ej api.ErrorJSON + if json.Unmarshal([]byte(e.Body), &ej) == nil { + return ej.Message + } + } + return fmt.Sprintf("unexpected HTTP status %s, body=%q", http.StatusText(e.StatusCode), e.Body) +} + +func Successful(resp *http.Response) error { + if resp == nil { + return errors.New("nil response") + } + if resp.StatusCode/100 != 2 { + b, _ := readAtMost(resp.Body, HTTPStatusErrorBodyMaxLength) + return &HTTPStatusError{ + StatusCode: resp.StatusCode, + Body: string(b), + } + } + return nil +} diff --git a/pkg/limayaml/default.TEMPLATE.yaml b/pkg/limayaml/default.yaml similarity index 100% rename from pkg/limayaml/default.TEMPLATE.yaml rename to pkg/limayaml/default.yaml diff --git a/pkg/limayaml/template.go b/pkg/limayaml/template.go index f29975fd076..050932801b3 100644 --- a/pkg/limayaml/template.go +++ b/pkg/limayaml/template.go @@ -4,5 +4,5 @@ import ( _ "embed" ) -//go:embed default.TEMPLATE.yaml +//go:embed default.yaml var DefaultTemplate []byte diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go index d9b077883a4..fe848c377f6 100644 --- a/pkg/qemu/qemu.go +++ b/pkg/qemu/qemu.go @@ -171,7 +171,7 @@ func Cmdline(cfg Config) (string, []string, error) { // QEMU process args = append(args, "-name", "lima-"+cfg.Name) - args = append(args, "-pidfile", filepath.Join(cfg.InstanceDir, "qemu-pid")) + args = append(args, "-pidfile", filepath.Join(cfg.InstanceDir, "qemu.pid")) return exe, args, nil } diff --git a/pkg/start/start.go b/pkg/start/start.go index b018e0d7d86..eb30ba6c0f3 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -64,7 +64,11 @@ func Start(ctx context.Context, instName, instDir string, y *limayaml.LimaYAML) } }() if err := hAgent.Run(ctx); err == nil { - logrus.Info("READY. Run `lima bash` to open the shell.") + shellCmd := fmt.Sprintf("limactl shell %s", instName) + if instName == "default" { + shellCmd = "lima" + } + logrus.Infof("READY. Run `%s` to open the shell.", shellCmd) } else { logrus.WithError(err).Warn("DEGRADED. The VM seems running, but file sharing and port forwarding may not work.") }