rmm2/agent/main.go
cynfo3000 e74cdce0ba Agent-Updater, Multi-Plattform Firmware, Frontend Firmware-Seite, Agent-Tab, Zertifikate-Redesign
Updater:
- Separater Go-Daemon (rmm-updater) als eigener rc.d Service
- Pollt alle 60s: Firmware vorhanden + Hash unterschiedlich + Flag gesetzt
- Download -> SHA256 Verify -> Agent Stop -> Replace -> Agent Start
- Automatisches Rollback bei Fehler
- Deployed und getestet auf allen 3 Firewalls (.1, .19, .33)

Backend:
- Firmware-DB multi-plattform (freebsd/linux/windows)
- update_requested Flag pro Agent in agents Tabelle
- Heartbeat-Response liefert update_available wenn Flag gesetzt
- Neue Endpoints: request-update, request-update-all, firmware/download
- Auth: JWT + CombinedAuth Middleware (API-Key OR JWT)
- Customers CRUD + Agent-Zuordnung
- Users CRUD + Passwort-Aenderung

Frontend:
- Firmware-Seite: Multi-Plattform Upload, Agent-Liste mit Update-Trigger
- Agent-Tab im AgentPanel: Update-Button, nuetzliche Befehle, Agent-Info
- Zertifikate-Tab redesigned: Karten-Layout mit Restlaufzeit und Farbbalken
- Sidebar: Firmware-Eintrag hinzugefuegt

Plugin:
- install.sh erweitert: Installiert Agent + Updater + rc.d Services
- rmm_updater rc.d Service

README:
- Architektur-Diagramm erweitert (Frontend + Updater)
- Auth, Customers, Users, Firmware API dokumentiert
- Updater-Flow dokumentiert
- Frontend-Seiten und Deploy beschrieben
- DB-Schema aktualisiert (neue Tabellen)
- Dateistruktur erweitert (updater/, frontend/, firmware.go, auth.go)
2026-02-28 22:43:03 +01:00

189 lines
5.0 KiB
Go

