drop old root command, fix flags, etc
This commit is contained in:
parent
5e3a83e585
commit
fbd550f48d
5 changed files with 78 additions and 290 deletions
57
common.go
57
common.go
|
@ -3,10 +3,14 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"libvirt.org/go/libvirt"
|
||||
"libvirt.org/go/libvirtxml"
|
||||
|
||||
|
@ -15,6 +19,17 @@ import (
|
|||
"mkvm/volumes/pools"
|
||||
)
|
||||
|
||||
var (
|
||||
argMemoryMB int
|
||||
argCPUs int
|
||||
argDiskSizeGB int
|
||||
|
||||
// cloudinit args
|
||||
argSSHKeys []string
|
||||
argSSHKeyURLs []string
|
||||
argPackages []string
|
||||
)
|
||||
|
||||
type domainModifier func(*libvirtxml.Domain) error
|
||||
|
||||
func getLibvirtAndPool() (*libvirt.Connect, pools.StoragePool, error) {
|
||||
|
@ -179,3 +194,45 @@ func setSMBIOS(smbios map[int]map[string]string) domainModifier {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
|
|
16
debian.go
16
debian.go
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"entanglement.garden/common/cloudinit"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -28,6 +29,12 @@ var debianCmd = cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
name := args[0]
|
||||
|
||||
for _, u := range argSSHKeyURLs {
|
||||
if err := downloadSSHKeys(u); err != nil {
|
||||
logrus.WithError(err).WithField("url", u).Fatal("error downloading SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
conn, pool, err := getLibvirtAndPool()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("error connecting to libvirt")
|
||||
|
@ -54,7 +61,11 @@ var debianCmd = cobra.Command{
|
|||
|
||||
serverURL := fmt.Sprintf("http://%s", bind)
|
||||
|
||||
err = buildCloudConfig(name, fmt.Sprintf("%s/phone-home", serverURL))
|
||||
cloudconfig := cloudinit.UserData{
|
||||
Packages: argPackages,
|
||||
SSHAuthorizedKeys: argSSHKeys,
|
||||
}
|
||||
err = buildCloudConfig(cloudconfig, name, fmt.Sprintf("%s/phone-home", serverURL))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("error building cloud config")
|
||||
}
|
||||
|
@ -79,6 +90,9 @@ var debianCmd = cobra.Command{
|
|||
func init() {
|
||||
debianCmd.Flags().StringVarP(&debianRelease, "release", "r", "bookworm", "debian release to install. Options: bookworm (default), trixie, sid")
|
||||
debianCmd.Flags().StringArrayVarP(&debianPackages, "packages", "p", nil, "apt packages to install")
|
||||
debianCmd.Flags().StringArrayVar(&argSSHKeys, "ssh-keys", nil, "SSH key(s) authorzed to access the VM")
|
||||
debianCmd.Flags().StringArrayVarP(&argSSHKeyURLs, "ssh-key-urls", "s", nil, "URL(s) to SSH key(s) authorzed to access the VM. Expected in authorized_keys format.")
|
||||
registerGlobalFlags(debianCmd)
|
||||
|
||||
rootCmd.AddCommand(&debianCmd)
|
||||
}
|
||||
|
|
39
http.go
39
http.go
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
|
@ -61,19 +60,11 @@ func (httphandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
}
|
||||
|
||||
func buildCloudConfig(name string, phoneHomeURL string) error {
|
||||
userdata, err := yaml.Marshal(cloudinit.UserData{
|
||||
Packages: argPackages,
|
||||
SSHAuthorizedKeys: argSSHKeys,
|
||||
PhoneHome: cloudinit.PhoneHome{
|
||||
URL: phoneHomeURL,
|
||||
Post: []string{"pub_key_dsa", "pub_key_rsa", "pub_key_ed25519", "fqdn"},
|
||||
},
|
||||
})
|
||||
func buildCloudConfig(base cloudinit.UserData, name string, phoneHomeURL string) error {
|
||||
userdata, err := yaml.Marshal(base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpStaticContent["/user-data"] = append([]byte("#cloud-config\n"), userdata...)
|
||||
|
||||
metadata, err := yaml.Marshal(cloudinit.MetaData{
|
||||
|
@ -85,36 +76,16 @@ func buildCloudConfig(name string, phoneHomeURL string) error {
|
|||
}
|
||||
httpStaticContent["/meta-data"] = metadata
|
||||
|
||||
httpStaticContent["/vendor-data"] = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCloudConfigPrefix(prefix string, name string, phoneHomeURL string) error {
|
||||
userdata, err := yaml.Marshal(cloudinit.UserData{
|
||||
Packages: argPackages,
|
||||
SSHAuthorizedKeys: argSSHKeys,
|
||||
vendorData, err := yaml.Marshal(cloudinit.UserData{
|
||||
PhoneHome: cloudinit.PhoneHome{
|
||||
URL: phoneHomeURL,
|
||||
Post: []string{"pub_key_dsa"},
|
||||
Post: []string{"pub_key_dsa", "pub_key_rsa", "pub_key_ed25519", "fqdn"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpStaticContent[fmt.Sprintf("/%s/user-data", prefix)] = append([]byte("#cloud-config\n"), userdata...)
|
||||
|
||||
metadata, err := yaml.Marshal(cloudinit.MetaData{
|
||||
InstanceID: name,
|
||||
LocalHostname: name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpStaticContent[fmt.Sprintf("/%s/meta-data", prefix)] = metadata
|
||||
|
||||
httpStaticContent[fmt.Sprintf("/%s/vendor-data", prefix)] = nil
|
||||
httpStaticContent["/vendor-data"] = vendorData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
147
main.go
147
main.go
|
@ -1,12 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -14,14 +8,13 @@ import (
|
|||
"libvirt.org/go/libvirtxml"
|
||||
|
||||
"mkvm/config"
|
||||
"mkvm/libvirtx"
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
nicSource = libvirtxml.DomainInterfaceSource{}
|
||||
rootCmd = cobra.Command{
|
||||
Use: "mkvm name [name [name]]",
|
||||
Use: "mkvm",
|
||||
Short: "create virtual machine(s) via libvirt",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
|
@ -31,149 +24,11 @@ var (
|
|||
logrus.WithError(err).Fatal("error loading config")
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, u := range argSSHKeyURLs {
|
||||
if err := downloadSSHKeys(u); err != nil {
|
||||
logrus.WithError(err).WithField("url", u).Fatal("error downloading SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := libvirtx.New()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("error connecting to libvirt")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverInterfaceName := ""
|
||||
if config.C.Network != "" {
|
||||
nicSource.Network = &libvirtxml.DomainInterfaceSourceNetwork{Network: config.C.Network}
|
||||
libvirtnet, err := conn.LookupNetworkByName(config.C.Network)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("network", config.C.Network).Fatal("error finding libvirt network")
|
||||
}
|
||||
|
||||
xmlstr, err := libvirtnet.GetXMLDesc(0)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("network", config.C.Network).Fatal("error getting network xml description")
|
||||
}
|
||||
|
||||
var net libvirtxml.Network
|
||||
if err := net.Unmarshal(xmlstr); err != nil {
|
||||
logrus.WithError(err).WithField("network", config.C.Network).Fatal("error parsing network xml description")
|
||||
}
|
||||
|
||||
serverInterfaceName = net.Bridge.Name
|
||||
} else if config.C.Bridge != "" {
|
||||
nicSource.Bridge = &libvirtxml.DomainInterfaceSourceBridge{Bridge: config.C.Bridge}
|
||||
serverInterfaceName = config.C.Bridge
|
||||
} else {
|
||||
logrus.Fatal("no network or bridge configured")
|
||||
}
|
||||
|
||||
serverInterface, err := net.InterfaceByName(serverInterfaceName)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("error finding local network interface to run server on")
|
||||
}
|
||||
|
||||
serverInterfaceAddrs, err := serverInterface.Addrs()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("error finding local network interface's IP")
|
||||
}
|
||||
|
||||
if len(serverInterfaceAddrs) == 0 {
|
||||
logrus.WithField("interface", serverInterfaceName).Fatal("bridge interface does not have an IP on this machine")
|
||||
}
|
||||
|
||||
serverBindIP, _, err := net.ParseCIDR(serverInterfaceAddrs[0].String())
|
||||
if err != nil {
|
||||
logrus.WithField("interface", serverInterfaceName).WithField("address", serverInterfaceAddrs[0].String()).WithError(err).Fatal("error parsing local address")
|
||||
}
|
||||
|
||||
port := rand.Intn(65535-1025) + 1025
|
||||
serverBind := fmt.Sprintf("%s:%d", serverBindIP, port)
|
||||
|
||||
go runHTTPServer(serverBind)
|
||||
|
||||
for i, name := range args {
|
||||
metadataURL := fmt.Sprintf("http://%s/%d", serverBind, i)
|
||||
if err := buildCloudConfigPrefix(fmt.Sprint(i), name, metadataURL); err != nil {
|
||||
logrus.WithError(err).WithField("vm", name).Error("error building cloudconfig for vm")
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
if err := mkvm(conn, metadataURL, name); err != nil {
|
||||
logrus.WithError(err).Error("unexpected error building VM")
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("waiting for VM(s) to finish provisioning")
|
||||
wg.Wait()
|
||||
},
|
||||
}
|
||||
|
||||
argMemoryMB int
|
||||
argCPUs int
|
||||
argImage string
|
||||
argDiskSizeGB int
|
||||
|
||||
// cloudinit args
|
||||
argSSHKeys []string
|
||||
argSSHKeyURLs []string
|
||||
argPackages []string
|
||||
)
|
||||
|
||||
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 main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().IntVarP(&argMemoryMB, "memory", "m", 1024, "amount of memory (in MB) to assign to the VM")
|
||||
rootCmd.Flags().IntVarP(&argCPUs, "cpu", "c", 2, "the number of vCPU cores to assign to the VM")
|
||||
rootCmd.Flags().StringVarP(&argImage, "image", "", "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2", "URL of the image to download")
|
||||
rootCmd.Flags().IntVarP(&argDiskSizeGB, "disk", "d", 25, "disk size (in GB)")
|
||||
|
||||
rootCmd.Flags().StringArrayVar(&argSSHKeys, "ssh-keys", nil, "SSH key(s) authorzed to access the VM")
|
||||
rootCmd.Flags().StringArrayVarP(&argSSHKeyURLs, "ssh-key-urls", "s", nil, "URL(s) to SSH key(s) authorzed to access the VM. Expected in authorized_keys format.")
|
||||
rootCmd.Flags().StringArrayVarP(&argPackages, "packages", "p", nil, "packages to install on the VM")
|
||||
}
|
||||
|
|
109
mkvm.go
109
mkvm.go
|
@ -1,109 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"libvirt.org/go/libvirt"
|
||||
"libvirt.org/go/libvirtxml"
|
||||
|
||||
"mkvm/config"
|
||||
"mkvm/volumes"
|
||||
"mkvm/volumes/pools"
|
||||
)
|
||||
|
||||
func mkvm(conn *libvirt.Connect, metadataURL string, name string) error {
|
||||
logger := logrus.WithField("vm", name)
|
||||
logger.Debug("creating vm")
|
||||
|
||||
pool, err := pools.GetPool(conn, config.C.StoragePool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = volumes.Create(conn, pool, argDiskSizeGB, argImage, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := []libvirtxml.DomainDisk{pool.GetDomainDiskXML(name)}
|
||||
|
||||
smbios := map[int]map[string]string{
|
||||
1: {"serial": fmt.Sprintf("ds=nocloud-net;s=%s/", metadataURL)},
|
||||
}
|
||||
|
||||
qemuArgs := []libvirtxml.DomainQEMUCommandlineArg{}
|
||||
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)
|
||||
}
|
||||
|
||||
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: disks,
|
||||
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,
|
||||
},
|
||||
QEMUCommandline: &libvirtxml.DomainQEMUCommandline{Args: qemuArgs},
|
||||
}
|
||||
|
||||
domainXMLString, err := domainXML.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("defining domain from xml")
|
||||
domain, err := conn.DomainDefineXML(domainXMLString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error defining domain from xml description: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("booting domain")
|
||||
err = domain.Create()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating domain: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("domain booted")
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue