Skip to content

Commit

Permalink
cmd: add vm list command
Browse files Browse the repository at this point in the history
Get a list of virter VMs based on available metadata. If virter metadata
is found, also show the ID and access network based on the defined VM XML.

When getting a list of VMs for shell completion, only include VMs that
were created by virter.
  • Loading branch information
WanzenBug authored and chrboe committed Aug 27, 2024
1 parent ff883d1 commit d9f1b81
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 7 deletions.
21 changes: 15 additions & 6 deletions cmd/vm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"slices"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/crypto/ssh"
Expand All @@ -19,6 +21,7 @@ func vmCommand() *cobra.Command {

vmCmd.AddCommand(vmCommitCommand())
vmCmd.AddCommand(vmExecCommand())
vmCmd.AddCommand(vmListCommand())
vmCmd.AddCommand(vmExistsCommand())
vmCmd.AddCommand(vmHostKeyCommand())
vmCmd.AddCommand(vmRmCommand())
Expand Down Expand Up @@ -59,18 +62,24 @@ func suggestVmNames(cmd *cobra.Command, args []string, toComplete string) ([]str
}
defer v.ForceDisconnect()

vms, err := v.ListVM()
vms, err := v.VMList()
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

filtered := make([]string, 0, len(vms))
outer:
for _, vm := range vms {
for _, arg := range args {
if arg == vm {
continue outer
}
if slices.Contains(args, vm) {
// already mentioned in previous argument
continue
}
info, err := v.VMInfo(vm)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
if info.ID == 0 {
// not a VM created by virter
continue
}

filtered = append(filtered, vm)
Expand Down
56 changes: 56 additions & 0 deletions cmd/vm_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cmd

import (
"strconv"

"github.com/rodaine/table"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/LINBIT/virter/internal/virter"
)

func vmListCommand() *cobra.Command {
existsCmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List all VMs",
Long: `List all VMs along with their ID and access network if they were created by Virter`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
v, err := InitVirter()
if err != nil {
log.Fatal(err)
}
defer v.ForceDisconnect()

vms, err := v.VMList()
if err != nil {
log.Fatal(err)
}

vmInfos := make([]*virter.VMInfo, 0, len(vms))

for _, vm := range vms {
vmInfo, err := v.VMInfo(vm)
if err != nil {
log.Fatal(err)
}

vmInfos = append(vmInfos, vmInfo)
}

t := table.New("Name", "ID", "Access Network")
for _, val := range vmInfos {
id := ""
if val.ID != 0 {
id = strconv.Itoa(int(val.ID))
}
t.AddRow(val.Name, id, val.AccessNetwork)
}
t.Print()
},
ValidArgsFunction: suggestVmNames,
}
return existsCmd
}
10 changes: 10 additions & 0 deletions internal/virter/dhcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ func AddToMAC(mac net.HardwareAddr, addend uint) (net.HardwareAddr, error) {
return out, nil
}

func IDFromMAC(mac, base net.HardwareAddr) uint {
var macValue, baseValue big.Int
macValue.SetBytes(mac)
baseValue.SetBytes(base)

macValue.Sub(&macValue, &baseValue)

return uint(macValue.Int64())
}

func (v *Virter) getIPNet(network libvirt.Network) (*net.IPNet, error) {
networkDescription, err := getNetworkDescription(v.libvirt, network)
if err != nil {
Expand Down
60 changes: 59 additions & 1 deletion internal/virter/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package virter
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"net"
"path/filepath"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/LINBIT/containerapi"
sshclient "github.com/LINBIT/gosshclient"
libvirt "github.com/digitalocean/go-libvirt"
lx "github.com/libvirt/libvirt-go-xml"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -66,7 +68,7 @@ func (v *Virter) anyImageExists(vmConfig VMConfig) (bool, error) {
return false, nil
}

func (v *Virter) ListVM() ([]string, error) {
func (v *Virter) VMList() ([]string, error) {
domains, _, err := v.libvirt.ConnectListAllDomains(-1, 0)
if err != nil {
return nil, fmt.Errorf("failed to list domains")
Expand All @@ -80,6 +82,62 @@ func (v *Virter) ListVM() ([]string, error) {
return result, nil
}

type VMInfo struct {
Name string
ID uint
AccessNetwork string
}

// VMInfo returns the ID and access network of the named VM
func (v *Virter) VMInfo(vmName string) (*VMInfo, error) {
dom, err := v.libvirt.DomainLookupByName(vmName)
if err != nil {
return nil, fmt.Errorf("failed to lookup domain: %w", err)
}

xmldesc, err := v.libvirt.DomainGetXMLDesc(dom, 0)
if err != nil {
return nil, fmt.Errorf("failed to get XML description: %w", err)
}

desc := lx.Domain{}
err = xml.Unmarshal([]byte(xmldesc), &desc)
if err != nil {
return nil, fmt.Errorf("could not decode domain xml '%s': %w", vmName, err)
}

meta := metaWrapper{}
err = xml.Unmarshal([]byte(desc.Metadata.XML), &meta)
if err != nil {
// not a virter VM
return &VMInfo{Name: vmName}, nil
}

if len(desc.Devices.Interfaces) < 1 {
return nil, fmt.Errorf("no interfaces found for VM '%s'", vmName)
}

if desc.Devices.Interfaces[0].Source == nil || desc.Devices.Interfaces[0].Source.Network == nil || desc.Devices.Interfaces[0].Source.Network.Network == "" {
return nil, fmt.Errorf("no network found for VM '%s'", vmName)
}

network, err := v.NetworkGet(desc.Devices.Interfaces[0].Source.Network.Network)
if err != nil {
return nil, fmt.Errorf("failed to get network '%s' for VM '%s': %w", desc.Devices.Interfaces[0].Source.Network.Network, vmName, err)
}

if desc.Devices.Interfaces[0].MAC == nil {
return nil, fmt.Errorf("no MAC address found for VM '%s'", vmName)
}

vmMac, err := net.ParseMAC(desc.Devices.Interfaces[0].MAC.Address)
if err != nil {
return nil, fmt.Errorf("failed to parse VM MAC address '%s' for VM '%s': %w", desc.Devices.Interfaces[0].MAC.Address, vmName, err)
}

return &VMInfo{Name: vmName, AccessNetwork: network.Name, ID: IDFromMAC(vmMac, QemuBaseMAC())}, nil
}

// VMRun starts a VM.
func (v *Virter) VMRun(vmConfig VMConfig) error {
// checks
Expand Down

0 comments on commit d9f1b81

Please sign in to comment.