383 lines
9.4 KiB
Go
383 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
BackendURL string `yaml:"backend_url"`
|
|
APIKey string `yaml:"api_key"`
|
|
AgentName string `yaml:"agent_name"`
|
|
Insecure bool `yaml:"insecure"`
|
|
}
|
|
|
|
// Plattform-spezifische Pfade und Befehle
|
|
var (
|
|
agentBinaryPath string
|
|
updaterBinaryPath string
|
|
configPath string
|
|
agentIDPath string
|
|
platformName string
|
|
|
|
// Dateinamen im ZIP-Bundle
|
|
agentZipName string
|
|
updaterZipName string
|
|
)
|
|
|
|
func init() {
|
|
switch runtime.GOOS {
|
|
case "freebsd":
|
|
agentBinaryPath = "/usr/local/rmm/rmm-agent"
|
|
updaterBinaryPath = "/usr/local/rmm/rmm-updater"
|
|
configPath = "/usr/local/rmm/config.yaml"
|
|
agentIDPath = "/usr/local/rmm/agent_id"
|
|
platformName = "freebsd"
|
|
agentZipName = "rmm-agent"
|
|
updaterZipName = "rmm-updater-freebsd"
|
|
default: // linux
|
|
agentBinaryPath = "/usr/local/bin/rmm-agent"
|
|
updaterBinaryPath = "/usr/local/bin/rmm-updater"
|
|
configPath = "/etc/rmm/config.yaml"
|
|
agentIDPath = "/etc/rmm/agent_id"
|
|
platformName = "linux"
|
|
agentZipName = "rmm-agent-linux"
|
|
updaterZipName = "rmm-updater-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":
|
|
// Versuche zuerst via rc.d service
|
|
err := exec.Command("/usr/sbin/service", "rmm_agent", "start").Run()
|
|
if err != nil {
|
|
// Fallback: direkt via daemon starten
|
|
log.Printf("service start fehlgeschlagen (%v), versuche daemon-Fallback", err)
|
|
err = exec.Command("/usr/sbin/daemon",
|
|
"-p", "/var/run/rmm-agent.pid",
|
|
"-r",
|
|
agentBinaryPath,
|
|
"--config", configPath,
|
|
"--insecure",
|
|
).Start()
|
|
}
|
|
return err
|
|
default:
|
|
return exec.Command("/usr/bin/systemctl", "start", "rmm-agent").Run()
|
|
}
|
|
}
|
|
|
|
func restartUpdater() {
|
|
switch runtime.GOOS {
|
|
case "freebsd":
|
|
exec.Command("/usr/sbin/service", "rmm_updater", "restart").Run()
|
|
default:
|
|
exec.Command("/usr/bin/systemctl", "restart", "rmm-updater").Run()
|
|
}
|
|
}
|
|
|
|
const (
|
|
checkInterval = 60 * time.Second
|
|
logPrefix = "[rmm-updater] "
|
|
)
|
|
|
|
func main() {
|
|
log.SetPrefix(logPrefix)
|
|
log.SetFlags(log.LstdFlags)
|
|
|
|
log.Printf("RMM Updater gestartet (platform=%s, agent=%s, updater=%s)", platformName, agentBinaryPath, updaterBinaryPath)
|
|
|
|
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
|
|
}
|
|
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 := strings.TrimSpace(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 (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)
|
|
|
|
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
|
|
}
|
|
|
|
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 lokaler Agent schon den gleichen Hash hat
|
|
if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash {
|
|
return // Schon aktuell
|
|
}
|
|
|
|
// 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)
|
|
|
|
agentResp, err := client.Do(agentReq)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer agentResp.Body.Close()
|
|
|
|
var agentInfo struct {
|
|
Agent struct {
|
|
UpdateRequested bool `json:"update_requested"`
|
|
} `json:"agent"`
|
|
}
|
|
if err := json.NewDecoder(agentResp.Body).Decode(&agentInfo); err != nil {
|
|
return
|
|
}
|
|
if !agentInfo.Agent.UpdateRequested {
|
|
return
|
|
}
|
|
|
|
log.Printf("Update verfuegbar: v%s (%d bytes, platform=%s)", info.Version, info.Size, platformName)
|
|
|
|
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 {
|
|
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)
|
|
|
|
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)
|
|
}
|
|
|
|
bundle, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("download lesen fehlgeschlagen: %v", err)
|
|
}
|
|
|
|
// Hash des gesamten Bundles verifizieren
|
|
hash := sha256.Sum256(bundle)
|
|
hashStr := hex.EncodeToString(hash[:])
|
|
if hashStr != expectedHash {
|
|
return fmt.Errorf("hash mismatch: erwartet %s, bekommen %s", expectedHash[:16], hashStr[:16])
|
|
}
|
|
|
|
log.Printf("Bundle heruntergeladen (%d bytes, Hash OK)", len(bundle))
|
|
|
|
// Versuche als ZIP zu entpacken
|
|
agentBinary, updaterBinary, err := extractZipBundle(bundle)
|
|
if err != nil {
|
|
// Fallback: Kein ZIP — einzelne Agent-Binary (Abwaertskompatibilitaet)
|
|
log.Printf("Kein ZIP-Bundle, verwende als einzelne Agent-Binary")
|
|
agentBinary = bundle
|
|
updaterBinary = nil
|
|
}
|
|
|
|
// Agent stoppen
|
|
log.Println("Agent wird gestoppt...")
|
|
stopAgent()
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Agent-Binary ersetzen
|
|
if err := replaceBinary(agentBinaryPath, agentBinary); err != nil {
|
|
return fmt.Errorf("agent binary ersetzen fehlgeschlagen: %v", err)
|
|
}
|
|
log.Println("Agent-Binary aktualisiert")
|
|
|
|
// Agent starten
|
|
log.Println("Agent wird gestartet...")
|
|
if err := startAgent(); err != nil {
|
|
log.Printf("WARNUNG: Agent-Start fehlgeschlagen: %v — versuche Rollback", err)
|
|
rollbackBinary(agentBinaryPath)
|
|
startAgent()
|
|
return fmt.Errorf("agent start fehlgeschlagen")
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Updater-Binary ersetzen (wenn im Bundle enthalten)
|
|
if updaterBinary != nil {
|
|
log.Println("Updater-Binary wird aktualisiert...")
|
|
if err := replaceBinary(updaterBinaryPath, updaterBinary); err != nil {
|
|
log.Printf("WARNUNG: Updater-Binary ersetzen fehlgeschlagen: %v", err)
|
|
} else {
|
|
log.Println("Updater-Binary aktualisiert, starte neu...")
|
|
// Updater neustart — dieser Prozess wird beendet und neu gestartet
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
restartUpdater()
|
|
}()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func extractZipBundle(data []byte) (agentBin []byte, updaterBin []byte, err error) {
|
|
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("kein gueltiges ZIP: %v", err)
|
|
}
|
|
|
|
for _, f := range reader.File {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
content, err := io.ReadAll(rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
switch f.Name {
|
|
case agentZipName:
|
|
agentBin = content
|
|
log.Printf("ZIP: Agent-Binary gefunden (%s, %d bytes)", f.Name, len(content))
|
|
case updaterZipName:
|
|
updaterBin = content
|
|
log.Printf("ZIP: Updater-Binary gefunden (%s, %d bytes)", f.Name, len(content))
|
|
default:
|
|
log.Printf("ZIP: Unbekannte Datei ignoriert: %s", f.Name)
|
|
}
|
|
}
|
|
|
|
if agentBin == nil {
|
|
return nil, nil, fmt.Errorf("agent binary nicht im ZIP gefunden (erwartet: %s)", agentZipName)
|
|
}
|
|
|
|
return agentBin, updaterBin, nil
|
|
}
|
|
|
|
func replaceBinary(path string, newContent []byte) error {
|
|
backupPath := path + ".bak"
|
|
|
|
// Backup erstellen
|
|
if _, err := os.Stat(path); err == nil {
|
|
os.Rename(path, backupPath)
|
|
}
|
|
|
|
// Neue Binary schreiben
|
|
if err := os.WriteFile(path, newContent, 0755); err != nil {
|
|
// Rollback
|
|
os.Rename(backupPath, path)
|
|
return err
|
|
}
|
|
|
|
// Backup loeschen
|
|
os.Remove(backupPath)
|
|
return nil
|
|
}
|
|
|
|
func rollbackBinary(path string) {
|
|
backupPath := path + ".bak"
|
|
if _, err := os.Stat(backupPath); err == nil {
|
|
os.Rename(backupPath, path)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|