Files
mesh-catalog/modules/logrotate/cmd/logrotate-tools/main.go
T
jochen 2e082d1680 logrotate: rotation on every machine, its base configuration owned
Rotation ran on one machine of four; the others carried package and fail2ban
rules nothing read, and one log had reached 4.9 GB. The module installs
logrotate, owns /etc/logrotate.conf whole (the distribution's base plus
compress/delaycompress, dropping a hand-set olddir that collides same-named
logs) and enables logrotate.timer. Seven tools from a Go bundle, the journal's
usage and vacuum among them (to-be 42 Phase 1).
2026-10-04 12:50:20 +02:00

129 lines
5.1 KiB
Go

// 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)
},
},
}
}