diff --git a/README.md b/README.md index 9a5f74db679..916968a3cb3 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,20 @@ Use `:` to specify a source or target inside an instance. `limactl disk list`: list all existing disks +#### `limactl show-ssh` +- `limactl show-ssh --format=cmd ` (default): Full `ssh` command line +- `limactl show-ssh --format=args `: Similar to the `cmd` format but omits `ssh` and the destination address +- `limactl show-ssh --format=options `: ssh option key value pairs +- `limactl show-ssh --format=config `: `~/.ssh/config` format + +The config file is also automatically created inside the instance directory: +```console +$ limactl ls --format='{{.SSHConfigFile}}' default +/Users/example/.lima/default/ssh.config + +$ ssh -F /Users/example/.lima/default/ssh.config lima-default +``` + #### `limactl completion` - To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`. diff --git a/cmd/limactl/show_ssh.go b/cmd/limactl/show_ssh.go index fd05fcb94c9..5295508f632 100644 --- a/cmd/limactl/show_ssh.go +++ b/cmd/limactl/show_ssh.go @@ -34,6 +34,10 @@ const showSSHExample = ` User example Hostname 127.0.0.1 Port 60022 + + To show the config file path: + $ limactl ls --format='{{.SSHConfigFile}}' default + /Users/example/.lima/default/ssh.config ` func newShowSSHCommand() *cobra.Command { diff --git a/docs/internal.md b/docs/internal.md index 64dbe264aeb..6d735e5b5a5 100644 --- a/docs/internal.md +++ b/docs/internal.md @@ -54,6 +54,7 @@ VZ: SSH: - `ssh.sock`: SSH control master socket +- `ssh.config`: SSH config file for `ssh -F`. Not consumed by Lima itself. Guest agent: - `ga.sock`: Forwarded to `/run/lima-guestagent.sock` in the guest, via SSH diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index bbcb120348c..3db6c141539 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -1,6 +1,7 @@ package hostagent import ( + "bytes" "context" "encoding/json" "errors" @@ -111,6 +112,9 @@ 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 { + return nil, err + } sshConfig := &ssh.SSHConfig{ AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts), } @@ -150,6 +154,28 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt return a, nil } +func writeSSHConfigFile(inst *store.Instance, sshLocalPort int, sshOpts []string) error { + if inst.Dir == "" { + return fmt.Errorf("directory is unknown for the instance %q", inst.Name) + } + var b bytes.Buffer + if _, err := fmt.Fprintf(&b, `# This SSH config file can be passed to 'ssh -F'. +# This file is created by Lima, but not used by Lima itself currently. +# Modifications to this file will be lost on restarting the Lima instance. +`); err != nil { + return err + } + if err := sshutil.Format(&b, inst.Name, sshutil.FormatConfig, + append(sshOpts, + "Hostname=127.0.0.1", + fmt.Sprintf("Port=%d", sshLocalPort), + )); err != nil { + return err + } + fileName := filepath.Join(inst.Dir, filenames.SSHConfig) + return os.WriteFile(fileName, b.Bytes(), 0600) +} + func determineSSHLocalPort(y *limayaml.LimaYAML, instName string) (int, error) { if *y.SSH.LocalPort > 0 { return *y.SSH.LocalPort, nil diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index 18c2bcee882..9e72b7cac9d 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -37,6 +37,7 @@ const ( SerialLog = "serial.log" SerialSock = "serial.sock" SSHSock = "ssh.sock" + SSHConfig = "ssh.config" GuestAgentSock = "ga.sock" HostAgentPID = "ha.pid" HostAgentSock = "ha.sock" diff --git a/pkg/store/instance.go b/pkg/store/instance.go index ced2645f434..fbeb61c821b 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -47,6 +47,7 @@ type Instance struct { AdditionalDisks []limayaml.Disk `json:"additionalDisks,omitempty"` Networks []limayaml.Network `json:"network,omitempty"` SSHLocalPort int `json:"sshLocalPort,omitempty"` + SSHConfigFile string `json:"sshConfigFile,omitempty"` HostAgentPID int `json:"hostAgentPID,omitempty"` DriverPID int `json:"driverPID,omitempty"` Errors []error `json:"errors,omitempty"` @@ -101,6 +102,7 @@ func Inspect(instName string) (*Instance, error) { inst.AdditionalDisks = y.AdditionalDisks inst.Networks = y.Networks 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 {