package collector import ( "encoding/json" "log" "os" "os/exec" "strconv" "strings" ) type ProxmoxInfo struct { Available bool `json:"available"` Version string `json:"version"` Node string `json:"node"` VMs []ProxmoxVM `json:"vms,omitempty"` Containers []ProxmoxContainer `json:"containers,omitempty"` Storage []ProxmoxStorage `json:"storage,omitempty"` Cluster *ProxmoxCluster `json:"cluster,omitempty"` Subscription *ProxmoxSubscription `json:"subscription,omitempty"` ZFSPools []ZFSPool `json:"zfs_pools,omitempty"` } type ProxmoxVM struct { VMID int `json:"vmid"` Name string `json:"name"` Status string `json:"status"` Type string `json:"type"` CPUUsage *float64 `json:"cpu_usage,omitempty"` MemoryUsed *int64 `json:"memory_used,omitempty"` MemoryMax *int64 `json:"memory_max,omitempty"` DiskUsed *int64 `json:"disk_used,omitempty"` DiskMax *int64 `json:"disk_max,omitempty"` Uptime *int64 `json:"uptime,omitempty"` Node string `json:"node"` } type ProxmoxContainer struct { VMID int `json:"vmid"` Name string `json:"name"` Status string `json:"status"` Type string `json:"type"` CPUUsage *float64 `json:"cpu_usage,omitempty"` MemoryUsed *int64 `json:"memory_used,omitempty"` MemoryMax *int64 `json:"memory_max,omitempty"` DiskUsed *int64 `json:"disk_used,omitempty"` DiskMax *int64 `json:"disk_max,omitempty"` Uptime *int64 `json:"uptime,omitempty"` Node string `json:"node"` } type ProxmoxStorage struct { Name string `json:"storage"` Type string `json:"type"` Total int64 `json:"total"` Used int64 `json:"used"` Available int64 `json:"available"` Enabled bool `json:"enabled"` Active bool `json:"active"` Content string `json:"content"` } type ProxmoxCluster struct { Name string `json:"name"` Nodes []ProxmoxClusterNode `json:"nodes"` Quorum bool `json:"quorum"` } type ProxmoxClusterNode struct { Name string `json:"name"` Status string `json:"status"` Local bool `json:"local"` } type ProxmoxSubscription struct { Status string `json:"status"` ProductName string `json:"productname,omitempty"` Key string `json:"key,omitempty"` NextDueDate string `json:"next_due_date,omitempty"` } type ZFSPool struct { Name string `json:"name"` Size int64 `json:"size"` Allocated int64 `json:"allocated"` Free int64 `json:"free"` Health string `json:"health"` Fragmentation string `json:"fragmentation,omitempty"` ScrubStatus string `json:"scrub_status,omitempty"` } func CollectProxmox() *ProxmoxInfo { proxmox := &ProxmoxInfo{ Available: false, } // Prüfen ob pvesh verfügbar ist // Voller Pfad fuer pvesh, da systemd reduzierten PATH hat if _, err := os.Stat("/usr/bin/pvesh"); err != nil { log.Printf("Proxmox: pvesh nicht gefunden: %v", err) return nil // Proxmox nicht verfügbar } log.Printf("Proxmox: pvesh gefunden, sammle Daten...") proxmox.Available = true // Version abrufen if data := pveshGet("/version"); data != nil { log.Printf("Proxmox: Version data: %v", data) if versionData, ok := data.(map[string]interface{}); ok { if version, ok := versionData["version"].(string); ok { proxmox.Version = version } } } // Node-Name bestimmen proxmox.Node = getLocalNodeName() // VMs sammeln proxmox.VMs = collectProxmoxVMs() // Container sammeln proxmox.Containers = collectProxmoxContainers() // Storage sammeln proxmox.Storage = collectProxmoxStorage() // Cluster-Info sammeln proxmox.Cluster = collectProxmoxCluster() // Subscription-Info sammeln proxmox.Subscription = collectProxmoxSubscription() // ZFS-Pools sammeln proxmox.ZFSPools = collectZFSPools() return proxmox } // pvesh get wrapper func pveshGet(path string) interface{} { cmd := exec.Command("/usr/bin/pvesh", "get", path, "--output-format", "json") output, err := cmd.Output() if err != nil { log.Printf("Proxmox: pvesh get %s fehler: %v", path, err) return nil } log.Printf("Proxmox: pvesh get %s: %d bytes", path, len(output)) var result interface{} if err := json.Unmarshal(output, &result); err != nil { log.Printf("Proxmox: JSON parse fehler fuer %s: %v", path, err) return nil } return result } func getLocalNodeName() string { // Hostname als Node-Name verwenden if hostname, err := os.Hostname(); err == nil { return hostname } return "localhost" } func collectProxmoxVMs() []ProxmoxVM { var vms []ProxmoxVM nodeName := getLocalNodeName() data := pveshGet("/nodes/" + nodeName + "/qemu") if data == nil { return vms } if vmList, ok := data.([]interface{}); ok { for _, item := range vmList { if vmData, ok := item.(map[string]interface{}); ok { vm := ProxmoxVM{ Type: "qemu", Node: nodeName, } if vmid, ok := vmData["vmid"].(float64); ok { vm.VMID = int(vmid) } if name, ok := vmData["name"].(string); ok { vm.Name = name } if status, ok := vmData["status"].(string); ok { vm.Status = status } if cpu, ok := vmData["cpu"].(float64); ok { vm.CPUUsage = &cpu } if mem, ok := vmData["mem"].(float64); ok { memUsed := int64(mem) vm.MemoryUsed = &memUsed } if maxmem, ok := vmData["maxmem"].(float64); ok { maxMem := int64(maxmem) vm.MemoryMax = &maxMem } if disk, ok := vmData["disk"].(float64); ok { diskUsed := int64(disk) vm.DiskUsed = &diskUsed } if maxdisk, ok := vmData["maxdisk"].(float64); ok { maxDisk := int64(maxdisk) vm.DiskMax = &maxDisk } if uptime, ok := vmData["uptime"].(float64); ok { uptimeVal := int64(uptime) vm.Uptime = &uptimeVal } vms = append(vms, vm) } } } return vms } func collectProxmoxContainers() []ProxmoxContainer { var containers []ProxmoxContainer nodeName := getLocalNodeName() data := pveshGet("/nodes/" + nodeName + "/lxc") if data == nil { return containers } if containerList, ok := data.([]interface{}); ok { for _, item := range containerList { if containerData, ok := item.(map[string]interface{}); ok { container := ProxmoxContainer{ Type: "lxc", Node: nodeName, } if vmid, ok := containerData["vmid"].(float64); ok { container.VMID = int(vmid) } if name, ok := containerData["name"].(string); ok { container.Name = name } if status, ok := containerData["status"].(string); ok { container.Status = status } if cpu, ok := containerData["cpu"].(float64); ok { container.CPUUsage = &cpu } if mem, ok := containerData["mem"].(float64); ok { memUsed := int64(mem) container.MemoryUsed = &memUsed } if maxmem, ok := containerData["maxmem"].(float64); ok { maxMem := int64(maxmem) container.MemoryMax = &maxMem } if disk, ok := containerData["disk"].(float64); ok { diskUsed := int64(disk) container.DiskUsed = &diskUsed } if maxdisk, ok := containerData["maxdisk"].(float64); ok { maxDisk := int64(maxdisk) container.DiskMax = &maxDisk } if uptime, ok := containerData["uptime"].(float64); ok { uptimeVal := int64(uptime) container.Uptime = &uptimeVal } containers = append(containers, container) } } } return containers } func collectProxmoxStorage() []ProxmoxStorage { var storages []ProxmoxStorage nodeName := getLocalNodeName() data := pveshGet("/nodes/" + nodeName + "/storage") if data == nil { return storages } if storageList, ok := data.([]interface{}); ok { for _, item := range storageList { if storageData, ok := item.(map[string]interface{}); ok { storage := ProxmoxStorage{} if name, ok := storageData["storage"].(string); ok { storage.Name = name } if storageType, ok := storageData["type"].(string); ok { storage.Type = storageType } if total, ok := storageData["total"].(float64); ok { storage.Total = int64(total) } if used, ok := storageData["used"].(float64); ok { storage.Used = int64(used) } if avail, ok := storageData["avail"].(float64); ok { storage.Available = int64(avail) } if enabled, ok := storageData["enabled"].(float64); ok { storage.Enabled = enabled > 0 } if active, ok := storageData["active"].(float64); ok { storage.Active = active > 0 } if content, ok := storageData["content"].(string); ok { storage.Content = content } storages = append(storages, storage) } } } return storages } func collectProxmoxCluster() *ProxmoxCluster { data := pveshGet("/cluster/status") if data == nil { return nil } cluster := &ProxmoxCluster{} if clusterData, ok := data.([]interface{}); ok { for _, item := range clusterData { if itemData, ok := item.(map[string]interface{}); ok { if itemType, ok := itemData["type"].(string); ok { if itemType == "cluster" { if name, ok := itemData["name"].(string); ok { cluster.Name = name } if quorum, ok := itemData["quorate"].(float64); ok { cluster.Quorum = quorum > 0 } } else if itemType == "node" { node := ProxmoxClusterNode{} if name, ok := itemData["name"].(string); ok { node.Name = name } if online, ok := itemData["online"].(float64); ok { if online > 0 { node.Status = "online" } else { node.Status = "offline" } } if local, ok := itemData["local"].(float64); ok { node.Local = local > 0 } cluster.Nodes = append(cluster.Nodes, node) } } } } } if cluster.Name == "" && len(cluster.Nodes) == 0 { return nil // Kein Cluster } return cluster } func collectProxmoxSubscription() *ProxmoxSubscription { nodeName := getLocalNodeName() data := pveshGet("/nodes/" + nodeName + "/subscription") if data == nil { return nil } subscription := &ProxmoxSubscription{} if subData, ok := data.(map[string]interface{}); ok { if status, ok := subData["status"].(string); ok { subscription.Status = status } if productName, ok := subData["productname"].(string); ok { subscription.ProductName = productName } if key, ok := subData["key"].(string); ok { subscription.Key = key } if nextDueDate, ok := subData["nextduedate"].(string); ok { subscription.NextDueDate = nextDueDate } } return subscription } func collectZFSPools() []ZFSPool { var pools []ZFSPool // zpool list -H -o name,size,alloc,free,health cmd := exec.Command("/usr/sbin/zpool", "list", "-H", "-o", "name,size,alloc,free,health") output, err := cmd.Output() if err != nil { return pools // ZFS nicht verfügbar oder keine Pools } lines := strings.Split(string(output), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } fields := strings.Fields(line) if len(fields) < 5 { continue } pool := ZFSPool{ Name: fields[0], Health: fields[4], } // Größen parsen (können Suffixe wie K, M, G, T haben) pool.Size = parseZFSSize(fields[1]) pool.Allocated = parseZFSSize(fields[2]) pool.Free = parseZFSSize(fields[3]) // Zusätzliche Informationen aus zpool status pool.ScrubStatus = getZFSPoolScrubStatus(pool.Name) pools = append(pools, pool) } return pools } // Parst ZFS-Größen mit Einheiten (z.B. "1.2T" -> Bytes) func parseZFSSize(sizeStr string) int64 { if sizeStr == "-" || sizeStr == "" { return 0 } sizeStr = strings.TrimSpace(sizeStr) if len(sizeStr) == 0 { return 0 } // Letzes Zeichen prüfen für Einheit lastChar := sizeStr[len(sizeStr)-1] var multiplier int64 = 1 var numberStr string switch lastChar { case 'K': multiplier = 1024 numberStr = sizeStr[:len(sizeStr)-1] case 'M': multiplier = 1024 * 1024 numberStr = sizeStr[:len(sizeStr)-1] case 'G': multiplier = 1024 * 1024 * 1024 numberStr = sizeStr[:len(sizeStr)-1] case 'T': multiplier = 1024 * 1024 * 1024 * 1024 numberStr = sizeStr[:len(sizeStr)-1] default: numberStr = sizeStr } if value, err := strconv.ParseFloat(numberStr, 64); err == nil { return int64(value * float64(multiplier)) } return 0 } // Holt Scrub-Status für einen ZFS-Pool func getZFSPoolScrubStatus(poolName string) string { cmd := exec.Command("/usr/sbin/zpool", "status", poolName) output, err := cmd.Output() if err != nil { return "" } lines := strings.Split(string(output), "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.Contains(line, "scrub") { return line } } return "" }