feat: Versions-Spalte im Dienste-Tab für Superadmin

Bislang war die einzige Möglichkeit, die laufende Manticore-/PostgreSQL-/
Postfix-/nginx-Version zu sehen, SSH + <binary> --version. serviceVersion()
löst das best-effort pro Dienst auf (archivmail/-web: appVersion-Konstante,
manticore: searchd --version, postgresql: psql --version, postfix: postconf
mail_version, nginx: nginx -v). Fehler werden verschluckt (leerer String),
eine unbekannte Version darf den Dienst-Status nicht auf "Fehler" kippen.

manticore war bisher gar nicht in der Dienste-Whitelist (allowedServices) —
jetzt ergänzt, damit es überhaupt in der Liste auftaucht und
start/stop/restart wie die anderen Dienste möglich ist.
This commit is contained in:
sysops
2026-07-05 19:12:38 +02:00
parent bbefa838c3
commit 8564d7c11f
3 changed files with 59 additions and 4 deletions
+54 -4
View File
@@ -17,6 +17,7 @@ import (
var allowedServices = []string{
"archivmail",
"archivmail-web",
"manticore",
"postgresql@17-main",
"postfix",
"nginx",
@@ -29,9 +30,57 @@ type ServiceStatus struct {
Sub string `json:"sub"` // running, dead, exited, ...
Enabled string `json:"enabled"` // enabled, disabled, static, unknown
Description string `json:"description"`
Version string `json:"version,omitempty"` // best-effort, empty if not determinable
ExternalBlocked *bool `json:"external_blocked,omitempty"` // only set for archivmail
}
// serviceVersion resolves the installed version of a service, best-effort.
// Superadmin-visible "which version is actually running" overview — before
// this, the only way to see e.g. the Manticore version after an upgrade
// (PROJ-67) was SSH + `searchd --version`. Errors are swallowed on purpose:
// an unknown version must never turn the whole services list red.
func (s *Server) serviceVersion(name string) string {
switch name {
case "archivmail", "archivmail-web":
return s.appVersion
case "manticore":
out, err := exec.Command("searchd", "--version").CombinedOutput()
if err != nil {
return ""
}
return firstLine(string(out))
case "postgresql@17-main":
out, err := exec.Command("psql", "--version").Output()
if err != nil {
return ""
}
return firstLine(string(out))
case "postfix":
out, err := exec.Command("postconf", "mail_version").Output()
if err != nil {
return ""
}
_, v, ok := strings.Cut(firstLine(string(out)), "=")
if !ok {
return ""
}
return strings.TrimSpace(v)
case "nginx":
out, err := exec.Command("nginx", "-v").CombinedOutput()
if err != nil {
return ""
}
return firstLine(string(out))
default:
return ""
}
}
func firstLine(s string) string {
line, _, _ := strings.Cut(s, "\n")
return strings.TrimSpace(line)
}
func isAllowedService(name string) bool {
for _, s := range allowedServices {
if s == name {
@@ -41,7 +90,7 @@ func isAllowedService(name string) bool {
return false
}
func systemctlShow(name string) ServiceStatus {
func (s *Server) systemctlShow(name string) ServiceStatus {
svc := ServiceStatus{Name: name, DisplayName: name}
out, err := exec.Command("systemctl", "show", name+".service",
"--property=ActiveState,SubState,UnitFileState,Description",
@@ -72,6 +121,7 @@ func systemctlShow(name string) ServiceStatus {
blocked := nftAPIBlocked()
svc.ExternalBlocked = &blocked
}
svc.Version = s.serviceVersion(name)
return svc
}
@@ -87,7 +137,7 @@ func nftAPIBlocked() bool {
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
result := make([]ServiceStatus, 0, len(allowedServices))
for _, name := range allowedServices {
result = append(result, systemctlShow(name))
result = append(result, s.systemctlShow(name))
}
writeJSON(w, http.StatusOK, result)
}
@@ -142,7 +192,7 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) {
Detail: name,
Success: true,
})
writeJSON(w, http.StatusOK, systemctlShow(name))
writeJSON(w, http.StatusOK, s.systemctlShow(name))
return
}
@@ -166,5 +216,5 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) {
Success: true,
})
writeJSON(w, http.StatusOK, systemctlShow(name))
writeJSON(w, http.StatusOK, s.systemctlShow(name))
}