Skip to content

Commit

Permalink
vm: add tools and docs for convenient ssh integration
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelColledge committed Sep 21, 2022
1 parent f09e2cc commit 1ddabd0
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ By default, Virter uses the libvirt network named `default`.

Check out [`doc/networks.md`](./doc/networks.md) for more details on VM networking.

### Connecting with `ssh`

SSH can be configured for convenient access to virtual machines created by Virter.
See [`doc/ssh.md`](./doc/ssh.md) for details.

### DHCP Leases

Libvirt produces some weird behavior when MAC or IP addresses are reused while
Expand Down
2 changes: 2 additions & 0 deletions cmd/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func vmCommand() *cobra.Command {

vmCmd.AddCommand(vmCommitCommand())
vmCmd.AddCommand(vmExecCommand())
vmCmd.AddCommand(vmExistsCommand())
vmCmd.AddCommand(vmHostKeyCommand())
vmCmd.AddCommand(vmRmCommand())
vmCmd.AddCommand(vmRunCommand())
vmCmd.AddCommand(vmSSHCommand())
Expand Down
30 changes: 30 additions & 0 deletions cmd/vm_exists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"os"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func vmExistsCommand() *cobra.Command {
existsCmd := &cobra.Command{
Use: "exists vm_name",
Short: "Check whether a VM exists",
Long: `Check whether a VM exists and was created by Virter.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
v, err := InitVirter()
if err != nil {
log.Fatal(err)
}
defer v.ForceDisconnect()

if err := v.VMExists(args[0]); err != nil {
os.Exit(1)
}
},
ValidArgsFunction: suggestVmNames,
}
return existsCmd
}
33 changes: 33 additions & 0 deletions cmd/vm_hostkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cmd

import (
"fmt"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func vmHostKeyCommand() *cobra.Command {
hostKeyCmd := &cobra.Command{
Use: "host-key vm_name",
Short: "Get the host key for a VM",
Long: `Get the host key for a VM in the format of an OpenSSH known_hosts file.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
v, err := InitVirter()
if err != nil {
log.Fatal(err)
}
defer v.ForceDisconnect()

knownHostsText, err := v.VMGetKnownHosts(args[0])
if err != nil {
log.Fatal(err)
}

fmt.Print(knownHostsText)
},
ValidArgsFunction: suggestVmNames,
}
return hostKeyCmd
}
58 changes: 58 additions & 0 deletions doc/ssh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Virter SSH Integration

The simplest way to connect to a virtual machine that was started with Virter
is to use `virter vm ssh ...`. However, you may also choose to use `ssh` and
related tools to connect. This can be made very convenient by adding the
following to your `~/.ssh/config`:

```
Match exec "virter vm exists %h"
User root
IdentityAgent none
IdentityFile ~/.config/virter/id_rsa
KnownHostsCommand /usr/bin/env virter vm host-key %n
```

Now you can easily connect:

```
$ ssh foo
[root@foo ~]#
```

## Name resolution

Depending on your configuration, `ssh` may or may not be able to resolve the VM
name to a hostname. If not, you will see an error similar to:

```
$ ssh foo
ssh: Could not resolve hostname foo: Name or service not known
```

Fix this by installing and configuring the [libvirt NSS
modules](https://libvirt.org/nss.html). In particular, you will need to install
a package such as `libvirt-nss` or `libnss-libvirt`. Then add `libvirt_guest`
to the `hosts:` configuration in the file `/etc/nsswitch.conf`.

## SSH integration with qualified names

If you have configured a network domain in your libvirt network, you can also
connect to the VM using the fully qualified domain name (FQDN). For instance,
with the domain name `test`, you can use this configuration in your
`~/.ssh/config`:

```
Host *.test
User root
IdentityAgent none
IdentityFile ~/.config/virter/id_rsa
KnownHostsCommand /bin/bash -c 'virter vm host-key "$(basename "%n" .test)"'
```

Now you can easily connect:

```
$ ssh foo.test
[root@foo ~]#
```
22 changes: 22 additions & 0 deletions internal/virter/vm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package virter

import (
"bytes"
"context"
"fmt"
"net"
Expand All @@ -20,6 +21,14 @@ import (
"github.com/LINBIT/virter/pkg/sshkeys"
)

func (v *Virter) VMExists(vmName string) error {
if _, err := v.getMetaForVM(vmName); err != nil {
return fmt.Errorf("failed to get VM metadata: %w", err)
}

return nil
}

func (v *Virter) anyImageExists(vmConfig VMConfig) (bool, error) {
vmName := vmConfig.Name
imgs := []string{
Expand Down Expand Up @@ -502,6 +511,19 @@ func (v *Virter) getKnownHostsFor(vmNames ...string) (sshkeys.KnownHosts, error)
return knownHosts, nil
}

func (v *Virter) VMGetKnownHosts(vmName string) (string, error) {
knownHosts, err := v.getKnownHostsFor(vmName)
if err != nil {
return "", fmt.Errorf("failed to fetch host keys: %w", err)
}

buf := new(bytes.Buffer)
if err := knownHosts.AsKnownHostsFile(buf); err != nil {
return "", fmt.Errorf("failed to write known hosts file: %w", err)
}
return buf.String(), nil
}

func (v *Virter) getSSHUserName(vmName string) string {
meta, err := v.getMetaForVM(vmName)

Expand Down

0 comments on commit 1ddabd0

Please sign in to comment.