Files
mesh-catalog/modules/logrotate/cmd/logrotate-tools/logrotate_test.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

189 lines
6.8 KiB
Go

package main
import (
"strings"
"testing"
)
const state = `logrotate state -- version 2
"/var/log/nginx/error.log" 2026-3-15-0:34:52
"/var/log/wtmp" 2024-6-27-11:0:0
"/var/log/old.log" 2026-1-2
`
func TestTheStatusFileIsReadPerLog(t *testing.T) {
r := ParseState(state)
if len(r) != 3 || r[0].Log != "/var/log/nginx/error.log" || !strings.HasPrefix(r[0].LastRotated, "2026-03-15T00:34:52") || !strings.HasPrefix(r[1].LastRotated, "2026-01-02T00:00:00") {
t.Fatalf("%+v", r)
}
m := machine(fake(func(c call) Ran {
if c.String() == "sudo -n cat "+StateFile {
return Ran{Stdout: state}
}
return Ran{Stdout: "ActiveState=active\n"}
}, nil), 1000)
s, err := m.Status("nginx")
if err != nil || len(s["logs"].([]Rotation)) != 1 || s["state_file_present"] != true {
t.Fatalf("%v %v", s, err)
}
}
func TestAMachineWhereLogrotateNeverRanSaysSo(t *testing.T) {
m := machine(fake(func(c call) Ran {
if c.name == "sudo" {
return Ran{Status: 1, Stderr: "cat: /var/lib/logrotate.status: No such file or directory\n"}
}
return Ran{Stdout: "LoadState=not-found\n"}
}, nil), 1000)
s, err := m.Status("")
if err != nil || s["state_file_present"] != false || !strings.Contains(s["note"].(string), "never run") {
t.Fatalf("%v %v", s, err)
}
refused := machine(fake(func(call) Ran { return Ran{Status: 1, Stderr: "sudo: a password is required\n"} }, nil), 1000)
if _, err := refused.Status(""); err == nil || !strings.Contains(err.Error(), "without a prompt") {
t.Fatalf("a refusal is an error: %v", err)
}
}
const samba = `/var/log/samba/log.smbd /var/log/samba/log.nmbd /var/log/samba/*.log {
notifempty
missingok
copytruncate
}
# a comment { with a brace
/var/log/one.log
/var/log/two.log {
postrotate
kill -HUP 1
endscript
}
`
func TestARuleFilesLogsAreThePathsBeforeEachBrace(t *testing.T) {
got := RulesIn(samba)
if strings.Join(got, " ") != "/var/log/samba/log.smbd /var/log/samba/log.nmbd /var/log/samba/*.log /var/log/one.log /var/log/two.log" {
t.Fatalf("%v", got)
}
}
func TestADirectiveIsNotALog(t *testing.T) {
got := RulesIn("weekly\nolddir /var/log/archive\ninclude /etc/logrotate.d\n/var/log/wtmp {\n monthly\n}\n")
if strings.Join(got, " ") != "/var/log/wtmp" {
t.Fatalf("%v", got)
}
}
func TestGlobalsAreTheBaseWithoutIncludesOrBlocks(t *testing.T) {
g := manifest(t).resource(t, "config")["content"].(string)
got := Globals(g)
if strings.Join(got, "|") != "weekly|rotate 4|create|compress|delaycompress|tabooext + .pacorig .pacnew .pacsave" {
t.Fatalf("%v", got)
}
}
func TestForcingOneRuleCarriesTheGlobalsInAFileRootOwns(t *testing.T) {
var calls []call
m := machine(fake(func(c call) Ran {
if c.args[1] == "logrotate" {
return Ran{Stderr: "reading config file /run/mesh-logrotate-force.conf\nrotating log /var/log/samba/log.smbd, log->rotateCount is 4\nerror: error renaming x: Permission denied\n"}
}
return Ran{}
}, &calls), 1000)
files := map[string]string{BaseConf: "weekly\nrotate 4\ninclude /etc/logrotate.d\n/var/log/wtmp {\n monthly\n}\n", RulesDir + "/samba": samba}
m.ReadFile = func(p string) ([]byte, error) {
if s, ok := files[p]; ok {
return []byte(s), nil
}
return nil, errNoFile
}
var written string
removed := false
r, err := m.Force("samba", func(s string) (string, func(), error) {
written = s
return "/tmp/x.conf", func() { removed = true }, nil
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(written, "weekly\nrotate 4\n") || strings.Contains(written, "include") || strings.Contains(written, "wtmp") || !removed {
t.Fatalf("written %q removed %v", written, removed)
}
var seen []string
for _, c := range calls {
seen = append(seen, c.String())
}
want := []string{
"sudo -n install -m 0644 -o root -g root /tmp/x.conf " + forcedConf,
"sudo -n logrotate -f -v " + forcedConf + " " + RulesDir + "/samba",
"sudo -n rm -f " + forcedConf,
}
if strings.Join(seen, "\n") != strings.Join(want, "\n") {
t.Fatalf("ran:\n%s", strings.Join(seen, "\n"))
}
if r["ok"] != false || strings.Join(r["rotated"].([]string), ",") != "/var/log/samba/log.smbd" || len(r["errors"].([]string)) != 1 {
t.Fatalf("%v", r)
}
if _, err := m.Force("../../etc/shadow", nil); err == nil {
t.Fatal("a path was taken for a rule file")
}
if _, err := m.Force("absent", nil); err == nil {
t.Fatal("a rule file that is not there was forced")
}
}
func TestBigLogsAreSortedAndBounded(t *testing.T) {
m := machine(byLine(map[string]Ran{
"sudo -n find /var/log -xdev -type f -printf %s\t%TY-%Tm-%Td %TH:%TM\t%p\n": {Status: 1, Stdout: "10\t2026-10-04 10:00\t/var/log/a.log\n4294967296\t2026-10-04 11:00\t/var/log/journal/x/system.journal\n2048\t2026-10-01 09:00\t/var/log/b.log\n", Stderr: "find: something vanished\n"},
}, nil), 1000)
r, err := m.BigLogs(2, true)
if err != nil {
t.Fatal(err)
}
l := r["largest"].([]LogFile)
if r["files"] != 3 || len(l) != 2 || !l[0].Journal || l[0].Size != "4.0G" || l[1].Path != "/var/log/b.log" || l[1].Size != "2.0K" {
t.Fatalf("%v", r)
}
r, _ = m.BigLogs(5, false)
if l := r["largest"].([]LogFile); len(l) != 2 || l[0].Path != "/var/log/b.log" || r["journal"] != "4.0G" {
t.Fatalf("journals counted, not listed: %v", r)
}
}
func TestTheDryRunReportsErrorsAndNotItsOwnWarning(t *testing.T) {
m := machine(byLine(map[string]Ran{
"sudo -n logrotate -d /etc/logrotate.conf": {Stderr: "warning: logrotate in debug mode does nothing except printing debug messages!\nerror: /etc/logrotate.d/x:3 unknown option 'bogus'\nwarning: something real\n"},
}, nil), 1000)
r, err := m.Check()
if err != nil || r["ok"] != false || len(r["errors"].([]string)) != 1 || len(r["warnings"].([]string)) != 1 {
t.Fatalf("%v %v", r, err)
}
}
func TestTheJournalIsMeasuredAndVacuumedAsRoot(t *testing.T) {
var calls []call
m := machine(fake(func(c call) Ran {
switch c.String() {
case "sudo -n journalctl --disk-usage":
return Ran{Stdout: "Archived and active journals take up 4G in the file system.\n"}
case "systemd-analyze cat-config systemd/journald.conf":
return Ran{Stdout: "# /etc/systemd/journald.conf\n[Journal]\n#SystemMaxUse=\nSystemMaxUse=1G\n"}
case "sudo -n journalctl --vacuum-size=500M --vacuum-time=4weeks":
return Ran{Stderr: "Deleted archived journal /var/log/journal/x/system@a.journal (128M).\nVacuuming done, freed 128M of archived journals from /var/log/journal/x.\n"}
}
return Ran{Status: 99}
}, &calls), 1000)
u, err := m.JournalUsage()
if err != nil || u["usage"] != "4G" || u["settings"].(map[string]string)["SystemMaxUse"] != "1G" {
t.Fatalf("%v %v", u, err)
}
v, err := m.Vacuum("500M", "4weeks")
if err != nil || v["files_deleted"] != 1 || v["usage_after"] != "4G" {
t.Fatalf("%v %v", v, err)
}
for _, bad := range [][2]string{{"", ""}, {"lots", ""}, {"", "forever"}, {"1G; rm", ""}} {
if _, err := m.Vacuum(bad[0], bad[1]); err == nil {
t.Errorf("%v accepted", bad)
}
}
}