package main
import (
"flag"
"log"
"math"
"os"
"path/filepath"
"strings"
"time"
"github.com/cynfo/rmm-agent/client"
"github.com/cynfo/rmm-agent/collector"
"github.com/cynfo/rmm-agent/config"
"github.com/cynfo/rmm-agent/ws"
)
var Version = "dev"
func main() {
cfgPath := flag.String("config", "config.yaml", "Pfad zur Konfigurationsdatei")
insecure := flag.Bool("insecure", false, "TLS-Zertifikatpruefung deaktivieren")
flag.Parse()
cfg, err := config.Load(*cfgPath)
if err != nil {
log.Fatalf("Konfiguration laden fehlgeschlagen: %v", err)
}
if *insecure {
cfg.Insecure = true
}
log.Printf("RMM Agent v%s startet: %s", Version, cfg.AgentName)
log.Printf("Backend: %s, Intervall: %ds", cfg.BackendURL, cfg.IntervalSeconds)
c := client.New(cfg.BackendURL, cfg.APIKey, cfg.Insecure)
// Agent-ID persistent laden oder neu registrieren
idFile := filepath.Join(filepath.Dir(*cfgPath), "agent_id")
var agentID string
if data, err := os.ReadFile(idFile); err == nil && len(strings.TrimSpace(string(data))) > 0 {
agentID = strings.TrimSpace(string(data))
log.Printf("Agent-ID aus Datei geladen: %s", agentID)
}
// Registrierung mit Retry (sendet bestehende ID mit, falls vorhanden)
backoff := 5 * time.Second
for {
hostname := collector.CollectHostname()
if hostname == "" {
hostname, _ = os.Hostname()
}
regReq := map[string]string{
"name": cfg.AgentName,
"hostname": hostname,
"ip": getLocalIP(),
"opnsense_version": collector.CollectOPNsenseVersion(),
"agent_version": Version,
}
if agentID != "" {
regReq["agent_id"] = agentID
}
newID, regErr := c.Register(regReq)
if regErr != nil {
log.Printf("Registrierung fehlgeschlagen: %v (Retry in %v)", regErr, backoff)
time.Sleep(backoff)
backoff = time.Duration(math.Min(float64(backoff*2), float64(5*time.Minute)))
continue
}
agentID = newID
break
}
// ID persistent speichern
if err := os.WriteFile(idFile, []byte(agentID), 0600); err != nil {
log.Printf("WARNUNG: Agent-ID konnte nicht gespeichert werden: %v", err)
}
log.Printf("Agent registriert mit ID: %s", agentID)
// WebSocket-Client starten
wsClient := ws.NewClient(cfg.BackendURL, agentID, cfg.APIKey, cfg.Insecure)
if err := wsClient.Start(); err != nil {
log.Printf("WebSocket-Client starten fehlgeschlagen: %v", err)
}
defer wsClient.Stop()
// Heartbeat-Loop
ticker := time.NewTicker(time.Duration(cfg.IntervalSeconds) * time.Second)
defer ticker.Stop()
// Ersten Heartbeat sofort senden
sendHeartbeat(c, agentID)
backoff = 5 * time.Second
for range ticker.C {
if err := sendHeartbeat(c, agentID); err != nil {
log.Printf("Heartbeat fehlgeschlagen: %v", err)
backoff = time.Duration(math.Min(float64(backoff*2), float64(5*time.Minute)))
} else {
backoff = 5 * time.Second
}
}
}
func sendHeartbeat(c *client.Client, agentID string) error {
log.Println("Sammle Systemdaten...")
hw := collector.CollectHardware()
cpu := collector.CollectCPU()
mem := collector.CollectMemory()
disks := collector.CollectDisks()
net := collector.CollectNetwork()
svcs := collector.CollectServices()
wg := collector.CollectWireGuard()
dhcp := collector.CollectDHCP()
routes := collector.CollectRoutes()
gateways := collector.CollectGateways()
certs := collector.CollectCertificates()
plugins := collector.CollectPlugins()
updates := collector.CollectUpdates()
cronJobs := collector.CollectCron()
req := map[string]interface{}{
"agent_id": agentID,
"agent_version": Version,
"system_data": map[string]interface{}{
"agent_id": agentID,
"hostname": collector.CollectHostname(),
"opnsense_version": collector.CollectOPNsenseVersion(),
"freebsd_version": collector.CollectFreeBSDVersion(),
"uptime_seconds": collector.CollectUptime(),
"hardware": map[string]interface{}{
"manufacturer": hw.Manufacturer,
"model": hw.Model,
"serial": hw.Serial,
"bios_version": hw.BIOSVersion,
},
"cpu": map[string]interface{}{
"model": cpu.Model,
"cores": cpu.Cores,
"threads": cpu.Threads,
"freq_mhz": cpu.FreqMHz,
"usage_percent": cpu.UsagePercent,
},
"memory": map[string]interface{}{
"total_bytes": mem.TotalBytes,
"used_bytes": mem.UsedBytes,
"free_bytes": mem.FreeBytes,
},
"disks": disks,
"network_interfaces": net,
"services": svcs,
"wireguard": wg,
"dhcp": dhcp,
"routes": routes,
"gateways": gateways,
"certificates": certs,
"plugins": plugins,
"updates": updates,
"cron_jobs": cronJobs,
},
}
if err := c.Heartbeat(req); err != nil {
return err
}
log.Println("Heartbeat gesendet")
return nil
}
// getLocalIP versucht die primaere IP-Adresse zu ermitteln
func getLocalIP() string {
// Versuche ueber ifconfig die erste nicht-loopback IP
ifaces := collector.CollectNetwork()
for _, iface := range ifaces {
if iface.IP != "" && iface.Status == "up" {
return iface.IP
}
}
return ""
}