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)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
module github.com/cynfo/rmm-updater
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -0,0 +1,4 @@
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"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"`
|
||||
AgentName string `yaml:"agent_name"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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] "
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetPrefix(logPrefix)
|
||||
log.SetFlags(log.LstdFlags)
|
||||
|
||||
log.Println("RMM Updater gestartet")
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Config laden fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
agentID, err := loadAgentID()
|
||||
if err != nil {
|
||||
log.Fatalf("Agent-ID laden fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Backend: %s, Agent: %s (%s)", cfg.BackendURL, cfg.AgentName, agentID)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure},
|
||||
},
|
||||
}
|
||||
|
||||
for {
|
||||
checkAndUpdate(client, cfg, agentID)
|
||||
time.Sleep(checkInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() (*Config, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg Config
|
||||
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
|
||||
}
|
||||
|
||||
func loadAgentID() (string, error) {
|
||||
data, err := os.ReadFile(agentIDPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("agent_id nicht gefunden: %v", err)
|
||||
}
|
||||
id := string(data)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("agent_id leer")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
|
||||
// Firmware-Info vom Backend holen
|
||||
url := fmt.Sprintf("%s/api/v1/firmware", cfg.BackendURL)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("X-API-Key", cfg.APIKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Firmware-Info Abfrage fehlgeschlagen: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return // Keine Firmware vorhanden
|
||||
}
|
||||
|
||||
var info struct {
|
||||
Available bool `json:"available"`
|
||||
Version string `json:"version"`
|
||||
Hash string `json:"hash"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil || !info.Available {
|
||||
return
|
||||
}
|
||||
|
||||
// Pruefen ob lokale Binary schon die richtige Version hat
|
||||
if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash {
|
||||
return // Schon aktuell
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
hbResp, err := client.Do(hbReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer hbResp.Body.Close()
|
||||
|
||||
var agentInfo struct {
|
||||
Agent struct {
|
||||
UpdateRequested bool `json:"update_requested"`
|
||||
} `json:"agent"`
|
||||
}
|
||||
if err := json.NewDecoder(hbResp.Body).Decode(&agentInfo); err != nil {
|
||||
return
|
||||
}
|
||||
if !agentInfo.Agent.UpdateRequested {
|
||||
return // Kein Update angefordert
|
||||
}
|
||||
|
||||
log.Printf("Update verfuegbar: v%s (%d bytes)", info.Version, info.Size)
|
||||
|
||||
// Binary herunterladen
|
||||
if err := downloadAndApply(client, cfg, agentID, info.Hash); err != nil {
|
||||
log.Printf("Update fehlgeschlagen: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Update auf v%s erfolgreich", info.Version)
|
||||
}
|
||||
|
||||
func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash string) error {
|
||||
// Download
|
||||
url := fmt.Sprintf("%s/api/v1/firmware/download", cfg.BackendURL)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("X-API-Key", cfg.APIKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download fehlgeschlagen: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("download HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
binary, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download lesen fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
// Hash verifizieren
|
||||
hash := sha256.Sum256(binary)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
if hashStr != expectedHash {
|
||||
return fmt.Errorf("hash mismatch: erwartet %s, bekommen %s", expectedHash[:16], hashStr[:16])
|
||||
}
|
||||
|
||||
log.Printf("Binary heruntergeladen (%d bytes, Hash OK)", len(binary))
|
||||
|
||||
// Temporaere Datei schreiben
|
||||
tmpPath := agentBinaryPath + ".new"
|
||||
if err := os.WriteFile(tmpPath, binary, 0755); err != nil {
|
||||
return fmt.Errorf("tmp schreiben fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
// Agent stoppen
|
||||
log.Println("Agent wird gestoppt...")
|
||||
exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Binary ersetzen
|
||||
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)
|
||||
}
|
||||
os.Chmod(agentBinaryPath, 0755)
|
||||
|
||||
// Agent starten
|
||||
log.Println("Agent wird gestartet...")
|
||||
if err := exec.Command("/usr/sbin/service", "rmm_agent", "start").Run(); 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()
|
||||
return fmt.Errorf("agent start fehlgeschlagen")
|
||||
}
|
||||
|
||||
// Backup aufraeumen
|
||||
os.Remove(backupPath)
|
||||
|
||||
// Update-Flag loeschen
|
||||
clearURL := fmt.Sprintf("%s/api/v1/agents/%s/request-update", cfg.BackendURL, agentID)
|
||||
clearReq, _ := http.NewRequest("DELETE", clearURL, nil)
|
||||
clearReq.Header.Set("X-API-Key", cfg.APIKey)
|
||||
client.Do(clearReq)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user