Initial commit — cynfo RMM (clean history, no secrets)
This commit is contained in:
+26
-5
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user