Bug principal : l'SSHAuthenticator est créé au démarrage avec host="" (DB vide avant installation). Après configure, il gardait host vide. Le login lisait maintenant le ssh_host depuis la DB à chaque requête. Logs ajoutés : - ssh_auth.go : dial SSH, succès, échec avec détail d'erreur - auth.go : host SSH utilisé, résultat auth à chaque login - updates.go : credentials SSH, démarrage/fin de job - terminal.go : ouverture/échec session SSH Frontend : - auth.store.ts : gère les réponses non-JSON sur erreur HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
6.8 KiB
Go
206 lines
6.8 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"
|
|
"log"
|
|
"math/rand"
|
|
"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"
|
|
)
|
|
|
|
// 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)
|
|
log.Printf("[updates/run] Credentials SSH — host=%s user=%s password_len=%d", sshHost, sshUser, len(sshPass))
|
|
if sshHost == "" || sshUser == "" || sshPass == "" {
|
|
log.Printf("[updates/run] SSH non configuré — host=%q user=%q password_empty=%v", 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))
|
|
|
|
log.Printf("[updates/run] Job %s démarré — target=%s user=%d", jobID, body.Target, claims.UserID)
|
|
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
|
|
cmdPreview := command
|
|
if len(cmdPreview) > 80 {
|
|
cmdPreview = cmdPreview[:80] + "..."
|
|
}
|
|
log.Printf("[updates/execute] Job %s — SSH %s@%s commande: %s", jobID, sshUser, sshHost, cmdPreview)
|
|
err := h.sshPool.StreamCommand(sshHost, sshUser, sshPass, command, outputChan)
|
|
if err != nil {
|
|
log.Printf("[updates/execute] Job %s — Erreur SSH : %v", jobID, err)
|
|
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
|
|
log.Printf("[updates/execute] Job %s — terminé (%d octets de sortie)", jobID, len(fullOutput))
|
|
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))
|
|
}
|