Files
mesh-catalog/modules/systemd/cmd/systemd-tools/main.go
T
jochen c42f1ce45b systemd: port to Go, and read a system unit's journal as root
The journal verb ran journalctl as the operator account, which outside the
journal's group sees only its own entries: every system service read
'-- No entries --', and a person reached for a shell. The read now
escalates with sudo -n like the acts; ported to Go with every test. hq
issue 255.
2026-10-05 18:09:42 +02:00

151 lines
5.2 KiB
Go

// systemd's tools: the node-service-manager seat's eight verbs — the units on this machine in both scopes,
// read and acted on by name — and the module's own reading of what has failed (novox/hq ADR 0177). The node
// tools runtime launches this bundle as a process of its own and serves what it serves (ADR 0188, ADR 0193);
// it runs as the operator account, so acts on the system manager, and reads of its journal, escalate with
// sudo -n, and the user scope is the account's own manager (client.go). The host applies units; this answers
// about them. stdout is the MCP channel; this says nothing else.
package main
import (
"fmt"
"os"
"os/user"
"strconv"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
const seat = "node-service-manager"
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
var (
scopeArg = str(`"system" (the default) or "user": the operator account's own manager`)
unitArgS = str("the unit's name, as the service manager knows it")
)
func scopeOf(a map[string]any) (Scope, error) {
s, _ := a["scope"].(string)
switch s {
case "", "system":
return System, nil
case "user":
return User, nil
}
return "", fmt.Errorf("scope %q: \"system\" or \"user\"", s)
}
func unitOf(a map[string]any) (string, error) {
u, _ := a["unit"].(string)
if u = strings.TrimSpace(u); u == "" {
return "", fmt.Errorf("a unit is required")
}
return u, nil
}
func tools(m *Manager) []stdio.Tool {
act := func(verb, description string) stdio.Tool {
return stdio.Tool{Name: seat + "." + verb, Description: description,
Input: map[string]any{"scope": scopeArg, "unit": unitArgS},
Run: func(a map[string]any) (any, error) {
scope, err := scopeOf(a)
if err != nil {
return nil, err
}
unit, err := unitOf(a)
if err != nil {
return nil, err
}
return m.Act(scope, verb, unit)
}}
}
return []stdio.Tool{
{Name: seat + ".units",
Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
Input: map[string]any{"scope": scopeArg, "pattern": str("a glob the unit's name must match (optional)")},
Run: func(a map[string]any) (any, error) {
scope, err := scopeOf(a)
if err != nil {
return nil, err
}
pattern, _ := a["pattern"].(string)
units, err := m.Units(scope, pattern)
if err != nil {
return nil, err
}
return map[string]any{"scope": string(scope), "units": units}, nil
}},
{Name: seat + ".status",
Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and mesh_declared — true when its unit file is one the mesh wrote (a unit the mesh only puts into a state is not recognised from here).",
Input: map[string]any{"scope": scopeArg, "unit": unitArgS},
Run: func(a map[string]any) (any, error) {
scope, err := scopeOf(a)
if err != nil {
return nil, err
}
unit, err := unitOf(a)
if err != nil {
return nil, err
}
return m.Status(scope, unit)
}},
act("start", "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at its next apply."),
act("stop", "Stop one unit; for a unit the mesh declares, the answer says the host will restore its declared state."),
act("restart", "Restart one unit."),
act("enable", "Make one unit start at boot (or at the account's login, in user scope)."),
act("disable", "Stop one unit starting at boot (or at login, in user scope)."),
{Name: seat + ".journal",
Description: "The last lines of one unit's journal (at most 2000) — a system service's included: the read is escalated, so it is the service's own lines and not only the operator account's.",
Input: map[string]any{"scope": scopeArg, "unit": unitArgS,
"lines": map[string]any{"type": "number", "description": "how many lines from the end (default 100, at most 2000)"}},
Run: func(a map[string]any) (any, error) {
scope, err := scopeOf(a)
if err != nil {
return nil, err
}
unit, err := unitOf(a)
if err != nil {
return nil, err
}
// Bounded so the answer stays well below what the runtime carries back in one reply.
n := 100
if v, ok := a["lines"].(float64); ok && v >= 1 {
n = min(int(v), 2000)
}
return m.Journal(scope, unit, n)
}},
{Name: "systemd_failed",
Description: "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, not as nothing failed.",
Run: func(map[string]any) (any, error) { return m.Failed(), nil }},
}
}
func fromEnv() *Manager {
me, _ := user.Current()
name := ""
if me != nil {
name = me.Username
}
account := strings.TrimSpace(os.Getenv("MESH_OPERATOR_ACCOUNT"))
if account == "" {
account = name
}
uid := os.Getuid()
if me != nil {
if n, err := strconv.Atoi(me.Uid); err == nil {
uid = n
}
}
return &Manager{Account: account, UID: uid, User: name, Run: execRunner, Read: readFile, Env: os.Environ()}
}
func main() {
if err := stdio.Serve("", tools(fromEnv())); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}