rmm2/agent/ws/handler.go
cynfo3000 23c8f2cd66 Major-Update Ablauf korrigiert und getestet
Agent:
- major_update: opnsense-update -r <ver> -bkp + pkg-static update -f + pkg-static upgrade -y
- Reboot ist bei Major-Updates default true (immer noetig)
- update_check: reboot_required Flag hinzugefuegt
- update: reboot_required korrekt aus opnsense-update -c Exit-Code
- Alle Steps liefern strukturierte Ergebnisse fuer Frontend-Workflow

Getestet auf 192.168.85.19:
- Normales Update: 25.7 → 25.7.11_9 (93 Pakete) erfolgreich
- Major-Upgrade: 25.7.11_9 → 26.1.2_5 erfolgreich (Base+Kernel+Packages+Reboot)

README: Update-Ablauf dokumentiert (Normal vs Major)
2026-02-28 09:35:33 +01:00

471 lines
13 KiB
Go

package ws
import (
"context"
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"time"
)
type Handler struct {
client *Client
}
func NewHandler(client *Client) *Handler {
return &Handler{client: client}
}
func (h *Handler) HandleCommand(msg Message) {
log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)
var response Message
response.Type = "response"
response.ID = msg.ID
switch msg.Command {
case "exec":
response = h.handleExec(msg)
case "update":
response = h.handleUpdate(msg)
case "major_update":
response = h.handleMajorUpdate(msg)
case "update_check":
response = h.handleUpdateCheck(msg)
case "reboot":
response = h.handleReboot(msg)
case "tunnel_connect":
response = h.handleTunnelConnect(msg)
case "tunnel_disconnect":
h.handleTunnelDisconnect(msg)
return
case "tunnel_open":
response.Status = "ok"
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
case "tunnel_close":
response.Status = "ok"
default:
response.Status = "error"
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
}
if err := h.client.SendMessage(response); err != nil {
log.Printf("Response senden fehlgeschlagen: %v", err)
}
}
// handleExec fuehrt einen beliebigen Befehl aus
func (h *Handler) handleExec(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
command, ok := msg.Params["command"].(string)
if !ok || command == "" {
response.Status = "error"
response.Error = "Parameter 'command' erforderlich"
return response
}
timeoutSec := 30
if t, ok := msg.Params["timeout"]; ok {
if ts, ok := t.(float64); ok {
timeoutSec = int(ts)
} else if ts, ok := t.(string); ok {
if parsed, err := strconv.Atoi(ts); err == nil {
timeoutSec = parsed
}
}
}
output, err := h.runCommand(command, timeoutSec)
if err != nil {
response.Status = "error"
response.Error = err.Error()
response.Data = map[string]interface{}{"output": output}
} else {
response.Status = "ok"
response.Data = map[string]interface{}{"output": output}
}
return response
}
// handleUpdateCheck prueft ob Updates verfuegbar sind
func (h *Handler) handleUpdateCheck(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
log.Println("Update-Check gestartet...")
result := map[string]interface{}{}
// 1. OPNsense Core Update pruefen
coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
if coreErr != nil {
// Exit 1 = Update verfuegbar ODER Reboot noetig
info := strings.TrimSpace(coreOutput)
result["core_update_available"] = true
result["core_update_info"] = info
} else {
result["core_update_available"] = false
result["core_update_info"] = "Kein Core-Update verfuegbar"
}
// Reboot-Check: Base/Kernel Version != installierte Version?
baseVer, _ := h.runCommand("cat /usr/local/opnsense/version/base", 5)
kernelVer, _ := h.runCommand("cat /usr/local/opnsense/version/kernel", 5)
coreJson, _ := h.runCommand("cat /usr/local/opnsense/version/core", 5)
baseVer = strings.TrimSpace(baseVer)
kernelVer = strings.TrimSpace(kernelVer)
rebootRequired := false
// Pruefen ob CORE_SERIES sich von Base/Kernel unterscheidet
if strings.Contains(coreJson, "CORE_NEXT") && baseVer != "" && kernelVer != "" {
// Einfacher Check: wenn base/kernel neuer als core → reboot noetig
coreVer := ""
for _, line := range strings.Split(coreJson, "\n") {
if strings.Contains(line, "CORE_PKGVERSION") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
coreVer = strings.Trim(strings.TrimSpace(parts[1]), "\",")
}
}
}
if coreVer != "" && baseVer != "" && !strings.HasPrefix(baseVer, strings.Split(coreVer, ".")[0]+"."+strings.Split(coreVer, ".")[1]) {
// Base-Version Major.Minor unterscheidet sich von Core → Reboot
rebootRequired = true
}
}
result["reboot_required"] = rebootRequired
// 2. Paket-Updates pruefen
pkgOutput, _ := h.runCommand("/usr/sbin/pkg upgrade -n", 60)
pendingPackages := h.parsePkgUpgrade(pkgOutput)
result["package_update_available"] = len(pendingPackages) > 0
result["pending_packages"] = pendingPackages
result["pending_count"] = len(pendingPackages)
response.Status = "ok"
response.Data = result
log.Printf("Update-Check abgeschlossen: Core=%v, Packages=%d",
result["core_update_available"], len(pendingPackages))
return response
}
// handleUpdate fuehrt ein normales Update durch (Core + Packages)
func (h *Handler) handleUpdate(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
log.Println("Normales Update gestartet...")
reboot := false
if r, ok := msg.Params["reboot"].(bool); ok {
reboot = r
}
result := map[string]interface{}{
"steps": []map[string]interface{}{},
}
var steps []map[string]interface{}
// Schritt 1: opnsense-update (Core + Firmware)
log.Println("Update Schritt 1: opnsense-update")
coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update", 600)
step1 := map[string]interface{}{
"step": "opnsense-update",
"output": coreOutput,
"ok": coreErr == nil,
}
if coreErr != nil {
step1["error"] = coreErr.Error()
}
steps = append(steps, step1)
// Schritt 2: pkg upgrade -y (Paket-Updates)
log.Println("Update Schritt 2: pkg upgrade -y")
pkgOutput, pkgErr := h.runCommand("/usr/sbin/pkg upgrade -y", 600)
step2 := map[string]interface{}{
"step": "pkg upgrade",
"output": pkgOutput,
"ok": pkgErr == nil,
}
if pkgErr != nil {
step2["error"] = pkgErr.Error()
}
steps = append(steps, step2)
// Schritt 3: opnsense-update -c (Nachpruefung)
log.Println("Update Schritt 3: Nachpruefung")
checkOutput, checkErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
rebootRequired := checkErr != nil // Exit 1 nach Update = Reboot noetig
step3 := map[string]interface{}{
"step": "verify",
"output": checkOutput,
"reboot_required": rebootRequired,
}
steps = append(steps, step3)
result["steps"] = steps
result["reboot_required"] = rebootRequired
result["reboot_requested"] = reboot
// Gesamtstatus
if coreErr == nil && pkgErr == nil {
response.Status = "ok"
if rebootRequired {
result["message"] = "Update erfolgreich — Reboot erforderlich"
} else {
result["message"] = "Update erfolgreich abgeschlossen"
}
} else {
response.Status = "ok"
result["message"] = "Update mit Warnungen abgeschlossen"
}
// Reboot wenn gewuenscht
if reboot {
log.Println("Reboot nach Update angefordert...")
result["reboot"] = "Reboot in 5 Sekunden..."
response.Data = result
// Response erst senden, dann rebooten
go func() {
time.Sleep(5 * time.Second)
log.Println("Reboot wird ausgefuehrt...")
exec.Command("/sbin/shutdown", "-r", "now").Run()
}()
return response
}
response.Data = result
log.Println("Update abgeschlossen")
return response
}
// handleMajorUpdate fuehrt ein Major-Upgrade durch
// Ablauf: opnsense-update -r <ver> -bkp → pkg-static update -f → pkg-static upgrade -y → Reboot
func (h *Handler) handleMajorUpdate(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
targetVersion, ok := msg.Params["version"].(string)
if !ok || targetVersion == "" {
response.Status = "error"
response.Error = "Parameter 'version' erforderlich (z.B. '26.1')"
return response
}
reboot := true // Major-Update braucht immer Reboot, default an
if r, ok := msg.Params["reboot"].(bool); ok {
reboot = r
}
log.Printf("Major-Update auf Version %s gestartet...", targetVersion)
result := map[string]interface{}{
"target_version": targetVersion,
}
var steps []map[string]interface{}
hasError := false
// Schritt 1: opnsense-update -r <version> -bkp (Release wechseln + Base + Kernel + Packages)
log.Printf("Schritt 1: opnsense-update -r %s -bkp", targetVersion)
releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s -bkp", targetVersion)
releaseOutput, releaseErr := h.runCommand(releaseCmd, 900)
step1 := map[string]interface{}{
"step": fmt.Sprintf("opnsense-update -r %s -bkp", targetVersion),
"output": releaseOutput,
"ok": releaseErr == nil,
}
if releaseErr != nil {
step1["error"] = releaseErr.Error()
hasError = true
}
steps = append(steps, step1)
// Schritt 2: pkg-static update -f (Repository-Katalog auf neuen Release aktualisieren)
log.Println("Schritt 2: pkg-static update -f")
catalogOutput, catalogErr := h.runCommand("/usr/local/sbin/pkg-static update -f", 120)
step2 := map[string]interface{}{
"step": "pkg-static update -f",
"output": catalogOutput,
"ok": catalogErr == nil,
}
if catalogErr != nil {
step2["error"] = catalogErr.Error()
hasError = true
}
steps = append(steps, step2)
// Schritt 3: pkg-static upgrade -y (alle Pakete auf neue Version)
log.Println("Schritt 3: pkg-static upgrade -y")
pkgOutput, pkgErr := h.runCommand("/usr/local/sbin/pkg-static upgrade -y", 900)
step3 := map[string]interface{}{
"step": "pkg-static upgrade -y",
"output": pkgOutput,
"ok": pkgErr == nil,
}
if pkgErr != nil {
step3["error"] = pkgErr.Error()
hasError = true
}
steps = append(steps, step3)
result["steps"] = steps
result["reboot_required"] = true
result["reboot_requested"] = reboot
if !hasError {
result["message"] = fmt.Sprintf("Major-Update auf %s abgeschlossen — Reboot erforderlich", targetVersion)
} else {
result["message"] = fmt.Sprintf("Major-Update auf %s mit Warnungen — Reboot erforderlich", targetVersion)
}
response.Status = "ok"
if reboot {
log.Println("Reboot nach Major-Update angefordert...")
result["reboot"] = "Reboot in 5 Sekunden..."
response.Data = result
go func() {
time.Sleep(5 * time.Second)
log.Println("Reboot wird ausgefuehrt...")
exec.Command("/sbin/shutdown", "-r", "now").Run()
}()
return response
}
response.Data = result
log.Printf("Major-Update auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion)
return response
}
// handleReboot fuehrt einen Reboot durch
func (h *Handler) handleReboot(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
delaySec := 5
if d, ok := msg.Params["delay"].(float64); ok && d > 0 {
delaySec = int(d)
}
log.Printf("Reboot angefordert (Verzoegerung: %ds)", delaySec)
response.Status = "ok"
response.Data = map[string]interface{}{
"message": fmt.Sprintf("Reboot in %d Sekunden", delaySec),
}
go func() {
time.Sleep(time.Duration(delaySec) * time.Second)
log.Println("Reboot wird ausgefuehrt...")
exec.Command("/sbin/shutdown", "-r", "now").Run()
}()
return response
}
// --- Tunnel Handlers ---
func (h *Handler) handleTunnelConnect(msg Message) Message {
response := Message{Type: "response", ID: msg.ID}
sessionID, _ := msg.Params["session_id"].(string)
tunnelID, _ := msg.Params["tunnel_id"].(string)
targetHost, _ := msg.Params["target_host"].(string)
targetPortFloat, _ := msg.Params["target_port"].(float64)
targetPort := int(targetPortFloat)
if sessionID == "" || targetHost == "" || targetPort <= 0 {
response.Status = "error"
response.Error = "session_id, target_host und target_port erforderlich"
return response
}
if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
response.Status = "error"
response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
return response
}
response.Status = "ok"
response.Data = map[string]interface{}{"session_id": sessionID}
return response
}
func (h *Handler) handleTunnelDisconnect(msg Message) {
sessionID, _ := msg.Params["session_id"].(string)
if sessionID != "" {
h.client.tunnelManager.DisconnectSession(sessionID)
}
}
// --- Helper ---
// runCommand fuehrt einen Shell-Befehl mit Timeout aus
func (h *Handler) runCommand(command string, timeoutSec int) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
output, err := cmd.CombinedOutput()
return string(output), err
}
// parsePkgUpgrade parst die Ausgabe von "pkg upgrade -n"
func (h *Handler) parsePkgUpgrade(output string) []map[string]string {
var packages []map[string]string
inUpgradeBlock := false
for _, line := range strings.Split(output, "\n") {
trimmed := strings.TrimSpace(line)
if strings.Contains(trimmed, "packages to be UPGRADED") ||
strings.Contains(trimmed, "will be affected") {
inUpgradeBlock = true
continue
}
if inUpgradeBlock && strings.Contains(trimmed, "->") {
parts := strings.SplitN(trimmed, ":", 2)
if len(parts) == 2 {
pkg := strings.TrimSpace(parts[0])
versions := strings.TrimSpace(parts[1])
arrowParts := strings.SplitN(versions, "->", 2)
if len(arrowParts) == 2 {
packages = append(packages, map[string]string{
"package": pkg,
"current_version": strings.TrimSpace(arrowParts[0]),
"new_version": strings.TrimSpace(arrowParts[1]),
})
}
}
}
if inUpgradeBlock && (trimmed == "" || strings.HasPrefix(trimmed, "Number of")) {
if !strings.HasPrefix(trimmed, "Number of") {
inUpgradeBlock = false
}
}
}
return packages
}
// SendSessionData schickt Tunnel-Daten als Binary-Message zurueck zum Backend
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
idBytes := []byte(sessionID)
if len(idBytes) > 255 {
return fmt.Errorf("session_id zu lang")
}
message := make([]byte, 1+len(idBytes)+len(data))
message[0] = byte(len(idBytes))
copy(message[1:], idBytes)
copy(message[1+len(idBytes):], data)
return h.client.SendBinaryData(message)
}