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:
cynfo3000
2026-03-01 22:03:18 +01:00
parent c114df215a
commit 7f9fbd257f
39 changed files with 5775 additions and 112 deletions
+61 -42
View File
@@ -11,13 +11,12 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"gopkg.in/yaml.v3"
)
// Config wird aus der gleichen config.yaml wie der Agent gelesen
type Config struct {
BackendURL string `yaml:"backend_url"`
APIKey string `yaml:"api_key"`
@@ -25,26 +24,57 @@ type Config struct {
Insecure bool `yaml:"insecure"`
}
type HeartbeatResponse struct {
Message string `json:"message"`
UpdateAvailable bool `json:"update_available"`
UpdateVersion string `json:"update_version"`
UpdateHash string `json:"update_hash"`
// Plattform-spezifische Pfade und Befehle
var (
agentBinaryPath string
configPath string
agentIDPath string
platformName string
)
func init() {
switch runtime.GOOS {
case "freebsd":
agentBinaryPath = "/usr/local/rmm/rmm-agent"
configPath = "/usr/local/rmm/config.yaml"
agentIDPath = "/usr/local/rmm/agent_id"
platformName = "freebsd"
default: // linux
agentBinaryPath = "/usr/local/bin/rmm-agent"
configPath = "/etc/rmm/config.yaml"
agentIDPath = "/etc/rmm/agent_id"
platformName = "linux"
}
}
func stopAgent() error {
switch runtime.GOOS {
case "freebsd":
return exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run()
default:
return exec.Command("/usr/bin/systemctl", "stop", "rmm-agent").Run()
}
}
func startAgent() error {
switch runtime.GOOS {
case "freebsd":
return exec.Command("/usr/sbin/service", "rmm_agent", "start").Run()
default:
return exec.Command("/usr/bin/systemctl", "start", "rmm-agent").Run()
}
}
const (
agentBinaryPath = "/usr/local/rmm/rmm-agent"
configPath = "/usr/local/rmm/config.yaml"
agentIDPath = "/usr/local/rmm/agent_id"
checkInterval = 60 * time.Second
logPrefix = "[rmm-updater] "
checkInterval = 60 * time.Second
logPrefix = "[rmm-updater] "
)
func main() {
log.SetPrefix(logPrefix)
log.SetFlags(log.LstdFlags)
log.Println("RMM Updater gestartet")
log.Printf("RMM Updater gestartet (platform=%s, binary=%s)", platformName, agentBinaryPath)
cfg, err := loadConfig()
if err != nil {
@@ -80,9 +110,6 @@ func loadConfig() (*Config, error) {
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
// Default: insecure wenn nicht explizit gesetzt (self-signed certs sind Standard)
// yaml unmarshal setzt false als default fuer bool — wir brauchen true als default
// Workaround: immer insecure wenn Backend self-signed nutzt
cfg.Insecure = true
return &cfg, nil
}
@@ -100,8 +127,8 @@ func loadAgentID() (string, error) {
}
func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
// Firmware-Info vom Backend holen
url := fmt.Sprintf("%s/api/v1/firmware", cfg.BackendURL)
// Firmware-Info vom Backend holen (plattformspezifisch)
url := fmt.Sprintf("%s/api/v1/firmware?platform=%s", cfg.BackendURL, platformName)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-API-Key", cfg.APIKey)
@@ -113,7 +140,7 @@ func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
defer resp.Body.Close()
if resp.StatusCode != 200 {
return // Keine Firmware vorhanden
return
}
var info struct {
@@ -128,35 +155,34 @@ func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
// Pruefen ob lokale Binary schon die richtige Version hat
if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash {
return // Schon aktuell
return
}
// Heartbeat-Response pruefen: Update angefordert?
hbURL := fmt.Sprintf("%s/api/v1/agents/%s", cfg.BackendURL, agentID)
hbReq, _ := http.NewRequest("GET", hbURL, nil)
hbReq.Header.Set("X-API-Key", cfg.APIKey)
// Update angefordert?
agentURL := fmt.Sprintf("%s/api/v1/agents/%s", cfg.BackendURL, agentID)
agentReq, _ := http.NewRequest("GET", agentURL, nil)
agentReq.Header.Set("X-API-Key", cfg.APIKey)
hbResp, err := client.Do(hbReq)
agentResp, err := client.Do(agentReq)
if err != nil {
return
}
defer hbResp.Body.Close()
defer agentResp.Body.Close()
var agentInfo struct {
Agent struct {
UpdateRequested bool `json:"update_requested"`
} `json:"agent"`
}
if err := json.NewDecoder(hbResp.Body).Decode(&agentInfo); err != nil {
if err := json.NewDecoder(agentResp.Body).Decode(&agentInfo); err != nil {
return
}
if !agentInfo.Agent.UpdateRequested {
return // Kein Update angefordert
return
}
log.Printf("Update verfuegbar: v%s (%d bytes)", info.Version, info.Size)
log.Printf("Update verfuegbar: v%s (%d bytes, platform=%s)", info.Version, info.Size, platformName)
// Binary herunterladen
if err := downloadAndApply(client, cfg, agentID, info.Hash); err != nil {
log.Printf("Update fehlgeschlagen: %v", err)
return
@@ -166,8 +192,7 @@ func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
}
func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash string) error {
// Download
url := fmt.Sprintf("%s/api/v1/firmware/download", cfg.BackendURL)
url := fmt.Sprintf("%s/api/v1/firmware/download?platform=%s", cfg.BackendURL, platformName)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-API-Key", cfg.APIKey)
@@ -203,16 +228,15 @@ func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash st
// Agent stoppen
log.Println("Agent wird gestoppt...")
exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run()
stopAgent()
time.Sleep(2 * time.Second)
// Binary ersetzen
// Binary ersetzen mit Rollback
backupPath := agentBinaryPath + ".bak"
if _, err := os.Stat(agentBinaryPath); err == nil {
os.Rename(agentBinaryPath, backupPath)
}
if err := os.Rename(tmpPath, agentBinaryPath); err != nil {
// Rollback
os.Rename(backupPath, agentBinaryPath)
return fmt.Errorf("binary ersetzen fehlgeschlagen: %v", err)
}
@@ -220,10 +244,10 @@ func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash st
// Agent starten
log.Println("Agent wird gestartet...")
if err := exec.Command("/usr/sbin/service", "rmm_agent", "start").Run(); err != nil {
if err := startAgent(); err != nil {
log.Printf("WARNUNG: Agent-Start fehlgeschlagen: %v — versuche Rollback", err)
os.Rename(backupPath, agentBinaryPath)
exec.Command("/usr/sbin/service", "rmm_agent", "start").Run()
startAgent()
return fmt.Errorf("agent start fehlgeschlagen")
}
@@ -247,8 +271,3 @@ func hashFile(path string) (string, error) {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}
// resolveDir gibt das Verzeichnis eines Pfads zurueck
func resolveDir(path string) string {
return filepath.Dir(path)
}