Initial commit — cynfo RMM (clean history, no secrets)

This commit is contained in:
cynfo3000
2026-03-06 09:18:45 +01:00
parent d4b6ca86e5
commit 48ac353d28
35 changed files with 732 additions and 52 deletions
+26 -5
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"log"
"strings"
"net/http"
"github.com/cynfo/rmm-backend/ws"
@@ -32,12 +33,32 @@ func (h *Handler) uploadFirmware(w http.ResponseWriter, r *http.Request) {
return
}
// Binary aus Body lesen (max 50MB)
// Binary lesen — Multipart (curl -F) oder raw Body (Content-Type: application/octet-stream)
r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024)
binary, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen")
return
var binary []byte
if ct := r.Header.Get("Content-Type"); strings.HasPrefix(ct, "multipart/") {
if err := r.ParseMultipartForm(50 << 20); err != nil {
writeError(w, http.StatusBadRequest, "Multipart-Parse fehlgeschlagen")
return
}
file, _, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "Feld 'file' fehlt im Multipart-Form")
return
}
defer file.Close()
binary, err = io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen")
return
}
} else {
var err error
binary, err = io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen")
return
}
}
if len(binary) == 0 {
+4 -1
View File
@@ -16,7 +16,8 @@ import (
)
type Handler struct {
db *db.Database
db *db.Database
scheduler *TaskScheduler
}
func NewHandler(database *db.Database) *Handler {
@@ -86,7 +87,9 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
mux.HandleFunc("POST /api/v1/tasks", h.createTask(hub))
mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.deleteTask)
mux.HandleFunc("POST /api/v1/tasks/{id}/cancel", h.cancelTask)
mux.HandleFunc("PUT /api/v1/tasks/{id}", h.updateTask)
mux.HandleFunc("PUT /api/v1/tasks/{id}/toggle", h.toggleTask)
mux.HandleFunc("POST /api/v1/tasks/{id}/run", h.runTaskNow())
// WebSocket und Tunnel-Routes
h.setupTunnelRoutes(mux, hub)
+51 -3
View File
@@ -275,8 +275,56 @@ func (s *TaskScheduler) executeUpdateWithHealthCheck(agentID string, reboot bool
return result, nil
}
func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) {
payload, err := json.Marshal(task)
// WebhookPayload ist der strukturierte Payload fuer ERP-Serviceberichte
type WebhookPayload struct {
CustomerID string `json:"customer_id"`
Hostname string `json:"hostname"`
OPNsenseVersion string `json:"opnsense_version"`
Timestamp string `json:"timestamp"`
Trigger string `json:"trigger"`
Reachable bool `json:"reachable"`
UpdateStatus string `json:"update_status"`
}
func (s *TaskScheduler) sendWebhook(taskID int, url string, task *models.Task) {
// Agent-Daten aus DB holen (hostname, opnsense_version)
agent, _, err := s.db.GetAgent(task.AgentID)
if err != nil || agent == nil {
log.Printf("Scheduler: Webhook Task #%d — Agent %s nicht gefunden: %v", taskID, task.AgentID, err)
s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Agent nicht gefunden: %v", err))
return
}
// Erreichbarkeit aus Health-Check-Result extrahieren
reachable := true
if result, ok := task.Result.(map[string]interface{}); ok {
if hc, ok := result["health_check"].(map[string]interface{}); ok {
if online, ok := hc["agent_online"].(bool); ok {
reachable = online
}
}
}
// Trigger aus Action ableiten
trigger := task.Action + "_completed"
// Timestamp: FinishedAt oder jetzt
timestamp := time.Now().UTC().Format(time.RFC3339)
if task.FinishedAt != nil && *task.FinishedAt != "" {
timestamp = *task.FinishedAt
}
payload := WebhookPayload{
CustomerID: task.CustomerNumber,
Hostname: agent.Hostname,
OPNsenseVersion: agent.OPNsenseVersion,
Timestamp: timestamp,
Trigger: trigger,
Reachable: reachable,
UpdateStatus: task.Status,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
log.Printf("Scheduler: Webhook-Payload fuer Task #%d serialisieren fehlgeschlagen: %v", taskID, err)
s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("JSON-Fehler: %v", err))
@@ -284,7 +332,7 @@ func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) {
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(url, "application/json", bytes.NewReader(payload))
resp, err := client.Post(url, "application/json", bytes.NewReader(payloadJSON))
if err != nil {
log.Printf("Scheduler: Webhook fuer Task #%d fehlgeschlagen: %v", taskID, err)
s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Fehler: %v", err))
+84
View File
@@ -180,6 +180,90 @@ func (h *Handler) cancelTask(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"message": "Task abgebrochen"})
}
// PUT /api/v1/tasks/{id}
func (h *Handler) updateTask(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
return
}
var req struct {
Name string `json:"name"`
ScheduleType string `json:"schedule_type"`
ScheduleTime string `json:"schedule_time"`
ScheduleWeekday *int `json:"schedule_weekday"`
ScheduleMonthday *int `json:"schedule_monthday"`
Reboot bool `json:"reboot"`
HealthCheck bool `json:"health_check"`
HealthCheckTimeout int `json:"health_check_timeout"`
WebhookURL string `json:"webhook_url"`
Active *bool `json:"active"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
active := true
if req.Active != nil {
active = *req.Active
}
if req.HealthCheckTimeout <= 0 {
req.HealthCheckTimeout = 600
}
task, err := h.db.UpdateTask(id, req.Name, req.ScheduleType, req.ScheduleTime,
req.ScheduleWeekday, req.ScheduleMonthday,
req.Reboot, req.HealthCheck, req.HealthCheckTimeout, req.WebhookURL, active)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler: "+err.Error())
return
}
if task == nil {
writeError(w, http.StatusNotFound, "Task nicht gefunden")
return
}
h.auditLog(r, "task.update", "task", idStr, "", "")
writeJSON(w, http.StatusOK, task)
}
// SetScheduler setzt den Scheduler im Handler (fuer Run-Now)
func (h *Handler) SetScheduler(s *TaskScheduler) {
h.scheduler = s
}
// POST /api/v1/tasks/{id}/run — Task sofort ausfuehren
func (h *Handler) runTaskNow() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
return
}
task, err := h.db.GetTask(id)
if err != nil || task == nil {
writeError(w, http.StatusNotFound, "Task nicht gefunden")
return
}
if h.scheduler == nil {
writeError(w, http.StatusInternalServerError, "Scheduler nicht verfuegbar")
return
}
// Sofort im Hintergrund starten
go h.scheduler.executeTask(*task)
h.auditLog(r, "task.run_now", "task", idStr, "", task.Action)
writeJSON(w, http.StatusOK, map[string]string{"message": "Task gestartet"})
}
}
// DELETE /api/v1/tasks/{id}
func (h *Handler) deleteTask(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")