2024-12-09 03:17:56 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-12-09 06:59:06 +00:00
|
|
|
"io"
|
2024-12-09 03:17:56 +00:00
|
|
|
"math/rand"
|
|
|
|
"net"
|
2024-12-09 06:59:06 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2024-12-09 03:17:56 +00:00
|
|
|
|
|
|
|
"github.com/sirupsen/logrus"
|
2024-12-09 06:59:06 +00:00
|
|
|
"github.com/spf13/cobra"
|
2024-12-09 03:17:56 +00:00
|
|
|
"libvirt.org/go/libvirt"
|
|
|
|
"libvirt.org/go/libvirtxml"
|
|
|
|
|
|
|
|
"mkvm/config"
|
|
|
|
"mkvm/libvirtx"
|
|
|
|
"mkvm/volumes/pools"
|
|
|
|
)
|
|
|
|
|
2024-12-09 06:59:06 +00:00
|
|
|
var (
|
|
|
|
argMemoryMB int
|
|
|
|
argCPUs int
|
|
|
|
argDiskSizeGB int
|
|
|
|
|
|
|
|
// cloudinit args
|
|
|
|
argSSHKeys []string
|
|
|
|
argSSHKeyURLs []string
|
|
|
|
argPackages []string
|
|
|
|
)
|
|
|
|
|
2024-12-09 03:17:56 +00:00
|
|
|
type domainModifier func(*libvirtxml.Domain) error
|
|
|
|
|
|
|
|
func getLibvirtAndPool() (*libvirt.Connect, pools.StoragePool, error) {
|
|
|
|
conn, err := libvirtx.New()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
pool, err := pools.GetPool(conn, config.C.StoragePool)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return conn, pool, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getServerListenAddress(conn *libvirt.Connect) (string, error) {
|
|
|
|
serverInterfaceName := ""
|
|
|
|
if config.C.Network != "" {
|
|
|
|
nicSource.Network = &libvirtxml.DomainInterfaceSourceNetwork{Network: config.C.Network}
|
|
|
|
libvirtnet, err := conn.LookupNetworkByName(config.C.Network)
|
|
|
|
if err != nil {
|
|
|
|
logrus.WithField("network", config.C.Network).Error("error finding libvirt network")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
xmlstr, err := libvirtnet.GetXMLDesc(0)
|
|
|
|
if err != nil {
|
|
|
|
logrus.WithField("network", config.C.Network).Error("error getting network xml description")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
var net libvirtxml.Network
|
|
|
|
if err := net.Unmarshal(xmlstr); err != nil {
|
|
|
|
logrus.WithField("network", config.C.Network).Error("error parsing network xml description")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
serverInterfaceName = net.Bridge.Name
|
|
|
|
} else if config.C.Bridge != "" {
|
|
|
|
nicSource.Bridge = &libvirtxml.DomainInterfaceSourceBridge{Bridge: config.C.Bridge}
|
|
|
|
serverInterfaceName = config.C.Bridge
|
|
|
|
} else {
|
|
|
|
return "", errors.New("no network or bridge configured")
|
|
|
|
}
|
|
|
|
|
|
|
|
serverInterface, err := net.InterfaceByName(serverInterfaceName)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Error("error finding local network interface to run server on")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
serverInterfaceAddrs, err := serverInterface.Addrs()
|
|
|
|
if err != nil {
|
|
|
|
logrus.Error("error finding local network interface's IP")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(serverInterfaceAddrs) == 0 {
|
|
|
|
return "", fmt.Errorf("bridge interface %s does not have an IP on this machine", serverInterfaceName)
|
|
|
|
}
|
|
|
|
|
|
|
|
serverBindIP, _, err := net.ParseCIDR(serverInterfaceAddrs[0].String())
|
|
|
|
if err != nil {
|
|
|
|
logrus.WithField("interface", serverInterfaceName).WithField("address", serverInterfaceAddrs[0].String()).Error("error parsing local address")
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
port := rand.Intn(65535-1025) + 1025
|
|
|
|
return fmt.Sprintf("%s:%d", serverBindIP, port), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func createDomain(conn *libvirt.Connect, pool pools.StoragePool, name string, modifiers ...domainModifier) error {
|
|
|
|
interfaces := []libvirtxml.DomainInterface{
|
|
|
|
{
|
|
|
|
Model: &libvirtxml.DomainInterfaceModel{Type: "virtio"},
|
|
|
|
Source: &nicSource,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the domain
|
|
|
|
domainXML := &libvirtxml.Domain{
|
|
|
|
Type: "kvm",
|
|
|
|
Name: name,
|
|
|
|
Memory: &libvirtxml.DomainMemory{Value: uint(argMemoryMB), Unit: "MiB"},
|
|
|
|
VCPU: &libvirtxml.DomainVCPU{Value: uint(argCPUs)},
|
|
|
|
OS: &libvirtxml.DomainOS{
|
|
|
|
Type: &libvirtxml.DomainOSType{Arch: "x86_64", Type: "hvm"},
|
|
|
|
BootDevices: []libvirtxml.DomainBootDevice{{Dev: "hd"}},
|
|
|
|
},
|
|
|
|
Features: &libvirtxml.DomainFeatureList{
|
|
|
|
ACPI: &libvirtxml.DomainFeature{},
|
|
|
|
APIC: &libvirtxml.DomainFeatureAPIC{},
|
|
|
|
VMPort: &libvirtxml.DomainFeatureState{State: "off"},
|
|
|
|
},
|
|
|
|
CPU: &libvirtxml.DomainCPU{Mode: "host-model"},
|
|
|
|
Devices: &libvirtxml.DomainDeviceList{
|
|
|
|
Emulator: "/usr/bin/kvm",
|
|
|
|
Disks: []libvirtxml.DomainDisk{pool.GetDomainDiskXML(name)},
|
|
|
|
Channels: []libvirtxml.DomainChannel{
|
|
|
|
{
|
|
|
|
Source: &libvirtxml.DomainChardevSource{
|
|
|
|
UNIX: &libvirtxml.DomainChardevSourceUNIX{Path: "/var/lib/libvirt/qemu/f16x86_64.agent", Mode: "bind"},
|
|
|
|
},
|
|
|
|
Target: &libvirtxml.DomainChannelTarget{
|
|
|
|
VirtIO: &libvirtxml.DomainChannelTargetVirtIO{Name: "org.qemu.guest_agent.0"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Consoles: []libvirtxml.DomainConsole{{Target: &libvirtxml.DomainConsoleTarget{}}},
|
|
|
|
Serials: []libvirtxml.DomainSerial{{}},
|
|
|
|
Interfaces: interfaces,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, modifier := range modifiers {
|
|
|
|
if err := modifier(domainXML); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
domainXMLString, err := domainXML.Marshal()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Debug("defining domain from xml")
|
|
|
|
domain, err := conn.DomainDefineXML(domainXMLString)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error defining domain from xml description: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Debug("booting domain")
|
|
|
|
err = domain.Create()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error creating domain: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func setSMBIOS(smbios map[int]map[string]string) domainModifier {
|
|
|
|
return func(d *libvirtxml.Domain) error {
|
|
|
|
qemuArgs := []libvirtxml.DomainQEMUCommandlineArg{}
|
|
|
|
if d.QEMUCommandline != nil {
|
|
|
|
qemuArgs = d.QEMUCommandline.Args
|
|
|
|
}
|
|
|
|
|
|
|
|
for smbiosType, values := range smbios {
|
|
|
|
arg := libvirtxml.DomainQEMUCommandlineArg{
|
|
|
|
Value: fmt.Sprintf("type=%d", smbiosType),
|
|
|
|
}
|
|
|
|
for key, value := range values {
|
|
|
|
arg.Value = fmt.Sprintf("%s,%s=%s", arg.Value, key, value)
|
|
|
|
}
|
|
|
|
qemuArgs = append(qemuArgs, libvirtxml.DomainQEMUCommandlineArg{Value: "-smbios"}, arg)
|
|
|
|
}
|
|
|
|
|
|
|
|
d.QEMUCommandline = &libvirtxml.DomainQEMUCommandline{Args: qemuArgs}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2024-12-09 06:59:06 +00:00
|
|
|
|
|
|
|
func downloadSSHKeys(url string) error {
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"url": url,
|
|
|
|
"status": resp.Status,
|
|
|
|
"body": string(body),
|
|
|
|
}).Error("non-200 response from SSH key URL")
|
|
|
|
return fmt.Errorf("non-200 response from SSH key URL: %s", resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
count := 0
|
|
|
|
for _, key := range strings.Split(string(body), "\n") {
|
|
|
|
key = strings.TrimSpace(key)
|
|
|
|
if key == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
argSSHKeys = append(argSSHKeys, key)
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.WithField("url", url).WithField("keys", count).Debug("downloaded SSH authorized keys")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func registerGlobalFlags(cmd cobra.Command) {
|
|
|
|
cmd.Flags().IntVarP(&argMemoryMB, "memory", "m", 1024, "amount of memory (in MB) to assign to the VM")
|
|
|
|
cmd.Flags().IntVarP(&argCPUs, "cpu", "c", 2, "the number of vCPU cores to assign to the VM")
|
|
|
|
cmd.Flags().IntVarP(&argDiskSizeGB, "disk", "d", 25, "disk size (in GB)")
|
|
|
|
}
|