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")
|
||||
|
||||
@@ -422,6 +422,27 @@ func (d *Database) UpdateTaskStatusDirect(id int, status string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) UpdateTask(id int, name, scheduleType, scheduleTime string, weekday, monthday *int,
|
||||
reboot, healthCheck bool, healthCheckTimeout int, webhookURL string, active bool) (*models.Task, error) {
|
||||
|
||||
now := time.Now()
|
||||
nextRun := CalculateNextRun(scheduleType, scheduleTime, weekday, monthday, now)
|
||||
|
||||
_, err := d.db.Exec(`
|
||||
UPDATE tasks SET
|
||||
name = $1, schedule_type = $2, schedule_time = $3,
|
||||
schedule_weekday = $4, schedule_monthday = $5,
|
||||
reboot = $6, health_check = $7, health_check_timeout = $8,
|
||||
webhook_url = $9, active = $10, next_run = $11
|
||||
WHERE id = $12
|
||||
`, name, scheduleType, scheduleTime, weekday, monthday,
|
||||
reboot, healthCheck, healthCheckTimeout, webhookURL, active, nextRun, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Task aktualisieren: %w", err)
|
||||
}
|
||||
return d.GetTask(id)
|
||||
}
|
||||
|
||||
func (d *Database) DeleteTask(id int) error {
|
||||
res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id)
|
||||
if err != nil {
|
||||
|
||||
@@ -78,6 +78,7 @@ func main() {
|
||||
// Geschuetzte Routes
|
||||
protectedMux := http.NewServeMux()
|
||||
handler := api.NewHandler(database)
|
||||
handler.SetScheduler(scheduler)
|
||||
handler.SetupRoutes(protectedMux, hub)
|
||||
// GET /api/v1/auth/me braucht JWT
|
||||
protectedMux.HandleFunc("GET /api/v1/auth/me", authHandler.Me)
|
||||
|
||||
@@ -264,9 +264,17 @@ type CaddySite struct {
|
||||
TLS bool `json:"tls"`
|
||||
}
|
||||
|
||||
type CaddyUpstream struct {
|
||||
Address string `json:"address"`
|
||||
NumRequests int `json:"num_requests"`
|
||||
Fails int `json:"fails"`
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
|
||||
type CaddyData struct {
|
||||
Version string `json:"version"`
|
||||
Sites []CaddySite `json:"sites"`
|
||||
Version string `json:"version"`
|
||||
Sites []CaddySite `json:"sites"`
|
||||
Upstreams []CaddyUpstream `json:"upstreams,omitempty"`
|
||||
}
|
||||
|
||||
// HeartbeatRequest
|
||||
|
||||
Reference in New Issue
Block a user