Update-Commands: update, major_update, update_check, reboot
Agent:
- update: opnsense-update + pkg upgrade -y, optionaler Reboot
- major_update: opnsense-update -r <version>, optionaler Reboot
- update_check: Prueft Core- und Paket-Updates, liefert Details
- reboot: Shutdown mit konfigurierbarer Verzoegerung
Backend:
- Synchrone API-Endpoints (warten auf Agent-Response)
- Response-Waiter-System im Hub (Command-ID basiert)
- Konfigurierbare Timeouts pro Command-Typ
- POST /agents/{id}/update[-check] und /major-update und /reboot
README mit Update-Beispielen und API-Doku aktualisiert
This commit is contained in:
+322
-34
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -27,17 +28,23 @@ func (h *Handler) HandleCommand(msg Message) {
|
||||
switch msg.Command {
|
||||
case "exec":
|
||||
response = h.handleExec(msg)
|
||||
case "tunnel_open":
|
||||
// Legacy: wird nicht mehr verwendet, aber fuer Kompatibilitaet
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
|
||||
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 // Keine Response noetig
|
||||
return
|
||||
case "tunnel_open":
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
|
||||
case "tunnel_close":
|
||||
// Legacy: alle Sessions fuer diesen Tunnel schliessen
|
||||
response.Status = "ok"
|
||||
default:
|
||||
response.Status = "error"
|
||||
@@ -49,11 +56,9 @@ func (h *Handler) HandleCommand(msg Message) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleExec fuehrt einen beliebigen Befehl aus
|
||||
func (h *Handler) handleExec(msg Message) Message {
|
||||
response := Message{
|
||||
Type: "response",
|
||||
ID: msg.ID,
|
||||
}
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
command, ok := msg.Params["command"].(string)
|
||||
if !ok || command == "" {
|
||||
@@ -73,34 +78,269 @@ func (h *Handler) handleExec(msg Message) Message {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
output, err := h.runCommand(command, timeoutSec)
|
||||
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
response.Data = map[string]interface{}{
|
||||
"output": string(output),
|
||||
}
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
} else {
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"output": string(output),
|
||||
}
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
response := Message{
|
||||
Type: "response",
|
||||
ID: msg.ID,
|
||||
// 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
|
||||
result["core_update_available"] = true
|
||||
result["core_update_info"] = strings.TrimSpace(coreOutput)
|
||||
} else {
|
||||
result["core_update_available"] = false
|
||||
result["core_update_info"] = "Kein Core-Update verfuegbar"
|
||||
}
|
||||
|
||||
// 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)
|
||||
stillPending := checkErr != nil
|
||||
step3 := map[string]interface{}{
|
||||
"step": "verify",
|
||||
"output": checkOutput,
|
||||
"updates_pending": stillPending,
|
||||
}
|
||||
steps = append(steps, step3)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_requested"] = reboot
|
||||
|
||||
// Gesamtstatus
|
||||
if coreErr == nil && pkgErr == nil {
|
||||
response.Status = "ok"
|
||||
result["message"] = "Update erfolgreich abgeschlossen"
|
||||
} else {
|
||||
response.Status = "ok" // Nicht "error" — teilweise Updates sind 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
|
||||
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.7')"
|
||||
return response
|
||||
}
|
||||
|
||||
reboot := false
|
||||
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{}
|
||||
|
||||
// Schritt 1: opnsense-update -r <version> (Release wechseln)
|
||||
log.Printf("Major-Update Schritt 1: opnsense-update -r %s", targetVersion)
|
||||
releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s", targetVersion)
|
||||
releaseOutput, releaseErr := h.runCommand(releaseCmd, 300)
|
||||
step1 := map[string]interface{}{
|
||||
"step": fmt.Sprintf("opnsense-update -r %s", targetVersion),
|
||||
"output": releaseOutput,
|
||||
"ok": releaseErr == nil,
|
||||
}
|
||||
if releaseErr != nil {
|
||||
step1["error"] = releaseErr.Error()
|
||||
steps = append(steps, step1)
|
||||
|
||||
// Bei Fehler im Release-Wechsel abbrechen
|
||||
result["steps"] = steps
|
||||
result["message"] = "Release-Wechsel fehlgeschlagen"
|
||||
response.Status = "error"
|
||||
response.Error = "Release-Wechsel fehlgeschlagen"
|
||||
response.Data = result
|
||||
return response
|
||||
}
|
||||
steps = append(steps, step1)
|
||||
|
||||
// Schritt 2: opnsense-update (eigentliches Update laden)
|
||||
log.Println("Major-Update Schritt 2: opnsense-update")
|
||||
updateOutput, updateErr := h.runCommand("/usr/local/sbin/opnsense-update", 900)
|
||||
step2 := map[string]interface{}{
|
||||
"step": "opnsense-update",
|
||||
"output": updateOutput,
|
||||
"ok": updateErr == nil,
|
||||
}
|
||||
if updateErr != nil {
|
||||
step2["error"] = updateErr.Error()
|
||||
}
|
||||
steps = append(steps, step2)
|
||||
|
||||
// Schritt 3: pkg upgrade -y
|
||||
log.Println("Major-Update Schritt 3: pkg upgrade -y")
|
||||
pkgOutput, pkgErr := h.runCommand("/usr/sbin/pkg upgrade -y", 600)
|
||||
step3 := map[string]interface{}{
|
||||
"step": "pkg upgrade",
|
||||
"output": pkgOutput,
|
||||
"ok": pkgErr == nil,
|
||||
}
|
||||
if pkgErr != nil {
|
||||
step3["error"] = pkgErr.Error()
|
||||
}
|
||||
steps = append(steps, step3)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_requested"] = reboot
|
||||
|
||||
if updateErr == nil {
|
||||
result["message"] = fmt.Sprintf("Major-Update auf %s abgeschlossen", targetVersion)
|
||||
} else {
|
||||
result["message"] = fmt.Sprintf("Major-Update auf %s mit Warnungen", 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", 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)
|
||||
@@ -120,23 +360,71 @@ func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
}
|
||||
|
||||
response.Data = map[string]interface{}{"session_id": sessionID}
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelDisconnect(msg Message) {
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
if sessionID == "" {
|
||||
return
|
||||
if sessionID != "" {
|
||||
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||
}
|
||||
|
||||
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||
}
|
||||
|
||||
// SendSessionData schickt Daten als Binary-Message zurueck zum Backend
|
||||
// --- 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 {
|
||||
|
||||
Reference in New Issue
Block a user