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
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BackupTask struct {
|
||||
UPID string `json:"upid"`
|
||||
Status string `json:"status"`
|
||||
StartTime int64 `json:"starttime"`
|
||||
EndTime int64 `json:"endtime"`
|
||||
Node string `json:"node"`
|
||||
}
|
||||
|
||||
type BackupData struct {
|
||||
Jobs []map[string]interface{} `json:"jobs"`
|
||||
Tasks []BackupTask `json:"tasks"`
|
||||
}
|
||||
|
||||
func CollectBackups() *BackupData {
|
||||
if _, err := os.Stat("/usr/bin/pvesh"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &BackupData{
|
||||
Jobs: []map[string]interface{}{},
|
||||
Tasks: []BackupTask{},
|
||||
}
|
||||
|
||||
// Hostname fuer Node-Filterung
|
||||
hostname, _ := os.Hostname()
|
||||
nodeName := strings.TrimSpace(hostname)
|
||||
|
||||
// Backup Jobs aus Cluster-Config — nur Jobs die auf diesem Node laufen
|
||||
cmd := exec.Command("/usr/bin/pvesh", "get", "/cluster/backup", "--output-format", "json")
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
var allJobs []map[string]interface{}
|
||||
if err := json.Unmarshal(out, &allJobs); err == nil {
|
||||
for _, j := range allJobs {
|
||||
// next-run -> next_run umbenennen fuer Frontend-Kompatibilitaet
|
||||
if nr, ok := j["next-run"]; ok {
|
||||
j["next_run"] = nr
|
||||
delete(j, "next-run")
|
||||
}
|
||||
// Filtern: Job gehoert zu diesem Node wenn:
|
||||
// - node == unser hostname
|
||||
// - node leer/nicht gesetzt (= alle Nodes)
|
||||
jobNode, _ := j["node"].(string)
|
||||
if jobNode == "" || strings.EqualFold(jobNode, nodeName) {
|
||||
result.Jobs = append(result.Jobs, j)
|
||||
}
|
||||
}
|
||||
log.Printf("Backups: %d/%d Jobs fuer Node %s", len(result.Jobs), len(allJobs), nodeName)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Backups: Jobs laden fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
since := fmt.Sprintf("%d", time.Now().AddDate(0, 0, -30).Unix())
|
||||
cmd = exec.Command("/usr/bin/pvesh", "get", "/nodes/"+nodeName+"/tasks",
|
||||
"--output-format", "json",
|
||||
"--typefilter", "vzdump",
|
||||
"--limit", "500",
|
||||
"--since", since,
|
||||
)
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
var tasks []BackupTask
|
||||
if err := json.Unmarshal(out, &tasks); err == nil {
|
||||
result.Tasks = tasks
|
||||
log.Printf("Backups: %d Tasks (letzte 30 Tage)", len(tasks))
|
||||
} else {
|
||||
log.Printf("Backups: Tasks JSON parse fehler: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Backups: Tasks laden fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CPUInfo struct {
|
||||
Model string
|
||||
Cores int
|
||||
Threads int
|
||||
FreqMHz int
|
||||
UsagePercent float64
|
||||
}
|
||||
|
||||
func CollectCPU() CPUInfo {
|
||||
cpu := CPUInfo{}
|
||||
|
||||
// CPU-Informationen aus /proc/cpuinfo lesen
|
||||
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
cpuCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "model name":
|
||||
if cpu.Model == "" { // Nur vom ersten CPU nehmen
|
||||
cpu.Model = value
|
||||
}
|
||||
case "cpu MHz":
|
||||
if cpu.FreqMHz == 0 { // Nur vom ersten CPU nehmen
|
||||
if freq, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
cpu.FreqMHz = int(freq)
|
||||
}
|
||||
}
|
||||
case "processor":
|
||||
cpuCount++
|
||||
case "cpu cores":
|
||||
if cores, err := strconv.Atoi(value); err == nil && cpu.Cores == 0 {
|
||||
cpu.Cores = cores
|
||||
}
|
||||
case "siblings":
|
||||
if threads, err := strconv.Atoi(value); err == nil && cpu.Threads == 0 {
|
||||
cpu.Threads = threads
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Falls cpu cores und siblings nicht vorhanden, nutze processor count
|
||||
if cpu.Cores == 0 {
|
||||
cpu.Cores = cpuCount
|
||||
}
|
||||
if cpu.Threads == 0 {
|
||||
cpu.Threads = cpuCount
|
||||
}
|
||||
}
|
||||
|
||||
// CPU-Usage berechnen
|
||||
cpu.UsagePercent = calculateCPUUsage()
|
||||
|
||||
return cpu
|
||||
}
|
||||
|
||||
// Berechnet die aktuelle CPU-Auslastung über zwei /proc/stat Messungen
|
||||
func calculateCPUUsage() float64 {
|
||||
stat1 := readCPUStat()
|
||||
if stat1 == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Kurz warten für zweite Messung
|
||||
|
||||
stat2 := readCPUStat()
|
||||
if stat2 == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Delta berechnen
|
||||
totalDelta := (stat2.total - stat1.total)
|
||||
idleDelta := (stat2.idle - stat1.idle)
|
||||
|
||||
if totalDelta <= 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
usage := 100.0 * float64(totalDelta-idleDelta) / float64(totalDelta)
|
||||
if usage < 0 {
|
||||
usage = 0
|
||||
}
|
||||
if usage > 100 {
|
||||
usage = 100
|
||||
}
|
||||
|
||||
return usage
|
||||
}
|
||||
|
||||
type cpuStat struct {
|
||||
total int64
|
||||
idle int64
|
||||
}
|
||||
|
||||
// Liest CPU-Statistiken aus /proc/stat
|
||||
func readCPUStat() *cpuStat {
|
||||
data, err := os.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Erste Zeile sollte "cpu ..." sein
|
||||
firstLine := strings.TrimSpace(lines[0])
|
||||
if !strings.HasPrefix(firstLine, "cpu ") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := strings.Fields(firstLine)
|
||||
if len(fields) < 5 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cpu user nice system idle iowait irq softirq ...
|
||||
var values []int64
|
||||
for i := 1; i < len(fields) && i <= 7; i++ { // max 7 Werte lesen
|
||||
if val, err := strconv.ParseInt(fields[i], 10, 64); err == nil {
|
||||
values = append(values, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) < 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Total = Summe aller Werte
|
||||
total := int64(0)
|
||||
for _, val := range values {
|
||||
total += val
|
||||
}
|
||||
|
||||
// Idle = idle + iowait (falls iowait verfügbar)
|
||||
idle := values[3] // idle
|
||||
if len(values) > 4 {
|
||||
idle += values[4] // + iowait
|
||||
}
|
||||
|
||||
return &cpuStat{
|
||||
total: total,
|
||||
idle: idle,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type DiskInfo struct {
|
||||
Filesystem string `json:"filesystem"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
FreeBytes int64 `json:"free_bytes"`
|
||||
MountPoint string `json:"mount_point"`
|
||||
}
|
||||
|
||||
func CollectDisks() []DiskInfo {
|
||||
var disks []DiskInfo
|
||||
|
||||
// df -T zur Anzeige aller mounted filesystems
|
||||
output, err := exec.Command("/usr/bin/df", "-T", "-P").CombinedOutput()
|
||||
if err != nil {
|
||||
return disks
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) < 2 {
|
||||
return disks
|
||||
}
|
||||
|
||||
// Erste Zeile ist Header, überspringen
|
||||
for _, line := range lines[1:] {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
filesystem := fields[0]
|
||||
fstype := fields[1]
|
||||
totalKB := fields[2]
|
||||
usedKB := fields[3]
|
||||
availKB := fields[4]
|
||||
mountpoint := fields[6]
|
||||
|
||||
// Überspringe spezielle Filesysteme
|
||||
if shouldSkipFilesystem(filesystem, fstype, mountpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Konvertiere kB zu Bytes
|
||||
total, _ := strconv.ParseInt(totalKB, 10, 64)
|
||||
used, _ := strconv.ParseInt(usedKB, 10, 64)
|
||||
avail, _ := strconv.ParseInt(availKB, 10, 64)
|
||||
|
||||
// Für bessere Genauigkeit: Versuche statvfs syscall
|
||||
if stat := getStatvfs(mountpoint); stat != nil {
|
||||
total = int64(stat.Blocks) * int64(stat.Bsize)
|
||||
avail = int64(stat.Bavail) * int64(stat.Bsize)
|
||||
used = total - avail
|
||||
} else {
|
||||
// Fallback: df-Werte in Bytes konvertieren
|
||||
total *= 1024
|
||||
used *= 1024
|
||||
avail *= 1024
|
||||
}
|
||||
|
||||
disks = append(disks, DiskInfo{
|
||||
Filesystem: filesystem,
|
||||
TotalBytes: total,
|
||||
UsedBytes: used,
|
||||
FreeBytes: avail,
|
||||
MountPoint: mountpoint,
|
||||
})
|
||||
}
|
||||
|
||||
return disks
|
||||
}
|
||||
|
||||
// Bestimmt ob ein Filesystem übersprungen werden soll
|
||||
func shouldSkipFilesystem(filesystem, fstype, mountpoint string) bool {
|
||||
// Überspringe virtuelle Filesysteme
|
||||
skipTypes := []string{
|
||||
"proc", "sysfs", "devfs", "devtmpfs", "tmpfs", "debugfs",
|
||||
"securityfs", "cgroup", "cgroup2", "pstore", "bpf",
|
||||
"tracefs", "configfs", "fusectl", "binfmt_misc",
|
||||
"autofs", "mqueue", "hugetlbfs", "sunrpc",
|
||||
}
|
||||
|
||||
for _, skipType := range skipTypes {
|
||||
if fstype == skipType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Überspringe spezielle Mount-Points
|
||||
skipMounts := []string{
|
||||
"/dev", "/proc", "/sys", "/run", "/tmp",
|
||||
"/dev/shm", "/run/lock", "/run/user",
|
||||
}
|
||||
|
||||
for _, skipMount := range skipMounts {
|
||||
if strings.HasPrefix(mountpoint, skipMount) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Überspringe Loop-Devices außer root
|
||||
if strings.HasPrefix(filesystem, "/dev/loop") && mountpoint != "/" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Überspringe Bind-Mounts (heuristic)
|
||||
if strings.Contains(mountpoint, "/snap/") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Holt präzise Filesystem-Statistiken via statvfs syscall
|
||||
func getStatvfs(path string) *syscall.Statfs_t {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &stat
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MemoryInfo struct {
|
||||
TotalBytes int64
|
||||
UsedBytes int64
|
||||
FreeBytes int64
|
||||
}
|
||||
|
||||
func CollectMemory() MemoryInfo {
|
||||
mem := MemoryInfo{}
|
||||
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return mem
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
values := make(map[string]int64)
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSuffix(parts[0], ":")
|
||||
valueStr := parts[1]
|
||||
|
||||
// Wert in kB, konvertiere zu Bytes
|
||||
if value, err := strconv.ParseInt(valueStr, 10, 64); err == nil {
|
||||
values[key] = value * 1024 // kB zu Bytes
|
||||
}
|
||||
}
|
||||
|
||||
// Basis-Werte
|
||||
memTotal := values["MemTotal"]
|
||||
memFree := values["MemFree"]
|
||||
memAvailable := values["MemAvailable"]
|
||||
buffers := values["Buffers"]
|
||||
cached := values["Cached"]
|
||||
|
||||
mem.TotalBytes = memTotal
|
||||
|
||||
// Verfügbarer Speicher: Nutze MemAvailable falls vorhanden, sonst berechne
|
||||
if memAvailable > 0 {
|
||||
mem.FreeBytes = memAvailable
|
||||
} else {
|
||||
// Fallback: Free + Buffers + Cached (grobe Schätzung)
|
||||
mem.FreeBytes = memFree + buffers + cached
|
||||
}
|
||||
|
||||
// Benutzter Speicher = Total - Free
|
||||
if mem.FreeBytes > mem.TotalBytes {
|
||||
mem.FreeBytes = mem.TotalBytes
|
||||
}
|
||||
mem.UsedBytes = mem.TotalBytes - mem.FreeBytes
|
||||
|
||||
return mem
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NetworkInterface struct {
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
IP string `json:"ip"`
|
||||
MAC string `json:"mac"`
|
||||
Status string `json:"status"`
|
||||
RxBytes int64 `json:"rx_bytes"`
|
||||
TxBytes int64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
func CollectNetwork() []NetworkInterface {
|
||||
var interfaces []NetworkInterface
|
||||
|
||||
// Interface-Statistiken aus /proc/net/dev lesen
|
||||
stats := readNetDevStats()
|
||||
|
||||
// Interface-Informationen aus /sys/class/net/* sammeln
|
||||
entries, err := os.ReadDir("/sys/class/net")
|
||||
if err != nil {
|
||||
return interfaces
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
ifName := entry.Name()
|
||||
|
||||
// Überspringe loopback
|
||||
if ifName == "lo" {
|
||||
continue
|
||||
}
|
||||
|
||||
iface := NetworkInterface{
|
||||
Name: ifName,
|
||||
Role: determineInterfaceRole(ifName),
|
||||
}
|
||||
|
||||
// Status aus operstate lesen
|
||||
if data, err := os.ReadFile("/sys/class/net/" + ifName + "/operstate"); err == nil {
|
||||
state := strings.TrimSpace(string(data))
|
||||
if state == "up" {
|
||||
iface.Status = "up"
|
||||
} else {
|
||||
iface.Status = "down"
|
||||
}
|
||||
}
|
||||
|
||||
// MAC-Adresse lesen
|
||||
if data, err := os.ReadFile("/sys/class/net/" + ifName + "/address"); err == nil {
|
||||
iface.MAC = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// Statistiken hinzufügen
|
||||
if stat, exists := stats[ifName]; exists {
|
||||
iface.RxBytes = stat.rxBytes
|
||||
iface.TxBytes = stat.txBytes
|
||||
}
|
||||
|
||||
// IP-Adresse über ip addr bestimmen
|
||||
iface.IP = getInterfaceIP(ifName)
|
||||
|
||||
interfaces = append(interfaces, iface)
|
||||
}
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
// Bestimmt die Rolle eines Interfaces basierend auf dem Namen
|
||||
func determineInterfaceRole(ifName string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(ifName, "eth"):
|
||||
return "lan"
|
||||
case strings.HasPrefix(ifName, "ens"):
|
||||
return "lan"
|
||||
case strings.HasPrefix(ifName, "enp"):
|
||||
return "lan"
|
||||
case strings.HasPrefix(ifName, "wlan"):
|
||||
return "wan"
|
||||
case strings.HasPrefix(ifName, "wlp"):
|
||||
return "wan"
|
||||
case strings.HasPrefix(ifName, "ppp"):
|
||||
return "wan"
|
||||
case strings.HasPrefix(ifName, "tun"):
|
||||
return "vpn"
|
||||
case strings.HasPrefix(ifName, "tap"):
|
||||
return "vpn"
|
||||
case strings.HasPrefix(ifName, "br"):
|
||||
return "bridge"
|
||||
case strings.HasPrefix(ifName, "docker"):
|
||||
return "virtual"
|
||||
case strings.HasPrefix(ifName, "veth"):
|
||||
return "virtual"
|
||||
case strings.HasPrefix(ifName, "virbr"):
|
||||
return "virtual"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// Liest die erste IPv4-Adresse eines Interfaces
|
||||
func getInterfaceIP(ifName string) string {
|
||||
output, err := exec.Command("/usr/sbin/ip", "-4", "addr", "show", ifName).CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "inet ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
// inet 192.168.1.100/24 ... -> 192.168.1.100
|
||||
ipCidr := fields[1]
|
||||
if parts := strings.SplitN(ipCidr, "/", 2); len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Network-Statistiken
|
||||
type netDevStat struct {
|
||||
rxBytes int64
|
||||
txBytes int64
|
||||
}
|
||||
|
||||
// Liest Interface-Statistiken aus /proc/net/dev
|
||||
func readNetDevStats() map[string]netDevStat {
|
||||
stats := make(map[string]netDevStat)
|
||||
|
||||
data, err := os.ReadFile("/proc/net/dev")
|
||||
if err != nil {
|
||||
return stats
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) < 3 {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Ersten 2 Zeilen sind Header
|
||||
for _, line := range lines[2:] {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Interface: rx_bytes rx_packets ... tx_bytes tx_packets ...
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
ifName := strings.TrimSpace(parts[0])
|
||||
values := strings.Fields(strings.TrimSpace(parts[1]))
|
||||
|
||||
if len(values) >= 9 {
|
||||
// rx_bytes ist das erste Feld, tx_bytes das 9te Feld (0-basiert: Index 8)
|
||||
rxBytes, _ := strconv.ParseInt(values[0], 10, 64)
|
||||
txBytes, _ := strconv.ParseInt(values[8], 10, 64)
|
||||
|
||||
stats[ifName] = netDevStat{
|
||||
rxBytes: rxBytes,
|
||||
txBytes: txBytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
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 ""
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ServiceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func CollectServices() []ServiceInfo {
|
||||
var services []ServiceInfo
|
||||
|
||||
// systemctl list-units --type=service --all --no-pager --no-legend
|
||||
output, err := exec.Command("/usr/bin/systemctl", "list-units", "--type=service", "--all", "--no-pager", "--no-legend").CombinedOutput()
|
||||
if err != nil {
|
||||
return services
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Format: UNIT LOAD ACTIVE SUB DESCRIPTION
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
serviceName := fields[0]
|
||||
load := fields[1]
|
||||
active := fields[2]
|
||||
sub := fields[3]
|
||||
|
||||
// Beschreibung ist der Rest ab Feld 4
|
||||
description := ""
|
||||
if len(fields) > 4 {
|
||||
description = strings.Join(fields[4:], " ")
|
||||
}
|
||||
|
||||
// Status bestimmen
|
||||
status := determineServiceStatus(load, active, sub)
|
||||
|
||||
// .service suffix entfernen für bessere Lesbarkeit
|
||||
displayName := strings.TrimSuffix(serviceName, ".service")
|
||||
|
||||
services = append(services, ServiceInfo{
|
||||
Name: displayName,
|
||||
Description: description,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// Bestimmt den Service-Status basierend auf systemctl Ausgabe
|
||||
func determineServiceStatus(load, active, sub string) string {
|
||||
// Priorität auf active state
|
||||
switch active {
|
||||
case "active":
|
||||
// Wenn active, schaue auf sub-state
|
||||
switch sub {
|
||||
case "running":
|
||||
return "running"
|
||||
case "exited":
|
||||
return "stopped" // one-shot service, beendet
|
||||
default:
|
||||
return "active"
|
||||
}
|
||||
case "inactive":
|
||||
return "stopped"
|
||||
case "failed":
|
||||
return "error"
|
||||
case "activating":
|
||||
return "starting"
|
||||
case "deactivating":
|
||||
return "stopping"
|
||||
default:
|
||||
// Falls load state problematisch ist
|
||||
if load == "not-found" {
|
||||
return "not-found"
|
||||
} else if load == "error" {
|
||||
return "error"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Basis-Systemdaten sammeln
|
||||
|
||||
func CollectHostname() string {
|
||||
if data, err := os.ReadFile("/etc/hostname"); err == nil {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
return hostname
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func CollectOSVersion() string {
|
||||
// Versuche verschiedene Wege die OS-Version zu bestimmen
|
||||
|
||||
// 1. /etc/os-release (Standard für moderne Linux-Distributionen)
|
||||
if data, err := os.ReadFile("/etc/os-release"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var name, version string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "NAME=") {
|
||||
name = strings.Trim(strings.SplitN(line, "=", 2)[1], "\"")
|
||||
} else if strings.HasPrefix(line, "VERSION=") {
|
||||
version = strings.Trim(strings.SplitN(line, "=", 2)[1], "\"")
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
if version != "" {
|
||||
return name + " " + version
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// 2. /etc/debian_version (Debian/Ubuntu)
|
||||
if data, err := os.ReadFile("/etc/debian_version"); err == nil {
|
||||
version := strings.TrimSpace(string(data))
|
||||
return "Debian " + version
|
||||
}
|
||||
|
||||
// 3. /etc/redhat-release (RHEL/CentOS)
|
||||
if data, err := os.ReadFile("/etc/redhat-release"); err == nil {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// 4. uname fallback
|
||||
if output, err := exec.Command("/usr/bin/uname", "-sr").CombinedOutput(); err == nil {
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
return "Linux (unknown)"
|
||||
}
|
||||
|
||||
func CollectUptime() int64 {
|
||||
if data, err := os.ReadFile("/proc/uptime"); err == nil {
|
||||
parts := strings.Fields(string(data))
|
||||
if len(parts) > 0 {
|
||||
if uptime, err := strconv.ParseFloat(parts[0], 64); err == nil {
|
||||
return int64(uptime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Boot-Zeit berechnen
|
||||
if stat, err := os.Stat("/proc/1"); err == nil {
|
||||
bootTime := stat.ModTime()
|
||||
return int64(time.Since(bootTime).Seconds())
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Hardware-Informationen sammeln
|
||||
type HardwareInfo struct {
|
||||
Manufacturer string
|
||||
Model string
|
||||
Serial string
|
||||
BIOSVersion string
|
||||
}
|
||||
|
||||
func CollectHardware() HardwareInfo {
|
||||
hw := HardwareInfo{}
|
||||
|
||||
// DMI-Informationen aus /sys/class/dmi/id/ lesen
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/sys_vendor"); err == nil {
|
||||
hw.Manufacturer = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/product_name"); err == nil {
|
||||
hw.Model = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/product_serial"); err == nil {
|
||||
hw.Serial = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/bios_version"); err == nil {
|
||||
hw.BIOSVersion = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// Fallback für Manufacturer
|
||||
if hw.Manufacturer == "" {
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/board_vendor"); err == nil {
|
||||
hw.Manufacturer = strings.TrimSpace(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback für Model
|
||||
if hw.Model == "" {
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/board_name"); err == nil {
|
||||
hw.Model = strings.TrimSpace(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return hw
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UpdateInfo struct {
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
Updates []PendingUpdateInfo `json:"updates"`
|
||||
}
|
||||
|
||||
type PendingUpdateInfo struct {
|
||||
Package string `json:"package"`
|
||||
CurrentVer string `json:"current_version"`
|
||||
NewVer string `json:"new_version"`
|
||||
}
|
||||
|
||||
func CollectUpdates() *UpdateInfo {
|
||||
updates := &UpdateInfo{
|
||||
UpdateAvailable: false,
|
||||
PendingCount: 0,
|
||||
Updates: []PendingUpdateInfo{},
|
||||
}
|
||||
|
||||
// apt list --upgradable 2>/dev/null
|
||||
cmd := exec.Command("/usr/bin/apt", "list", "--upgradable")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Fallback: apt-check falls verfügbar
|
||||
return collectUpdatesAptCheck()
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "WARNING:") || strings.HasPrefix(line, "Listing...") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Format: package/suite version [upgradable from: old-version]
|
||||
// Beispiel: curl/focal-updates 7.68.0-1ubuntu2.7 amd64 [upgradable from: 7.68.0-1ubuntu2.6]
|
||||
|
||||
if !strings.Contains(line, "[upgradable from:") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Package-Name extrahieren (bis zum ersten /)
|
||||
parts := strings.SplitN(line, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
packageName := parts[0]
|
||||
|
||||
// Neue Version extrahieren (zwischen / und [upgradable from:)
|
||||
remaining := parts[1]
|
||||
beforeUpgradable := strings.Split(remaining, " [upgradable from:")
|
||||
if len(beforeUpgradable) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Neue Version ist im ersten Teil, aber wir müssen die Architektur entfernen
|
||||
versionParts := strings.Fields(beforeUpgradable[0])
|
||||
if len(versionParts) < 1 {
|
||||
continue
|
||||
}
|
||||
newVersion := versionParts[0]
|
||||
|
||||
// Alte Version extrahieren (zwischen "from:" und "]")
|
||||
oldVersionPart := beforeUpgradable[1]
|
||||
oldVersion := strings.TrimSuffix(strings.TrimSpace(oldVersionPart), "]")
|
||||
|
||||
updates.Updates = append(updates.Updates, PendingUpdateInfo{
|
||||
Package: packageName,
|
||||
CurrentVer: oldVersion,
|
||||
NewVer: newVersion,
|
||||
})
|
||||
}
|
||||
|
||||
updates.PendingCount = len(updates.Updates)
|
||||
updates.UpdateAvailable = updates.PendingCount > 0
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Fallback-Methode mit apt-check (Ubuntu/Debian)
|
||||
func collectUpdatesAptCheck() *UpdateInfo {
|
||||
updates := &UpdateInfo{
|
||||
UpdateAvailable: false,
|
||||
PendingCount: 0,
|
||||
Updates: []PendingUpdateInfo{},
|
||||
}
|
||||
|
||||
// /usr/lib/update-notifier/apt-check
|
||||
cmd := exec.Command("/usr/lib/update-notifier/apt-check")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Noch ein Fallback: apt-get -s upgrade
|
||||
return collectUpdatesAptGetSimulate()
|
||||
}
|
||||
|
||||
// apt-check gibt "updates;security-updates" auf stderr aus
|
||||
result := strings.TrimSpace(string(output))
|
||||
parts := strings.SplitN(result, ";", 2)
|
||||
|
||||
if len(parts) >= 1 && parts[0] != "0" {
|
||||
// Packages verfügbar, aber keine Details ohne apt list
|
||||
updates.UpdateAvailable = true
|
||||
updates.PendingCount = 1 // Platzhalter
|
||||
updates.Updates = []PendingUpdateInfo{
|
||||
{
|
||||
Package: "system-updates",
|
||||
CurrentVer: "various",
|
||||
NewVer: "available",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Letzter Fallback mit apt-get simulate
|
||||
func collectUpdatesAptGetSimulate() *UpdateInfo {
|
||||
updates := &UpdateInfo{
|
||||
UpdateAvailable: false,
|
||||
PendingCount: 0,
|
||||
Updates: []PendingUpdateInfo{},
|
||||
}
|
||||
|
||||
// apt-get -s upgrade (simulate)
|
||||
cmd := exec.Command("/usr/bin/apt-get", "-s", "upgrade")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return updates
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Suche nach "Inst package-name" Zeilen
|
||||
if strings.HasPrefix(line, "Inst ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 3 {
|
||||
packageName := fields[1]
|
||||
|
||||
// Sehr simple Erkennung, bessere Parsing schwierig ohne apt list
|
||||
updates.Updates = append(updates.Updates, PendingUpdateInfo{
|
||||
Package: packageName,
|
||||
CurrentVer: "unknown",
|
||||
NewVer: "available",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updates.PendingCount = len(updates.Updates)
|
||||
updates.UpdateAvailable = updates.PendingCount > 0
|
||||
|
||||
return updates
|
||||
}
|
||||
Reference in New Issue
Block a user