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