. "$BST_LIB" # logrotate closes the loop the two log ports before it left open: sysklogd (86) # WRITES log files and cronie (87) can RUN something at a wall-clock time, but # nothing on this image could BOUND what syslogd writes. So this suite is about # one question -- does a rotation actually happen, and does the right thing end up # on disk -- and it takes every verdict from THE BYTES ON DISK rather than from # logrotate's own narration. logrotate is very talkative in -v/-d mode and it is # entirely possible for it to announce a rotation it did not perform; a renamed # file is not. # # Three things shape the file. # # 1. THE ONE REAL QUESTION IS FILE-BACKED mmap, and the suite is arranged so the # answer is legible rather than confusing. config.c readConfigFile() reads # EVERY configuration file by mmap'ing it whole (MAP_PRIVATE, plus MAP_POPULATE # where defined) and parsing the mapping byte by byte. There is no read() # fallback: MAP_FAILED abandons the file, and a mapping of zeroed pages would # parse as a config of NUL bytes -- logrotate would find nothing to do and exit # 0, which is indistinguishable from success unless something checks the disk. # BRINTOS-38 recorded exactly that shape for libmagic's compiled DB in July # (which is why file(1) and libarchive both force # ac_cv_func_mmap_fixed_mapped=no), while BRINTOS-281 measured file-backed # MAP_SHARED WORKING on this arm. MAP_PRIVATE had not been re-measured since, # so the recipe leaves the call unpatched and the cases below are the # measurement. logrotate/config-parsed is the decisive one: "considering log # " can only be printed if the config's BYTES were parsed, not merely if # the file opened. If that case is red and logrotate/version is green, the # answer is mmap and not logrotate. # # 2. NOTHING HERE WAITS. logrotate is a one-shot program -- it runs, rotates and # exits -- so unlike 86-sysklogd and 87-cronie there is no daemon to come up and # no wall clock to spend. That matters because it means this suite cannot be # hurt by linux-in-the-browser#120 (relative timers do not elapse) and needs # none of their polling scaffolding. The one case that is ABOUT time, # state-drives-daily, seeds the state file with a date in 2001 and lets # logrotate compare it against time(2), which is the clock that works here. # # 3. SIGNALS ARE NOT ON ITS PATH. Every daemon port before this one had to skip a # shutdown case because kill(2) succeeds here and delivers nothing. logrotate # never signals anything: the classic "kill -HUP the daemon after rotating" # step lives in the administrator's postrotate script, not in logrotate. So the # postrotate case below covers what logrotate actually owns -- fork+exec of a # shell -- and asserts its side effect on disk. require logrotate logrotate/present D=${TMPDIR:-/tmp}/.bst-logrotate rm -rf "$D"; mkdir -p "$D" ST=$D/state # A log with known, checkable content. Deliberately not empty: `notifempty` is a # default of several stock configs and an empty log would silently not rotate. mklog() { rm -f "$1" "$1".* 2>/dev/null printf 'LOGROTATE-PAYLOAD-%s\n' 1 2 3 4 5 > "$1" } # --- identity and packaging (no config file -> independent of mmap) ------------ # These run FIRST and on purpose: they are what makes a red in the config-driven # block below attributable. If every case here is green and the config cases are # red, the binary is fine and the config READ is what failed. check_out logrotate/version "logrotate 3.21.0" logrotate --version # oe-core installs the binary in sbindir (/usr/sbin). Pinning where a bare # `logrotate` resolves is the same care 85-psmisc takes for killall/fuser: an # applet resolved ahead of $PATH would make this suite test busybox instead. check_eq logrotate/resolves-to-port "/usr/sbin/logrotate" "$(command -v logrotate)" # ...and the reason no such care is needed beyond the check: busybox on this image # has no logrotate applet, so there is nothing to shadow it. Absence is the only # falsifiable form of that claim. if busybox --list 2>/dev/null | grep -qx logrotate; then bst_notok logrotate/no-busybox-applet "busybox provides a logrotate applet that would shadow this port" else bst_ok logrotate/no-busybox-applet fi # Every path the recipe installs on THIS image. The stock config and the two # example fragments are part of the package, not decoration -- logrotate/stock-conf # below parses them. inv="" for p in /usr/sbin/logrotate /etc/logrotate.conf /etc/logrotate.d/btmp /etc/logrotate.d/wtmp; do [ -f "$p" ] || inv="$inv $p" done check_eq logrotate/inventory "" "$inv" # NOTHING SCHEDULES IT HERE, and that is deliberate rather than an omission. # brintos.conf sets DISTRO_FEATURES = "", so both arms of the recipe's do_install # are false: no systemd units (there is no systemd) and no /etc/cron.daily entry. # The package is the binary plus its config; an operator wires it to the # already-ported cronie when they want it periodic. Asserted as an absence so that # a DISTRO_FEATURES change which silently starts rotating this image's logs on a # timer is caught rather than assumed. sched="" for p in /etc/cron.daily/logrotate /lib/systemd/system/logrotate.timer \ /usr/lib/systemd/system/logrotate.timer /lib/systemd/system/logrotate.service; do [ -e "$p" ] && sched="$sched $p" done check_eq logrotate/no-scheduler "" "$sched" # popt is what parses this command line -- the port depends on BRINTOS-163's # library, so a bad option is also the cheapest live check that popt works. # Upstream returns 2 here (logrotate.c "return 2" on the arg < -1 branch), not 1. # The trailing reason reads "unknown error" on every platform: that branch prints # poptStrerror(rc) while popt's code was assigned to arg, and rc is still its # initial 0 -- an upstream cosmetic bug, so match on the part popt actually drives. out=$(logrotate --no-such-option 2>&1); rc=$? case "$out" in *"bad argument --no-such-option"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 2 ] && [ "$hit" -eq 1 ]; then bst_ok logrotate/badopt else bst_notok logrotate/badopt "rc=$rc (want 2) hit=$hit: $(_bst_1line "$out")"; fi # --- the mmap probe: is the config actually READ? ----------------------------- # -d is debug mode: logrotate parses everything and performs nothing, so these # three cases touch no file and can be run before any rotation exists. LOG=$D/probe.log CONF=$D/probe.conf mklog "$LOG" cat > "$CONF" <" is emitted from the parsed log # stanza, so it can only appear if the MAPPED BYTES were the config's own. A # zero-filled mapping parses to no stanzas and prints nothing of the kind. check_out logrotate/config-parsed "considering log $LOG" logrotate -d -s "$ST" "$CONF" # And the decision the parsed directives produce. `rotate 3` + -f must reach "log # needs rotating"; this is the parse carried all the way through to a verdict. check_out logrotate/config-decision "log needs rotating" logrotate -d -f -s "$ST" "$CONF" # --- rotation, checked on disk ------------------------------------------------ # Without `create` or `copytruncate` a rotation is a rename: the log BECOMES .1 # and no new log is made. Both halves are asserted, because a tool that copied # instead of renaming would pass the first and fail the second. LOG=$D/basic.log CONF=$D/basic.conf mklog "$LOG" printf '%s {\n rotate 3\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/basic.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -f "$LOG.1" ] && [ ! -e "$LOG" ] && grep -q 'LOGROTATE-PAYLOAD-5' "$LOG.1" 2>/dev/null; then bst_ok logrotate/rotate-basic else bst_notok logrotate/rotate-basic "rc=$rc log=$([ -e "$LOG" ] && echo present || echo gone) .1=$([ -f "$LOG.1" ] && wc -c < "$LOG.1" || echo absent): $(_bst_1line "$(cat "$D/basic.out")")" fi # `create` makes a fresh empty log in place of the rotated one. It is its own case # because it is the one rotation path that calls fchown(2) -- and this rootfs # reports the HOST's uid for a file while the process is uid 0 (the same mismatch # 87-cronie records for crond's owner checks), so if chown is refused here it is # THIS case that says so rather than a vague rotation failure. LOG=$D/create.log CONF=$D/create.conf mklog "$LOG" printf '%s {\n rotate 3\n create\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/create.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -f "$LOG" ] && [ ! -s "$LOG" ] && [ -s "$LOG.1" ]; then bst_ok logrotate/create else bst_notok logrotate/create "rc=$rc new=$([ -f "$LOG" ] && wc -c < "$LOG" || echo absent) .1=$([ -f "$LOG.1" ] && wc -c < "$LOG.1" || echo absent): $(_bst_1line "$(cat "$D/create.out")")" fi # copytruncate is the path for a producer that holds the log open and would keep # writing to a renamed inode -- syslogd's case exactly. The assertion that makes it # meaningful is the INODE: it must be the same file, truncated, not a new one. LOG=$D/copytrunc.log CONF=$D/copytrunc.conf mklog "$LOG" ino_before=$(ls -i "$LOG" | awk '{print $1}') printf '%s {\n rotate 3\n copytruncate\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/ct.out" 2>&1; rc=$? ino_after=$(ls -i "$LOG" 2>/dev/null | awk '{print $1}') if [ "$rc" -eq 0 ] && [ -f "$LOG" ] && [ ! -s "$LOG" ] && [ "$ino_before" = "$ino_after" ] && grep -q 'LOGROTATE-PAYLOAD-5' "$LOG.1" 2>/dev/null; then bst_ok logrotate/copytruncate else bst_notok logrotate/copytruncate "rc=$rc ino $ino_before->$ino_after size=$([ -f "$LOG" ] && wc -c < "$LOG" || echo absent): $(_bst_1line "$(cat "$D/ct.out")")" fi # Generation retention: `rotate 2` over three forced runs must leave exactly .1 and # .2 and must have DISCARDED the oldest. The absence of .3 is the half that proves # retention rather than mere renaming. LOG=$D/keep.log CONF=$D/keep.conf printf '%s {\n rotate 2\n create\n missingok\n}\n' "$LOG" > "$CONF" mklog "$LOG" n=0 while [ $n -lt 3 ]; do printf 'GEN-%s\n' "$n" >> "$LOG" logrotate -f -s "$ST" "$CONF" >>"$D/keep.out" 2>&1 n=$((n + 1)) done if [ -f "$LOG.1" ] && [ -f "$LOG.2" ] && [ ! -e "$LOG.3" ]; then bst_ok logrotate/retention else bst_notok logrotate/retention ".1=$([ -f "$LOG.1" ] && echo y || echo n) .2=$([ -f "$LOG.2" ] && echo y || echo n) .3=$([ -e "$LOG.3" ] && echo PRESENT || echo n)" fi # --- compression: a real fork+exec of an already-ported tool ------------------- # `compress` shells out to the compressor configured at build time (/bin/gzip), # so this case covers logrotate's fork/exec path AND the gzip port at once. The # verdict is the ROUND TRIP -- the payload has to come back out -- because a # zero-length or truncated .gz would still satisfy a mere existence check. LOG=$D/comp.log CONF=$D/comp.conf mklog "$LOG" printf '%s {\n rotate 3\n compress\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/comp.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -f "$LOG.1.gz" ]; then bst_ok logrotate/compress else bst_notok logrotate/compress "rc=$rc no $LOG.1.gz (have: $(_bst_1line "$(ls "$D" | tr '\n' ' ')")): $(_bst_1line "$(cat "$D/comp.out")")"; fi if [ -f "$LOG.1.gz" ] && gzip -dc "$LOG.1.gz" 2>/dev/null | grep -q 'LOGROTATE-PAYLOAD-5'; then bst_ok logrotate/compress-roundtrip else bst_notok logrotate/compress-roundtrip "payload did not survive: $(_bst_1line "$(gzip -dc "$LOG.1.gz" 2>&1 | head -2)")" fi # --- dateext: the rotated name comes from the wall clock ---------------------- # The default dateformat is -%Y%m%d, so the rotated file is named for TODAY. Built # from date(1) here rather than hardcoded, and date(1) is the clock 87-cronie # established as trustworthy on this runtime. LOG=$D/dateext.log CONF=$D/dateext.conf mklog "$LOG" printf '%s {\n rotate 3\n dateext\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/de.out" 2>&1; rc=$? today=$(date -u +%Y%m%d) if [ "$rc" -eq 0 ] && [ -f "$LOG-$today" ]; then bst_ok logrotate/dateext else bst_notok logrotate/dateext "rc=$rc no $LOG-$today (have: $(_bst_1line "$(ls "$D" | grep dateext | tr '\n' ' ')"))"; fi # --- postrotate: logrotate's own fork+exec of a shell ------------------------- # What an administrator actually puts here is `kill -HUP` on a daemon, which # cannot work on this runtime (signals are delivered nowhere -- see 87-cronie). # So the script writes a FILE instead: what is under test is that logrotate runs # the script at all, and that is exactly the part logrotate owns. LOG=$D/post.log CONF=$D/post.conf MARK=$D/post.mark mklog "$LOG" rm -f "$MARK" { printf '%s {\n rotate 3\n missingok\n create\n' "$LOG" printf ' postrotate\n echo POSTROTATE-RAN > %s\n endscript\n}\n' "$MARK" } > "$CONF" logrotate -f -s "$ST" "$CONF" >"$D/post.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && grep -q 'POSTROTATE-RAN' "$MARK" 2>/dev/null; then bst_ok logrotate/postrotate else bst_notok logrotate/postrotate "rc=$rc mark=$([ -f "$MARK" ] && echo present || echo absent): $(_bst_1line "$(cat "$D/post.out")")" fi # --- the trigger conditions --------------------------------------------------- # A correct NEGATIVE, and the more important of the two: without -f, a log far # below its size threshold must NOT be rotated. A logrotate that rotated # everything unconditionally would pass every case above and fail only this one. LOG=$D/small.log CONF=$D/small.conf mklog "$LOG" printf '%s {\n rotate 3\n size 1M\n missingok\n}\n' "$LOG" > "$CONF" logrotate -s "$D/state-small" "$CONF" >"$D/small.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -s "$LOG" ] && [ ! -e "$LOG.1" ]; then bst_ok logrotate/size-below-threshold else bst_notok logrotate/size-below-threshold "rc=$rc rotated when it should not have (.1=$([ -e "$LOG.1" ] && echo PRESENT || echo absent))" fi # ...and the positive half, with no -f: the same directive must fire once the log # is actually over the threshold. ~4 KiB of payload against a 1k threshold. LOG=$D/big.log CONF=$D/big.conf : > "$LOG" n=0 while [ $n -lt 64 ]; do printf 'PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD-%s\n' "$n" >> "$LOG" n=$((n + 1)) done printf '%s {\n rotate 3\n size 1k\n missingok\n}\n' "$LOG" > "$CONF" logrotate -s "$D/state-big" "$CONF" >"$D/big.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -f "$LOG.1" ]; then bst_ok logrotate/size-triggered else bst_notok logrotate/size-triggered "rc=$rc size=$(wc -c < "$LOG" 2>/dev/null) not rotated: $(_bst_1line "$(cat "$D/big.out")")"; fi # --- the state file ----------------------------------------------------------- # logrotate's memory between runs. It is written on every non-debug run and read # back on the next, and the next case makes a rotation DEPEND on its contents. if grep -q 'logrotate state -- version 2' "$ST" 2>/dev/null && grep -q "$D/basic.log" "$ST" 2>/dev/null; then bst_ok logrotate/state-written else bst_notok logrotate/state-written "state file lacks header or the rotated path: $(_bst_1line "$(head -3 "$ST" 2>&1)")" fi # THE TIME-BASED PATH, made deterministic. `daily` rotates when the last rotation # recorded in the state file was on an earlier day -- so seeding 2001 and running # with NO -f makes logrotate compare a stored date against time(2) and decide for # itself. This is the only case that exercises the wall clock, and it needs no # waiting at all, which is why linux-in-the-browser#120 cannot reach it. LOG=$D/daily.log CONF=$D/daily.conf DST=$D/state-daily mklog "$LOG" printf '%s {\n rotate 3\n daily\n missingok\n}\n' "$LOG" > "$CONF" printf 'logrotate state -- version 2\n"%s" 2001-9-9-1:46:40\n' "$LOG" > "$DST" logrotate -s "$DST" "$CONF" >"$D/daily.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ -f "$LOG.1" ]; then bst_ok logrotate/state-drives-daily else bst_notok logrotate/state-drives-daily "rc=$rc a 2001 last-rotation did not trigger daily: $(_bst_1line "$(cat "$D/daily.out")")"; fi # --- notifempty: the other correct negative ------------------------------------ # An empty log with `notifempty` must be left alone even under -f. Paired with # size-below-threshold, these two are what keep the suite from being satisfiable # by a tool that simply renames everything it is pointed at. LOG=$D/empty.log CONF=$D/empty.conf : > "$LOG" printf '%s {\n rotate 3\n notifempty\n missingok\n}\n' "$LOG" > "$CONF" logrotate -f -s "$D/state-empty" "$CONF" >"$D/empty.out" 2>&1; rc=$? if [ "$rc" -eq 0 ] && [ ! -e "$LOG.1" ]; then bst_ok logrotate/notifempty else bst_notok logrotate/notifempty "rc=$rc an empty log was rotated under notifempty"; fi # --- the config the package actually ships ------------------------------------ # Parsed, not merely present -- and on this arm it is NOT parsed, which is the # one thing this case must not blur. The stock file carries `include # /etc/logrotate.d`, so it should walk the include directory and its two example # fragments. Instead logrotate's own st_uid != ROOT_UID guard rejects it: this # rootfs reports the HOST's uid for a file on the cloud (virtiofs) mount while the # process is uid 0, the same mismatch 87-cronie works around with -p for crond. # # EXIT STATUS CANNOT DECIDE THIS CASE. That guard does `close(fd); return 0`, so # an ignored config is indistinguishable from a parsed one by rc alone -- exactly # the "announces what it did not do" shape this file refuses to trust anywhere # else. So match the OUTPUT, and take the three states apart: # parsed -> green, the real answer # owner-refused -> an attributed skip against the platform issue # neither -> a genuine red, because it is a state nobody has explained # Written so it needs no edit when the platform is fixed: the day the mount # reports uid 0, the first arm matches and this case turns green by itself. out=$(logrotate -d -s "$D/state-stock" /etc/logrotate.conf 2>&1) case "$out" in *"reading config file /etc/logrotate.conf"*) bst_ok logrotate/stock-conf ;; *"because the file owner is wrong"*) bst_skip logrotate/stock-conf "the shipped /etc/logrotate.conf is IGNORED rather than parsed, and logrotate still exits 0: this rootfs reports the host's uid (stat says $(ls -ln /etc/logrotate.conf 2>/dev/null | awk '{print $3}')) where the process is uid 0, so the st_uid guard skips the file -- brintos/linux-in-the-browser#139. Not a logrotate defect and not fixable in the recipe; the identical config on tmpfs parses (see logrotate/config-parsed). Turns green by itself when 139 lands" ;; *) bst_notok logrotate/stock-conf "neither parsed nor owner-refused: $(_bst_1line "$out")" ;; esac rm -rf "$D"