From f1d475c7e54c1009ec458c338a2c40b0513a0c9f Mon Sep 17 00:00:00 2001 From: enzo Date: Sun, 22 Mar 2026 04:01:59 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20module=20viewServices=20=E2=80=94=20ges?= =?UTF-8?q?tion=20services=20systemd=20via=20systemctl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - module.json : métadonnées du module (nav_href, nav_icon, nav_color, nav_label_key) - backend/go.mod : dépendance sur core/backend via replace directive - backend/services.go : implémente modules.Module, routes /api/services et /api/services/{name}/* - Utilise r.RunOnTarget du Registry (pas d'accès internal) - Liste services, statut détaillé, actions start/stop/restart/reload/enable/disable - Enregistrement du nav item via r.RegisterNavItem - frontend/services.html : page de gestion des services systemd - Composant servicePage Alpine.js inline (autonome, indépendant de core app.js) Co-Authored-By: Claude Sonnet 4.6 --- backend/go.mod | 10 ++ backend/services.go | 163 ++++++++++++++++++++++++++++++ frontend/services.html | 223 +++++++++++++++++++++++++++++++++++++++++ module.json | 12 +++ 4 files changed, 408 insertions(+) create mode 100644 backend/go.mod create mode 100644 backend/services.go create mode 100644 frontend/services.html create mode 100644 module.json diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..69bbb42 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,10 @@ +module git.geronzi.fr/proxmoxPanel/viewServices/backend + +go 1.26 + +require ( + git.geronzi.fr/proxmoxPanel/core/backend v0.0.0 + github.com/go-chi/chi/v5 v5.2.5 +) + +replace git.geronzi.fr/proxmoxPanel/core/backend => ../../core/backend diff --git a/backend/services.go b/backend/services.go new file mode 100644 index 0000000..6f338bb --- /dev/null +++ b/backend/services.go @@ -0,0 +1,163 @@ +// Module viewServices — gestion des services systemd via systemctl. +// Dépôt indépendant : https://git.geronzi.fr/proxmoxPanel/viewServices +package viewservices + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "git.geronzi.fr/proxmoxPanel/core/backend/modules" + "github.com/go-chi/chi/v5" +) + +// ServicesModule gère les opérations systemctl sur le host et les LXC. +type ServicesModule struct{} + +// New crée un ServicesModule. +func New() *ServicesModule { return &ServicesModule{} } + +func (m *ServicesModule) ID() string { return "viewServices" } + +// Register enregistre les routes du module dans le CORE. +func (m *ServicesModule) Register(r modules.Registry) error { + r.RegisterNavItem(modules.NavItemDef{ + ID: "viewServices", + Href: "/viewServices/services.html", + Icon: "lnid-gear-2", + Color: "#fb923c", + LabelKey: "nav.services", + }) + r.RegisterRoute("GET", "/api/services", m.listServices(r), false) + r.RegisterRoute("GET", "/api/services/{name}/status", m.serviceStatus(r), false) + r.RegisterRoute("POST", "/api/services/{name}/{action}", m.serviceAction(r), true) + return nil +} + +// ServiceEntry représente un service systemd dans la liste. +type ServiceEntry struct { + Name string `json:"name"` + LoadState string `json:"load_state"` + ActiveState string `json:"active_state"` + SubState string `json:"sub_state"` + Description string `json:"description"` +} + +// listServices retourne la liste des services systemd d'une cible. +// GET /api/services?target=host|lxc:ID +func (m *ServicesModule) listServices(r modules.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + target := req.URL.Query().Get("target") + if target == "" { + target = "host" + } + + out, err := r.RunOnTarget(target, "systemctl list-units --type=service --all --no-legend --plain --no-pager 2>/dev/null") + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Erreur commande SSH : " + err.Error()}) + return + } + + services := parseServiceList(out) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(services) + } +} + +// parseServiceList parse la sortie de systemctl list-units. +func parseServiceList(output string) []ServiceEntry { + var services []ServiceEntry + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "UNIT") || strings.HasPrefix(line, "Legend") || + strings.HasPrefix(line, "To show") || strings.HasPrefix(line, "Pass") { + continue + } + line = strings.TrimPrefix(line, "● ") + line = strings.TrimPrefix(line, " ") + + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + name := strings.TrimSuffix(fields[0], ".service") + desc := "" + if len(fields) >= 5 { + desc = strings.Join(fields[4:], " ") + } + services = append(services, ServiceEntry{ + Name: name, + LoadState: fields[1], + ActiveState: fields[2], + SubState: fields[3], + Description: desc, + }) + } + return services +} + +// serviceStatus retourne le statut détaillé d'un service. +// GET /api/services/{name}/status?target=host|lxc:ID +func (m *ServicesModule) serviceStatus(r modules.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + target := req.URL.Query().Get("target") + if target == "" { + target = "host" + } + + out, _ := r.RunOnTarget(target, fmt.Sprintf("systemctl status %s --no-pager 2>&1", name)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"output": out}) + } +} + +// serviceAction exécute une action (start/stop/restart/reload) sur un service. +// POST /api/services/{name}/{action} +// Body: { "target": "host" | "lxc:ID" } +func (m *ServicesModule) serviceAction(r modules.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + action := chi.URLParam(req, "action") + + // Valider l'action pour éviter l'injection de commandes + allowed := map[string]bool{ + "start": true, "stop": true, "restart": true, + "reload": true, "enable": true, "disable": true, + } + if !allowed[action] { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Action invalide"}) + return + } + + var body struct { + Target string `json:"target"` + } + if err := json.NewDecoder(req.Body).Decode(&body); err != nil || body.Target == "" { + body.Target = "host" + } + + out, err := r.RunOnTarget(body.Target, fmt.Sprintf("systemctl %s %s 2>&1", action, name)) + + w.Header().Set("Content-Type", "application/json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "error": fmt.Sprintf("Erreur action %s sur %s : %v", action, name, err), + "output": out, + }) + return + } + + json.NewEncoder(w).Encode(map[string]string{ + "message": fmt.Sprintf("Action « %s » exécutée sur %s", action, name), + "output": out, + }) + } +} diff --git a/frontend/services.html b/frontend/services.html new file mode 100644 index 0000000..2399cf7 --- /dev/null +++ b/frontend/services.html @@ -0,0 +1,223 @@ + + + + + + + ProxmoxPanel — Services + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ + + + + +
+ + +
+
+ Chargement… +
+ + +
+ + + + + + + + + + + + + + + + +
Actions
+
+ +
+
+
+ + + diff --git a/module.json b/module.json new file mode 100644 index 0000000..6988931 --- /dev/null +++ b/module.json @@ -0,0 +1,12 @@ +{ + "id": "viewServices", + "name": "Services", + "description": "Gestion des services systemd via systemctl", + "version": "1.0.0", + "author": "proxmoxPanel", + "core_min_version": "1.0.0", + "nav_href": "/viewServices/services.html", + "nav_icon": "lnid-gear-2", + "nav_color": "#fb923c", + "nav_label_key": "nav.services" +}