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:
parent
23c8f2cd66
commit
e7beada405
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
build/
|
build/
|
||||||
|
rmm-agent
|
||||||
|
rmm-backend
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
*.pid
|
||||||
|
|||||||
40
README.md
40
README.md
@ -133,6 +133,19 @@ daemon -f -p /var/run/rmm-agent.pid -o /usr/local/rmm/rmm-agent.log \
|
|||||||
| POST | `/api/v1/agents/{id}/major-update` | Major-Upgrade auf Zielversion |
|
| POST | `/api/v1/agents/{id}/major-update` | Major-Upgrade auf Zielversion |
|
||||||
| POST | `/api/v1/agents/{id}/reboot` | Reboot ausfuehren |
|
| POST | `/api/v1/agents/{id}/reboot` | Reboot ausfuehren |
|
||||||
|
|
||||||
|
### Config-Backup
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| POST | `/api/v1/agents/{id}/backup` | Backup jetzt ausloesen (via WebSocket) |
|
||||||
|
| GET | `/api/v1/agents/{id}/backups` | Alle Backups auflisten (`?limit=N`) |
|
||||||
|
| GET | `/api/v1/agents/{id}/backups/{id\|latest}` | Backup herunterladen (`?format=xml` fuer Raw-XML) |
|
||||||
|
| DELETE | `/api/v1/agents/{id}/backups/{id}` | Einzelnes Backup loeschen |
|
||||||
|
| GET | `/api/v1/agents/{id}/backups/diff?a={id}&b={id}` | Zeilenweises Diff zwischen zwei Backups |
|
||||||
|
|
||||||
|
Backups werden dedupliziert: gleicher SHA256-Hash = kein neuer Eintrag.
|
||||||
|
Config-XML wird Base64-kodiert vom Agent zum Backend uebertragen und als Klartext in SQLite gespeichert.
|
||||||
|
|
||||||
### Tunnel-Management
|
### Tunnel-Management
|
||||||
|
|
||||||
| Methode | Endpoint | Beschreibung |
|
| Methode | Endpoint | Beschreibung |
|
||||||
@ -208,6 +221,27 @@ curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" -X POST \
|
|||||||
-d '{"command":"uptime","timeout":30}'
|
-d '{"command":"uptime","timeout":30}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Backup-Beispiele
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup ausloesen
|
||||||
|
curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" -X POST \
|
||||||
|
https://192.168.85.13:8443/api/v1/agents/e92e87a684b20db15d8d2344583c5887/backup
|
||||||
|
|
||||||
|
# Alle Backups auflisten
|
||||||
|
curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \
|
||||||
|
https://192.168.85.13:8443/api/v1/agents/e92e87a684b20db15d8d2344583c5887/backups
|
||||||
|
|
||||||
|
# Neuestes Backup als XML herunterladen
|
||||||
|
curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \
|
||||||
|
"https://192.168.85.13:8443/api/v1/agents/e92e87a684b20db15d8d2344583c5887/backups/latest?format=xml" \
|
||||||
|
-o config-backup.xml
|
||||||
|
|
||||||
|
# Diff zwischen Backup 1 und 3
|
||||||
|
curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \
|
||||||
|
"https://192.168.85.13:8443/api/v1/agents/e92e87a684b20db15d8d2344583c5887/backups/diff?a=1&b=3"
|
||||||
|
```
|
||||||
|
|
||||||
### Update-Beispiele
|
### Update-Beispiele
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -269,6 +303,9 @@ Major-Upgrades setzen `reboot_required: true` und `reboot` ist standardmaessig a
|
|||||||
// Reboot
|
// Reboot
|
||||||
{"type":"command","id":"cmd5","command":"reboot","params":{"delay":5}}
|
{"type":"command","id":"cmd5","command":"reboot","params":{"delay":5}}
|
||||||
|
|
||||||
|
// Config-Backup (liest /conf/config.xml, liefert Base64 + SHA256 Hash)
|
||||||
|
{"type":"command","id":"cmd8","command":"backup","params":{}}
|
||||||
|
|
||||||
// Tunnel-Session oeffnen (automatisch vom Backend gesendet)
|
// Tunnel-Session oeffnen (automatisch vom Backend gesendet)
|
||||||
{"type":"command","id":"cmd6","command":"tunnel_connect","params":{"session_id":"xxx","tunnel_id":"xxx","target_host":"127.0.0.1","target_port":22}}
|
{"type":"command","id":"cmd6","command":"tunnel_connect","params":{"session_id":"xxx","tunnel_id":"xxx","target_host":"127.0.0.1","target_port":22}}
|
||||||
|
|
||||||
@ -324,7 +361,8 @@ rmm/
|
|||||||
│ │ └── system.go # SystemData, alle Collector-Structs
|
│ │ └── system.go # SystemData, alle Collector-Structs
|
||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── handlers.go # REST API Handler
|
│ │ ├── handlers.go # REST API Handler
|
||||||
│ │ ├── tunnel_proxy.go # Tunnel + Exec REST Endpoints
|
│ │ ├── tunnel_proxy.go # Tunnel + Exec + Update REST Endpoints
|
||||||
|
│ │ ├── backup.go # Config-Backup Endpoints (trigger, list, download, diff)
|
||||||
│ │ └── middleware.go # API-Key Auth, Logging
|
│ │ └── middleware.go # API-Key Auth, Logging
|
||||||
│ └── ws/
|
│ └── ws/
|
||||||
│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps
|
│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps
|
||||||
|
|||||||
@ -2,8 +2,12 @@ package ws
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -36,6 +40,8 @@ func (h *Handler) HandleCommand(msg Message) {
|
|||||||
response = h.handleUpdateCheck(msg)
|
response = h.handleUpdateCheck(msg)
|
||||||
case "reboot":
|
case "reboot":
|
||||||
response = h.handleReboot(msg)
|
response = h.handleReboot(msg)
|
||||||
|
case "backup":
|
||||||
|
response = h.handleBackup(msg)
|
||||||
case "tunnel_connect":
|
case "tunnel_connect":
|
||||||
response = h.handleTunnelConnect(msg)
|
response = h.handleTunnelConnect(msg)
|
||||||
case "tunnel_disconnect":
|
case "tunnel_disconnect":
|
||||||
@ -243,7 +249,10 @@ func (h *Handler) handleUpdate(msg Message) Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleMajorUpdate fuehrt ein Major-Upgrade durch
|
// 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 {
|
func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||||
response := Message{Type: "response", ID: msg.ID}
|
response := Message{Type: "response", ID: msg.ID}
|
||||||
|
|
||||||
@ -254,21 +263,100 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
reboot := true // Major-Update braucht immer Reboot, default an
|
// Phase 2: Wenn phase=2, nur pkg upgrade ausfuehren (nach Reboot)
|
||||||
if r, ok := msg.Params["reboot"].(bool); ok {
|
phase := "1"
|
||||||
reboot = r
|
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{}{
|
result := map[string]interface{}{
|
||||||
"target_version": targetVersion,
|
"target_version": targetVersion,
|
||||||
|
"phase": phase,
|
||||||
}
|
}
|
||||||
var steps []map[string]interface{}
|
var steps []map[string]interface{}
|
||||||
hasError := false
|
hasError := false
|
||||||
|
|
||||||
// Schritt 1: opnsense-update -r <version> -bkp (Release wechseln + Base + Kernel + Packages)
|
if phase == "2" {
|
||||||
log.Printf("Schritt 1: opnsense-update -r %s -bkp", targetVersion)
|
// 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)
|
releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s -bkp", targetVersion)
|
||||||
releaseOutput, releaseErr := h.runCommand(releaseCmd, 900)
|
releaseOutput, releaseErr := h.runCommand(releaseCmd, 900)
|
||||||
step1 := map[string]interface{}{
|
step1 := map[string]interface{}{
|
||||||
@ -282,48 +370,20 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
|||||||
}
|
}
|
||||||
steps = append(steps, step1)
|
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["steps"] = steps
|
||||||
result["reboot_required"] = true
|
result["reboot_required"] = true
|
||||||
result["reboot_requested"] = reboot
|
result["reboot_requested"] = reboot
|
||||||
|
|
||||||
if !hasError {
|
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 {
|
} 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"
|
response.Status = "ok"
|
||||||
|
|
||||||
if reboot {
|
if reboot {
|
||||||
log.Println("Reboot nach Major-Update angefordert...")
|
log.Println("Reboot nach Phase 1 angefordert...")
|
||||||
result["reboot"] = "Reboot in 5 Sekunden..."
|
result["reboot"] = "Reboot in 5 Sekunden..."
|
||||||
response.Data = result
|
response.Data = result
|
||||||
|
|
||||||
@ -337,7 +397,7 @@ func (h *Handler) handleMajorUpdate(msg Message) Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response.Data = result
|
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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,6 +426,44 @@ func (h *Handler) handleReboot(msg Message) Message {
|
|||||||
return response
|
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 ---
|
// --- Tunnel Handlers ---
|
||||||
|
|
||||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||||
|
|||||||
335
backend/api/backup.go
Normal file
335
backend/api/backup.go
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cynfo/rmm-backend/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
// POST /api/v1/agents/{id}/backup — Backup ausloesen (via WebSocket an Agent)
|
||||||
|
func (h *Handler) triggerBackup(hub *ws.Hub) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
|
||||||
|
if !hub.IsAgentConnected(agentID) {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdID := generateID()
|
||||||
|
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"id": cmdID,
|
||||||
|
"command": "backup",
|
||||||
|
"params": map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response-Channel registrieren
|
||||||
|
responseCh := hub.RegisterResponseWaiter(cmdID)
|
||||||
|
defer hub.UnregisterResponseWaiter(cmdID)
|
||||||
|
|
||||||
|
cmdJSON, _ := json.Marshal(cmd)
|
||||||
|
if err := hub.SendToAgent(agentID, cmdJSON); err != nil {
|
||||||
|
log.Printf("Backup-Command senden fehlgeschlagen: %v", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Backup-Command (ID: %s) an Agent %s gesendet", cmdID, agentID)
|
||||||
|
|
||||||
|
// Auf Response warten (config.xml kann gross sein, 60s Timeout)
|
||||||
|
select {
|
||||||
|
case resp := <-responseCh:
|
||||||
|
status, _ := resp["status"].(string)
|
||||||
|
if status != "ok" {
|
||||||
|
errMsg, _ := resp["error"].(string)
|
||||||
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Backup fehlgeschlagen: %s", errMsg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := resp["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Ungueltiges Response-Format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config aus Base64 dekodieren
|
||||||
|
configB64, _ := data["config"].(string)
|
||||||
|
hash, _ := data["hash"].(string)
|
||||||
|
sizeFloat, _ := data["size"].(float64)
|
||||||
|
size := int(sizeFloat)
|
||||||
|
|
||||||
|
configXML, err := base64.StdEncoding.DecodeString(configB64)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Base64-Dekodierung fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In DB speichern (dedupliziert)
|
||||||
|
backupID, isNew, err := h.db.SaveConfigBackup(agentID, hash, size, string(configXML))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Backup speichern fehlgeschlagen: %v", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "Backup speichern fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "Backup gespeichert"
|
||||||
|
if !isNew {
|
||||||
|
msg = "Config unveraendert (Hash identisch)"
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"backup_id": backupID,
|
||||||
|
"hash": hash,
|
||||||
|
"size": size,
|
||||||
|
"new": isNew,
|
||||||
|
"message": msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
writeError(w, http.StatusGatewayTimeout, "Timeout: Agent hat nicht rechtzeitig geantwortet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/agents/{id}/backups — Alle Backups auflisten
|
||||||
|
func (h *Handler) listBackups() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
|
||||||
|
limit := 50
|
||||||
|
if l := r.URL.Query().Get("limit"); l != "" {
|
||||||
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||||
|
limit = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backups, err := h.db.GetConfigBackups(agentID, limit)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Backups laden fehlgeschlagen: %v", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, backups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/agents/{id}/backups/{backup_id} — Einzelnes Backup herunterladen
|
||||||
|
func (h *Handler) getBackup() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
backupIDStr := r.PathValue("backup_id")
|
||||||
|
|
||||||
|
// "latest" als Spezialwert
|
||||||
|
if backupIDStr == "latest" {
|
||||||
|
backup, err := h.db.GetLatestConfigBackup(agentID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if backup == nil {
|
||||||
|
writeError(w, http.StatusNotFound, "Kein Backup vorhanden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: raw XML oder JSON?
|
||||||
|
if r.URL.Query().Get("format") == "xml" {
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s.xml", agentID[:8]))
|
||||||
|
w.Write([]byte(backup["config_xml"].(string)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, backup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backupID, err := strconv.ParseInt(backupIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backup, err := h.db.GetConfigBackup(agentID, backupID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if backup == nil {
|
||||||
|
writeError(w, http.StatusNotFound, "Backup nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw XML Download
|
||||||
|
if r.URL.Query().Get("format") == "xml" {
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s_%d.xml", agentID[:8], backupID))
|
||||||
|
w.Write([]byte(backup["config_xml"].(string)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, backup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/agents/{id}/backups/{backup_id} — Backup loeschen
|
||||||
|
func (h *Handler) deleteBackup() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
backupIDStr := r.PathValue("backup_id")
|
||||||
|
|
||||||
|
backupID, err := strconv.ParseInt(backupIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := h.db.DeleteConfigBackup(agentID, backupID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !deleted {
|
||||||
|
writeError(w, http.StatusNotFound, "Backup nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Backup geloescht"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/agents/{id}/backups/diff?a={id1}&b={id2} — Diff zwischen zwei Backups
|
||||||
|
func (h *Handler) diffBackups() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
|
||||||
|
aStr := r.URL.Query().Get("a")
|
||||||
|
bStr := r.URL.Query().Get("b")
|
||||||
|
if aStr == "" || bStr == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "Parameter 'a' und 'b' (Backup-IDs) erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
aID, err := strconv.ParseInt(aStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'a'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bID, err := strconv.ParseInt(bStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'b'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backupA, err := h.db.GetConfigBackup(agentID, aID)
|
||||||
|
if err != nil || backupA == nil {
|
||||||
|
writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", aID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backupB, err := h.db.GetConfigBackup(agentID, bID)
|
||||||
|
if err != nil || backupB == nil {
|
||||||
|
writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", bID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlA := backupA["config_xml"].(string)
|
||||||
|
xmlB := backupB["config_xml"].(string)
|
||||||
|
|
||||||
|
if backupA["hash"] == backupB["hash"] {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"identical": true,
|
||||||
|
"a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]},
|
||||||
|
"b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]},
|
||||||
|
"diff": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeilenweises Diff berechnen
|
||||||
|
diff := computeLineDiff(xmlA, xmlB)
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"identical": false,
|
||||||
|
"a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]},
|
||||||
|
"b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]},
|
||||||
|
"diff": diff,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeLineDiff erzeugt ein einfaches zeilenweises Diff (unified-style)
|
||||||
|
func computeLineDiff(a, b string) string {
|
||||||
|
linesA := strings.Split(a, "\n")
|
||||||
|
linesB := strings.Split(b, "\n")
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
maxLen := len(linesA)
|
||||||
|
if len(linesB) > maxLen {
|
||||||
|
maxLen = len(linesB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einfaches sequenzielles Diff — fuer XML config.xml voellig ausreichend
|
||||||
|
i, j := 0, 0
|
||||||
|
for i < len(linesA) || j < len(linesB) {
|
||||||
|
if i < len(linesA) && j < len(linesB) && linesA[i] == linesB[j] {
|
||||||
|
// Gleiche Zeile — uebergehen (nur Context bei Aenderungen zeigen)
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorausschauen ob die Zeile weiter unten in der anderen Datei existiert
|
||||||
|
foundInB := -1
|
||||||
|
if i < len(linesA) {
|
||||||
|
for k := j; k < len(linesB) && k < j+10; k++ {
|
||||||
|
if linesA[i] == linesB[k] {
|
||||||
|
foundInB = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundInA := -1
|
||||||
|
if j < len(linesB) {
|
||||||
|
for k := i; k < len(linesA) && k < i+10; k++ {
|
||||||
|
if linesB[j] == linesA[k] {
|
||||||
|
foundInA = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundInB >= 0 && (foundInA < 0 || foundInB-j <= foundInA-i) {
|
||||||
|
// Zeilen in B eingefuegt
|
||||||
|
for j < foundInB {
|
||||||
|
result = append(result, fmt.Sprintf("+ %s", linesB[j]))
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
} else if foundInA >= 0 {
|
||||||
|
// Zeilen in A entfernt
|
||||||
|
for i < foundInA {
|
||||||
|
result = append(result, fmt.Sprintf("- %s", linesA[i]))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Zeile geaendert
|
||||||
|
if i < len(linesA) {
|
||||||
|
result = append(result, fmt.Sprintf("- %s", linesA[i]))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if j < len(linesB) {
|
||||||
|
result = append(result, fmt.Sprintf("+ %s", linesB[j]))
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "\n")
|
||||||
|
}
|
||||||
@ -26,6 +26,13 @@ func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
|||||||
mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
||||||
mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
||||||
|
|
||||||
|
// Backup-Endpoints
|
||||||
|
mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub))
|
||||||
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups())
|
||||||
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups/{backup_id}", h.getBackup())
|
||||||
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/backups/{backup_id}", h.deleteBackup())
|
||||||
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups/diff", h.diffBackups())
|
||||||
|
|
||||||
// WebSocket-Endpoint
|
// WebSocket-Endpoint
|
||||||
mux.HandleFunc("GET /api/v1/agent/ws", ws.NewWebSocketHandler(hub))
|
mux.HandleFunc("GET /api/v1/agent/ws", ws.NewWebSocketHandler(hub))
|
||||||
|
|
||||||
@ -212,9 +219,10 @@ func (h *Handler) agentMajorUpdate(hub *ws.Hub) http.HandlerFunc {
|
|||||||
var req struct {
|
var req struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Reboot bool `json:"reboot"`
|
Reboot bool `json:"reboot"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Version == "" {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Version == "" {
|
||||||
writeError(w, http.StatusBadRequest, "Parameter 'version' erforderlich (z.B. '26.7')")
|
writeError(w, http.StatusBadRequest, "Parameter 'version' erforderlich (z.B. '26.1')")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +230,9 @@ func (h *Handler) agentMajorUpdate(hub *ws.Hub) http.HandlerFunc {
|
|||||||
"version": req.Version,
|
"version": req.Version,
|
||||||
"reboot": req.Reboot,
|
"reboot": req.Reboot,
|
||||||
}
|
}
|
||||||
|
if req.Phase != "" {
|
||||||
|
params["phase"] = req.Phase
|
||||||
|
}
|
||||||
|
|
||||||
h.sendCommandAndWait(hub, agentID, "major_update", params, 1200, w)
|
h.sendCommandAndWait(hub, agentID, "major_update", params, 1200, w)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,19 @@ func (d *Database) migrate() error {
|
|||||||
last_heartbeat TEXT
|
last_heartbeat TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS config_backups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
config_xml TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_config_backups_agent ON config_backups(agent_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_config_backups_hash ON config_backups(agent_id, hash);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_data (
|
CREATE TABLE IF NOT EXISTS system_data (
|
||||||
agent_id TEXT PRIMARY KEY,
|
agent_id TEXT PRIMARY KEY,
|
||||||
data_json TEXT NOT NULL,
|
data_json TEXT NOT NULL,
|
||||||
@ -219,6 +232,123 @@ func (d *Database) GetSystemData(agentID string) (*models.SystemData, error) {
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Config Backup Methoden ---
|
||||||
|
|
||||||
|
// SaveConfigBackup speichert ein Config-Backup (dedupliziert nach Hash)
|
||||||
|
func (d *Database) SaveConfigBackup(agentID, hash string, size int, configXML string) (int64, bool, error) {
|
||||||
|
// Pruefen ob ein Backup mit gleichem Hash schon existiert
|
||||||
|
var existingID int64
|
||||||
|
err := d.db.QueryRow("SELECT id FROM config_backups WHERE agent_id = ? AND hash = ? LIMIT 1", agentID, hash).Scan(&existingID)
|
||||||
|
if err == nil {
|
||||||
|
// Gleicher Hash existiert bereits — kein neues Backup noetig
|
||||||
|
return existingID, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
res, err := d.db.Exec(
|
||||||
|
"INSERT INTO config_backups (agent_id, hash, size, config_xml, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
agentID, hash, size, configXML, now,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, fmt.Errorf("Backup speichern: %w", err)
|
||||||
|
}
|
||||||
|
id, _ := res.LastInsertId()
|
||||||
|
return id, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBackups listet alle Backups eines Agents
|
||||||
|
func (d *Database) GetConfigBackups(agentID string, limit int) ([]map[string]interface{}, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
rows, err := d.db.Query(
|
||||||
|
"SELECT id, hash, size, created_at FROM config_backups WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?",
|
||||||
|
agentID, limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var backups []map[string]interface{}
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
var hash, createdAt string
|
||||||
|
var size int
|
||||||
|
if err := rows.Scan(&id, &hash, &size, &createdAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
backups = append(backups, map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"hash": hash,
|
||||||
|
"size": size,
|
||||||
|
"created_at": createdAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if backups == nil {
|
||||||
|
backups = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
return backups, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBackup holt ein einzelnes Backup inkl. config_xml
|
||||||
|
func (d *Database) GetConfigBackup(agentID string, backupID int64) (map[string]interface{}, error) {
|
||||||
|
var hash, configXML, createdAt string
|
||||||
|
var size int
|
||||||
|
var id int64
|
||||||
|
err := d.db.QueryRow(
|
||||||
|
"SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = ? AND id = ?",
|
||||||
|
agentID, backupID,
|
||||||
|
).Scan(&id, &hash, &size, &configXML, &createdAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"hash": hash,
|
||||||
|
"size": size,
|
||||||
|
"config_xml": configXML,
|
||||||
|
"created_at": createdAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestConfigBackup holt das neueste Backup
|
||||||
|
func (d *Database) GetLatestConfigBackup(agentID string) (map[string]interface{}, error) {
|
||||||
|
var hash, configXML, createdAt string
|
||||||
|
var size int
|
||||||
|
var id int64
|
||||||
|
err := d.db.QueryRow(
|
||||||
|
"SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = ? ORDER BY created_at DESC LIMIT 1",
|
||||||
|
agentID,
|
||||||
|
).Scan(&id, &hash, &size, &configXML, &createdAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"hash": hash,
|
||||||
|
"size": size,
|
||||||
|
"config_xml": configXML,
|
||||||
|
"created_at": createdAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteConfigBackup loescht ein einzelnes Backup
|
||||||
|
func (d *Database) DeleteConfigBackup(agentID string, backupID int64) (bool, error) {
|
||||||
|
res, err := d.db.Exec("DELETE FROM config_backups WHERE agent_id = ? AND id = ?", agentID, backupID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
return rows > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteAgent - Agent und zugehoerige Daten loeschen
|
// DeleteAgent - Agent und zugehoerige Daten loeschen
|
||||||
func (d *Database) DeleteAgent(id string) (bool, error) {
|
func (d *Database) DeleteAgent(id string) (bool, error) {
|
||||||
// Foreign Key Cascade aktivieren
|
// Foreign Key Cascade aktivieren
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func (c *Connection) readPump() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Read-Timeouts setzen
|
// Read-Timeouts setzen
|
||||||
c.Conn.SetReadLimit(512 * 1024) // 512KB max message size
|
c.Conn.SetReadLimit(4 * 1024 * 1024) // 4MB max message size (fuer config.xml Backups)
|
||||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
c.Conn.SetPongHandler(func(string) error {
|
c.Conn.SetPongHandler(func(string) error {
|
||||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user