feat: module viewServices — gestion services systemd via systemctl
- 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 <noreply@anthropic.com>
This commit is contained in:
commit
f1d475c7e5
4 changed files with 408 additions and 0 deletions
163
backend/services.go
Normal file
163
backend/services.go
Normal file
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue