Config-Backup Feature: Agent liest config.xml, Backend speichert versioniert mit Deduplizierung
- Agent: neuer 'backup' Command (SHA256 Hash, Base64 Transfer) - Backend: config_backups Tabelle in SQLite - API: trigger, list, download (JSON/XML), delete, diff - Deduplizierung: gleicher Hash = kein neuer Eintrag - WebSocket ReadLimit auf 4MB erhoeht (grosse config.xml) - README aktualisiert mit Backup-Docs
This commit is contained in:
+137
-39
@@ -2,8 +2,12 @@ package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -36,6 +40,8 @@ func (h *Handler) HandleCommand(msg Message) {
|
||||
response = h.handleUpdateCheck(msg)
|
||||
case "reboot":
|
||||
response = h.handleReboot(msg)
|
||||
case "backup":
|
||||
response = h.handleBackup(msg)
|
||||
case "tunnel_connect":
|
||||
response = h.handleTunnelConnect(msg)
|
||||
case "tunnel_disconnect":
|
||||
@@ -243,7 +249,10 @@ func (h *Handler) handleUpdate(msg Message) Message {
|
||||
}
|
||||
|
||||
// handleMajorUpdate fuehrt ein Major-Upgrade durch
|
||||
// Ablauf: opnsense-update -r <ver> -bkp → pkg-static update -f → pkg-static upgrade -y → Reboot
|
||||
// Ablauf: Phase 1: opnsense-update -r <ver> -bkp → Reboot
|
||||
// Phase 2 (nach Reboot): pkg-static update -f → pkg-static upgrade -y → optionaler Reboot
|
||||
// WICHTIG: Das neue Paket-Repository wird erst NACH dem Reboot aktiv,
|
||||
// daher muss pkg upgrade zwingend nach dem Reboot laufen.
|
||||
func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
@@ -254,21 +263,100 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||
return response
|
||||
}
|
||||
|
||||
reboot := true // Major-Update braucht immer Reboot, default an
|
||||
if r, ok := msg.Params["reboot"].(bool); ok {
|
||||
reboot = r
|
||||
// Phase 2: Wenn phase=2, nur pkg upgrade ausfuehren (nach Reboot)
|
||||
phase := "1"
|
||||
if p, ok := msg.Params["phase"].(string); ok && p != "" {
|
||||
phase = p
|
||||
} else if p, ok := msg.Params["phase"].(float64); ok {
|
||||
phase = fmt.Sprintf("%.0f", p)
|
||||
}
|
||||
|
||||
log.Printf("Major-Update auf Version %s gestartet...", targetVersion)
|
||||
log.Printf("Major-Update auf Version %s, Phase %s gestartet...", targetVersion, phase)
|
||||
|
||||
result := map[string]interface{}{
|
||||
"target_version": targetVersion,
|
||||
"phase": phase,
|
||||
}
|
||||
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)
|
||||
if phase == "2" {
|
||||
// Phase 2: Nach Reboot — Repo auf neue Version umstellen, dann pkg upgrade
|
||||
// Das OPNsense pkg-Repo wird vom opnsense Package kontrolliert (CORE_REPOSITORY in /usr/local/opnsense/version/core)
|
||||
// Nach opnsense-update -r <ver> -bkp + Reboot zeigt das Repo noch die alte Version
|
||||
// Wir muessen die Repo-Config manuell umschreiben
|
||||
repoConf := "/usr/local/etc/pkg/repos/OPNsense.conf"
|
||||
log.Printf("Phase 2: Repo-Config auf %s umstellen", targetVersion)
|
||||
// Aktuelle Series aus dem Repo-Config lesen und durch targetVersion ersetzen
|
||||
sedCmd := fmt.Sprintf("/usr/bin/sed -i '' 's|/[0-9][0-9]\\.[0-9]/latest|/%s/latest|g' %s", targetVersion, repoConf)
|
||||
sedOutput, sedErr := h.runCommand(sedCmd, 10)
|
||||
step0 := map[string]interface{}{
|
||||
"step": "repo-switch to " + targetVersion,
|
||||
"output": sedOutput,
|
||||
"ok": sedErr == nil,
|
||||
}
|
||||
if sedErr != nil {
|
||||
step0["error"] = sedErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step0)
|
||||
|
||||
log.Println("Phase 2: pkg-static update -f")
|
||||
catalogOutput, catalogErr := h.runCommand("/usr/local/sbin/pkg-static update -f", 120)
|
||||
step1 := map[string]interface{}{
|
||||
"step": "pkg-static update -f",
|
||||
"output": catalogOutput,
|
||||
"ok": catalogErr == nil,
|
||||
}
|
||||
if catalogErr != nil {
|
||||
step1["error"] = catalogErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step1)
|
||||
|
||||
log.Println("Phase 2: pkg-static upgrade -y")
|
||||
pkgOutput, pkgErr := h.runCommand("/usr/local/sbin/pkg-static upgrade -y", 900)
|
||||
step2 := map[string]interface{}{
|
||||
"step": "pkg-static upgrade -y",
|
||||
"output": pkgOutput,
|
||||
"ok": pkgErr == nil,
|
||||
}
|
||||
if pkgErr != nil {
|
||||
step2["error"] = pkgErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step2)
|
||||
|
||||
// Verify
|
||||
verOutput, _ := h.runCommand("/usr/local/sbin/opnsense-version", 10)
|
||||
verStep := map[string]interface{}{
|
||||
"step": "verify",
|
||||
"output": strings.TrimSpace(verOutput),
|
||||
"ok": true,
|
||||
}
|
||||
steps = append(steps, verStep)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_required"] = false
|
||||
|
||||
if !hasError {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s abgeschlossen — Packages aktualisiert", targetVersion)
|
||||
} else {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s mit Fehlern", targetVersion)
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = result
|
||||
return response
|
||||
}
|
||||
|
||||
// Phase 1: Base + Kernel installieren, dann Reboot
|
||||
reboot := true
|
||||
if r, ok := msg.Params["reboot"].(bool); ok {
|
||||
reboot = r
|
||||
}
|
||||
|
||||
log.Printf("Phase 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{}{
|
||||
@@ -282,48 +370,20 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||
}
|
||||
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)
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s abgeschlossen — Reboot erforderlich, danach Phase 2 ausfuehren", targetVersion)
|
||||
} else {
|
||||
result["message"] = fmt.Sprintf("Major-Update auf %s mit Warnungen — Reboot erforderlich", targetVersion)
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s mit Fehlern", targetVersion)
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
|
||||
if reboot {
|
||||
log.Println("Reboot nach Major-Update angefordert...")
|
||||
log.Println("Reboot nach Phase 1 angefordert...")
|
||||
result["reboot"] = "Reboot in 5 Sekunden..."
|
||||
response.Data = result
|
||||
|
||||
@@ -337,7 +397,7 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||
}
|
||||
|
||||
response.Data = result
|
||||
log.Printf("Major-Update auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion)
|
||||
log.Printf("Major-Update Phase 1 auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion)
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -366,6 +426,44 @@ func (h *Handler) handleReboot(msg Message) Message {
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Backup Handler ---
|
||||
|
||||
// handleBackup liest die OPNsense config.xml und sendet sie zurueck
|
||||
func (h *Handler) handleBackup(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
configPath := "/conf/config.xml"
|
||||
if p, ok := msg.Params["path"].(string); ok && p != "" {
|
||||
configPath = p
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Config lesen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// SHA256-Hash berechnen
|
||||
hash := sha256.Sum256(data)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
|
||||
// Base64-kodiert senden (config.xml ist Text, aber sicherstellen dass nichts verloren geht)
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"config": encoded,
|
||||
"hash": hashStr,
|
||||
"size": len(data),
|
||||
"path": configPath,
|
||||
"encoding": "base64",
|
||||
}
|
||||
|
||||
log.Printf("Backup erstellt: %s (%d Bytes, Hash: %s)", configPath, len(data), hashStr[:12])
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Tunnel Handlers ---
|
||||
|
||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
|
||||
Reference in New Issue
Block a user