# BST-TIMEOUT: 900 . "$BST_LIB" # watchdog 5.16 -- the daemon watchdog-config (99-watchdog-config, next door) # was the configuration for. Three programs ship: /usr/sbin/watchdog, the daemon # that runs health tests and feeds /dev/watchdog while they pass; # /usr/sbin/wd_keepalive, which does nothing but feed it; and # /usr/sbin/wd_identify, which opens it once and prints its identity. # # THERE IS NO /dev/watchdog BEHIND THIS MACHINE, AND THAT ABSENCE IS THE # SUBJECT. Every case below is either installed content, or one bounded # invocation whose expected behaviour is defined BY the device not being there. # Nothing here starts a daemon: the init scripts are installed and not enabled, # section 7 asserts no watchdog process is running, and every run is driven # through bst_probe with `--loop-exit 1` so it exits after a single interval. # # WHAT THE TICKET EXPECTED AND WHAT UPSTREAM ACTUALLY DOES. BRINTOS-1973 asked # for `watchdog` and `wd_keepalive` each to report the missing device and exit # NON-ZERO. Read from 5.16's source, neither does, and the reasons are separate # and deliberate: # # * open_watchdog() (keep_alive.c:47) logs `cannot open (errno = ...)` # and then RETURNS -1 WITHOUT EXITING, under upstream's own comment "we can # use watchdog even if there is no watchdog device". The daemon goes on to # run its health tests. So the message is real and the non-zero status is # not, and `watchdog` left alone would run forever -- which is why the only # thing that bounds it here is --loop-exit. # * wd_keepalive has NO foreground option at all. It always double-forks # (wd_daemon, daemon-pid.c:121) and the parent _exit(0)s, so a shell can # never see the child's status; and open_logging(NULL, MSG_TO_SYSLOG) has # already closed terminal output by the time the device is opened, so the # message goes to syslog and not to stderr. Its one bounded, observable, # pre-fork path is a config naming no device: ` no watchdog device # configured, aborting`, exit 0. That is section 6. # # wd_identify is the program that DOES do what the ticket described -- open the # device, log the documented message, exit(1) -- and it does it with no daemon, # no fork and no main loop. Section 5 is built on it for that reason, and the # two other behaviours are asserted as what they are rather than bent to fit. # # WHY NOTHING HERE CAN REBOOT THE MACHINE, WHICH IS NOT OBVIOUS. watchdog's # wd_action() calls do_shutdown() on an unrepaired check failure, and # do_shutdown() reboots. Two things keep it unreachable: # # * Every configuration run passes -q (--no-action), which makes wd_action # log and return instead of shutting down, whatever any check reports. # * The ONE run that omits -q -- section 5's, because -q also skips the device # open and would take the message with it -- configures no check at all, so # the only thing in that iteration that can fail is check_file_table()'s # open of /proc/uptime. That call is made with act == NULL, which is the one # path attempt_repair() cannot defer (the retry timer needs a list entry), so # it would shut down on the first failure. It is therefore GUARDED: the run # happens only if /proc/uptime is readable, and skips with the reason if not. # # Every run is also guarded on /dev/watchdog being ABSENT. If some later image # grows a real one, opening it ARMS it, and a suite that armed a watchdog and # walked away would reboot the guest a minute later for reasons nobody would # connect to this file. S=watchdog R=${BST_CC_ROOT:-} WD=/usr/sbin/watchdog KA=/usr/sbin/wd_keepalive ID=/usr/sbin/wd_identify LIBEXEC=$R/usr/libexec/watchdog-tests MAN=$LIBEXEC/install-manifest W=${TMPDIR:-/tmp}/bst-$S.$$ mkdir -p "$W" 2>/dev/null # --- section 1: the -tests package arrived, and the image equals its manifest - check $S/manifest-present test -f "$MAN" if [ ! -f "$MAN" ]; then bst_notok $S/manifest "no $MAN: watchdog-tests is not on this image, so nothing below has a subject" rm -rf "$W" 2>/dev/null exit 0 fi # `bst_local` first: the manifest is read with `while ... done < file`, which has # to stay in this shell to keep the variables it sets, and a top-level stdin # redirect from the virtiofs root makes dup2(2) return 513 -- ash treats that as # fatal and exits, taking every case below with it while the run still reads as # a clean pass (brintos/linux#73). MANL=$(bst_local "$MAN") check_eq $S/manifest-root / "$(LC_ALL=C awk '$1=="root"{print $2; exit}' "$MANL")" check_eq $S/manifest-version 5.16 "$(LC_ALL=C awk '$1=="version"{print $2; exit}' "$MANL")" # EXACT, not a floor. The recipe installs exactly five files across ${PN} and # ${PN}-keepalive and asserts that count at build time too, so a poky bump that # added a sixth is expected to move this number deliberately and visibly. count=$(LC_ALL=C awk '$1=="count"{print $2; exit}' "$MANL") check_eq $S/manifest-count 5 "$count" # The count agrees with the body it heads. A header that had drifted from its own # entries would otherwise satisfy every per-file case below while claiming to # cover something it does not list. check_eq $S/manifest-count-agrees "$count" \ "$(LC_ALL=C awk 'NR>3' "$MANL" | LC_ALL=C awk 'END{print NR+0}')" if ! command -v sha256sum >/dev/null 2>&1 || ! command -v stat >/dev/null 2>&1; then bst_skip $S/per-file "no sha256sum and/or stat on this image; the manifest cannot be checked against the tree" else LC_ALL=C awk 'NR>3' "$MANL" > "$W/body" while IFS=' ' read -r mode sha rel; do [ -n "$rel" ] || continue if [ ! -f "$R$rel" ]; then bst_notok $S/installed:$rel "manifest names it, image does not have it" continue fi bst_ok $S/installed:$rel check_eq $S/mode:$rel "$mode" "$(stat -c %a "$R$rel")" check_eq $S/digest:$rel "$sha" "$(sha256sum "$R$rel" | cut -d' ' -f1)" done < "$W/body" fi # The mode against 0755 rather than against the manifest. The pair above is # self-consistent by construction -- both sides descend from the same do_install # -- so it would stay green if the recipe started installing these world-writable. for f in /usr/sbin/watchdog /usr/sbin/wd_keepalive /usr/sbin/wd_identify \ /etc/init.d/watchdog /etc/init.d/wd_keepalive; do check_eq $S/mode-is-0755:$f 755 "$(stat -c %a "$R$f" 2>/dev/null)" done # /etc/watchdog.conf belongs to watchdog-config, and this package must not also # claim it -- dpkg refuses the pair, and that failure would surface at do_rootfs # on the shared image rather than in either port. The recipe asserts it over ${D}; # this asserts the same fact about the tree that actually shipped. check $S/config-from-other-package test -f "$R/etc/watchdog.conf" if command -v dpkg >/dev/null 2>&1; then check_eq $S/config-owner watchdog-config \ "$(dpkg -S /etc/watchdog.conf 2>/dev/null | LC_ALL=C awk -F: 'NR==1{print $1; exit}')" else bst_skip $S/config-owner "no dpkg on this image" fi # --- section 2: the version, and the usage each program prints ---------------- # NONE of the three has a -h. getopt does not recognise it, so it falls through # to `default:` and reaches usage(), which prints to stderr and exit(1)s. That is # what the ticket's "-h" asks about and it is worth stating precisely: the # assertion is that an UNKNOWN OPTION is refused with the usage text and a # non-zero status, not that a help flag exists. check_rc $S/usage-rc:watchdog 1 $WD -h check_rc $S/usage-rc:wd_keepalive 1 $KA -h check_rc $S/usage-rc:wd_identify 1 $ID -h # The version the recipe pins, read out of the program rather than off the # package. MAJOR_VERSION/MINOR_VERSION are compiled in from configure.ac, so this # is the one case that would catch a .deb labelled 5.16 built from other sources. # # The line is SELECTED, not taken at NR==1. getopt itself writes first -- glibc # prints `: invalid option -- 'h'` before control ever reaches the # `default:` label -- so a positional read asserts the C library's diagnostic # instead of the program's. Measured on an x86-64 control build of these same # sources, where reading NR==1 reddened all three. check_eq $S/version:watchdog 'watchdog version 5.16, usage:' \ "$($WD -h 2>&1 | LC_ALL=C grep -F 'version 5.16, usage:' | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/version:wd_keepalive 'wd_keepalive version 5.16, usage:' \ "$($KA -h 2>&1 | LC_ALL=C grep -F 'version 5.16, usage:' | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/version:wd_identify 'wd_identify version 5.16, usage:' \ "$($ID -h 2>&1 | LC_ALL=C grep -F 'version 5.16, usage:' | LC_ALL=C awk 'NR==1{print; exit}')" # The option lists differ between the three, and the difference is the point: # wd_keepalive advertises two options where watchdog advertises eight, because it # has no checks to configure and no foreground mode. Asserted by count and by the # two lines that are the reason for the ticket's design. check_eq $S/usage-optcount:watchdog 8 \ "$($WD -h 2>&1 | LC_ALL=C awk '/^ -/{n++} END{print n+0}')" check_eq $S/usage-optcount:wd_keepalive 2 \ "$($KA -h 2>&1 | LC_ALL=C awk '/^ -/{n++} END{print n+0}')" check_eq $S/usage-optcount:wd_identify 2 \ "$($ID -h 2>&1 | LC_ALL=C awk '/^ -/{n++} END{print n+0}')" # usage() exit(1)s, so these go through `sh -c '...; exit 0'` -- check_out # requires status 0 as well as the needle, and a bare $WD -h would red on the # status while saying nothing about the text. check_out $S/usage-has-foreground '-F | --foreground run in foreground' \ sh -c "$WD -h 2>&1; exit 0" check_out $S/usage-has-loop-exit '-X | --loop-exit run a fixed number of loops then exit' \ sh -c "$WD -h 2>&1; exit 0" # wd_keepalive has NO --foreground. This is the case that records why section 6 # cannot assert a status the way section 5 does, and it must come back FALSE. check_eq $S/keepalive-has-no-foreground 0 \ "$($KA -h 2>&1 | LC_ALL=C grep -c -- '--foreground')" # --- section 3: every directive the ticket names, parsed and read back -------- # Two independent read-backs per run, and they are not duplicates: # # * The PARSER's own echo, from read-conf.c under -v: `Integer 'interval' # found = 5`. It says the directive was recognised and what value it took. # * print_info()'s summary, emitted after the configuration is complete. It # says what the daemon is actually going to DO with it -- including values it # DERIVED, which the parser never saw. # # A directive that parsed into the wrong variable would pass the first and fail # the second. # # Every run here carries -q (no device is opened and no shutdown is reachable), # -f (no sanity-check and no PID file, so nothing depends on /var/run being # writable) and -X 1 (one interval, then exit). log-dir is pointed at $W because # watchdog mkdir()s it unconditionally and this rootfs is read-only. cat > "$W/full.conf" < "$W/subject" if [ -e /dev/watchdog ]; then bst_skip $S/config-run "/dev/watchdog exists on this image; opening it would ARM a real watchdog" bst_skip $S/config-parse "/dev/watchdog exists on this image" else bst_probe $S/config-run "$W/full.out" 60 \ $WD -F -f -q -v -c "$W/full.conf" -X 1 E=$W/full.out.err # The parser's echo, one case per directive the ticket names. check_eq $S/parse-interval "watchdog: Integer 'interval' found = 1" \ "$(LC_ALL=C grep -F "Integer 'interval'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/parse-max-load-1 "watchdog: Integer 'max-load-1' found = 24" \ "$(LC_ALL=C grep -F "Integer 'max-load-1'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/parse-min-memory "watchdog: Integer 'min-memory' found = 1" \ "$(LC_ALL=C grep -F "Integer 'min-memory'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/parse-file "watchdog: List 'file' added as '$W/subject'" \ "$(LC_ALL=C grep -F "List 'file'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/parse-test-binary "watchdog: List 'test-binary' added as '/bin/true'" \ "$(LC_ALL=C grep -F "List 'test-binary'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" check_eq $S/parse-log-dir "watchdog: String 'log-dir' found as '$W/log'" \ "$(LC_ALL=C grep -F "String 'log-dir'" "$E" | LC_ALL=C awk 'NR==1{print; exit}')" # print_info()'s summary. The load line is the one that catches a value that # parsed into the wrong place: 18 and 12 were never in the file. read_config # derives max-load-5 as maxload1*3/4 and max-load-15 as maxload1/2 when they # are not given, so `load=24,18,12` is a statement about arithmetic the # parser echo above cannot make. check_eq $S/info-interval-and-load \ 'watchdog: int=1s realtime=no sync=no load=24,18,12 soft=no' \ "$(LC_ALL=C grep -F ' int=1s' "$E" | LC_ALL=C awk 'NR==1{print; exit}')" # min-memory read back as a page count. The page SIZE is not pinned -- that is # a property of this arm, not of the port -- so the assertion stops at the # part the directive controls. check_eq $S/info-min-memory \ 'watchdog: memory: minimum pages = 1 free, 0 allocatable, max swap 0' \ "$(LC_ALL=C grep -F ' memory: minimum pages' "$E" | LC_ALL=C sed 's/ (.*//' | LC_ALL=C awk 'NR==1{print; exit}')" # `file` read back with its change interval, which defaults to 0 because the # fixture gives no `change` line. A file test with mtime 0 is a "does it still # exist" test rather than a "has it been touched" one, and the two are one # directive apart. check_eq $S/info-file "watchdog: file: $W/subject:0" \ "$(LC_ALL=C grep -F ' file: ' "$E" | LC_ALL=C awk 'NR==1{print; exit}')" # `test-binary` read back as V0. add_test_binaries() adds anything found in # test-directory as V1 -- the same list, a different calling convention (the # binary is its own repair binary) -- so the version marker is what says which # of the two put this entry there. check_eq $S/info-test-binary 'watchdog: test binary V0: /bin/true' \ "$(LC_ALL=C grep -F 'test binary V0' "$E" | LC_ALL=C awk 'NR==1{print; exit}')" # The flags this run was driven with, read back. It is the case that says -q # and -f actually took effect -- i.e. that the safety this whole section rests # on is real and not merely spelled on the command line. check_eq $S/info-flags \ 'watchdog: alive=/dev/watchdog heartbeat=[none] to=root no_act=yes force=yes' \ "$(LC_ALL=C grep -F ' alive=' "$E" | LC_ALL=C awk 'NR==1{print; exit}')" # NOT opened, because -q was given. The distinction between "no device" and # "device not asked for" is exactly what section 5 turns around. check_eq $S/no-act-skips-device 0 \ "$(LC_ALL=C grep -c 'cannot open /dev/watchdog' "$E")" fi # `ping` is asserted at PARSE level only, and deliberately so. open_netcheck() # runs BEFORE print_info(), needs getprotobyname("icmp") and a SOCK_RAW socket, # and fatal_error()s on either -- so a ping fixture may never reach the summary. # The parser echo is emitted during read_config(), before any of that. cat > "$W/ping.conf" <&1") check_eq $S/parse-ping "watchdog: List 'ping' added as '127.0.0.1'" \ "$(printf '%s\n' "$pout" | LC_ALL=C grep -F "List 'ping'" | LC_ALL=C awk 'NR==1{print; exit}')" # What the platform does with an ICMP raw socket is the PLATFORM's answer, # not this port's, so the case does not pin one of them -- it asserts that # whichever answer comes back is one watchdog RECOGNISED and reported before # returning, rather than a hang or a silent death. Three outcomes qualify: # the socket was refused, the icmp protocol entry was not found, or the run # completed its interval with ping checking live. Anything else is a finding # and names itself. case "$pout" in *"error opening socket"*) pv=socket-refused ;; *"unknown protocol icmp"*) pv=protocol-unknown ;; *"loop exit on interval counter reached"*) pv=ran ;; *) pv=unrecognised ;; esac if [ "$pv" = unrecognised ]; then bst_notok $S/ping-outcome "no recognised outcome: $(_bst_1line "$pout")" else bst_ok $S/ping-outcome fi fi # --- section 4: a malformed directive, refused with its documented complaint --- # Three distinct malformations, three distinct complaints, and the difference # between them is upstream's design rather than an accident: a line it cannot # parse is a WARNING and the daemon carries on, while a value that would make the # main loop nonsensical is FATAL. A port that turned the first into the second # would break every real watchdog.conf carrying a stray line. cat > "$W/bad-value.conf" < "$W/bad-name.conf" < "$W/no-equals.conf" < "$W/zero-interval.conf" <&1; exit 0" # A config file that is not there at all: also fatal, also before anything opens. check_rc $S/missing-config-rc 2 $WD -F -f -q -c "$W/no-such-file.conf" -X 1 check_out $S/missing-config-complaint "Can't open config file" \ sh -c "$WD -F -f -q -c \"$W/no-such-file.conf\" -X 1 2>&1; exit 0" # --- section 5: the missing device, which is what this port is about ---------- # wd_identify is the program that answers the ticket's question cleanly: it opens # the configured device once, and on failure logs the documented message and # exit(1)s. No daemon, no fork, no main loop, nothing to bound but the open. # # It is run against the image's own /etc/watchdog.conf -- the file # watchdog-config ships, whose sole active device directive is # `watchdog-device = /dev/watchdog` -- so this is a statement about the shipped # configuration meeting the shipped platform, not about a fixture. if [ -e /dev/watchdog ]; then bst_skip $S/identify-missing-device "/dev/watchdog exists on this image; opening it would ARM a real watchdog" bst_skip $S/identify-missing-device-rc "/dev/watchdog exists on this image" bst_skip $S/watchdog-missing-device "/dev/watchdog exists on this image" else check_rc $S/identify-missing-device-rc 1 $ID -c /etc/watchdog.conf check_out $S/identify-missing-device \ "cannot open /dev/watchdog (errno = 2 = 'No such file or directory')" \ sh -c "$ID -c /etc/watchdog.conf 2>&1; exit 0" # The same open, from the daemon, and the case that records the behaviour the # ticket did not expect: `watchdog` logs the identical message and CARRIES ON. # It is run with --loop-exit 1 and exits 0 -- the status is 0 because the # daemon completed the run it was asked for, not because the device was found. # # This is the ONE run in the file without -q, because -q would skip the open # and take the message with it. It is guarded on /proc/uptime for the reason # the header gives: check_file_table() is the only check this fixture leaves # active, and it is the one whose failure cannot be deferred. cat > "$W/dev.conf" < "$W/nodev.conf" <&1; exit 0" # The same for wd_identify, which takes the other branch: it prints to STDOUT # rather than stderr and exits 1. The pair is worth having because the two # programs are one `exit(0)` / `exit(1)` apart on the identical condition, and a # reader would reasonably expect them to agree. check_rc $S/identify-nodev-rc 1 $ID -c "$W/nodev.conf" check_eq $S/identify-nodev "No watchdog hardware configured in \"$W/nodev.conf\"" \ "$($ID -c "$W/nodev.conf" 2>/dev/null | LC_ALL=C awk 'NR==1{print; exit}')" # --- section 7: nothing was started, and nothing is running ------------------- # Everything above ran a program and waited for it. These are the cases that say # this file left the machine as it found it -- and they are also what would catch # an image on which something else enabled the daemon. # A ZOMBIE IS NOT A DAEMON, and `pgrep -x' cannot tell the two apart. Every run # above is bounded and has already returned, but an exited child keeps its slot # -- and its comm -- until its parent reaps it, and on this runtime one of them # is still there when section 7 runs: measured as `162 [watchdog] ' on # the batch-1949 image. `pgrep -x watchdog' matches that, so a bare status check # reds on the file's own tidy exit rather than on anything running. So read the # state out of /proc//stat (field 3) and count only what is not Z. A live # match is reported with its pid, state and ppid, which is what a red needs to # name the process rather than merely announce it. _wd_live() { _wl= for _p in $(pgrep -x "$1" 2>/dev/null); do _st=$(LC_ALL=C awk '{print $3}' "/proc/$_p/stat" 2>/dev/null) [ "$_st" = Z ] && continue _pp=$(LC_ALL=C awk '{print $4}' "/proc/$_p/stat" 2>/dev/null) _wl="$_wl$_p:${_st:-?}:ppid=${_pp:-?} " done printf '%s' "$_wl" } if command -v pgrep >/dev/null 2>&1; then check_eq $S/no-daemon-running '' "$(_wd_live watchdog)" check_eq $S/no-keepalive-running '' "$(_wd_live wd_keepalive)" else bst_skip $S/no-daemon-running "no pgrep on this image" bst_skip $S/no-keepalive-running "no pgrep on this image" fi check_rc $S/no-pidfile 1 test -e /var/run/watchdog.pid check_rc $S/no-keepalive-pidfile 1 test -e /var/run/wd_keepalive.pid # The init scripts are INSTALLED and not ENABLED. update-rc.d's postinst runs at # rootfs time, and this distro has no runlevel links, so the S/K symlinks must be # absent -- the daemon reaching a runlevel here would feed nothing and reboot the # guest on its first failing check. check_eq $S/init-not-enabled 0 \ "$(ls "$R"/etc/rc*.d/*watchdog "$R"/etc/rc*.d/*wd_keepalive 2>/dev/null | LC_ALL=C awk 'END{print NR+0}')" # --- section 8: negative controls --------------------------------------------- # Everything above is a grep, a status compare or a stat against something that # exists. If any of those were answering yes unconditionally -- a grep matching # empty, a status compare reading 0 against 0 -- the sections would still be # green. These must come back FALSE, using the same primitives. check_rc $S/control-absent 1 test -e /usr/sbin/watchdog.nonesuch check_eq $S/control-grep-misses 0 \ "$(LC_ALL=C grep -c -F 'this string is in no watchdog output' "$W/bn.out.err" 2>/dev/null)" # A run that DID produce the output the greps above read. Without this, every # `grep -c ... = 0` case in the file would pass against an empty capture. check $S/control-capture-nonempty test -s "$W/bn.out.err" rm -rf "$W" 2>/dev/null