commit fd85215d5744998a53c6e11a7ce2a866badcd22c Author: Jeff Carr Date: Sat Oct 26 04:08:35 2024 -0500 go vet works diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a630ed4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +go.mod +go.sum diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b96172c --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +VERSION = $(shell git describe --tags) + +# create the go.mod and go.sum if this is a brand new repo +# REDOMOD = $(shell if [ -e go.mod ]; then echo go.mod; else echo no go mod; fi) +REDOMOD = $(shell if [ -e go.sum ]; then echo go.sum exists; else GO111MODULE= go mod init; GO111MODULE= go mod tidy; fi) + +all: + GO111MODULE=off go vet + +goimports: + goimports -w *.go + +redomod: + rm -f go.* + GO111MODULE= go mod init + GO111MODULE= go mod tidy + +clean: + rm -f go.* diff --git a/libvirtxml.go b/libvirtxml.go new file mode 100644 index 0000000..a2e53e7 --- /dev/null +++ b/libvirtxml.go @@ -0,0 +1,981 @@ +// Copyright 2024 WIT.COM Inc Licensed GPL 3.0 + +package virtigoxml + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "go.wit.com/log" + "libvirt.org/go/libvirtxml" +) + +func makeStandardXml(hostname string) *libvirtxml.Domain { + log.Info("create new xml file for:", hostname) + domcfg := &libvirtxml.Domain{} + + addDefaultXml(domcfg, "standard.x86") + addDefaultXml(domcfg, "memory") + addDefaultXml(domcfg, "network") + addDefaultXml(domcfg, "spice") + addDefaultXml(domcfg, "qcow") + + return domcfg +} + +func writeoutXml(domcfg *libvirtxml.Domain, filename string) bool { + xmldoc, err := domcfg.Marshal() + + if err != nil { + fmt.Println("can't make xml file error:\n", err) + return false + } + + outfile := "/tmp/" + filename + ".xml" + regfile, _ := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE, 0666) + fmt.Fprintln(regfile, xmldoc) + + log.Info("File is in", outfile) + regfile.Close() + return true +} + +func setDiskFilename(domcfg *libvirtxml.Domain, filename string) { + for i, x := range domcfg.Devices.Disks { + // Create a new DomainDiskSourceFile struct + newSource := &libvirtxml.DomainDiskSourceFile{ + File: filename, // Set the file name here + } + + // Assign it to the disk's source + domcfg.Devices.Disks[i].Source.File = newSource + + // fmt.Printf("Disk Source %s\n", name) + fmt.Printf("Disk Device %s\n", x.Source.File) + } +} + +func addDefaultXml(d *libvirtxml.Domain, filename string) { + /* + fullname := "resources/xml/" + filename + ".xml" + pfile, err := resources.ReadFile(fullname) + if err != nil { + log.Println("ERROR:", err) + return + } + */ + + err := d.Unmarshal(filename) + if err != nil { + log.Info("Marshal failed on file", filename) + return + } +} + +func readXml(filename string) (*libvirtxml.Domain, error) { + log.Verbose("parse xml file:", filename) + + hostname := filepath.Base(filename) + hostname = strings.TrimSuffix(hostname, ".xml") + + pfile, err := os.ReadFile(filename) + if err != nil { + log.Println("ERROR:", err) + return nil, err + } + + domcfg := &libvirtxml.Domain{} + + err = domcfg.Unmarshal(string(pfile)) + if err != nil { + log.Info("Marshal failed on file", filename, err) + return nil, err + } + + if domcfg.Name != hostname { + log.Info("ERROR: filename:", filename) + log.Info("ERROR: domcfg.Name != name", domcfg.Name, hostname) + log.Info("ERROR: xml filenames must match the xml name") + os.Exit(-1) + } + + return domcfg, nil +} + +func mergeXml(domcfg *libvirtxml.Domain, filename string) error { + log.Info("merge xml file:", filename) + + pfile, err := os.ReadFile(filename) + if err != nil { + log.Println("ERROR:", err) + return err + } + + err = domcfg.Unmarshal(string(pfile)) + if err != nil { + log.Info("Marshal failed on file", filename) + return err + } + return nil +} + +func setSimpleDisk(domcfg *libvirtxml.Domain, filename string) { + // Clear out the existing disks (if any) + domcfg.Devices.Disks = nil + + // Define a new disk with "mynew.qcow2" + newDisk := libvirtxml.DomainDisk{ + Device: "disk", + Driver: &libvirtxml.DomainDiskDriver{ + Name: "qemu", + Type: "qcow2", + }, + Source: &libvirtxml.DomainDiskSource{ + File: &libvirtxml.DomainDiskSourceFile{ + File: filename, + }, + }, + Target: &libvirtxml.DomainDiskTarget{ + Dev: "vda", + Bus: "virtio", + }, + } + + // Add the new disk to the domain configuration + domcfg.Devices.Disks = append(domcfg.Devices.Disks, newDisk) +} + +func getMacs(domcfg *libvirtxml.Domain) []string { + var macs []string + // Iterate over the network interfaces and print the MAC addresses + for _, iface := range domcfg.Devices.Interfaces { + if iface.MAC != nil { + // iface.MAC.Address = "aa:bb:aa:bb:aa:ff" + fmt.Printf("MAC Address: %+v\n", iface.MAC) + // fmt.Printf("Interface: %s, MAC Address: %s\n", iface.Target.Dev, iface.MAC.Address) + macs = append(macs, iface.MAC.Address) + } else { + fmt.Printf("Interface: %s, MAC Address: not available\n", iface.Target.Dev) + } + } + return macs +} + +// removes all the ethernet interfaces +func clearEthernet(domcfg *libvirtxml.Domain) { + // Clear out the existing disks (if any) + domcfg.Devices.Interfaces = nil +} + +// add a new ethernet interface with mac assigned to bridge name +func addEthernetBridge(domcfg *libvirtxml.Domain, mac string, brname string) { + // Define a new disk with "mynew.qcow2" + // type DomainInterfaceType string + + var ib *libvirtxml.DomainInterfaceSourceBridge + ib = new(libvirtxml.DomainInterfaceSourceBridge) + ib.Bridge = brname + + newNet := libvirtxml.DomainInterface{ + MAC: &libvirtxml.DomainInterfaceMAC{ + Address: mac, + }, + Source: &libvirtxml.DomainInterfaceSource{ + Bridge: ib, + }, + Model: &libvirtxml.DomainInterfaceModel{ + Type: "virtio", + }, + /* this is for raw tap. use this for people who don't + who don't have bridge groups or proper cluster backend networking + literally leaving this blank makes the interface 'tap0' + */ + // Target: &libvirtxml.DomainInterfaceTarget{ + // }, + } + + // Add the new disk to the domain configuration + domcfg.Devices.Interfaces = append(domcfg.Devices.Interfaces, newNet) +} + +// makes an ethernet interface with qemu on dom0 as 'tapXXX' +// doesn't require a bridge group or any changes to dom0 networking (probably) +func addEthernetTap(domcfg *libvirtxml.Domain, mac string) { + newNet := libvirtxml.DomainInterface{ + MAC: &libvirtxml.DomainInterfaceMAC{ + Address: mac, + }, + /* this is for raw tap. use this for people who don't + who don't have bridge groups or proper cluster backend networking + literally leaving this blank makes the interface 'tap0' + */ + Target: &libvirtxml.DomainInterfaceTarget{ + }, + } + + // Add the new disk to the domain configuration + domcfg.Devices.Interfaces = append(domcfg.Devices.Interfaces, newNet) +} + +func setRandomMacs(domcfg *libvirtxml.Domain) { + for i, x := range domcfg.Devices.Interfaces { + // Create a new DomainDiskInterfaces struct + newMac := &libvirtxml.DomainInterfaceMAC{ + Address: "aa:bb:cc:dd:ee:ff", // make sure this is unique + } + + // Assign it to the disk's source + domcfg.Devices.Interfaces[i].MAC = newMac + + // fmt.Printf("Disk Source %s\n", name) + // fmt.Printf("mac addr %+v\n", x.MAC) + fmt.Printf("mac addr %s\n", x.MAC.Address) + } +} + +// go through the libvirt xml object and dump out everything +// that is "standard". This is just a way to double check that +// there might be something interesting in a VM +// 'standard' here means what I think is standard +func dumpNonStandardXML(domcfg *libvirtxml.Domain) (string, error) { + // dump type + if domcfg.Type == "kvm" { + domcfg.Type = "" + } else { + fmt.Printf("type: %+v\n", domcfg.Type) + } + + // dump normal OS settings + var standardOS bool = false + if domcfg.OS != nil { + if domcfg.OS.Type != nil { + // OS Type: &{Arch:x86_64 Machine:pc-i440fx-5.2 Type:hvm} + t := domcfg.OS.Type + if t.Arch == "x86_64" || t.Machine == "pc-i440fx-5.2" { + standardOS = true + } + } + } + if standardOS { + domcfg.OS = nil + } else { + fmt.Printf("OS: %+v\n", domcfg.OS) + fmt.Printf("OS Type: %+v\n", domcfg.OS.Type) + } + + // ignore XMLName and IOThreads probably + // skip is hard coded in isDomainEmpty() function + // fmt.Printf("XMLName: %+v\n", domcfg.XMLName) + // fmt.Printf("IOThreads: %+v\n", domcfg.IOThreads) + + // dump all the clock stuff if it's standard + var normalclock bool = true + if domcfg.Clock.Offset != "utc" { + normalclock = false + } + for i, t := range domcfg.Clock.Timer { + // fmt.Printf("Test Clock Timer: %d , %s , %+v\n", i, t.Name, t) + switch t.Name { + case "rtc": + if t.TickPolicy != "catchup" { + fmt.Printf("Clock Name: %+v , %+v\n", i, t) + normalclock = false + } + case "pit": + if t.TickPolicy != "delay" { + fmt.Printf("Clock Name: %+v , %+v\n", i, t) + normalclock = false + } + case "hpet": + if t.Present != "no" { + fmt.Printf("Clock Name: %+v , %+v\n", i, t) + normalclock = false + } + default: + fmt.Printf("Clock Name: %+v , %+v\n", i, t) + normalclock = false + } + } + if normalclock { + domcfg.Clock = nil + } else { + fmt.Printf("Clock was 'nonstandard' %+v\n", domcfg.Clock.Timer) + } + + // probably just dump Features for now + // fmt.Printf("Features: %+v\n", domcfg.Features) + // fmt.Printf("Feature VMPort: %+v\n", domcfg.Features.VMPort) + // ignore if ACPI is set or not + var featurematch bool = true + if domcfg.Features.ACPI != nil { + domcfg.Features.ACPI = nil + } else { + featurematch = false + } + // ignore if APIC is set or not + if domcfg.Features.APIC != nil { + domcfg.Features.APIC = nil + } else { + featurematch = false + } + // what is VMPort anyway? + if domcfg.Features.VMPort != nil { + if domcfg.Features.VMPort.State == "off" { + domcfg.Features.VMPort = nil + } + } else { + featurematch = false + } + // screwit, if all three of those match just erase + // this. not sure what uses it anyway but it's probably obscure + // and I'm not using it on any of my machines right now + // also, this is dumb that I'm doing this but I want to + // fine tooth comb through this right now + // also, I don't have a boss so nobody can tell me what to do + if featurematch { + domcfg.Features = nil + } + + // fmt.Printf("Features: %+v\n", domcfg.Features) + + // for i, f := range domcfg.Features { + // fmt.Printf("Feature: %+v , %+v\n", i, f) + // } + + // these should always just be strings? + domcfg.Name = "" + domcfg.UUID = "" + + // todo: actually check these for anything different + domcfg.Memory = nil + domcfg.CurrentMemory = nil + domcfg.VCPU = nil + // is this always "host-passthrough" and "host-model"? + // only Fabrice knows :) + if domcfg.CPU != nil { + switch domcfg.CPU.Mode { + case "host-passthrough": + domcfg.CPU = nil + case "host-model": + domcfg.CPU = nil + case "custom": + updatedXML, _ := xml.MarshalIndent(domcfg.CPU, "", " ") + log.Info("Ignoring custom CPU Start") + fmt.Println(string(updatedXML)) + log.Info("Ignoring custom CPU End (--xml-ignore-cpu=true)") + //if argv.IgnoreCpu { + // domcfg.CPU = nil + //} + default: + fmt.Printf("unknown CPU: %+v\n", domcfg.CPU) + fmt.Printf("unknown CPU Model: %+v\n", domcfg.CPU.Model) + fmt.Printf("unknown CPU Mode: %+v\n", domcfg.CPU.Mode) + updatedXML, _ := xml.MarshalIndent(domcfg.CPU, "", " ") + log.Info("Non-Standard XML Start") + fmt.Println(string(updatedXML)) + log.Info("Non-Standard XML End") + } + } + + var secnormal bool = true + if len(domcfg.SecLabel) != 0 { + for _, sec := range domcfg.SecLabel { + switch sec.Model { + case "apparmor": + // log.Info("ignoring SecLabel apparmor. not supported yet") + // log.Info("you must set this later if you need this") + // xmlAny(sec) + case "dac": + // log.Info("ignoring SecLabel dac. not supported yet") + // log.Info("you must set this later if you need this") + // xmlAny(sec) + default: + fmt.Printf("unknown SecLabel: %+v\n", sec) + fmt.Printf("unknown SecLabel.Model: %+v\n", sec.Model) + xmlAny(sec) + secnormal = false + } + } + } + if secnormal { + domcfg.SecLabel = nil + } + + // ignore Metadata + // this is probably something about what kind of OS you might be running + // todo: get this directly from the disk image + if domcfg.Metadata != nil { + var s string + s = domcfg.Metadata.XML + log.Info("Not saving Domain.Metadata.XML:", s) + log.Info("todo: get this from disk image") + domcfg.Metadata = nil + } + + // ignore Resource + if domcfg.Resource != nil { + if domcfg.Resource.Partition == "/machine" { + domcfg.Resource = nil + } else { + fmt.Printf("non-standard Domain.Resource: %+v\n", domcfg.Resource) + } + } + + // ignore Resource + if domcfg.ID != nil { + // ignore domain id + domcfg.ID = nil + } + + // this will move elsewhere in the protobuf someday + // ignore all these for now + if domcfg.OnPoweroff != "" { // normally "destroy" + domcfg.OnPoweroff = "" + } + if domcfg.OnCrash != "" { // normally "restart", often "destroy" + domcfg.OnCrash = "" + } + if domcfg.OnReboot != "" { // normally "restart" + domcfg.OnReboot = "" + } + // same with PM. move to protobuf + domcfg.PM = nil + + // only keep non-qemu stuff + var qemu bool = true + for _, disk := range domcfg.Devices.Disks { + if disk.Driver.Name != "qemu" { + fmt.Printf("- Disk: %s, Device: %s, Source: %s\n", disk.Device, disk.Driver.Name, disk.Source.File.File) + fmt.Printf("FOUND NON QEMU DISK\n") + fmt.Printf("FOUND NON QEMU DISKS\n") + qemu = false + } else { + } + } + if qemu { + domcfg.Devices.Disks = nil + } else { + // fmt.Printf("FOUND NON QEMU DISKS\n") + } + + // network interfaces get processed elsewhere + domcfg.Devices.Interfaces = nil + + // look for strange stuff here + var normalPCI bool = true + var keepPCI []libvirtxml.DomainController + for _, controller := range domcfg.Devices.Controllers { + switch controller.Type { + case "usb": + switch controller.Model { + case "ich9-ehci1": + case "piix3-uhci": + case "qemu-xhci": + // fmt.Printf("OK USB: %s, %d\n", controller.Model, *controller.Index) + case "ich9-uhci1": + // fmt.Printf("OK USB: %s, %d\n", controller.Model, *controller.Index) + case "ich9-uhci2": + // fmt.Printf("OK USB: %s, %d\n", controller.Model, *controller.Index) + case "ich9-uhci3": + // fmt.Printf("OK USB: %s, %d\n", controller.Model, *controller.Index) + default: + keepPCI = append(keepPCI, controller) + normalPCI = false + fmt.Printf("USB: %s, %d\n", controller.Model, *controller.Index) + // Domain:0xc0002d2760 Bus:0xc0002d2768 Slot:0xc0002d2770 Function:0xc0002d2778 MultiFunction: + pci := controller.Address.PCI + fmt.Printf("USB: Domain: %+v Slot %d Function %d\n", *pci.Domain, *pci.Slot, *pci.Function) + } + case "ide": + // fmt.Printf("IGNORE IDE\n") + case "virtio-serial": + // fmt.Printf("IGNORE virtio-serial\n") + case "sata": + // fmt.Printf("SATA: %s, %d\n", controller.Model, *controller.Index) + // fmt.Printf("SATA: %+v\n", controller) + case "scsi": + switch controller.Model { + case "virtio-scsi": + // fmt.Printf("IGNORE SCSI: lsilogic\n") + case "lsilogic": + // fmt.Printf("IGNORE SCSI: lsilogic\n") + default: + keepPCI = append(keepPCI, controller) + normalPCI = false + } + case "pci": + // these are the strings I've found so far + switch controller.Model { + case "pci-root": + case "pcie-root": + case "pcie-root-port": + case "pcie-to-pci-bridge": + default: + fmt.Printf("PCI: %s, %d\n", controller.Model, *controller.Index) + // Domain:0xc0002d2760 Bus:0xc0002d2768 Slot:0xc0002d2770 Function:0xc0002d2778 MultiFunction: + if controller.Address == nil { + fmt.Printf("PCI: controller.Address = nil\n") + } else { + pci := controller.Address.PCI + fmt.Printf("PCI: Domain: %+v Slot %d Function %d\n", *pci.Domain, *pci.Slot, *pci.Function) + } + normalPCI = false + keepPCI = append(keepPCI, controller) + } + default: + fmt.Printf("? controllerType: %s: %+v\n", controller.Type, controller) + normalPCI = false + keepPCI = append(keepPCI, controller) + } + } + if normalPCI { + domcfg.Devices.Controllers = nil + } else { + domcfg.Devices.Controllers = keepPCI + } + + // ignore serial and console + domcfg.Devices.Serials = nil + domcfg.Devices.Consoles = nil + + // ignore sound + domcfg.Devices.Sounds = nil + + // ignore input + domcfg.Devices.Inputs = nil + + // ignore MemoryBalloon. This is cool, but no mortal humans + // are going to use it at this point. By that I mean me. + // someday this will be in protobuf? + domcfg.Devices.MemBalloon = nil + + if domcfg.Devices.Emulator == "/usr/bin/qemu-system-x86_64" { + domcfg.Devices.Emulator = "" + } + + // ignore Graphics == Spice when AutoPort = 'yes' + var normalSpice bool = true + if domcfg.Devices.Graphics != nil { + for i, g := range domcfg.Devices.Graphics { + if g.VNC != nil { + // ignore vnc settings + // fmt.Printf("Ignore Graphics VNC settings: %d %+v\n", i, g) + continue + } + if g.Spice != nil { + // this is all moved to updateDroplet() + // this is a spice definition, just ignore it + // because port mappings and network access will be handled + // somewhere else someday + // fmt.Printf("Graphics: %d %+v\n", i, g) + var s *libvirtxml.DomainGraphicSpice + s = g.Spice + // fmt.Printf("Spice: %d %+v %s\n", i, s, s.AutoPort) + if s.AutoPort == "yes" { + // should ignore either way + } else { + // print out, but ignore the port number + // fmt.Printf("Spice Port = %d\n", s.Port) + } + continue + } + // figure out what to do with non-spice stuff + fmt.Printf("Unknown Graphics: %d %+v\n", i, g) + normalSpice = false + } + } + if normalSpice { + domcfg.Devices.Graphics = nil + } + + // blank out emulator. should be in dom0 + switch domcfg.Devices.Emulator { + case "": + domcfg.Devices.Emulator = "" + case "/usr/bin/kvm": + domcfg.Devices.Emulator = "" + case "/usr/bin/kvm-spice": + domcfg.Devices.Emulator = "" + default: + fmt.Printf("Unknown Emulator: %s\n", domcfg.Devices.Emulator) + } + + // ignore Channels == SpiceVMC + normalSpice = true + if domcfg.Devices.Channels != nil { + for _, c := range domcfg.Devices.Channels { + if c.Source != nil { + s := c.Source + if s != nil { + // fmt.Printf("Channels: %+v\n", s.SpiceVMC) + } else { + fmt.Printf("? Channels: %+v\n", c) + normalSpice = false + } + } else { + fmt.Printf("? Channels: %+v\n", c) + normalSpice = false + } + } + } + if normalSpice { + domcfg.Devices.Channels = nil + } + + // this is probably for spice to have keyboard and mouse input + normalSpice = true + if domcfg.Devices.RedirDevs != nil { + for _, c := range domcfg.Devices.RedirDevs { + s := c.Source + if s != nil { + if s.SpiceVMC != nil { + // this is the normal USB redirection (I guess) + } else { + normalSpice = false + } + } else { + normalSpice = false + } + // fmt.Printf("? RedirDevs: %+v\n", c) + // fmt.Printf("? RedirDevs Source: %+v\n", s) + // fmt.Printf("? RedirDevs SpiceVMC: %d\n", *s.SpiceVMC) + // fmt.Printf("? RedirDevs Address: %+v\n", c.Address) + // fmt.Printf("? RedirDevs USB: %+v\n", c.Address.USB) + } + } + if normalSpice { + domcfg.Devices.RedirDevs = nil + } + + var normalRNGs bool = true + if domcfg.Devices.RNGs != nil { + for _, rng := range domcfg.Devices.RNGs { + if rng.Model == "virtio" { + // nothing to do for this + } else { + fmt.Printf("? RNGs: %+v\n", rng) + normalRNGs = false + } + } + } + if normalRNGs { + domcfg.Devices.RNGs = nil + } + + // don't copy this over here yet. + // probably most domU's don't really use/need it set to what is in the XML + var normalVideo bool = true + if domcfg.Devices.Videos != nil { + for _, v := range domcfg.Devices.Videos { + switch v.Model.Type { + case "qxl": + if v.Model.VRam == 65536 { + // standard qxl video + } else { + fmt.Printf("? Video: %+v\n", v) + fmt.Printf("? Video Model: %+v\n", v.Model) + normalVideo = false + } + case "cirrus": + case "virtio": + // this should always be standard + //fmt.Printf("? Video: %+v\n", v) + //fmt.Printf("? Video Model: %+v\n", v.Model) + //fmt.Printf("? Video Address: %+v\n", v.Address) + //fmt.Printf("? Video PCI: %+v\n", v.Address.PCI) + default: + fmt.Printf("? Video: %+v\n", v) + fmt.Printf("? Video Model: %+v\n", v.Model) + normalVideo = false + } + } + } + if normalVideo { + domcfg.Devices.Videos = nil + } + + return finalEmptyCheck(domcfg) +} + +// this tries the final zero'ing out of the XML +// todo: if this fails, put the remaining XML in the protobuf file? +func finalEmptyCheck(domcfg *libvirtxml.Domain) (string, error) { + // dumpLibvirtxmlDomainNames() + if libvirtxmlDomainDevicesEmpty(*domcfg.Devices) { + // fmt.Println("Domain Devices are empty") + domcfg.Devices = nil + } else { + return warnUserOfNonStandardXML(domcfg) + } + + if libvirtxmlDomainEmpty(*domcfg) { + domcfg = nil + return warnUserOfNonStandardXML(domcfg) + } + + final, err := warnUserOfNonStandardXML(domcfg) + if err != nil { + fmt.Printf("todo: improve this libvirtXML parsing. %v\n", err) + os.Exit(-1) + } + return final, nil +} + +func xmlAny(a any) (string, error) { + updatedXML, err := xml.MarshalIndent(a, "", " ") + if err != nil { + fmt.Printf("Failed to marshal updated XML: %v\n", err) + return "", err + } + final := string(updatedXML) + if final == "" { + // everything seems to have been parsed pretty standard + return "", nil + } + log.Info("Non-Standard XML Start") + fmt.Println(final) + log.Info("Non-Standard XML End") + return final, nil +} + +func warnUserOfNonStandardXML(domcfg *libvirtxml.Domain) (string, error) { + updatedXML, err := xml.MarshalIndent(domcfg, "", " ") + if err != nil { + fmt.Printf("Failed to marshal updated XML: %v\n", err) + return "", err + } + final := string(updatedXML) + if final == "" { + // everything seems to have been parsed pretty standard + return "", nil + } + log.Info("Non-Standard XML Start") + fmt.Println(string(updatedXML)) + log.Info("Non-Standard XML End") + log.Info("") + log.Info("This XML must be removed by hand. Put this in the protobuf?") + return string(updatedXML), nil +} + +// dump out all the fields in libvirtxml.DomainDeviceList +func dumpLibvirtxmlDomainNames() { + var domain libvirtxml.Domain + t := reflect.TypeOf(domain) + + fmt.Println("Fields in libvirtxml.Domain:") + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fmt.Println("Domain:", field.Name) + } + + var device libvirtxml.DomainDeviceList + t = reflect.TypeOf(device) + + fmt.Println("Fields in libvirtxml.DomainDeviceList:") + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fmt.Println("DomainDeviceList:", field.Name) + } + + var iface libvirtxml.DomainInterface + t = reflect.TypeOf(iface) + + fmt.Println("Fields in libvirtxml.DomainInterface:") + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fmt.Println("DomainInterface:", field.Name) + } + + var isource libvirtxml.DomainInterfaceSource + listFields(isource, "libvirtxml.DomainInterfaceSource") + + var ibridge libvirtxml.DomainInterfaceSourceBridge + listFields(ibridge, "libvirtxml.DomainInterfaceSourceBridge") +} + +func listFields(a any, name string) { + t := reflect.TypeOf(a) + + fmt.Println("Fields in", name) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fmt.Println(name, field.Name) + } +} + +// dump out all the fields in libvirtxml.DomainDeviceList +func libvirtxmlDomainDevicesEmpty(mydom libvirtxml.DomainDeviceList) bool { + var empty bool = true + // Get the reflection object of the variable + v := reflect.ValueOf(mydom) + + // Ensure that we are working with a struct + if v.Kind() == reflect.Struct { + // fmt.Println("Fields and values in libvirtxml.DomainDeviceList:") + + // Loop through each field in the struct + for i := 0; i < v.NumField(); i++ { + // Get field name + field := v.Type().Field(i).Name + + // Get field value + value := v.Field(i) + + if !value.IsValid() { + fmt.Printf("Field: %s is nil or invalid\n", field) + continue + } + + // Check if the field is a string, array, or slice + switch value.Kind() { + case reflect.String: + if value.String() != "" { + fmt.Printf("Field: %s is a String with value: %s\n", field, value.String()) + empty = false + } + case reflect.Slice: + if value.Len() != 0 { + fmt.Printf("Field: %s is a Slice with length: %d\n", field, value.Len()) + empty = false + } + case reflect.Array: + if value.Len() != 0 { + fmt.Printf("Field: %s is an Array with length: %d\n", field, value.Len()) + empty = false + } + case reflect.Ptr: + if !value.IsValid() { + fmt.Println("Field ptr: value:", value) + fmt.Printf("Field ptr: %s is of type: %s\n", field, value.Kind()) + empty = false + } + default: + fmt.Printf("Field: %s is of type: %s\n", field, value.Kind()) + empty = false + } + // Print the field name and value + // fmt.Printf("Field: %s, Value: %v\n", field, value) + } + } else { + fmt.Println("Provided variable is not a struct.") + } + return empty +} + +// dump out all the fields in libvirtxml.DomainDeviceList +func libvirtxmlDomainEmpty(mydom libvirtxml.Domain) bool { + var empty bool = true + // Get the reflection object of the variable + v := reflect.ValueOf(mydom) + + // Ensure that we are working with a struct + if v.Kind() == reflect.Struct { + // fmt.Println("Fields and values in libvirtxml.DomainDeviceList:") + + // Loop through each field in the struct + for i := 0; i < v.NumField(); i++ { + // Get field name + field := v.Type().Field(i).Name + + // Get field value + value := v.Field(i) + + if !value.IsValid() { + fmt.Printf("Field: %s is invalid\n", field) + continue + } + // processed as Domain.Metadata & Domain.Resource + // if (field == "IOThreads") || (field == "XMLName") { + // fmt.Printf("Field: %s is: %s\n", field, value.String()) + // continue + // } + + // Check if the field is a string, array, or slice + switch value.Kind() { + case reflect.String: + if value.String() != "" { + fmt.Printf("Field: %s is a String with value: %s\n", field, value.String()) + empty = false + } + case reflect.Slice: + if value.Len() != 0 { + fmt.Printf("Field: %s is a Slice with length: %d\n", field, value.Len()) + empty = false + } + case reflect.Array: + if value.Len() != 0 { + fmt.Printf("Field: %s is an Array with length: %d\n", field, value.Len()) + empty = false + } + case reflect.Struct: + if IsStructEmptyOrNil(value) { + // fmt.Printf("XML Field ignore empty Struct %s\n", field) + } else { + fmt.Printf("Field Struct is not empty %s is %+v\n", field, value) + empty = false + } + case reflect.Uint: + // probably ignore ints. when has that ever gone wrong? + case reflect.Ptr: + if value.IsValid() { + if value.IsNil() { + // this means the value is actually nil + } else { + // there is something still here in the libvirt XML + fmt.Printf("Field Valid? field %s is of type: %s\n", field, value.Kind()) + fmt.Println("Field Valid? ptr: value:", value) + empty = false + } + } else { + fmt.Println("Invalid Field ptr: value:", value) + fmt.Printf("Invalid Field ptr: %s is of type: %s\n", field, value.Kind()) + empty = false + } + default: + fmt.Printf("Field: %s is of type: %s\n", field, value.Kind()) + empty = false + } + // Print the field name and value + // fmt.Printf("Field: %s, Value: %v\n", field, value) + } + } else { + fmt.Println("Provided variable is not a struct.") + } + return empty +} + +// IsStructEmptyOrNil checks if a struct or pointer to struct is empty, blank, or nil +func IsStructEmptyOrNil(value interface{}) bool { + val := reflect.ValueOf(value) + + // If the value is a pointer, check if it's nil and dereference it if not + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return true + } + val = val.Elem() + } + + // Ensure we're dealing with a struct after potential dereferencing + if val.Kind() != reflect.Struct { + return false // Not a struct + } + + // Check each field in the struct for its zero value + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + // Skip unexported fields as we can't access them + if !field.CanInterface() { + continue + } + if !reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()) { + return false // Found a non-zero field + } + } + + return true // All fields are zero values +} diff --git a/start.go b/start.go new file mode 100644 index 0000000..6bb15ba --- /dev/null +++ b/start.go @@ -0,0 +1,103 @@ +// Copyright 2024 WIT.COM Inc Licensed GPL 3.0 + +package virtigoxml + +import ( + "fmt" + "os" + "path/filepath" + + "libvirt.org/go/libvirtxml" + + "go.wit.com/log" + pb "go.wit.com/lib/protobuf/virtbuf" +) + +// generate the XML for 'virsh create' +func startDropletXml(d *pb.Droplet) { + if d == nil { + log.Info("droplet is nil") + os.Exit(0) + } + log.Info("start droplet here:", d.Hostname) + domcfg := &libvirtxml.Domain{} + + addDefaultXml(domcfg, "standard.x86") + // addDefaultXml(domcfg, "memory") + // addDefaultXml(domcfg, "network") + addDefaultXml(domcfg, "spice") + addDefaultXml(domcfg, "qcow") + + domcfg.Type = "kvm" + domcfg.Name = d.Hostname + domcfg.UUID = d.Uuid + + var i uint + i = uint(d.Memory / (1024 * 1024)) + + // var tmp string + // tmp = domcfg.VCPU + domcfg.VCPU = new(libvirtxml.DomainVCPU) + domcfg.VCPU.Value = uint(d.Cpus) + + domcfg.Memory = new(libvirtxml.DomainMemory) + domcfg.Memory.Value = i + domcfg.Memory.Unit = "MiB" + + fmt.Printf("Virt Memory %d %s\n", domcfg.Memory.Value, domcfg.Memory.Unit) + + // addEthernet(domcfg, "04:44:33:11:22:11", "worldbr") + // addEthernet(domcfg, "04:44:33:33:44:55", "greenbr") + + var count int = 0 + for _, n := range d.Networks { + log.Info("add network", d.Hostname, "mac addr", n.Mac, "interface", n.Name) + if n.Name != "worldbr" { + log.Info("OVERRIDE BRIDGE WITH 'worldbr'") + } + addEthernetBridge(domcfg, n.Mac, "worldbr") + // addEthernetTap(domcfg, n.Mac) + count += 1 + } + if count == 1 { + // this is normal + } else { + log.Info("WRONG NUMBER OF ETHERNET INTERFACES:", count) + } + + // add a check here to make these unique + // setRandomMacs(domcfg) + + for _, disk := range d.Disks { + fullname := findDisk([]string{"/home/nfs2", "/var/lib/libvirt/images"}, disk.Filename) + if fullname == "" { + log.Info("can not find disk", d.Hostname, "dir", disk.Filepath, "filename", disk.Filename) + os.Exit(-1) + } else { + // qcow := "/home/nfs/" + d.Hostname + ".qcow2" + setSimpleDisk(domcfg, fullname) + } + } + + writeoutXml(domcfg, d.Hostname) + os.Exit(-1) +} + +func findDisk(dirs []string, filename string) string { + for _, dirname := range dirs { + // log.Info("look in dir", dirname) + var count int + newdir, _ := os.ReadDir(dirname) + for _, file := range newdir { + count += 1 + if file.Name() == filename { + log.Info("Found file", filename, "in", dirname) + return filepath.Join(dirname, file.Name()) + } + } + if count == 0 { + log.Info("Warning? dirname", dirname, "was empty. Not mounted?") + } + } + return "" +}