cynfo3000 7f9fbd257f Linux/Proxmox Agent + Plattform-Updater + Proxmox Frontend
- 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
2026-03-01 22:03:18 +01:00

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 ""
}