// Handlers pour les mises à jour de paquets apt. // Supporte : host Proxmox, un LXC spécifique, ou tous les LXC. // La sortie est streamée ligne par ligne via WebSocket. package api import ( "fmt" "net/http" "time" "git.geronzi.fr/proxmoxPanel/core/backend/internal/audit" "git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto" "git.geronzi.fr/proxmoxPanel/core/backend/internal/db" "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh" "git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket" "github.com/go-chi/chi/v5" "math/rand" ) // UpdatesHandler contient les handlers de mises à jour. type UpdatesHandler struct { db *db.DB sshPool *ssh.Pool hub *websocket.Hub auditLogger *audit.Logger encryptor *crypto.Encryptor } // NewUpdatesHandler crée un UpdatesHandler. func NewUpdatesHandler(database *db.DB, sshPool *ssh.Pool, hub *websocket.Hub, auditLog *audit.Logger, enc *crypto.Encryptor) *UpdatesHandler { return &UpdatesHandler{ db: database, sshPool: sshPool, hub: hub, auditLogger: auditLog, encryptor: enc, } } // RunUpdate lance une mise à jour apt sur la cible spécifiée. // POST /api/updates/run // Body: { "target": "host" | "lxc:100" | "all" } func (h *UpdatesHandler) RunUpdate(w http.ResponseWriter, r *http.Request) { claims := GetClaims(r) var body struct { Target string `json:"target"` } if err := decodeJSON(r, &body); err != nil || body.Target == "" { JSONError(w, "Paramètre 'target' requis (host, lxc:ID, ou all)", http.StatusBadRequest) return } // Récupérer les credentials SSH depuis les settings sshHost, _, _ := h.db.GetSetting("ssh_host") sshUser, _, _ := h.db.GetSetting("ssh_username") encryptedPass, _, _ := h.db.GetSetting("ssh_password") sshPass, _ := h.encryptor.Decrypt(encryptedPass) if sshHost == "" || sshUser == "" || sshPass == "" { JSONError(w, "SSH non configuré", http.StatusServiceUnavailable) return } // Générer un ID de job unique jobID := generateJobID() // Enregistrer le job en base h.db.Exec(` INSERT INTO update_history (job_id, target, status, started_by) VALUES (?, ?, 'running', ?) `, jobID, body.Target, claims.UserID) h.auditLogger.Log(&claims.UserID, claims.Username, "update_start", body.Target, nil, clientIP(r)) // Lancer la mise à jour en arrière-plan go h.executeUpdate(jobID, body.Target, sshHost, sshUser, sshPass, claims.UserID) JSONResponse(w, http.StatusAccepted, map[string]string{ "job_id": jobID, "message": "Mise à jour démarrée", }) } // GetHistory retourne l'historique des mises à jour. // GET /api/updates/history func (h *UpdatesHandler) GetHistory(w http.ResponseWriter, r *http.Request) { rows, err := h.db.Query(` SELECT job_id, target, status, output, started_at, finished_at FROM update_history ORDER BY started_at DESC LIMIT 50 `) if err != nil { JSONError(w, "Erreur lecture historique", http.StatusInternalServerError) return } defer rows.Close() type entry struct { JobID string `json:"job_id"` Target string `json:"target"` Status string `json:"status"` Output string `json:"output"` StartedAt string `json:"started_at"` FinishedAt *string `json:"finished_at,omitempty"` } var entries []entry for rows.Next() { var e entry var finishedAt *string rows.Scan(&e.JobID, &e.Target, &e.Status, &e.Output, &e.StartedAt, &finishedAt) e.FinishedAt = finishedAt entries = append(entries, e) } JSONResponse(w, http.StatusOK, entries) } // WebSocketUpdate ouvre un WebSocket pour suivre un job de mise à jour en temps réel. // GET /ws/updates/{jobId} func (h *UpdatesHandler) WebSocketUpdate(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } claims := GetClaims(r) var userID int64 if claims != nil { userID = claims.UserID } jobID := chi.URLParam(r, "jobId") wsClient := h.hub.NewClient(conn, userID) wsClient.Subscribe("update:" + jobID) } // executeUpdate exécute la commande apt et streame la sortie via WebSocket. func (h *UpdatesHandler) executeUpdate(jobID, target, sshHost, sshUser, sshPass string, userID int64) { outputChan := make(chan string, 100) var command string switch { case target == "host": command = "DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y" case len(target) > 4 && target[:4] == "lxc:": lxcID := target[4:] command = fmt.Sprintf( "pct exec %s -- bash -c 'DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y'", lxcID, ) case target == "all": command = `for ct in $(pct list | awk 'NR>1 {print $1}'); do echo "=== LXC $ct ===" pct exec $ct -- bash -c 'DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y' 2>/dev/null || echo "SKIP LXC $ct" done` default: h.db.Exec(`UPDATE update_history SET status='error', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`, "Cible invalide : "+target, jobID) return } // Lancer le streaming SSH err := h.sshPool.StreamCommand(sshHost, sshUser, sshPass, command, outputChan) if err != nil { h.db.Exec(`UPDATE update_history SET status='error', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`, "Erreur SSH : "+err.Error(), jobID) h.hub.Publish("update:"+jobID, "update_error", map[string]string{"error": err.Error()}) return } // Collecter la sortie et la publier ligne par ligne var fullOutput string for chunk := range outputChan { fullOutput += chunk h.hub.Publish("update:"+jobID, "update_output", map[string]string{"chunk": chunk}) } // Finaliser le job h.db.Exec(`UPDATE update_history SET status='success', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`, fullOutput, jobID) h.hub.Publish("update:"+jobID, "update_done", map[string]string{"job_id": jobID}) } // generateJobID génère un identifiant unique pour un job de mise à jour. func generateJobID() string { const chars = "abcdefghijklmnopqrstuvwxyz0123456789" b := make([]byte, 8) for i := range b { b[i] = chars[rand.Intn(len(chars))] } return fmt.Sprintf("%d-%s", time.Now().Unix(), string(b)) }