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.
151 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|