core/backend/internal/api/updates.go
enzo 5dbcb1df07 feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ :
- API REST + WebSocket (chi, gorilla/websocket)
- Authentification PAM via SSH + JWT RS256
- Chiffrement AES-256-GCM pour secrets SQLite
- Pool SSH, client Proxmox REST, hub WebSocket pub/sub
- Système de modules compilés à initialisation conditionnelle
- Audit log, migrations SQLite versionnées

Frontend Vue 3 + Vite + TypeScript :
- Thème Neumorphism sombre/clair (CSS custom properties)
- Wizard d'installation, Dashboard drag-drop, Terminal xterm.js
- Toutes les vues CORE + stubs modules optionnels
- i18n EN/FR (vue-i18n v11)

Infrastructure :
- Docker multi-stage (Go → alpine, Node → nginx)
- docker-compose.yml, .gitattributes, LICENSE MIT, README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:08:53 +01:00

196 lines
6.1 KiB
Go

// 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))
}