// logrotate's tools bundle (novox/hq to-be 42 Phase 1, research 026/05): a process the node's // runtime launches and speaks MCP over stdio to, through the Go SDK (ADR 0188, ADR 0193). It reads // when each log was last rotated, the rule files and a dry run of them, and the largest logs; forces // one rule file; and reads and vacuums the journal. Acts go through sudo -n. package main import ( "context" "fmt" "os" stdio "git.novox.be/novox/mesh-sdk/go" ) // binaryName is what the build names this bundle's executable: the manifest's `binary`. const binaryName = "logrotate-tools" func bg() context.Context { return context.Background() } func main() { // An empty name serves as the module the runtime names (MESH_SERVED_MODULE): logrotate. if err := stdio.Serve("", tools(ThisMachine())); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // writeTemp writes a file only this account can write, and gives back how to remove it. func writeTemp(content string) (string, func(), error) { f, err := os.CreateTemp("", "mesh-logrotate-*.conf") if err != nil { return "", nil, err } _, werr := f.WriteString(content) cerr := f.Close() done := func() { os.Remove(f.Name()) } if werr != nil || cerr != nil { done() return "", nil, fmt.Errorf("writing %s: %v %v", f.Name(), werr, cerr) } return f.Name(), done, nil } func tools(m *Machine) []stdio.Tool { return []stdio.Tool{ { Name: "logrotate_status", Description: "When logrotate last rotated each log (its status file, read through sudo -n), narrowed to logs whose path holds a word; with the timer's last and next run and the last run's result. A machine where it never ran says so.", Input: schema(map[string]any{"match": map[string]any{"type": "string", "description": "only logs whose path holds this"}}), Run: func(args map[string]any) (any, error) { match, err := text(args, "match", false) if err != nil { return nil, err } return m.Status(match) }, }, { Name: "logrotate_configs", Description: "The base configuration's global settings and every rule file of /etc/logrotate.d with the logs it rotates.", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.Configs() }, }, { Name: "logrotate_check", Description: "A dry run of the whole configuration (logrotate -d through sudo -n, which changes nothing): its errors and warnings, so a broken rule is found before the night it runs.", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.Check() }, }, { Name: "logrotate_big_logs", Description: "The largest files under /var/log on its own filesystem (read through sudo -n), with size and modification time; with the total and how much of it is the journal. Journal files are listed only when asked (journals: true).", Input: schema(map[string]any{ "limit": map[string]any{"type": "integer", "description": "how many (default 20, at most 200)"}, "journals": map[string]any{"type": "boolean", "description": "list the journal's files too"}, }), Run: func(args map[string]any) (any, error) { n, err := whole(args, "limit", 20, 1, 200) if err != nil { return nil, err } j, err := flag(args, "journals") if err != nil { return nil, err } return m.BigLogs(n, j) }, }, { Name: "logrotate_force", Description: "Rotate now (logrotate -f -v, through sudo -n) the logs of one rule file of /etc/logrotate.d, with the base configuration's " + "global settings before it so it rotates as the nightly run would; or every log, given logrotate.conf. Answers what was rotated, the errors and the log.", Input: schema(map[string]any{"config": map[string]any{"type": "string", "description": "a file name in /etc/logrotate.d, or logrotate.conf for every log"}}, "config"), Run: func(args map[string]any) (any, error) { config, err := text(args, "config", true) if err != nil { return nil, err } return m.Force(config, writeTemp) }, }, { Name: "logrotate_journal_usage", Description: "How much the systemd journal holds on disk (journalctl --disk-usage, through sudo -n so every part is counted) and the journald settings that bound it.", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.JournalUsage() }, }, { Name: "logrotate_journal_vacuum", Description: "Remove archived journal files (through sudo -n) beyond a total size, older than an age, or both; answers what each directory freed and the usage after.", Input: schema(map[string]any{ "size": map[string]any{"type": "string", "description": "keep at most this much, e.g. 500M or 2G"}, "time": map[string]any{"type": "string", "description": "keep at most this old, e.g. 4weeks or 30d"}, }), Run: func(args map[string]any) (any, error) { size, err := text(args, "size", false) if err != nil { return nil, err } age, err := text(args, "time", false) if err != nil { return nil, err } return m.Vacuum(size, age) }, }, } }