diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index 8b4da0aa2f9..903d91e4569 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" @@ -21,11 +22,18 @@ func newDaemonCommand() *cobra.Command { RunE: daemonAction, } daemonCommand.Flags().Duration("tick", 3*time.Second, "tick for polling events") + daemonCommand.Flags().Int("port", 0, "tcp port") return daemonCommand } func daemonAction(cmd *cobra.Command, args []string) error { + unix := true socket := "/run/lima-guestagent.sock" + port, err := cmd.Flags().GetInt("port") + if err == nil && port != 0 { + unix = false + socket = fmt.Sprintf("127.0.0.1:%d", port) + } tick, err := cmd.Flags().GetDuration("tick") if err != nil { return err @@ -56,16 +64,24 @@ func daemonAction(cmd *cobra.Command, args []string) error { r := mux.NewRouter() server.AddRoutes(r, backend) srv := &http.Server{Handler: r} - err = os.RemoveAll(socket) - 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 unix { + err = os.RemoveAll(socket) + 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 + } + } else { + l, err = net.Listen("tcp4", socket) + if err != nil { + return err + } } 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 a82f05cf366..c77a3047890 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,16 @@ func newInstallSystemdCommand() *cobra.Command { Short: "install a systemd unit (user)", RunE: installSystemdAction, } + installSystemdCommand.Flags().Int("port", 0, "guestagent tcp port") return installSystemdCommand } func installSystemdAction(cmd *cobra.Command, args []string) error { - unit, err := generateSystemdUnit() + port, err := cmd.Flags().GetInt("port") + if err != nil { + return err + } + unit, err := generateSystemdUnit(port) if err != nil { return err } @@ -60,13 +66,18 @@ func installSystemdAction(cmd *cobra.Command, args []string) error { //go:embed lima-guestagent.TEMPLATE.service var systemdUnitTemplate string -func generateSystemdUnit() ([]byte, error) { +func generateSystemdUnit(port int) ([]byte, error) { selfExeAbs, err := os.Executable() if err != nil { return nil, err } + portString := "" + if port != 0 { + portString = fmt.Sprintf("%d", port) + } m := map[string]string{ "Binary": selfExeAbs, + "Port": portString, } return templateutil.Execute(systemdUnitTemplate, m) } diff --git a/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service b/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service index ddbd24b7e67..01c5bcaf133 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{{- if .Port}} --port {{.Port}}{{- end}} Type=simple Restart=on-failure diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go index 1c7173dbbd9..fe814a7c0b7 100644 --- a/cmd/limactl/hostagent.go +++ b/cmd/limactl/hostagent.go @@ -27,6 +27,7 @@ func newHostagentCommand() *cobra.Command { } hostagentCommand.Flags().StringP("pidfile", "p", "", "write pid to file") hostagentCommand.Flags().String("socket", "", "hostagent socket") + hostagentCommand.Flags().Int("port", 0, "hostagent tcp port") hostagentCommand.Flags().String("nerdctl-archive", "", "local file path (not URL) of nerdctl-full-VERSION-linux-GOARCH.tar.gz") return hostagentCommand } @@ -45,12 +46,21 @@ func hostagentAction(cmd *cobra.Command, args []string) error { } defer os.RemoveAll(pidfile) } + unix := true socket, err := cmd.Flags().GetString("socket") if err != nil { return err } - if socket == "" { - return fmt.Errorf("socket must be specified (limactl version mismatch?)") + port, err := cmd.Flags().GetInt("port") + if err != nil { + return err + } + if socket == "" && port == 0 { + return fmt.Errorf("socket or port must be specified (limactl version mismatch?)") + } + if port != 0 { + unix = false + socket = fmt.Sprintf(":%d", port) } instName := args[0] @@ -81,13 +91,21 @@ func hostagentAction(cmd *cobra.Command, args []string) error { r := mux.NewRouter() server.AddRoutes(r, backend) srv := &http.Server{Handler: r} - err = os.RemoveAll(socket) - if err != nil { - return err - } - l, err := net.Listen("unix", socket) - if err != nil { - return err + var l net.Listener + if unix { + err = os.RemoveAll(socket) + if err != nil { + return err + } + l, err = net.Listen("unix", socket) + if err != nil { + return err + } + } else { + l, err = net.Listen("tcp", socket) + if err != nil { + return err + } } go func() { defer os.RemoveAll(socket) 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 a1d447cacea..3c46cb12236 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh @@ -17,10 +17,15 @@ fi # Install or update the guestagent binary install -m 755 "${LIMA_CIDATA_MNT}"/lima-guestagent /usr/local/bin/lima-guestagent +port="" +if [ -n "${LIMA_CIDATA_GUEST_AGENT_PORT}" ]; then + port=" --port ${LIMA_CIDATA_GUEST_AGENT_PORT}" +fi + # Launch the guestagent service if [ -f /sbin/openrc-run ]; then # Install the openrc lima-guestagent service script - cat >/etc/init.d/lima-guestagent <<'EOF' + cat >/etc/init.d/lima-guestagent <&2 "lima-guestagent is not installed yet" + exit 1 +fi +`, + debugHint: `The guest agent (:1111) 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 @@ -126,12 +145,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 4e398dbec5b..6440988a824 100644 --- a/pkg/httpclientutil/httpclientutil.go +++ b/pkg/httpclientutil/httpclientutil.go @@ -16,6 +16,11 @@ import ( "github.com/lima-vm/lima/pkg/httputil" ) +// NewHTTPClient creates a client. +func NewHTTPClient() (*http.Client, error) { + return &http.Client{}, nil +} + // NewHTTPClientWithSocketPath creates a client. // socketPath is a path to the UNIX socket, without unix:// prefix. func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { diff --git a/pkg/start/start.go b/pkg/start/start.go index f87e267c5e7..2be91c1c57d 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -6,9 +6,12 @@ import ( "context" "errors" "fmt" + "net" "os" "os/exec" "path/filepath" + "runtime" + "strconv" "text/template" "time" @@ -84,6 +87,7 @@ func Start(ctx context.Context, inst *store.Instance) error { } haSockPath := filepath.Join(inst.Dir, filenames.HostAgentSock) + haPortPath := filepath.Join(inst.Dir, filenames.HostAgentPort) y, err := inst.LoadYAML() if err != nil { @@ -127,8 +131,23 @@ func Start(ctx context.Context, inst *store.Instance) error { } args = append(args, "hostagent", - "--pidfile", haPIDPath, - "--socket", haSockPath) + "--pidfile", haPIDPath) + if runtime.GOOS == "windows" { + port, err := findFreeTCPLocalPort() + if err != nil { + return err + } + if err := os.WriteFile(haPortPath, []byte(strconv.Itoa(port)+"\n"), 0644); err != nil { + logrus.WithError(err).Warn("could not write host agent port file") + return err + } + + args = append(args, + "--port", fmt.Sprintf("%d", port)) + } else { + args = append(args, + "--socket", haSockPath) + } if nerdctlArchiveCache != "" { args = append(args, "--nerdctl-archive", nerdctlArchiveCache) } @@ -170,6 +189,28 @@ func Start(ctx context.Context, inst *store.Instance) error { } } +func findFreeTCPLocalPort() (int, error) { + lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0") + if err != nil { + return 0, err + } + l, err := net.ListenTCP("tcp4", lAddr0) + if err != nil { + return 0, err + } + defer l.Close() + lAddr := l.Addr() + lTCPAddr, ok := lAddr.(*net.TCPAddr) + if !ok { + return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr) + } + port := lTCPAddr.Port + if port <= 0 { + return 0, fmt.Errorf("unexpected port %d", port) + } + return port, nil +} + func waitHostAgentStart(ctx context.Context, haPIDPath, haStderrPath string) error { begin := time.Now() deadlineDuration := 5 * time.Second diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index fc3be1dec2f..58f201a9a44 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -38,8 +38,10 @@ const ( SerialSock = "serial.sock" SSHSock = "ssh.sock" GuestAgentSock = "ga.sock" + GuestAgentPort = "ga.port" HostAgentPID = "ha.pid" HostAgentSock = "ha.sock" + HostAgentPort = "ha.port" HostAgentStdoutLog = "ha.stdout.log" HostAgentStderrLog = "ha.stderr.log" diff --git a/pkg/store/instance.go b/pkg/store/instance.go index 7173df8393d..e4bac7fd97d 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -18,6 +18,7 @@ import ( "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/store/dirnames" "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" ) type Status = string @@ -99,7 +100,14 @@ func Inspect(instName string) (*Instance, error) { } if inst.HostAgentPID != 0 { - haSock := filepath.Join(instDir, filenames.HostAgentSock) + haPort, err := ReadPortFile(filepath.Join(instDir, filenames.HostAgentPort)) + var haSock string + if err != nil { + haSock = filepath.Join(instDir, filenames.HostAgentSock) + } else { + haSock = fmt.Sprintf("%d", haPort) + } + logrus.Debugf("Connecting to %s", haSock) haClient, err := hostagentclient.NewHostAgentClient(haSock) if err != nil { inst.Status = StatusBroken @@ -193,6 +201,23 @@ func ReadPIDFile(path string) (int, error) { return pid, nil } +// ReadPortFile returns 0 if the port file does not exist +// (in which case a random port will be used)ยท +func ReadPortFile(path string) (int, error) { + b, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return 0, nil + } + return 0, err + } + port, err := strconv.Atoi(strings.TrimSpace(string(b))) + if err != nil { + return 0, err + } + return port, nil +} + type FormatData struct { Instance HostOS string