diff --git a/Makefile b/Makefile index 494cef7..731bd7d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.PHONY: build test clean + build: clean CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/ . diff --git a/app/app.go b/app/app.go index 8b7beb7..aa1c82e 100644 --- a/app/app.go +++ b/app/app.go @@ -1,8 +1,9 @@ package app import ( - "context" + "encoding/gob" "flag" + "fmt" "log" "net/http" "strconv" @@ -13,21 +14,24 @@ import ( const APIVersion string = "0.0.1" -var client *proxmox.Client = nil +var client ProxmoxClient func Run() { + gob.Register(proxmox.Client{}) + configPath := flag.String("config", "config.json", "path to config.json file") flag.Parse() config := GetConfig(*configPath) log.Println("Initialized config from " + *configPath) - client = NewClient(config.PVE.Token.ID, config.PVE.Token.Secret) + token := fmt.Sprintf(`%s@%s!%s`, config.PVE.Token.USER, config.PVE.Token.REALM, config.PVE.Token.ID) + client = NewClient(token, config.PVE.Token.Secret) router := gin.Default() router.GET("/version", func(c *gin.Context) { - PVEVersion, err := client.Version(context.Background()) + PVEVersion, err := client.Version() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } else { @@ -35,6 +39,14 @@ func Run() { } }) - router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort)) + router.GET("/nodes/:node", func(c *gin.Context) { + Node, err := client.Node(c.Param("node")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusOK, gin.H{"node": Node}) + } + }) + router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort)) } diff --git a/app/proxmox.go b/app/proxmox.go index 70bad7c..1170303 100644 --- a/app/proxmox.go +++ b/app/proxmox.go @@ -1,18 +1,127 @@ package app import ( + "context" + "crypto/tls" + "fmt" "net/http" + "strings" "github.com/luthermonson/go-proxmox" ) -func NewClient(tokenID string, secret string) *proxmox.Client { - HTTPClient := http.Client{} +type ProxmoxClient struct { + client *proxmox.Client +} + +func NewClient(token string, secret string) ProxmoxClient { + HTTPClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + println(token, secret) client := proxmox.NewClient("https://pve.tronnet.net/api2/json", proxmox.WithHTTPClient(&HTTPClient), - proxmox.WithAPIToken(tokenID, secret), + proxmox.WithAPIToken(token, secret), ) - return client + 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 and returns a Node's CPU, memory, swap, and Hardware (PCI) resources +func (pve ProxmoxClient) Node(nodeName string) (Host, error) { + host := Host{} + host.Hardware = make(map[string]PVEDevice) + + 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 + } + + vms, err := node.VirtualMachines(context.Background()) + if err != nil { + return host, err + } + + cts, err := node.Containers(context.Background()) + if err != nil { + return host, err + } + + // temporary helper which maps supersystem devices to each contained subsystem + // eg 0000:00:05 -> [0000:00:05.0, 0000:00:05.1, 0000:00:05.2, 0000:00:05.3, ...] + DeviceSubsystemMap := make(map[string][]string) + for _, device := range devices { + host.Hardware[device.BusID] = device + SupersystemID := strings.Split(device.BusID, ".")[0] + DeviceSubsystemMap[SupersystemID] = append(DeviceSubsystemMap[SupersystemID], device.BusID) + } + + host.Name = node.Name + host.Cores.Total = int64(node.CPUInfo.CPUs) + host.Memory.Total = int64(node.Memory.Total) + host.Swap.Total = int64(node.Swap.Total) + + for _, vm := range vms { + vm, err := node.VirtualMachine(context.Background(), int(vm.VMID)) + if err != nil { + return host, err + } + + host.Cores.Reserved += int64(vm.VirtualMachineConfig.Cores) + host.Memory.Reserved += int64(vm.VirtualMachineConfig.Memory * MiB) + + MarshallVirtualMachineConfig(vm.VirtualMachineConfig) + + for _, v := range vm.VirtualMachineConfig.HostPCIs { + HostPCIBusID := strings.Split(v, ",")[0] + if device, ok := host.Hardware[HostPCIBusID]; ok { // is a specific subsystem of device + device.Reserved = true + host.Hardware[HostPCIBusID] = device + } else if SubsystemBusIDs, ok := DeviceSubsystemMap[HostPCIBusID]; ok { // is a supersystem device containing multiple subsystems + for _, SubsystemBusID := range SubsystemBusIDs { + device := host.Hardware[SubsystemBusID] + device.Reserved = true + host.Hardware[SubsystemBusID] = device + } + } + } + } + + for _, ct := range cts { + ct, err := node.Container(context.Background(), int(ct.VMID)) + if err != nil { + return host, err + } + + host.Cores.Reserved += int64(ct.ContainerConfig.Cores) + host.Memory.Reserved += int64(ct.ContainerConfig.Memory * MiB) + host.Swap.Reserved += int64(ct.ContainerConfig.Swap * MiB) + } + + host.Cores.Free = host.Cores.Total - host.Cores.Reserved + host.Memory.Free = host.Memory.Total - host.Memory.Reserved + host.Swap.Free = host.Swap.Total - host.Swap.Reserved + + return host, err } diff --git a/app/types.go b/app/types.go index b015ff5..b643992 100644 --- a/app/types.go +++ b/app/types.go @@ -1,37 +1,57 @@ package app -type PVEBus int +type Resource struct { // number of virtual cores (usually threads) + Reserved int64 + Free int64 + Total int64 +} -const ( - IDE PVEBus = iota - SATA -) +type Host struct { + Name string + Cores Resource + Memory Resource + Swap Resource + Storage map[string]Storage + Hardware map[string]Device +} -type PVEDrive struct{} - -type PVEDisk struct{} - -type PVENet struct{} - -type PVEDevice struct{} +type Storage struct{} type QEMUInstance struct { Name string Proctype string - Cores int16 - Memory int32 - Drive map[int]PVEDrive - Disk map[int]PVEDisk - Net map[int]PVENet - Device map[int]PVEDevice + Cores Resource + Memory Resource + Drive map[int]Volume + Disk map[int]Volume + Net map[int]Net + Device map[int]Device } type LXCInstance struct { Name string - Cores int16 - Memory int32 - Swap int32 - RootDisk PVEDrive - MP map[int]PVEDisk - Net map[int]PVENet + Cores Resource + Memory Resource + Swap Resource + RootDisk Volume + MP map[int]Volume + Net map[int]Net +} + +type Volume struct { + Format string + Path string + Size string + Used string +} + +type Net struct{} + +type Device struct { + BusID string `json:"id"` + DeviceName string `json:"device_name"` + VendorName string `json:"vendor_name"` + SubsystemDeviceName string `json:"subsystem_device_name"` + SubsystemVendorName string `json:"subsystem_vendor_name"` + Reserved bool } diff --git a/app/utils.go b/app/utils.go index d188e80..3585045 100644 --- a/app/utils.go +++ b/app/utils.go @@ -4,14 +4,20 @@ import ( "encoding/json" "log" "os" + + "github.com/luthermonson/go-proxmox" ) +const MiB = 1024 * 1024 + type Config struct { ListenPort int `json:"listenPort"` PVE struct { Token struct { + USER string `json:"user"` + REALM string `json:"realm"` ID string `json:"id"` - Secret string `json:"secret"` + Secret string `json:"uuid"` } } } @@ -28,3 +34,17 @@ func GetConfig(configPath string) Config { } return config } + +func MarshallVirtualMachineConfig(v *proxmox.VirtualMachineConfig) { + v.HostPCIs = make(map[string]string) + v.HostPCIs["hostpci0"] = v.HostPCI0 + v.HostPCIs["hostpci1"] = v.HostPCI1 + v.HostPCIs["hostpci2"] = v.HostPCI2 + v.HostPCIs["hostpci3"] = v.HostPCI3 + v.HostPCIs["hostpci4"] = v.HostPCI4 + v.HostPCIs["hostpci5"] = v.HostPCI5 + v.HostPCIs["hostpci6"] = v.HostPCI6 + v.HostPCIs["hostpci7"] = v.HostPCI7 + v.HostPCIs["hostpci8"] = v.HostPCI8 + v.HostPCIs["hostpci9"] = v.HostPCI9 +}