From 4101c78bf8ca54b6448823a27cf3205558608876 Mon Sep 17 00:00:00 2001 From: Justin Alvarez Date: Mon, 7 Aug 2023 20:05:05 -0400 Subject: [PATCH] feat: add WSL2 driver Signed-off-by: Justin Alvarez --- Makefile | 11 + cmd/lima-guestagent/daemon_linux.go | 46 +++- cmd/lima-guestagent/install_systemd_linux.go | 32 ++- .../lima-guestagent.TEMPLATE.service | 2 +- cmd/limactl/delete.go | 1 + cmd/limactl/hostagent.go | 1 + cmd/limactl/shell.go | 2 +- examples/default.yaml | 5 + examples/experimental/wsl.yaml | 27 +++ go.mod | 9 +- go.sum | 6 +- pkg/cidata/cidata.TEMPLATE.d/boot.sh | 2 +- .../cidata.TEMPLATE.d/boot/02-wsl-setup.sh | 17 ++ .../boot/25-guestagent-base.sh | 6 +- pkg/cidata/cidata.TEMPLATE.d/lima.env | 2 + pkg/cidata/cidata.TEMPLATE.d/user-data | 2 +- pkg/cidata/cidata.go | 45 +++- pkg/cidata/template.go | 2 + pkg/downloader/downloader.go | 18 -- pkg/downloader/downloader_unix.go | 22 ++ pkg/downloader/downloader_windows.go | 22 ++ pkg/driverutil/driverutil.go | 4 + pkg/driverutil/instance.go | 4 + pkg/executil/command.go | 52 ++++ pkg/guestagent/api/client/client.go | 41 +++- pkg/guestagent/api/client/client_others.go | 16 ++ pkg/guestagent/api/client/client_windows.go | 19 ++ pkg/guestagent/guestagent_linux.go | 26 +- pkg/hostagent/hostagent.go | 70 ++++-- pkg/hostagent/port.go | 20 +- pkg/hostagent/port_darwin.go | 4 + pkg/hostagent/port_others.go | 8 +- pkg/hostagent/port_windows.go | 19 ++ pkg/hostagent/requirements.go | 30 ++- pkg/httpclientutil/httpclientutil.go | 19 -- pkg/httpclientutil/httpclientutil_others.go | 52 ++++ pkg/httpclientutil/httpclientutil_windows.go | 65 +++++ pkg/ioutilx/ioutilx.go | 31 +++ pkg/limayaml/defaults.go | 15 ++ pkg/limayaml/defaults_test.go | 21 ++ pkg/limayaml/limayaml.go | 15 ++ pkg/limayaml/validate.go | 4 +- pkg/osutil/osutil_windows.go | 11 +- pkg/sshutil/sshutil.go | 30 ++- pkg/start/ha_cmd_opts_others.go | 10 + pkg/start/ha_cmd_opts_windows.go | 9 + pkg/start/start.go | 1 + pkg/store/filenames/filenames.go | 3 + pkg/store/instance.go | 154 +++++++++--- pkg/store/instance_test.go | 11 +- pkg/windows/process_windows.go | 40 ++++ pkg/windows/registry_windows.go | 225 ++++++++++++++++++ pkg/windows/wsl_util_windows.go | 44 ++++ pkg/wsl2/fs.go | 35 +++ pkg/wsl2/lima-init.TEMPLATE.sh | 5 + pkg/wsl2/vm_windows.go | 131 ++++++++++ pkg/wsl2/wsl_driver_others.go | 41 ++++ pkg/wsl2/wsl_driver_windows.go | 165 +++++++++++++ 58 files changed, 1577 insertions(+), 153 deletions(-) create mode 100644 examples/experimental/wsl.yaml create mode 100755 pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl-setup.sh create mode 100644 pkg/downloader/downloader_unix.go create mode 100644 pkg/downloader/downloader_windows.go create mode 100644 pkg/executil/command.go create mode 100644 pkg/guestagent/api/client/client_others.go create mode 100644 pkg/guestagent/api/client/client_windows.go create mode 100644 pkg/hostagent/port_windows.go create mode 100644 pkg/httpclientutil/httpclientutil_others.go create mode 100644 pkg/httpclientutil/httpclientutil_windows.go create mode 100644 pkg/start/ha_cmd_opts_others.go create mode 100644 pkg/start/ha_cmd_opts_windows.go create mode 100644 pkg/windows/process_windows.go create mode 100644 pkg/windows/registry_windows.go create mode 100644 pkg/windows/wsl_util_windows.go create mode 100644 pkg/wsl2/fs.go create mode 100644 pkg/wsl2/lima-init.TEMPLATE.sh create mode 100755 pkg/wsl2/vm_windows.go create mode 100644 pkg/wsl2/wsl_driver_others.go create mode 100644 pkg/wsl2/wsl_driver_windows.go diff --git a/Makefile b/Makefile index 253deab4e583..cb458c41b1de 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,17 @@ GO_BUILDTAGS += no_vz endif endif +ifeq ($(GOOS),windows) +WINVER_MAJOR=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Major") +ifeq ($(WINVER_MAJOR),10) +WINVER_BUILD=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Build") +WINVER_BUILD_HIGH_ENOUGH=$(shell powershell.exe $(WINVER_BUILD) -ge 19041) +ifeq ($(WINVER_BUILD_HIGH_ENOUGH),False) +GO_BUILDTAGS += no_wsl +endif +endif +endif + PACKAGE := github.com/lima-vm/lima VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags) diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index 46db9dc8287f..aa7abae54975 100644 --- a/cmd/lima-guestagent/daemon_linux.go +++ b/cmd/lima-guestagent/daemon_linux.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "net" "net/http" "os" @@ -10,6 +11,7 @@ import ( "github.com/gorilla/mux" "github.com/lima-vm/lima/pkg/guestagent" "github.com/lima-vm/lima/pkg/guestagent/api/server" + "github.com/mdlayher/vsock" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,6 +23,9 @@ func newDaemonCommand() *cobra.Command { RunE: daemonAction, } daemonCommand.Flags().Duration("tick", 3*time.Second, "tick for polling events") + daemonCommand.Flags().Int("tcp-port", 0, "use tcp server instead a UNIX socket") + daemonCommand.Flags().Int("vsock-port", 0, "use vsock server instead a UNIX socket") + daemonCommand.MarkFlagsMutuallyExclusive("tcp-port", "vsock-port") return daemonCommand } @@ -30,6 +35,14 @@ func daemonAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } + tcpPort, err := cmd.Flags().GetInt("tcp-port") + if err != nil { + return err + } + vSockPort, err := cmd.Flags().GetInt("vsock-port") + if err != nil { + return err + } if tick == 0 { return errors.New("tick must be specified") } @@ -60,13 +73,32 @@ func daemonAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } - l, err := net.Listen("unix", socket) - if err != nil { - return err - } - if err := os.Chmod(socket, 0777); err != nil { - return err + + var l net.Listener + if vSockPort != 0 { + vsockL, err := vsock.Listen(uint32(vSockPort), nil) + if err != nil { + return err + } + l = vsockL + logrus.Infof("serving the guest agent on vsock port: %d", vSockPort) + } else if tcpPort != 0 { + tcpL, err := net.Listen("tcp", fmt.Sprintf(":%d", tcpPort)) + if err != nil { + return err + } + l = tcpL + logrus.Infof("serving the guest agent at :%d", tcpPort) + } else { + socketL, err := net.Listen("unix", socket) + if err != nil { + return err + } + if err := os.Chmod(socket, 0777); err != nil { + return err + } + l = socketL + logrus.Infof("serving the guest agent on %q", socket) } - logrus.Infof("serving the guest agent on %q", socket) return srv.Serve(l) } diff --git a/cmd/lima-guestagent/install_systemd_linux.go b/cmd/lima-guestagent/install_systemd_linux.go index 60fd5b36e1a1..c39ff82bab54 100644 --- a/cmd/lima-guestagent/install_systemd_linux.go +++ b/cmd/lima-guestagent/install_systemd_linux.go @@ -3,6 +3,7 @@ package main import ( _ "embed" "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -19,11 +20,22 @@ func newInstallSystemdCommand() *cobra.Command { Short: "install a systemd unit (user)", RunE: installSystemdAction, } + installSystemdCommand.Flags().Int("tcp-port", 0, "use tcp server on specified port") + installSystemdCommand.Flags().Int("vsock-port", 0, "use vsock server on specified port") + installSystemdCommand.MarkFlagsMutuallyExclusive("tcp-port", "vsock-port") return installSystemdCommand } -func installSystemdAction(_ *cobra.Command, _ []string) error { - unit, err := generateSystemdUnit() +func installSystemdAction(cmd *cobra.Command, _ []string) error { + tcp, err := cmd.Flags().GetInt("tcp-port") + if err != nil { + return err + } + vsock, err := cmd.Flags().GetInt("vsock-port") + if err != nil { + return err + } + unit, err := generateSystemdUnit(tcp, vsock) if err != nil { return err } @@ -40,11 +52,11 @@ func installSystemdAction(_ *cobra.Command, _ []string) error { return err } logrus.Infof("Written file %q", unitPath) - argss := [][]string{ + args := [][]string{ {"daemon-reload"}, {"enable", "--now", "lima-guestagent.service"}, } - for _, args := range argss { + for _, args := range args { cmd := exec.Command("systemctl", append([]string{"--system"}, args...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -60,13 +72,23 @@ func installSystemdAction(_ *cobra.Command, _ []string) error { //go:embed lima-guestagent.TEMPLATE.service var systemdUnitTemplate string -func generateSystemdUnit() ([]byte, error) { +func generateSystemdUnit(tcpPort, vsockPort int) ([]byte, error) { selfExeAbs, err := os.Executable() if err != nil { return nil, err } + + var args []string + if tcpPort != 0 { + args = append(args, fmt.Sprintf("--tcp-port %d", tcpPort)) + } + if vsockPort != 0 { + args = append(args, fmt.Sprintf("--vsock-port %d", vsockPort)) + } + m := map[string]string{ "Binary": selfExeAbs, + "Args": strings.Join(args, " "), } return textutil.ExecuteTemplate(systemdUnitTemplate, m) } diff --git a/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service b/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service index ddbd24b7e678..dc10e21c796d 100644 --- a/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service +++ b/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service @@ -2,7 +2,7 @@ Description=lima-guestagent [Service] -ExecStart={{.Binary}} daemon +ExecStart={{.Binary}} daemon {{.Args}} Type=simple Restart=on-failure diff --git a/cmd/limactl/delete.go b/cmd/limactl/delete.go index 55dd20c28b3e..97359c6fc321 100644 --- a/cmd/limactl/delete.go +++ b/cmd/limactl/delete.go @@ -62,6 +62,7 @@ func deleteInstance(ctx context.Context, inst *store.Instance, force bool) error if err := os.RemoveAll(inst.Dir); err != nil { return fmt.Errorf("failed to remove %q: %w", inst.Dir, err) } + return nil } diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go index 78e6b615676e..e412a8da4011 100644 --- a/cmd/limactl/hostagent.go +++ b/cmd/limactl/hostagent.go @@ -98,6 +98,7 @@ func hostagentAction(cmd *cobra.Command, args []string) error { return err } l, err := net.Listen("unix", socket) + logrus.Infof("hostagent socket created at %s", socket) if err != nil { return err } diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index b508c2eef7bd..00e75be024cb 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -184,7 +184,7 @@ func shellAction(cmd *cobra.Command, args []string) error { sshArgs = append(sshArgs, []string{ "-q", "-p", strconv.Itoa(inst.SSHLocalPort), - "127.0.0.1", + inst.SSHAddress, "--", script, }...) diff --git a/examples/default.yaml b/examples/default.yaml index 140888ddf79f..811c275314cd 100644 --- a/examples/default.yaml +++ b/examples/default.yaml @@ -442,6 +442,11 @@ hostResolver: # 🟢 Builtin default: /usr/local guestInstallPrefix: null +# Adjust options for the guestAgent which runs inside of the Linux VM +guestAgent: + # 🟢 Builtin default: UNIX (macOS, Linux), VSOCK (Windows/WSL2) + protocol: null + # ===================================================================== # # GLOBAL DEFAULTS AND OVERRIDES # ===================================================================== # diff --git a/examples/experimental/wsl.yaml b/examples/experimental/wsl.yaml new file mode 100644 index 000000000000..dae440e8c10c --- /dev/null +++ b/examples/experimental/wsl.yaml @@ -0,0 +1,27 @@ +# ===================================================================== # +# BASIC CONFIGURATION +# ===================================================================== # + +# Default values in this YAML file are specified by `null` instead of Lima's "builtin default" values, +# so they can be overridden by the $LIMA_HOME/_config/default.yaml mechanism documented at the end of this file. + +# VM type: "qemu" or "vz" (on macOS 13 and later). +# The vmType can be specified only on creating the instance. +# The vmType of existing instances cannot be changed. +# 🟢 Builtin default: "qemu" +vmType: wsl2 + +# OpenStack-compatible disk image. +# 🟢 Builtin default: null (must be specified) +# 🔵 This file: Ubuntu 23.04 Lunar Lobster images +images: +# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months. +- location: "file://C:/Users/Administrator/Downloads/fedora-38-cloud-rootfs.tar" + arch: "x86_64" + digest: "sha256:112400224d801c3781c1e301408685920ba980024bfb20877628a0bd82fdc574" + +mountType: wsl + +containerd: + system: true + user: false diff --git a/go.mod b/go.mod index e6d08f5f5cbc..2e23e503bcf7 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( require ( github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.1 github.com/VividCortex/ewma v1.2.0 // indirect github.com/a8m/envsubst v1.4.2 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect @@ -89,7 +89,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mdlayher/socket v0.4.1 // indirect - github.com/mdlayher/vsock v1.2.1 // indirect + github.com/mdlayher/vsock v1.2.1 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -103,11 +103,12 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect golang.org/x/crypto v0.12.0 // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b + golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.12.0 golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect diff --git a/go.sum b/go.sum index 630bb36fda5e..9dc02a4e245e 100644 --- a/go.sum +++ b/go.sum @@ -296,6 +296,8 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -305,8 +307,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot.sh b/pkg/cidata/cidata.TEMPLATE.d/boot.sh index b2fab88ed3ad..c239cf118061 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot.sh @@ -72,7 +72,7 @@ fi # Signal that provisioning is done. The instance-id in the meta-data file changes on every boot, # so any copy from a previous boot cycle will have different content. -cp "${LIMA_CIDATA_MNT}"/meta-data /run/lima-boot-done +sudo cp "${LIMA_CIDATA_MNT}"/meta-data /run/lima-boot-done INFO "Exiting with code $CODE" exit "$CODE" diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl-setup.sh new file mode 100755 index 000000000000..f26fc2699231 --- /dev/null +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl-setup.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# This script replaces the cloud-init functionality of creating a user and setting its SSH keys +# when using a WSL2 VM. +[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0 + +# create user +sudo useradd -u "${LIMA_CIDATA_UID}" "${LIMA_CIDATA_USER}" -d /home/"${LIMA_CIDATA_USER}".linux/ +sudo mkdir /home/"${LIMA_CIDATA_USER}".linux/.ssh/ +sudo cp "${LIMA_CIDATA_MNT}"/ssh_authorized_keys /home/"${LIMA_CIDATA_USER}".linux/.ssh/authorized_keys +sudo chown "${LIMA_CIDATA_USER}" /home/"${LIMA_CIDATA_USER}".linux/.ssh/authorized_keys + +# add lima to sudoers +sudo echo "lima ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/99_lima_sudoers + +# copy some CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded) +sudo mkdir -p /mnt/lima-cidata +sudo cp "${LIMA_CIDATA_MNT}"/meta-data /mnt/lima-cidata/meta-data \ No newline at end of file diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh index b585a5266edb..9865d78d9d1b 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh @@ -40,5 +40,9 @@ else # Remove legacy systemd service rm -f "/home/${LIMA_CIDATA_USER}.linux/.config/systemd/user/lima-guestagent.service" - sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd + if [ "$LIMA_CIDATA_VMTYPE" = "wsl2" ]; then + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --vsock-port "${LIMA_CIDATA_VSOCK_PORT}" + else + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd + fi fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env index e584a56e9ac0..2fe31050ce5a 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/lima.env +++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env @@ -38,3 +38,5 @@ LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION=1 {{- else}} LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION= {{- end}} +LIMA_CIDATA_VMTYPE={{ .VMType }} +LIMA_CIDATA_VSOCK_PORT={{ .VSockPort }} diff --git a/pkg/cidata/cidata.TEMPLATE.d/user-data b/pkg/cidata/cidata.TEMPLATE.d/user-data index fc54e11370c3..d9e0e45f7488 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/user-data +++ b/pkg/cidata/cidata.TEMPLATE.d/user-data @@ -33,8 +33,8 @@ write_files: LIMA_CIDATA_MNT="/mnt/lima-cidata" LIMA_CIDATA_DEV="/dev/disk/by-label/cidata" mkdir -p -m 700 "${LIMA_CIDATA_MNT}" - mount -o ro,mode=0700,dmode=0700,overriderockperm,exec,uid=0 "${LIMA_CIDATA_DEV}" "${LIMA_CIDATA_MNT}" export LIMA_CIDATA_MNT + mount -o ro,mode=0700,dmode=0700,overriderockperm,exec,uid=0 "${LIMA_CIDATA_DEV}" "${LIMA_CIDATA_MNT}" exec "${LIMA_CIDATA_MNT}"/boot.sh owner: root:root path: /var/lib/cloud/scripts/per-boot/00-lima.boot.sh diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index c5237058f05c..f454e33d73b9 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -7,22 +7,23 @@ import ( "net" "net/url" "os" + "path" "path/filepath" "strconv" "strings" "time" - "github.com/lima-vm/lima/pkg/networks" - "github.com/docker/go-units" "github.com/lima-vm/lima/pkg/iso9660util" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/localpathutil" + "github.com/lima-vm/lima/pkg/networks" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/sshutil" "github.com/lima-vm/lima/pkg/store/filenames" "github.com/lima-vm/lima/pkg/usrlocalsharelima" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) var netLookupIP = func(host string) []net.IP { @@ -105,7 +106,7 @@ func setupEnv(y *limayaml.LimaYAML) (map[string]string, error) { return env, nil } -func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string) error { +func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int) error { if err := limayaml.Validate(*y, false); err != nil { return err } @@ -129,6 +130,8 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort SlirpIPAddress: networks.SlirpIPAddress, RosettaEnabled: *y.Rosetta.Enabled, RosettaBinFmt: *y.Rosetta.BinFmt, + VMType: *y.VMType, + VSockPort: vsockPort, } // change instance id on every boot so network config will be processed again @@ -326,6 +329,14 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort }) } + if args.VMType == limayaml.WSL2 { + layout = append(layout, iso9660util.Entry{ + Path: "ssh_authorized_keys", + Reader: strings.NewReader(strings.Join(args.SSHPubKeys, "\n")), + }) + return writeCIDataDir(filepath.Join(instDir, filenames.CIDataISODir), layout) + } + return iso9660util.Write(filepath.Join(instDir, filenames.CIDataISO), "cidata", layout) } @@ -376,3 +387,31 @@ func getBootCmds(p []limayaml.Provision) []BootCmds { func diskDeviceNameFromOrder(order int) string { return fmt.Sprintf("vd%c", int('b')+order) } + +func writeCIDataDir(rootPath string, layout []iso9660util.Entry) error { + slices.SortFunc(layout, func(a, b iso9660util.Entry) int { + return strings.Compare(strings.ToLower(a.Path), strings.ToLower(b.Path)) + }) + + if err := os.RemoveAll(rootPath); err != nil { + return err + } + + for _, e := range layout { + if dir := path.Dir(e.Path); dir != "" && dir != "/" { + if err := os.MkdirAll(filepath.Join(rootPath, dir), 0700); err != nil { + return err + } + } + f, err := os.OpenFile(filepath.Join(rootPath, e.Path), os.O_CREATE|os.O_RDWR, 0700) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(f, e.Reader); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index 044c232e47a9..7eaed189b3e3 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -78,6 +78,8 @@ type TemplateArgs struct { RosettaEnabled bool RosettaBinFmt bool SkipDefaultDependencyResolution bool + VMType string + VSockPort int } func ValidateTemplateArgs(args TemplateArgs) error { diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go index af46a91070db..ea05e7ae67b8 100644 --- a/pkg/downloader/downloader.go +++ b/pkg/downloader/downloader.go @@ -297,24 +297,6 @@ func copyLocal(dst, src, ext string, decompress bool, description string, expect return fs.CopyFile(dstPath, srcPath) } -func Decompressor(ext string) ([]string, bool) { - var program string - switch ext { - case ".gz": - program = "gzip" - case ".bz2": - program = "bzip2" - case ".xz": - program = "xz" - case ".zst": - program = "zstd" - default: - return nil, false - } - // -d --decompress - return []string{program, "-d"}, true -} - func decompressLocal(dst, src, ext string, description string) error { command, found := Decompressor(ext) if !found { diff --git a/pkg/downloader/downloader_unix.go b/pkg/downloader/downloader_unix.go new file mode 100644 index 000000000000..ccb786886435 --- /dev/null +++ b/pkg/downloader/downloader_unix.go @@ -0,0 +1,22 @@ +//go:build !windows +// +build !windows + +package downloader + +func Decompressor(ext string) ([]string, bool) { + var program string + switch ext { + case ".gz": + program = "gzip" + case ".bz2": + program = "bzip2" + case ".xz": + program = "xz" + case ".zst": + program = "zstd" + default: + return nil, false + } + // -d --decompress + return []string{program, "-d"}, true +} diff --git a/pkg/downloader/downloader_windows.go b/pkg/downloader/downloader_windows.go new file mode 100644 index 000000000000..bf4b5c4b1d24 --- /dev/null +++ b/pkg/downloader/downloader_windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package downloader + +func Decompressor(ext string) ([]string, bool) { + var program string + switch ext { + case ".gz": + program = "7z" + case ".bz2": + program = "7z" + case ".xz": + program = "7z" + case ".zst": + program = "7z" + default: + return nil, false + } + + return []string{program, "x"}, true +} diff --git a/pkg/driverutil/driverutil.go b/pkg/driverutil/driverutil.go index 1cddfd10626c..a0d12787332f 100644 --- a/pkg/driverutil/driverutil.go +++ b/pkg/driverutil/driverutil.go @@ -3,6 +3,7 @@ package driverutil import ( "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/vz" + "github.com/lima-vm/lima/pkg/wsl2" ) // Drivers returns the available drivers. @@ -11,5 +12,8 @@ func Drivers() []string { if vz.Enabled { drivers = append(drivers, limayaml.VZ) } + if wsl2.Enabled { + drivers = append(drivers, limayaml.WSL2) + } return drivers } diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go index 891153ac11de..09760faa76c4 100644 --- a/pkg/driverutil/instance.go +++ b/pkg/driverutil/instance.go @@ -5,6 +5,7 @@ import ( "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/qemu" "github.com/lima-vm/lima/pkg/vz" + "github.com/lima-vm/lima/pkg/wsl2" ) func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { @@ -12,5 +13,8 @@ func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { if *limaDriver == limayaml.VZ { return vz.New(base) } + if *limaDriver == limayaml.WSL2 { + return wsl2.New(base) + } return qemu.New(base) } diff --git a/pkg/executil/command.go b/pkg/executil/command.go new file mode 100644 index 000000000000..f5aad2ebf619 --- /dev/null +++ b/pkg/executil/command.go @@ -0,0 +1,52 @@ +package executil + +import ( + "context" + "fmt" + "os/exec" + + "github.com/lima-vm/lima/pkg/ioutilx" +) + +type options struct { + ctx *context.Context +} + +type Opt func(*options) error + +// WithContext runs the command with CommandContext. +func WithContext(ctx *context.Context) Opt { + return func(o *options) error { + o.ctx = ctx + return nil + } +} + +func RunUTF16leCommand(args []string, opts ...Opt) (string, error) { + var o options + for _, f := range opts { + if err := f(&o); err != nil { + return "", err + } + } + + var cmd *exec.Cmd + if o.ctx != nil { + cmd = exec.CommandContext(*o.ctx, args[0], args[1:]...) + } else { + cmd = exec.Command(args[0], args[1:]...) + } + + out, err := cmd.StdoutPipe() + if err != nil { + return "", err + } + if err := cmd.Start(); err != nil { + return "", err + } + outString, err := ioutilx.FromUTF16leToString(out) + if err != nil { + return "", fmt.Errorf("failed to convert output from UTF16 when running command %v, err: %w", args, err) + } + return outString, nil +} diff --git a/pkg/guestagent/api/client/client.go b/pkg/guestagent/api/client/client.go index 285abe926ea9..9a49f701224a 100644 --- a/pkg/guestagent/api/client/client.go +++ b/pkg/guestagent/api/client/client.go @@ -7,10 +7,13 @@ import ( "context" "encoding/json" "fmt" + "net" "net/http" + "strconv" "github.com/lima-vm/lima/pkg/guestagent/api" "github.com/lima-vm/lima/pkg/httpclientutil" + "github.com/lima-vm/lima/pkg/limayaml" ) type GuestAgentClient interface { @@ -20,12 +23,40 @@ type GuestAgentClient interface { } // NewGuestAgentClient creates a client. -// socketPath is a path to the UNIX socket, without unix:// prefix. -func NewGuestAgentClient(socketPath string) (GuestAgentClient, error) { - hc, err := httpclientutil.NewHTTPClientWithSocketPath(socketPath) - if err != nil { - return nil, err +// remote is a path to the UNIX socket, without unix:// prefix or a remote hostname/IP address. +func NewGuestAgentClient(remote string, proto limayaml.GuestAgentProto, instanceName string) (GuestAgentClient, error) { + var hc *http.Client + switch proto { + case limayaml.GuestAgentTCPProto: + hc = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "tcp", remote) + }, + }, + } + case limayaml.GuestAgentUNIXProto: + hcSock, err := httpclientutil.NewHTTPClientWithSocketPath(remote) + if err != nil { + return nil, err + } + hc = hcSock + case limayaml.GuestAgentVSockProto: + _, p, err := net.SplitHostPort(remote) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(p) + if err != nil { + return nil, err + } + hc, err = newVSockGuestAgentClient(port, instanceName) + if err != nil { + return nil, err + } } + return NewGuestAgentClientWithHTTPClient(hc), nil } diff --git a/pkg/guestagent/api/client/client_others.go b/pkg/guestagent/api/client/client_others.go new file mode 100644 index 000000000000..7fcbdb184192 --- /dev/null +++ b/pkg/guestagent/api/client/client_others.go @@ -0,0 +1,16 @@ +//go:build !windows +// +build !windows + +package client + +import ( + "net/http" + + "github.com/lima-vm/lima/pkg/httpclientutil" +) + +func newVSockGuestAgentClient(port int, _ string) (*http.Client, error) { + hc := httpclientutil.NewHTTPClientWithVSockPort(port) + + return hc, nil +} diff --git a/pkg/guestagent/api/client/client_windows.go b/pkg/guestagent/api/client/client_windows.go new file mode 100644 index 000000000000..7dc19658b646 --- /dev/null +++ b/pkg/guestagent/api/client/client_windows.go @@ -0,0 +1,19 @@ +//go:build windows +// +build windows + +package client + +import ( + "net/http" + + "github.com/lima-vm/lima/pkg/httpclientutil" +) + +func newVSockGuestAgentClient(port int, instanceName string) (*http.Client, error) { + hc, err := httpclientutil.NewHTTPClientWithVSockPort(instanceName, port) + if err != nil { + return nil, err + } + + return hc, nil +} diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go index 1807bf6dde14..889a46a556e3 100644 --- a/pkg/guestagent/guestagent_linux.go +++ b/pkg/guestagent/guestagent_linux.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" "sync" + "syscall" "time" "github.com/elastic/go-libaudit/v2" @@ -25,9 +26,17 @@ func New(newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration } auditClient, err := libaudit.NewMulticastAuditClient(nil) - if err != nil { + switch { + case errors.Is(err, syscall.EPROTONOSUPPORT), errors.Is(err, syscall.EAFNOSUPPORT): + // system doesn't support auditing, skip + a.worthCheckingIPTables = true + go a.kubernetesServiceWatcher.Start() + go a.fixSystemTimeSkew() + return a, nil + case !errors.Is(err, nil): return nil, err } + auditStatus, err := auditClient.GetStatus() if err != nil { return nil, err @@ -36,9 +45,20 @@ func New(newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration if err = auditClient.SetEnabled(true, libaudit.WaitForReply); err != nil { return nil, err } - } + auditStatus, err := auditClient.GetStatus() + if err != nil { + return nil, err + } + if auditStatus.Enabled == 0 { + if err = auditClient.SetEnabled(true, libaudit.WaitForReply); err != nil { + return nil, err + } + } - go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle) + go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle) + } else { + a.worthCheckingIPTables = true + } go a.kubernetesServiceWatcher.Start() go a.fixSystemTimeSkew() return a, nil diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index e51420a70a31..0ec0a3f0f740 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -42,6 +42,7 @@ type HostAgent struct { tcpDNSLocalPort int instDir string instName string + instSSHAddress string sshConfig *ssh.SSHConfig portForwarder *portForwarder onClose []func() error // LIFO @@ -51,6 +52,8 @@ type HostAgent struct { eventEnc *json.Encoder eventEncMu sync.Mutex + + vSockPort int } type options struct { @@ -91,6 +94,9 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt if err != nil { return nil, err } + if *y.VMType == limayaml.WSL2 { + sshLocalPort = inst.SSHLocalPort + } var udpDNSLocalPort, tcpDNSLocalPort int if *y.HostResolver.Enabled { @@ -104,7 +110,16 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt } } - if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive); err != nil { + vSockPort := 0 + if *y.GuestAgent.Protocol == limayaml.GuestAgentVSockProto { + port, err := getFreeVSockPort() + if err != nil { + logrus.WithError(err).Error("failed to get free VSock port") + } + vSockPort = port + } + + if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort); err != nil { return nil, err } @@ -112,7 +127,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt if err != nil { return nil, err } - if err = writeSSHConfigFile(inst, sshLocalPort, sshOpts); err != nil { + if err = writeSSHConfigFile(inst, inst.SSHAddress, sshLocalPort, sshOpts); err != nil { return nil, err } sshConfig := &ssh.SSHConfig{ @@ -145,16 +160,18 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt tcpDNSLocalPort: tcpDNSLocalPort, instDir: inst.Dir, instName: instName, + instSSHAddress: inst.SSHAddress, sshConfig: sshConfig, portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules), driver: limaDriver, sigintCh: sigintCh, eventEnc: json.NewEncoder(stdout), + vSockPort: vSockPort, } return a, nil } -func writeSSHConfigFile(inst *store.Instance, sshLocalPort int, sshOpts []string) error { +func writeSSHConfigFile(inst *store.Instance, instSSHAddress string, sshLocalPort int, sshOpts []string) error { if inst.Dir == "" { return fmt.Errorf("directory is unknown for the instance %q", inst.Name) } @@ -167,7 +184,7 @@ func writeSSHConfigFile(inst *store.Instance, sshLocalPort int, sshOpts []string } if err := sshutil.Format(&b, inst.Name, sshutil.FormatConfig, append(sshOpts, - "Hostname=127.0.0.1", + fmt.Sprintf("Hostname=%s", instSSHAddress), fmt.Sprintf("Port=%d", sshLocalPort), )); err != nil { return err @@ -395,7 +412,7 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { a.onClose = append(a.onClose, func() error { logrus.Debugf("shutting down the SSH master") - if exitMasterErr := ssh.ExitMaster("127.0.0.1", a.sshLocalPort, a.sshConfig); exitMasterErr != nil { + if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil { logrus.WithError(exitMasterErr).Warn("failed to exit SSH master") } return nil @@ -411,7 +428,7 @@ sudo mkdir -p -m 700 /run/host-services sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock sudo chown -R "${USER}" /run/host-services` faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock" - stdout, stderr, err := ssh.ExecuteScript("127.0.0.1", a.sshLocalPort, a.sshConfig, faScript, faDesc) + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { mErr = errors.Join(mErr, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)) @@ -480,18 +497,20 @@ func (a *HostAgent) close() error { func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { // TODO: use vSock (when QEMU for macOS gets support for vSock) + localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) + remoteUnix := "/run/lima-guestagent.sock" + // Setup all socket forwards and defer their teardown - logrus.Debugf("Forwarding unix sockets") - for _, rule := range a.y.PortForwards { - if rule.GuestSocket != "" { - local := hostAddress(rule, guestagentapi.IPPort{}) - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbForward, rule.Reverse) + if *a.y.VMType != limayaml.WSL2 { + logrus.Debugf("Forwarding unix sockets") + for _, rule := range a.y.PortForwards { + if rule.GuestSocket != "" { + local := hostAddress(rule, guestagentapi.IPPort{}) + _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, localUnix, verbForward, rule.Reverse) + } } } - localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) - remoteUnix := "/run/lima-guestagent.sock" - a.onClose = append(a.onClose, func() error { logrus.Debugf("Stop forwarding unix sockets") var mErr error @@ -510,11 +529,18 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { return mErr }) + guestSocketAddr := localUnix + if *a.y.GuestAgent.Protocol == limayaml.GuestAgentVSockProto { + guestSocketAddr = fmt.Sprintf("0.0.0.0:%d", a.vSockPort) + } + for { - if !isGuestAgentSocketAccessible(ctx, localUnix) { - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + if !isGuestAgentSocketAccessible(ctx, guestSocketAddr, *a.y.GuestAgent.Protocol, a.instName) { + if *a.y.GuestAgent.Protocol != limayaml.GuestAgentVSockProto { + _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + } } - if err := a.processGuestAgentEvents(ctx, localUnix); err != nil { + if err := a.processGuestAgentEvents(ctx, guestSocketAddr, *a.y.GuestAgent.Protocol, a.instName); err != nil { if !errors.Is(err, context.Canceled) { logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly") } @@ -527,8 +553,8 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } -func isGuestAgentSocketAccessible(ctx context.Context, localUnix string) bool { - client, err := guestagentclient.NewGuestAgentClient(localUnix) +func isGuestAgentSocketAccessible(ctx context.Context, localUnix string, proto limayaml.GuestAgentProto, instanceName string) bool { + client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName) if err != nil { return false } @@ -536,8 +562,8 @@ func isGuestAgentSocketAccessible(ctx context.Context, localUnix string) bool { return err == nil } -func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix string) error { - client, err := guestagentclient.NewGuestAgentClient(localUnix) +func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix string, proto limayaml.GuestAgentProto, instanceName string) error { + client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName) if err != nil { return err } @@ -554,7 +580,7 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix strin for _, f := range ev.Errors { logrus.Warnf("received error from the guest: %q", f) } - a.portForwarder.OnEvent(ctx, ev) + a.portForwarder.OnEvent(ctx, ev, a.instSSHAddress) } if err := client.Events(ctx, onEvent); err != nil { diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index 31edb0e76987..91e86e82fe63 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -3,6 +3,7 @@ package hostagent import ( "context" "net" + "runtime" "github.com/lima-vm/lima/pkg/guestagent/api" "github.com/lima-vm/lima/pkg/limayaml" @@ -40,7 +41,15 @@ func hostAddress(rule limayaml.PortForward, guest api.IPPort) string { return host.String() } -func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) { +func (pf *portForwarder) forwardingAddresses(guest api.IPPort, localUnixIP net.IP) (string, string) { + if runtime.GOOS == "windows" { + guest.IP = localUnixIP + host := api.IPPort{ + IP: net.ParseIP("127.0.0.1"), + Port: guest.Port, + } + return host.String(), guest.String() + } for _, rule := range pf.rules { if rule.GuestSocket != "" { continue @@ -69,19 +78,22 @@ func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) return "", guest.String() } -func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) { +func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event, instSSHAddress string) { + localUnixIP := net.ParseIP(instSSHAddress) + for _, f := range ev.LocalPortsRemoved { - local, remote := pf.forwardingAddresses(f) + local, remote := pf.forwardingAddresses(f, localUnixIP) if local == "" { continue } logrus.Infof("Stopping forwarding TCP from %s to %s", remote, local) + if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil { logrus.WithError(err).Warnf("failed to stop forwarding tcp port %d", f.Port) } } for _, f := range ev.LocalPortsAdded { - local, remote := pf.forwardingAddresses(f) + local, remote := pf.forwardingAddresses(f, localUnixIP) if local == "" { logrus.Infof("Not forwarding TCP %s", remote) continue diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go index f004b9a13344..bd889674e6c5 100644 --- a/pkg/hostagent/port_darwin.go +++ b/pkg/hostagent/port_darwin.go @@ -156,3 +156,7 @@ func (plf *pseudoLoopbackForwarder) Close() error { _ = plf.ln.Close() return plf.onClose() } + +func getFreeVSockPort() (int, error) { + return 0, nil +} diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go index 35d276d6f316..601fb035ee9a 100644 --- a/pkg/hostagent/port_others.go +++ b/pkg/hostagent/port_others.go @@ -1,5 +1,5 @@ -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows package hostagent @@ -12,3 +12,7 @@ import ( func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error { return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) } + +func getFreeVSockPort() (int, error) { + return 0, nil +} diff --git a/pkg/hostagent/port_windows.go b/pkg/hostagent/port_windows.go new file mode 100644 index 000000000000..f0917c2ba0cb --- /dev/null +++ b/pkg/hostagent/port_windows.go @@ -0,0 +1,19 @@ +//go:build windows +// +build windows + +package hostagent + +import ( + "context" + + "github.com/lima-vm/lima/pkg/windows" + "github.com/lima-vm/sshocker/pkg/ssh" +) + +func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error { + return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) +} + +func getFreeVSockPort() (int, error) { + return windows.GetRandomFreeVSockPort(0, 2147483647) +} diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go index 0e92049ff765..a9fa9b95632a 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -42,7 +42,7 @@ func (a *HostAgent) waitForRequirements(label string, requirements []requirement func (a *HostAgent) waitForRequirement(r requirement) error { logrus.Debugf("executing script %q", r.description) - stdout, stderr, err := ssh.ExecuteScript("127.0.0.1", a.sshLocalPort, a.sshConfig, r.script, r.description) + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, r.script, r.description) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err) @@ -115,9 +115,26 @@ fi }) } - req = append(req, requirement{ - description: "the guest agent to be running", - script: `#!/bin/bash + if *a.y.GuestAgent.Protocol == limayaml.GuestAgentVSockProto { + req = append(req, requirement{ + description: "the guest agent to be running", + script: fmt.Sprintf(`#!/bin/bash +set -eux -o pipefail +if ! timeout 30s bash -c "until ss -a -n --vsock --listen | grep -q '*:%d'; do sleep 3; done"; then + echo >&2 "lima-guestagent is not installed yet" + exit 1 +fi +`, a.vSockPort), + debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. +Make sure that you are using an officially supported image. +Also see "/var/log/cloud-init-output.log" in the guest. +A possible workaround is to run "lima-guestagent install-systemd" in the guest. +`, + }) + } else { + req = append(req, requirement{ + description: "the guest agent to be running", + script: `#!/bin/bash set -eux -o pipefail sock="/run/lima-guestagent.sock" if ! timeout 30s bash -c "until [ -S \"${sock}\" ]; do sleep 3; done"; then @@ -125,12 +142,13 @@ if ! timeout 30s bash -c "until [ -S \"${sock}\" ]; do sleep 3; done"; then exit 1 fi `, - debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. + debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. Make sure that you are using an officially supported image. Also see "/var/log/cloud-init-output.log" in the guest. A possible workaround is to run "lima-guestagent install-systemd" in the guest. `, - }) + }) + } return req } diff --git a/pkg/httpclientutil/httpclientutil.go b/pkg/httpclientutil/httpclientutil.go index 4e398dbec5b6..90b6c3fecbf8 100644 --- a/pkg/httpclientutil/httpclientutil.go +++ b/pkg/httpclientutil/httpclientutil.go @@ -9,30 +9,11 @@ import ( "errors" "fmt" "io" - "net" "net/http" - "os" "github.com/lima-vm/lima/pkg/httputil" ) -// 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) diff --git a/pkg/httpclientutil/httpclientutil_others.go b/pkg/httpclientutil/httpclientutil_others.go new file mode 100644 index 000000000000..6160076fe630 --- /dev/null +++ b/pkg/httpclientutil/httpclientutil_others.go @@ -0,0 +1,52 @@ +//go:build !windows +// +build !windows + +package httpclientutil + +import ( + "context" + "net" + "net/http" + "os" + "runtime" + + "github.com/mdlayher/vsock" +) + +// NewHTTPClientWithSocketPath creates a client. +// socketPath is a path to the UNIX socket, without unix:// prefix. +func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { + // https://github.com/adrg/xdg/pull/14 + // TODO: move to separate file for compile time check instead of runtime + if runtime.GOOS == "windows" { + if _, err := os.Lstat(socketPath); err != nil { + return nil, err + } + } else { + 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 +} + +// NewHTTPClientWithVSockPort creates a client. +// port is the port to use for the vsock. +func NewHTTPClientWithVSockPort(port int) *http.Client { + hc := &http.Client{ + Transport: &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return vsock.Dial(2, uint32(port), nil) + }, + }, + } + return hc +} diff --git a/pkg/httpclientutil/httpclientutil_windows.go b/pkg/httpclientutil/httpclientutil_windows.go new file mode 100644 index 000000000000..5c84c1ed0e8c --- /dev/null +++ b/pkg/httpclientutil/httpclientutil_windows.go @@ -0,0 +1,65 @@ +//go:build windows +// +build windows + +package httpclientutil + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + + winio "github.com/Microsoft/go-winio" + "github.com/Microsoft/go-winio/pkg/guid" + "github.com/lima-vm/lima/pkg/windows" +) + +// NewHTTPClientWithSocketPath creates a client. +// socketPath is a path to the UNIX socket, without unix:// prefix. +func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { + // Use Lstat on windows, see: https://github.com/adrg/xdg/pull/14 + if _, err := os.Lstat(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 +} + +// NewHTTPClientWithVSockPort creates a client for a vsock port. +func NewHTTPClientWithVSockPort(instanceName string, port int) (*http.Client, error) { + VMIDStr, err := windows.GetInstanceVMID(fmt.Sprintf("lima-%s", instanceName)) + if err != nil { + return nil, err + } + VMIDGUID, err := guid.FromString(VMIDStr) + if err != nil { + return nil, err + } + + serviceGUID, err := guid.FromString(fmt.Sprintf("%x%s", port, windows.MagicVSOCKSuffix)) + if err != nil { + return nil, err + } + + sockAddr := &winio.HvsockAddr{ + VMID: VMIDGUID, + ServiceID: serviceGUID, + } + + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return winio.Dial(ctx, sockAddr) + }, + }, + } + return hc, nil +} diff --git a/pkg/ioutilx/ioutilx.go b/pkg/ioutilx/ioutilx.go index fa8a38ee56fd..f7e7e218dbe8 100644 --- a/pkg/ioutilx/ioutilx.go +++ b/pkg/ioutilx/ioutilx.go @@ -4,6 +4,12 @@ import ( "errors" "fmt" "io" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" ) // ReadAtMaximum reands n at maximum. @@ -20,3 +26,28 @@ func ReadAtMaximum(r io.Reader, n int64) ([]byte, error) { } return b, err } + +func FromUTF16le(r io.Reader) io.Reader { + o := transform.NewReader(r, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()) + return o +} + +func FromUTF16leToString(r io.Reader) (string, error) { + out, err := io.ReadAll(FromUTF16le(r)) + if err != nil { + return "", err + } + + return string(out), nil +} + +func CanonicalWindowsPath(orig string) string { + newPath := orig + out, err := exec.Command("cygpath", "-m", orig).CombinedOutput() + if err != nil { + logrus.WithError(err).Errorf("failed to convert path to mingw, maybe not using Git ssh?") + } else { + newPath = strings.TrimSpace(string(out)) + } + return newPath +} diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 910fe21b472c..82a2d20876ac 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -665,6 +665,19 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { if y.Rosetta.BinFmt == nil { y.Rosetta.BinFmt = pointer.Bool(false) } + + if y.GuestAgent.Protocol == nil { + y.GuestAgent.Protocol = d.GuestAgent.Protocol + } + if o.GuestAgent.Protocol != nil { + y.GuestAgent.Protocol = o.GuestAgent.Protocol + } + if y.GuestAgent.Protocol == nil { + y.GuestAgent.Protocol = pointer.String(GuestAgentUNIXProto) + if runtime.GOOS == "windows" { + y.GuestAgent.Protocol = pointer.String(GuestAgentVSockProto) + } + } } func executeGuestTemplate(format string) (bytes.Buffer, error) { @@ -828,6 +841,8 @@ func NewVMType(driver string) VMType { return VZ case "qemu": return QEMU + case "wsl2": + return WSL2 default: logrus.Warnf("Unknown driver: %s", driver) return driver diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index a9e37366151b..ff63dc1be5b0 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -105,6 +105,9 @@ func TestFillDefault(t *testing.T) { CACertificates: CACertificates{ RemoveDefaults: pointer.Bool(false), }, + GuestAgent: GuestAgent{ + Protocol: pointer.String(GuestAgentUNIXProto), + }, } if IsAccelOS() { if HasHostCPU() { @@ -120,6 +123,10 @@ func TestFillDefault(t *testing.T) { } } + if runtime.GOOS == "windows" { + builtin.GuestAgent.Protocol = pointer.String(GuestAgentVSockProto) + } + defaultPortForward := PortForward{ GuestIP: api.IPv4loopback1, GuestPortRange: [2]int{1, 65535}, @@ -371,6 +378,9 @@ func TestFillDefault(t *testing.T) { Enabled: pointer.Bool(true), BinFmt: pointer.Bool(true), }, + GuestAgent: GuestAgent{ + Protocol: pointer.String(GuestAgentVSockProto), + }, } expect = d @@ -406,6 +416,10 @@ func TestFillDefault(t *testing.T) { } } + expect.GuestAgent = GuestAgent{ + Protocol: pointer.String(GuestAgentVSockProto), + } + y = LimaYAML{} FillDefault(&y, &d, &LimaYAML{}, filePath) assert.DeepEqual(t, &y, &expect, opts...) @@ -566,6 +580,9 @@ func TestFillDefault(t *testing.T) { Enabled: pointer.Bool(false), BinFmt: pointer.Bool(false), }, + GuestAgent: GuestAgent{ + Protocol: pointer.String(GuestAgentTCPProto), + }, } y = filledDefaults @@ -618,6 +635,10 @@ func TestFillDefault(t *testing.T) { BinFmt: pointer.Bool(false), } + expect.GuestAgent = GuestAgent{ + Protocol: pointer.String(GuestAgentTCPProto), + } + FillDefault(&y, &d, &o, filePath) assert.DeepEqual(t, &y, &expect, opts...) } diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 2f530a2326ba..a73cb6949789 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -38,6 +38,7 @@ type LimaYAML struct { PropagateProxyEnv *bool `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty"` CACertificates CACertificates `yaml:"caCerts,omitempty" json:"caCerts,omitempty"` Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` + GuestAgent GuestAgent `yaml:"guestAgent,omitempty" json:"guestAgent,omitempty"` } type OS = string @@ -56,9 +57,11 @@ const ( REVSSHFS MountType = "reverse-sshfs" NINEP MountType = "9p" VIRTIOFS MountType = "virtiofs" + WSLMount MountType = "wsl" QEMU VMType = "qemu" VZ VMType = "vz" + WSL2 VMType = "wsl2" ) type Rosetta struct { @@ -243,6 +246,18 @@ type CACertificates struct { Certs []string `yaml:"certs,omitempty" json:"certs,omitempty"` } +type GuestAgentProto = string + +const ( + GuestAgentUNIXProto GuestAgentProto = "UNIX" + GuestAgentVSockProto GuestAgentProto = "VSOCK" + GuestAgentTCPProto GuestAgentProto = "TCP" +) + +type GuestAgent struct { + Protocol *string `yaml:"protocol,omitempty" json:"protocol,omitempty"` +} + // DEPRECATED types below // Types have been renamed to turn all references to the old names into compiler errors, diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 7495216d38a2..fe9739631783 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -56,6 +56,8 @@ func Validate(y LimaYAML, warn bool) error { switch *y.VMType { case QEMU: // NOP + case WSL2: + // NOP case VZ: if !IsNativeArch(*y.Arch) { return fmt.Errorf("field `arch` must be %q for VZ; got %q", NewArch(runtime.GOARCH), *y.Arch) @@ -159,7 +161,7 @@ func Validate(y LimaYAML, warn bool) error { } switch *y.MountType { - case REVSSHFS, NINEP, VIRTIOFS: + case REVSSHFS, NINEP, VIRTIOFS, WSLMount: default: return fmt.Errorf("field `mountType` must be %q or %q or %q, got %q", REVSSHFS, NINEP, VIRTIOFS, *y.MountType) } diff --git a/pkg/osutil/osutil_windows.go b/pkg/osutil/osutil_windows.go index 816731eab6b6..48a109ad0f6b 100644 --- a/pkg/osutil/osutil_windows.go +++ b/pkg/osutil/osutil_windows.go @@ -3,6 +3,9 @@ package osutil import ( "fmt" "io/fs" + "syscall" + + "golang.org/x/sys/windows" ) // UnixPathMax is the value of UNIX_PATH_MAX. @@ -14,7 +17,7 @@ type Stat struct { Gid uint32 //nolint:revive } -func SysStat(fi fs.FileInfo) (Stat, bool) { +func SysStat(_ fs.FileInfo) (Stat, bool) { return Stat{Uid: 0, Gid: 0}, false } @@ -26,10 +29,10 @@ const SigKill = Signal(9) type Signal int -func SysKill(pid int, sig Signal) error { - return fmt.Errorf("unimplemented") +func SysKill(pid int, _ Signal) error { + return windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(pid)) } -func Ftruncate(fd int, length int64) (err error) { +func Ftruncate(_ int, _ int64) (err error) { return fmt.Errorf("unimplemented") } diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go index 8cc25b632111..33f79f3762fc 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -16,6 +16,7 @@ import ( "sync" "github.com/coreos/go-semver/semver" + "github.com/lima-vm/lima/pkg/ioutilx" "github.com/lima-vm/lima/pkg/lockutil" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/store/dirnames" @@ -134,7 +135,13 @@ func CommonOpts(useDotSSH bool) ([]string, error) { if err != nil { return nil, err } - opts := []string{"IdentityFile=\"" + privateKeyPath + "\""} + var opts []string + if runtime.GOOS == "windows" { + privateKeyPath = ioutilx.CanonicalWindowsPath(privateKeyPath) + opts = []string{"IdentityFile=" + privateKeyPath} + } else { + opts = []string{"IdentityFile=\"" + privateKeyPath + "\""} + } // Append all private keys corresponding to ~/.ssh/*.pub to keep old instances working // that had been created before lima started using an internal identity. @@ -193,10 +200,18 @@ func CommonOpts(useDotSSH bool) ([]string, error) { // We prioritize AES algorithms when AES accelerator is available. if sshInfo.aesAccelerated { logrus.Debugf("AES accelerator seems available, prioritizing aes128-gcm@openssh.com and aes256-gcm@openssh.com") - opts = append(opts, "Ciphers=\"^aes128-gcm@openssh.com,aes256-gcm@openssh.com\"") + if runtime.GOOS == "windows" { + opts = append(opts, "Ciphers=^aes128-gcm@openssh.com,aes256-gcm@openssh.com") + } else { + opts = append(opts, "Ciphers=\"^aes128-gcm@openssh.com,aes256-gcm@openssh.com\"") + } } else { logrus.Debugf("AES accelerator does not seem available, prioritizing chacha20-poly1305@openssh.com") - opts = append(opts, "Ciphers=\"^chacha20-poly1305@openssh.com\"") + if runtime.GOOS == "windows" { + opts = append(opts, "Ciphers=^chacha20-poly1305@openssh.com") + } else { + opts = append(opts, "Ciphers=\"^chacha20-poly1305@openssh.com\"") + } } } return opts, nil @@ -216,11 +231,16 @@ func SSHOpts(instDir string, useDotSSH, forwardAgent bool, forwardX11 bool, forw if err != nil { return nil, err } + controlPath := fmt.Sprintf("ControlPath=\"%s\"", controlSock) + if runtime.GOOS == "windows" { + controlSock = ioutilx.CanonicalWindowsPath(controlSock) + controlPath = fmt.Sprintf("ControlPath=%s", controlSock) + } opts = append(opts, fmt.Sprintf("User=%s", u.Username), // guest and host have the same username, but we should specify the username explicitly (#85) "ControlMaster=auto", - fmt.Sprintf("ControlPath=\"%s\"", controlSock), - "ControlPersist=5m", + controlPath, + "ControlPersist=yes", ) if forwardAgent { opts = append(opts, "ForwardAgent=yes") diff --git a/pkg/start/ha_cmd_opts_others.go b/pkg/start/ha_cmd_opts_others.go new file mode 100644 index 000000000000..60f7f1f1e6dc --- /dev/null +++ b/pkg/start/ha_cmd_opts_others.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package start + +import ( + "syscall" +) + +var SysProcAttr = &syscall.SysProcAttr{} diff --git a/pkg/start/ha_cmd_opts_windows.go b/pkg/start/ha_cmd_opts_windows.go new file mode 100644 index 000000000000..047b8eb32394 --- /dev/null +++ b/pkg/start/ha_cmd_opts_windows.go @@ -0,0 +1,9 @@ +package start + +import ( + "syscall" +) + +var SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, +} diff --git a/pkg/start/start.go b/pkg/start/start.go index 1e223ed9cbc2..2df500907a6f 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -160,6 +160,7 @@ func Start(ctx context.Context, inst *store.Instance) error { } args = append(args, inst.Name) haCmd := exec.CommandContext(ctx, self, args...) + haCmd.SysProcAttr = SysProcAttr haCmd.Stdout = haStdoutW haCmd.Stderr = haStderrW diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index d3dd43dcfb86..8ab0f12b45e4 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -28,6 +28,7 @@ const ( const ( LimaYAML = "lima.yaml" CIDataISO = "cidata.iso" + CIDataISODir = "cidata" BaseDisk = "basedisk" DiffDisk = "diffdisk" Kernel = "kernel" @@ -52,6 +53,8 @@ const ( HostAgentStderrLog = "ha.stderr.log" VzIdentifier = "vz-identifier" VzEfi = "vz-efi" + WslRootFs = "wsl-rootfs" + WslRootFsDir = "wsl-root" // SocketDir is the default location for forwarded sockets with a relative paths in HostSocket SocketDir = "sock" diff --git a/pkg/store/instance.go b/pkg/store/instance.go index af9d3f18a1a2..2dd0018a51e4 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "os" + "os/exec" "os/user" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -17,6 +19,7 @@ import ( "time" "github.com/docker/go-units" + "github.com/lima-vm/lima/pkg/executil" hostagentclient "github.com/lima-vm/lima/pkg/hostagent/api/client" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/store/dirnames" @@ -27,10 +30,12 @@ import ( type Status = string const ( - StatusUnknown Status = "" - StatusBroken Status = "Broken" - StatusStopped Status = "Stopped" - StatusRunning Status = "Running" + StatusUnknown Status = "" + StatusUninitialized Status = "Uninitialized" + StatusInstalling Status = "Installing" + StatusBroken Status = "Broken" + StatusStopped Status = "Stopped" + StatusRunning Status = "Running" ) type Instance struct { @@ -52,6 +57,9 @@ type Instance struct { DriverPID int `json:"driverPID,omitempty"` Errors []error `json:"errors,omitempty"` Config *limayaml.LimaYAML `json:"config,omitempty"` + RootFsPath string `json:"rootfs,omitempty"` + DistroName string `json:"distroName,omitempty"` + SSHAddress string `json:"sshAddress,omitempty"` } func (inst *Instance) LoadYAML() (*limayaml.LimaYAML, error) { @@ -89,21 +97,9 @@ func Inspect(instName string) (*Instance, error) { inst.Arch = *y.Arch inst.VMType = *y.VMType inst.CPUType = y.CPUType[*y.Arch] - - inst.CPUs = *y.CPUs - memory, err := units.RAMInBytes(*y.Memory) - if err == nil { - inst.Memory = memory - } - disk, err := units.RAMInBytes(*y.Disk) - if err == nil { - inst.Disk = disk - } - inst.AdditionalDisks = y.AdditionalDisks - inst.Networks = y.Networks + inst.SSHAddress = "127.0.0.1" inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0 inst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig) - inst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID)) if err != nil { inst.Status = StatusBroken @@ -129,26 +125,59 @@ func Inspect(instName string) (*Instance, error) { } } - inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType))) - if err != nil { - inst.Status = StatusBroken - inst.Errors = append(inst.Errors, err) - } - - if inst.Status == StatusUnknown { - if inst.HostAgentPID > 0 && inst.DriverPID > 0 { - inst.Status = StatusRunning - } else if inst.HostAgentPID == 0 && inst.DriverPID == 0 { - inst.Status = StatusStopped - } else if inst.HostAgentPID > 0 && inst.DriverPID == 0 { - inst.Errors = append(inst.Errors, errors.New("host agent is running but driver is not")) + if inst.VMType == limayaml.WSL2 { + inst.DistroName = fmt.Sprintf("%s-%s", "lima", inst.Name) + status, err := GetWslStatus(instName, inst.DistroName) + if err != nil { inst.Status = StatusBroken + inst.Errors = append(inst.Errors, err) } else { - inst.Errors = append(inst.Errors, fmt.Errorf("%s driver is running but host agent is not", inst.VMType)) + inst.Status = status + } + + inst.SSHLocalPort = 22 + + if inst.Status == StatusStopped || inst.Status == StatusRunning { + sshAddr, err := GetWslSSHAddress(instName, inst.DistroName) + if err == nil { + inst.SSHAddress = sshAddr + } else { + inst.Errors = append(inst.Errors, err) + } + } + } else { + inst.CPUs = *y.CPUs + memory, err := units.RAMInBytes(*y.Memory) + if err == nil { + inst.Memory = memory + } + disk, err := units.RAMInBytes(*y.Disk) + if err == nil { + inst.Disk = disk + } + inst.AdditionalDisks = y.AdditionalDisks + inst.Networks = y.Networks + + inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType))) + if err != nil { inst.Status = StatusBroken + inst.Errors = append(inst.Errors, err) } - } + if inst.Status == StatusUnknown { + if inst.HostAgentPID > 0 && inst.DriverPID > 0 { + inst.Status = StatusRunning + } else if inst.HostAgentPID == 0 && inst.DriverPID == 0 { + inst.Status = StatusStopped + } else if inst.HostAgentPID > 0 && inst.DriverPID == 0 { + inst.Errors = append(inst.Errors, errors.New("host agent is running but driver is not")) + inst.Status = StatusBroken + } else { + inst.Errors = append(inst.Errors, fmt.Errorf("%s driver is running but host agent is not", inst.VMType)) + inst.Status = StatusBroken + } + } + } tmpl, err := template.New("format").Parse(y.Message) if err != nil { inst.Errors = append(inst.Errors, fmt.Errorf("message %q is not a valid template: %w", y.Message, err)) @@ -190,6 +219,10 @@ func ReadPIDFile(path string) (int, error) { if err != nil { return 0, err } + // os.FindProcess will only return running processes on Windows, exit early + if runtime.GOOS == "windows" { + return pid, nil + } err = proc.Signal(syscall.Signal(0)) if err != nil { if errors.Is(err, os.ErrProcessDone) { @@ -328,7 +361,7 @@ func PrintInstances(w io.Writer, instances []*Instance, format string, options * fmt.Fprintf(w, "%s\t%s\t%s", instance.Name, instance.Status, - fmt.Sprintf("127.0.0.1:%d", instance.SSHLocalPort), + fmt.Sprintf("%s:%d", instance.SSHAddress, instance.SSHLocalPort), ) if !hideType { fmt.Fprintf(w, "\t%s", @@ -375,3 +408,58 @@ func PrintInstances(w io.Writer, instances []*Instance, format string, options * } return nil } + +func GetWslStatus(instName, distroName string) (string, error) { + // Expected output (whitespace preserved): + // PS > wsl --list --verbose + // NAME STATE VERSION + // * Ubuntu Stopped 2 + out, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--list", + "--verbose", + }) + if err != nil { + return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w", err) + } + + if len(out) == 0 { + return StatusBroken, fmt.Errorf("failed to read instance state for instance %s, try running `wsl --list --verbose` to debug, err: %w", instName, err) + } + + var instState string + // wsl --list --verbose may have differernt headers depending on localization, just split by line + // Windows uses little endian by default, use unicode.UseBOM policy to retrieve BOM from the text, + // and unicode.LittleEndian as a fallback + for _, rows := range strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") { + cols := regexp.MustCompile(`\s+`).Split(strings.TrimSpace(rows), -1) + nameIdx := 0 + // '*' indicates default instance + if cols[0] == "*" { + nameIdx = 1 + } + if cols[nameIdx] == distroName { + instState = cols[nameIdx+1] + break + } + } + + if instState == "" { + return StatusUninitialized, nil + } + + return instState, nil +} + +func GetWslSSHAddress(instName, distroName string) (string, error) { + // Expected output (whitespace preserved, [] for optional): + // PS > wsl -d bash -c hostname -I | cut -d' ' -f1 + // 168.1.1.1 [10.0.0.1] + cmd := exec.Command("wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("failed to get hostname for instance %s, err: %w", instName, err) + } + + return strings.TrimSpace(string(out)), nil +} diff --git a/pkg/store/instance_test.go b/pkg/store/instance_test.go index 837ce1389b60..2b2088b71a61 100644 --- a/pkg/store/instance_test.go +++ b/pkg/store/instance_test.go @@ -19,11 +19,12 @@ var goarch = limayaml.NewArch(runtime.GOARCH) var space = strings.Repeat(" ", len(goarch)-4) var instance = Instance{ - Name: "foo", - Status: StatusStopped, - VMType: vmtype, - Arch: goarch, - Dir: "dir", + Name: "foo", + Status: StatusStopped, + VMType: vmtype, + Arch: goarch, + Dir: "dir", + SSHAddress: "127.0.0.1", } var table = "NAME STATUS SSH CPUS MEMORY DISK DIR\n" + diff --git a/pkg/windows/process_windows.go b/pkg/windows/process_windows.go new file mode 100644 index 000000000000..566c02112d9b --- /dev/null +++ b/pkg/windows/process_windows.go @@ -0,0 +1,40 @@ +//go:build windows +// +build windows + +package windows + +import ( + "encoding/json" + "fmt" + "os/exec" +) + +type CommandLineJSON []struct { + CommandLine string +} + +// GetProcessCommandLine returns a slice of string containing all commandlines for a given process name. +func GetProcessCommandLine(name string) ([]string, error) { + out, err := exec.Command( + "powershell.exe", + "-nologo", + "-noprofile", + fmt.Sprintf( + `Get-CimInstance Win32_Process -Filter "name = '%s'" | Select CommandLine | ConvertTo-Json`, + name, + ), + ).CombinedOutput() + if err != nil { + return nil, err + } + + var outJSON CommandLineJSON + json.Unmarshal([]byte(out), &outJSON) + + var ret []string + for _, s := range outJSON { + ret = append(ret, s.CommandLine) + } + + return ret, nil +} diff --git a/pkg/windows/registry_windows.go b/pkg/windows/registry_windows.go new file mode 100644 index 000000000000..a5298bc9322b --- /dev/null +++ b/pkg/windows/registry_windows.go @@ -0,0 +1,225 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/sys/windows/registry" +) + +const ( + guestCommunicationsPrefix = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices` + MagicVSOCKSuffix = "-facb-11e6-bd58-64006a7986d3" + wslDistroInfoPrefix = `SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss` +) + +// AddVSockRegistryKey makes a vsock server running on the host acceessible in guests. +func AddVSockRegistryKey(port int) error { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return err + } + + if slices.Contains(used, port) { + return fmt.Errorf("port %q in use", port) + } + + vsockKeyPath := fmt.Sprintf(`%x%s`, port, MagicVSOCKSuffix) + vSockKey, _, err := registry.CreateKey( + rootKey, + vsockKeyPath, + registry.ALL_ACCESS, + ) + if err != nil { + return fmt.Errorf( + "failed to create new key (%s%s): %w", + guestCommunicationsPrefix, + vsockKeyPath, + err, + ) + } + defer vSockKey.Close() + + return nil +} + +// RemoveVSockRegistryKey removes entries created by AddVSockRegistryKey. +func RemoveVSockRegistryKey(port int) error { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return err + } + defer rootKey.Close() + + vsockKeyPath := fmt.Sprintf(`%x%s`, port, MagicVSOCKSuffix) + if err := registry.DeleteKey(rootKey, vsockKeyPath); err != nil { + return fmt.Errorf( + "failed to create new key (%s%s): %w", + guestCommunicationsPrefix, + vsockKeyPath, + err, + ) + } + + return nil +} + +// IsVSockPortFree determines if a VSock port has been registiered already. +func IsVSockPortFree(port int) (bool, error) { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return false, err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return false, err + } + + if slices.Contains(used, port) { + return false, nil + } + + return true, nil +} + +// GetDistroID returns a DistroId GUID corresponding to a Lima instance name. +func GetDistroID(name string) (string, error) { + rootKey, err := registry.OpenKey( + registry.CURRENT_USER, + wslDistroInfoPrefix, + registry.READ, + ) + if err != nil { + return "", fmt.Errorf( + "failed to open Lxss key (%s): %w", + wslDistroInfoPrefix, + err, + ) + } + defer rootKey.Close() + + keys, err := rootKey.ReadSubKeyNames(-1) + + if err != nil { + return "", fmt.Errorf("failed to read subkey names for %s: %w", wslDistroInfoPrefix, err) + } + + var out string + for _, k := range keys { + subKey, err := registry.OpenKey( + registry.CURRENT_USER, + fmt.Sprintf(`%s\%s`, wslDistroInfoPrefix, k), + registry.READ, + ) + if err != nil { + return "", fmt.Errorf("failed to read subkey %q for key %q: %w", k, wslDistroInfoPrefix, err) + } + dn, _, err := subKey.GetStringValue("DistributionName") + if err != nil { + return "", fmt.Errorf("failed to read 'DistributionName' value for subkey %q of %q: %w", k, wslDistroInfoPrefix, err) + } + if dn == name { + out = k + break + } + } + + if out == "" { + return "", fmt.Errorf("failed to find matching DistroID for %q", name) + } + + return out, nil +} + +// GetRandomFreeVSockPort gets a list of all registered VSock ports and returns a non-registered port. +func GetRandomFreeVSockPort(min, max int) (int, error) { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return 0, err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return 0, err + } + + type pair struct{ v, offset int } + tree := make([]pair, 1, len(used)+1) + tree[0] = pair{0, min} + + sort.Ints(used) + for i, v := range used { + if tree[len(tree)-1].v+tree[len(tree)-1].offset == v { + tree[len(tree)-1].offset++ + } else { + tree = append(tree, pair{v - min - i, min + i + 1}) + } + } + + v := rand.Intn(max - min + 1 - len(used)) + + for len(tree) > 1 { + m := len(tree) / 2 + if v < tree[m].v { + tree = tree[:m] + } else { + tree = tree[m:] + } + } + + return tree[0].offset + v, nil +} + +func getGuestCommunicationServicesKey() (registry.Key, error) { + rootKey, err := registry.OpenKey( + registry.LOCAL_MACHINE, + guestCommunicationsPrefix, + registry.WRITE|registry.READ, + ) + if err != nil { + return 0, fmt.Errorf( + "failed to open GuestCommunicationServices key (%s): %w", + guestCommunicationsPrefix, + err, + ) + } + + return rootKey, nil +} + +func getUsedPorts(key registry.Key) ([]int, error) { + keys, err := key.ReadSubKeyNames(-1) + if err != nil { + return nil, fmt.Errorf("failed to read subkey names for %s: %w", guestCommunicationsPrefix, err) + } + + out := []int{} + for _, k := range keys { + split := strings.Split(k, MagicVSOCKSuffix) + if len(split) == 2 { + i, err := strconv.Atoi(split[0]) + if err != nil { + return nil, fmt.Errorf("failed convert %q to int: %w", split[0], err) + } + out = append(out, i) + } + } + + return out, nil +} diff --git a/pkg/windows/wsl_util_windows.go b/pkg/windows/wsl_util_windows.go new file mode 100644 index 000000000000..c15b40be42f9 --- /dev/null +++ b/pkg/windows/wsl_util_windows.go @@ -0,0 +1,44 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "regexp" + "strings" +) + +// GetInstanceVMID returns the VM ID of a running WSL instance. +func GetInstanceVMID(instanceName string) (string, error) { + distroID, err := GetDistroID(instanceName) + if err != nil { + return "", err + } + + cmdLines, err := GetProcessCommandLine("wslhost.exe") + if err != nil { + return "", err + } + + re := regexp.MustCompile(`--vm-id\s\{(?P.{36})\}`) + if err != nil { + return "", err + } + + vmID := "" + for _, cmdLine := range cmdLines { + if strings.Contains(cmdLine, distroID) { + if matches := re.FindStringSubmatch(cmdLine); matches != nil { + vmID = matches[re.SubexpIndex("vmID")] + break + } + } + } + + if vmID == "" { + return "", fmt.Errorf("failed to find VM ID for instance %q", instanceName) + } + + return vmID, nil +} diff --git a/pkg/wsl2/fs.go b/pkg/wsl2/fs.go new file mode 100644 index 000000000000..f278490c8d6a --- /dev/null +++ b/pkg/wsl2/fs.go @@ -0,0 +1,35 @@ +package wsl2 + +import ( + "errors" + "os" + "path/filepath" + + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/fileutils" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" +) + +// EnsureFs downloads the root fs. +func EnsureFs(driver *driver.BaseDriver) error { + rootFsArchive := filepath.Join(driver.Instance.Dir, filenames.WslRootFs) + if _, err := os.Stat(rootFsArchive); errors.Is(err, os.ErrNotExist) { + var ensuredBaseDisk bool + errs := make([]error, len(driver.Yaml.Images)) + for i, f := range driver.Yaml.Images { + if _, err := fileutils.DownloadFile(rootFsArchive, f.File, true, "the image", *driver.Yaml.Arch); err != nil { + errs[i] = err + continue + } + ensuredBaseDisk = true + break + } + if !ensuredBaseDisk { + return fileutils.Errors(errs) + } + } + logrus.Info("Download succeeded") + + return nil +} diff --git a/pkg/wsl2/lima-init.TEMPLATE.sh b/pkg/wsl2/lima-init.TEMPLATE.sh new file mode 100644 index 000000000000..0812eb63c2a9 --- /dev/null +++ b/pkg/wsl2/lima-init.TEMPLATE.sh @@ -0,0 +1,5 @@ +set -eu; \ +export LOG_FILE=/var/log/lima-init.log; \ +exec > >(tee \$LOG_FILE) 2>&1; \ +export LIMA_CIDATA_MNT=$(/usr/bin/wslpath '{{.CIDataPath}}'); \ +exec \$LIMA_CIDATA_MNT/boot.sh; diff --git a/pkg/wsl2/vm_windows.go b/pkg/wsl2/vm_windows.go new file mode 100755 index 000000000000..0e3a955b0b64 --- /dev/null +++ b/pkg/wsl2/vm_windows.go @@ -0,0 +1,131 @@ +//go:build windows +// +build windows + +package wsl2 + +import ( + "context" + _ "embed" + "fmt" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/executil" + "github.com/lima-vm/lima/pkg/store" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/lima-vm/lima/pkg/textutil" + "github.com/sirupsen/logrus" +) + +// startVM calls WSL to start a VM. +// Takes argument for VM name. +func startVM(ctx context.Context, driver *driver.BaseDriver) error { + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--distribution", + driver.Instance.DistroName, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +// initVM calls WSL to import a new VM specifically for Lima. +func initVM(ctx context.Context, driver *driver.BaseDriver) error { + rootFSPath := path.Join(driver.Instance.Dir, filenames.WslRootFsDir) + rootFSDir := path.Join(driver.Instance.Dir, filenames.WslRootFs) + logrus.Infof("Importing distro from %q to %q", rootFSPath, rootFSDir) + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--import", + driver.Instance.DistroName, + rootFSPath, + rootFSDir, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +// stopVM calls WSL to stop a running VM. +// Takes arguments for name. +func stopVM(ctx context.Context, driver *driver.BaseDriver) error { + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--terminate", + driver.Instance.DistroName, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +//go:embed lima-init.TEMPLATE.sh +var limaBoot string + +func provisionVM(ctx context.Context, driver *driver.BaseDriver, errCh *chan error) error { + ciDataPath := filepath.Join(driver.Instance.Dir, filenames.CIDataISODir) + m := map[string]string{ + "CIDataPath": ciDataPath, + } + out, err := textutil.ExecuteTemplate(limaBoot, m) + if err != nil { + return fmt.Errorf("failed to construct wsl boot.sh script: %w", err) + } + outString := strings.Replace(string(out), `\r\n`, `\n`, -1) + + go func() { + cmd := exec.CommandContext( + ctx, + "wsl.exe", + "-d", + driver.Instance.DistroName, + "bash", + "-c", + outString, + ) + if _, err := cmd.CombinedOutput(); err != nil { + *errCh <- fmt.Errorf( + "error running wslCommand that executes boot.sh: %w, "+ + "check /var/log/lima-init.log for more details", err) + } + + for { + select { + case <-ctx.Done(): + logrus.Info("Context closed, stopping vm") + if status, err := store.GetWslStatus(driver.Instance.Name, driver.Instance.DistroName); err == nil && + status == store.StatusRunning { + stopVM(ctx, driver) + } + } + } + }() + + return err +} + +func keepAlive(ctx context.Context, driver *driver.BaseDriver, errCh *chan error) { + keepAliveCmd := exec.CommandContext( + ctx, + "wsl.exe", + "-d", + driver.Instance.DistroName, + "bash", + "-c", + "nohup sleep 2147483647d >/dev/null 2>&1", + ) + + go func() { + if err := keepAliveCmd.Run(); err != nil { + *errCh <- fmt.Errorf( + "error running wsl keepAlive command: %w", err) + } + }() +} diff --git a/pkg/wsl2/wsl_driver_others.go b/pkg/wsl2/wsl_driver_others.go new file mode 100644 index 000000000000..f442600ad36d --- /dev/null +++ b/pkg/wsl2/wsl_driver_others.go @@ -0,0 +1,41 @@ +//go:build !windows || no_wsl +// +build !windows no_wsl + +package wsl2 + +import ( + "context" + "errors" + + "github.com/lima-vm/lima/pkg/driver" +) + +var ErrUnsupported = errors.New("vm driver 'wsl' requires Windows 10 build 19041 or later (Hint: try recompiling Lima if you are seeing this error on Windows 10+)") + +const Enabled = false + +type LimaWslDriver struct { + *driver.BaseDriver +} + +func New(driver *driver.BaseDriver) *LimaWslDriver { + return &LimaWslDriver{ + BaseDriver: driver, + } +} + +func (l *LimaWslDriver) Validate() error { + return ErrUnsupported +} + +func (l *LimaWslDriver) CreateDisk() error { + return ErrUnsupported +} + +func (l *LimaWslDriver) Start(_ context.Context) (chan error, error) { + return nil, ErrUnsupported +} + +func (l *LimaWslDriver) Stop(_ context.Context) error { + return ErrUnsupported +} diff --git a/pkg/wsl2/wsl_driver_windows.go b/pkg/wsl2/wsl_driver_windows.go new file mode 100644 index 000000000000..b773afe15fcb --- /dev/null +++ b/pkg/wsl2/wsl_driver_windows.go @@ -0,0 +1,165 @@ +//go:build windows +// +build windows + +package wsl2 + +import ( + "context" + "fmt" + "regexp" + + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/reflectutil" + "github.com/lima-vm/lima/pkg/store" + "github.com/sirupsen/logrus" +) + +const Enabled = true + +type LimaWslDriver struct { + *driver.BaseDriver +} + +func New(driver *driver.BaseDriver) *LimaWslDriver { + return &LimaWslDriver{ + BaseDriver: driver, + } +} + +func (l *LimaWslDriver) Validate() error { + // TODO: add new mount type for WSL2 since this is handled by WSL2 automatically and no other mount type should be used. + if *l.Yaml.MountType != limayaml.WSLMount { + return fmt.Errorf("field `mountType` must be %q for WSL2 driver, got %q", limayaml.WSLMount, *l.Yaml.MountType) + } + if *l.Yaml.Firmware.LegacyBIOS { + return fmt.Errorf("`firmware.legacyBIOS` configuration is not supported for WSL2 driver") + } + // TODO: revise this list for WSL2 + if unknown := reflectutil.UnknownNonEmptyFields(l.Yaml, "VMType", + "Arch", + "Images", + "CPUs", + "CPUType", + "Memory", + "Disk", + "Mounts", + "MountType", + "SSH", + "Firmware", + "Provision", + "Containerd", + "Probes", + "PortForwards", + "Message", + "Networks", + "Env", + "DNS", + "HostResolver", + "PropagateProxyEnv", + "CACertificates", + "Rosetta", + "AdditionalDisks", + "Audio", + "Video", + ); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: %+v", *l.Yaml.VMType, unknown) + } + + if !limayaml.IsNativeArch(*l.Yaml.Arch) { + return fmt.Errorf("unsupported arch: %q", *l.Yaml.Arch) + } + + for k, v := range l.Yaml.CPUType { + if v != "" { + logrus.Warnf("Ignoring: vmType %s: cpuType[%q]: %q", *l.Yaml.VMType, k, v) + } + } + + re, err := regexp.Compile(`.*tar\.*`) + if err != nil { + return fmt.Errorf("failed to compile file check regex: %w", err) + } + for i, image := range l.Yaml.Images { + if unknown := reflectutil.UnknownNonEmptyFields(image, "File"); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: images[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + // TODO: real filetype checks + match := re.MatchString(image.Location) + if image.Arch == *l.Yaml.Arch && !match { + return fmt.Errorf("unsupported image type for vmType: %s, tarball root file system required: %q", *l.Yaml.VMType, image.Location) + } + } + + for i, mount := range l.Yaml.Mounts { + if unknown := reflectutil.UnknownNonEmptyFields(mount); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: mounts[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + } + + for i, network := range l.Yaml.Networks { + if unknown := reflectutil.UnknownNonEmptyFields(network); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: networks[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + } + + audioDevice := *l.Yaml.Audio.Device + if audioDevice != "" { + logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Yaml.VMType, audioDevice) + } + + // TODO: add this back after figuring out why it was causing a panic + // videoDisplay := *l.Yaml.Video.Display + // if videoDisplay != "" { + // logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Yaml.VMType, videoDisplay) + // } + return nil +} + +func (l *LimaWslDriver) Start(ctx context.Context) (chan error, error) { + logrus.Infof("Starting WSL VM") + status, err := store.GetWslStatus(l.Instance.Name, l.Instance.DistroName) + if err != nil { + return nil, err + } + + if status == store.StatusUninitialized { + if err := EnsureFs(l.BaseDriver); err != nil { + return nil, err + } + if err := initVM(ctx, l.BaseDriver); err != nil { + return nil, err + } + } + + errCh := make(chan error) + + if err := startVM(ctx, l.BaseDriver); err != nil { + return nil, err + } + + if err := provisionVM(ctx, l.BaseDriver, &errCh); err != nil { + return nil, err + } + + keepAlive(ctx, l.BaseDriver, &errCh) + + return errCh, err +} + +// Requires WSLg, which requires specific version of WSL2 to be installed. +// TODO: Add check. +func (l *LimaWslDriver) CanRunGUI() bool { + // return *l.Yaml.Video.Display == "wsl" + return false +} + +func (l *LimaWslDriver) RunGUI() error { + return fmt.Errorf("RunGUI is not support for the given driver '%s' and diplay '%s'", "wsl", *l.Yaml.Video.Display) +} + +func (l *LimaWslDriver) Stop(ctx context.Context) error { + logrus.Info("Shutting down WSL2 VM") + + return stopVM(ctx, l.BaseDriver) +}