feat: page mises à jour avec liste des paquets par cible
- Backend: GET /api/updates/targets (pct list via SSH) - Backend: GET /api/updates/packages?target= (apt list --upgradable) - Frontend: grille de cards par cible (host + chaque LXC) - Bouton Check/Update par card, liste paquets dépliable (version actuelle → nouvelle) - Boutons globaux "Tout vérifier" et "Tout mettre à jour" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c6028b6951
commit
82e3b850d0
5 changed files with 493 additions and 102 deletions
|
|
@ -8,6 +8,8 @@ import (
|
|||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
|
||||
|
|
@ -38,6 +40,143 @@ func NewUpdatesHandler(database *db.DB, sshPool *ssh.Pool, hub *websocket.Hub, a
|
|||
}
|
||||
}
|
||||
|
||||
// sshCredentials récupère les credentials SSH depuis la configuration SQLite.
|
||||
func (h *UpdatesHandler) sshCredentials() (host, user, pass string, err error) {
|
||||
host, _, _ = h.db.GetSetting("ssh_host")
|
||||
user, _, _ = h.db.GetSetting("ssh_username")
|
||||
encPass, _, _ := h.db.GetSetting("ssh_password")
|
||||
if encPass != "" {
|
||||
pass, err = h.encryptor.Decrypt(encPass)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("impossible de déchiffrer le mot de passe SSH")
|
||||
}
|
||||
}
|
||||
if host == "" || user == "" || pass == "" {
|
||||
return "", "", "", fmt.Errorf("SSH non configuré")
|
||||
}
|
||||
return host, user, pass, nil
|
||||
}
|
||||
|
||||
// GetTargets retourne la liste des cibles disponibles : host Proxmox + tous les LXC.
|
||||
// GET /api/updates/targets
|
||||
func (h *UpdatesHandler) GetTargets(w http.ResponseWriter, r *http.Request) {
|
||||
sshHost, sshUser, sshPass, err := h.sshCredentials()
|
||||
if err != nil {
|
||||
JSONError(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
VMID int `json:"vmid,omitempty"`
|
||||
}
|
||||
|
||||
targets := []Target{
|
||||
{ID: "host", Name: "Proxmox Host", Status: "running"},
|
||||
}
|
||||
|
||||
output, err := h.sshPool.RunCommand(sshHost, sshUser, sshPass, "pct list 2>/dev/null")
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 || fields[0] == "VMID" {
|
||||
continue
|
||||
}
|
||||
vmid, parseErr := strconv.Atoi(fields[0])
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
status := fields[1]
|
||||
name := fields[0] // fallback VMID si pas de nom
|
||||
if len(fields) >= 3 {
|
||||
name = fields[len(fields)-1]
|
||||
}
|
||||
targets = append(targets, Target{
|
||||
ID: fmt.Sprintf("lxc:%d", vmid),
|
||||
Name: name,
|
||||
Status: status,
|
||||
VMID: vmid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusOK, targets)
|
||||
}
|
||||
|
||||
// GetPackages retourne la liste des paquets pouvant être mis à jour pour une cible.
|
||||
// GET /api/updates/packages?target=host|lxc:100
|
||||
func (h *UpdatesHandler) GetPackages(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.URL.Query().Get("target")
|
||||
if target == "" {
|
||||
JSONError(w, "Paramètre 'target' requis", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sshHost, sshUser, sshPass, err := h.sshCredentials()
|
||||
if err != nil {
|
||||
JSONError(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
var command string
|
||||
switch {
|
||||
case target == "host":
|
||||
command = "apt list --upgradable 2>/dev/null"
|
||||
case len(target) > 4 && target[:4] == "lxc:":
|
||||
lxcID := target[4:]
|
||||
command = fmt.Sprintf("pct exec %s -- apt list --upgradable 2>/dev/null", lxcID)
|
||||
default:
|
||||
JSONError(w, "Cible invalide", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := h.sshPool.RunCommand(sshHost, sshUser, sshPass, command)
|
||||
if err != nil {
|
||||
log.Printf("[updates/packages] Erreur SSH pour %s : %v", target, err)
|
||||
JSONError(w, "Erreur SSH : "+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusOK, parseAptPackages(output))
|
||||
}
|
||||
|
||||
// parseAptPackages analyse la sortie de `apt list --upgradable`.
|
||||
func parseAptPackages(output string) []map[string]string {
|
||||
var packages []map[string]string
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.Contains(line, "[upgradable from:") {
|
||||
continue
|
||||
}
|
||||
// Format : name/repo version arch [upgradable from: old_version]
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
name := strings.SplitN(parts[0], "/", 2)[0]
|
||||
version := parts[1]
|
||||
oldVersion := ""
|
||||
if idx := strings.Index(line, "upgradable from: "); idx >= 0 {
|
||||
oldVersion = strings.TrimRight(line[idx+len("upgradable from: "):], "] ")
|
||||
}
|
||||
packages = append(packages, map[string]string{
|
||||
"name": name,
|
||||
"version": version,
|
||||
"old_version": oldVersion,
|
||||
})
|
||||
}
|
||||
if packages == nil {
|
||||
packages = []map[string]string{}
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
// RunUpdate lance une mise à jour apt sur la cible spécifiée.
|
||||
// POST /api/updates/run
|
||||
// Body: { "target": "host" | "lxc:100" | "all" }
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ func main() {
|
|||
r.Group(func(r chi.Router) {
|
||||
r.Use(api.RequireAdmin)
|
||||
r.Post("/api/updates/run", updatesHandler.RunUpdate)
|
||||
r.Get("/api/updates/targets", updatesHandler.GetTargets)
|
||||
r.Get("/api/updates/packages", updatesHandler.GetPackages)
|
||||
})
|
||||
r.Get("/api/updates/history", updatesHandler.GetHistory)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue