- agent-linux/: Kompletter Linux/Proxmox RMM Agent - Collectors: CPU, Memory, Disk, Network, Services, Updates, Proxmox (VMs, Container, Storage, ZFS), Backups - Backup Collector: Jobs nach Node gefiltert (Cluster-aware) - WebSocket: Tunnel-Support, writeMux fuer stabile Verbindungen - Systemd Service Template - updater/: Plattform-unabhaengiger Updater (FreeBSD + Linux) - runtime.GOOS erkennt Plattform automatisch - FreeBSD: /usr/local/rmm/, service rmm_agent - Linux: /usr/local/bin/, /etc/rmm/, systemctl rmm-agent - Firmware-Download mit ?platform= Parameter - frontend/: Proxmox-Bereich - ProxmoxServers.jsx: Eigene Seite fuer PVE Server mit VM/CT Counts - ProxmoxPanel.jsx: Detail-Panel mit Uebersicht, VMs, Container, Storage, ZFS, Tunnel, Dienste, Updates, Backups - Agents.jsx: Filtert Linux-Agents raus (nur OPNsense) - Sidebar: Proxmox Navigationspunkt - Installationsanleitung mit rmm-agent-linux + rmm-updater-linux - backend: Platform-Feld fuer Agents, Firmware multi-platform
506 lines
13 KiB
Go
506 lines
13 KiB
Go
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 ""
|
|
} |