171 lines
5.3 KiB
Go
171 lines
5.3 KiB
Go
// Module viewServices — gestion des services systemd via systemctl.
|
|
// Dépôt indépendant : https://git.geronzi.fr/proxmoxPanel/viewServices
|
|
package viewservices
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.geronzi.fr/proxmoxPanel/core/backend/modules"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
//go:embed frontend/services.html
|
|
var servicesHTML []byte
|
|
|
|
// 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.RegisterPublicRoute("GET", "/viewServices/services.html", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Write(servicesHTML)
|
|
})
|
|
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,
|
|
})
|
|
}
|
|
}
|