package app import ( "context" "crypto/tls" "fmt" "net/http" "strconv" "strings" "github.com/luthermonson/go-proxmox" ) type ProxmoxClient struct { client *proxmox.Client } type PVEDevice struct { // used only for requests to PVE ID string `json:"id"` Device_Name string `json:"device_name"` Vendor_Name string `json:"vendor_name"` Subsystem_Device_Name string `json:"subsystem_device_name"` Subsystem_Vendor_Name string `json:"subsystem_vendor_name"` } type PVEProctype struct { Custom int Name string Vendor string } func NewClient(url string, token string, secret string) ProxmoxClient { HTTPClient := http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } client := proxmox.NewClient(url, proxmox.WithHTTPClient(&HTTPClient), proxmox.WithAPIToken(token, secret), ) return ProxmoxClient{client: client} } // Gets and returns the PVE API version func (pve ProxmoxClient) Version() (proxmox.Version, error) { version, err := pve.client.Version(context.Background()) if err != nil { return *version, err } return *version, err } // Gets all Nodes names func (pve ProxmoxClient) Nodes() ([]string, error) { nodes, err := pve.client.Nodes(context.Background()) if err != nil { return nil, err } names := []string{} for _, node := range nodes { names = append(names, node.Node) } return names, nil } // Gets a Node's resources but does not recursively expand instances func (pve ProxmoxClient) Node(nodeName string) (*Node, error) { host := Node{} host.Devices = make(map[DeviceBus]*Device) host.Instances = make(map[InstanceID]*Instance) node, err := pve.client.Node(context.Background(), nodeName) if err != nil { return &host, err } devices := []PVEDevice{} err = pve.client.Get(context.Background(), fmt.Sprintf("/nodes/%s/hardware/pci", nodeName), &devices) if err != nil { return &host, err } for _, device := range devices { x := strings.Split(device.ID, ".") if len(x) != 2 { // this should always be true, but skip if not continue } deviceid := DeviceBus(x[0]) functionid := FunctionID(x[1]) if _, ok := host.Devices[deviceid]; !ok { host.Devices[deviceid] = &Device{ Device_Bus: deviceid, Device_Name: device.Device_Name, Vendor_Name: device.Vendor_Name, Functions: make(map[FunctionID]*Function), } } host.Devices[deviceid].Functions[functionid] = &Function{ Function_ID: functionid, Function_Name: device.Subsystem_Device_Name, Vendor_Name: device.Subsystem_Vendor_Name, Reserved: false, } } proctypes := []PVEProctype{} err = pve.client.Get(context.Background(), fmt.Sprintf("/nodes/%s/capabilities/qemu/cpu", nodeName), &proctypes) if err != nil { return &host, err } for _, proctype := range proctypes { host.Proctypes = append(host.Proctypes, proctype.Name) } host.Name = node.Name host.Cores = uint64(node.CPUInfo.CPUs) host.Memory = uint64(node.Memory.Total) host.Swap = uint64(node.Swap.Total) host.pvenode = node return &host, err } // Get all VM IDs on specified host func (host *Node) VirtualMachines() ([]uint, error) { vms, err := host.pvenode.VirtualMachines(context.Background()) if err != nil { return nil, err } ids := []uint{} for _, vm := range vms { ids = append(ids, uint(vm.VMID)) } return ids, nil } // Get a VM's CPU, Memory but does not recursively link Devices, Disks, Drives, Nets func (host *Node) VirtualMachine(VMID uint) (*Instance, error) { instance := Instance{} vm, err := host.pvenode.VirtualMachine(context.Background(), int(VMID)) if err != nil { return &instance, err } config := vm.VirtualMachineConfig instance.configHostPCIs = config.MergeHostPCIs() instance.configNets = config.MergeNets() instance.configDisks = MergeVMDisksAndUnused(config) instance.configBoot = config.Boot instance.pveconfig = config instance.Type = VM instance.Name = vm.Name instance.Proctype = vm.VirtualMachineConfig.CPU instance.Cores = uint64(vm.VirtualMachineConfig.Cores) instance.Memory = uint64(vm.VirtualMachineConfig.Memory) * MiB instance.Volumes = make(map[VolumeID]*Volume) instance.Nets = make(map[NetID]*Net) instance.Devices = make(map[DeviceID]*Device) return &instance, nil } func MergeVMDisksAndUnused(vmc *proxmox.VirtualMachineConfig) map[string]string { mergedDisks := vmc.MergeDisks() for k, v := range vmc.MergeUnuseds() { mergedDisks[k] = v } return mergedDisks } // Get all CT IDs on specified host func (host *Node) Containers() ([]uint, error) { cts, err := host.pvenode.Containers(context.Background()) if err != nil { return nil, err } ids := []uint{} for _, ct := range cts { ids = append(ids, uint(ct.VMID)) } return ids, nil } // Get a CT's CPU, Memory, Swap but does not recursively link Devices, Disks, Drives, Nets func (host *Node) Container(VMID uint) (*Instance, error) { instance := Instance{} ct, err := host.pvenode.Container(context.Background(), int(VMID)) if err != nil { return &instance, err } config := ct.ContainerConfig instance.configHostPCIs = make(map[string]string) instance.configNets = config.MergeNets() instance.configDisks = MergeCTDisksAndUnused(config) instance.pveconfig = config instance.Type = CT instance.Name = ct.Name instance.Cores = uint64(ct.ContainerConfig.Cores) instance.Memory = uint64(ct.ContainerConfig.Memory) * MiB instance.Swap = uint64(ct.ContainerConfig.Swap) * MiB instance.Volumes = make(map[VolumeID]*Volume) instance.Nets = make(map[NetID]*Net) return &instance, nil } func MergeCTDisksAndUnused(cc *proxmox.ContainerConfig) map[string]string { mergedDisks := make(map[string]string) for k, v := range cc.MergeUnuseds() { mergedDisks[k] = v } for k, v := range cc.MergeMps() { mergedDisks[k] = v } mergedDisks["rootfs"] = cc.RootFS return mergedDisks } // get volume format, size, volumeid, and storageid from instance volume data string (eg: local:100/vm-100-disk-0.raw ... ) func GetVolumeInfo(host *Node, volume string) (*Volume, error) { volumeData := Volume{} volumeObj := PVEObjectStringToMap(volume) volumeFile := volumeObj[""] volumeStorage := strings.Split(volumeFile, ":")[0] storage, err := host.pvenode.Storage(context.Background(), volumeStorage) if err != nil { return &volumeData, nil } content, err := storage.GetContent(context.Background()) if err != nil { return &volumeData, nil } for _, c := range content { if c.Volid == volumeFile { volumeData.Storage = volumeStorage volumeData.Format = c.Format volumeData.Size = uint64(c.Size) volumeData.File = volumeFile volumeData.MP = volumeObj["mp"] } } return &volumeData, nil } func GetNetInfo(netstring string) (*Net, error) { n := Net{} netobj := PVEObjectStringToMap(netstring) rate, err := strconv.ParseUint(netobj["rate"], 10, 64) if err != nil { return &n, err } n.Rate = rate vlan, err := strconv.ParseUint(netobj["tag"], 10, 64) if err != nil { return &n, err } n.VLAN = vlan n.Value = netstring return &n, nil } // most pve objects (nets, disks, pcie, etc) have the following similar format: // objname: v1,k2=v2,k3=v3,k4=v4 ... // this function maps such strings to a map so that each individual key or value can be found more quickly // in pcie or disks, the first value often does not have a key name, in such cases the key will be empty string "" func PVEObjectStringToMap(objectstring string) map[string]string { objectmap := map[string]string{} for v := range strings.SplitSeq(objectstring, ",") { key := "" val := "" if strings.Contains(v, "=") { x := strings.Split(v, "=") key = x[0] val = x[1] } else { key = "" val = v } objectmap[key] = val } return objectmap }