. "$BST_LIB" # BST-TIMEOUT: 600 # lldpd 1.0.18 (meta-networking recipes-daemons/lldpd) -- Vincent Bernat's # 802.1AB (LLDP) neighbour-discovery daemon. What ships is /usr/sbin/lldpd, the # /usr/sbin/lldpcli control client, and the /etc/lldpd.conf this port bundles. # # THE SUBJECT IS A DAEMON ANSWERING ITS OWN CONTROL SOCKET, NOT A LINK. The # criterion this port was taken on is that lldpd comes up, and then `lldpcli # show configuration' and `lldpcli show chassis' report the chassis id and the # TLVs the CONFIGURATION set, with `lldpcli -f json show neighbors' emitting # JSON that parses. Every case below builds to that. Transmission on a real # link is a separate arm and is scored separately, because this kernel cannot # lend it -- see the AF_PACKET note. # # PRIVILEGE SEPARATION IS COMPILED OUT, AND WITHOUT THAT THERE IS NO PORT AT # ALL. lldpd's priv_init() forks a monitor whose child runs priv_setup_chroot() # and then `if (chroot(chrootdir) == -1) fatal("privsep", "unable to chroot")' # (src/daemon/priv.c) -- fatal, no fallback. chroot(2) is unwired on this arm # (brintos/linux#58, the same wall 88-openssh.test attributes its preauth-child # reds to), so a stock build exits during startup and NOTHING below is # reachable: no control socket, no lldpcli, no chassis. The bbappend passes # upstream's own --disable-privsep, whose #else branch forks nothing, chroots # nothing and drops nothing. That switch is asserted on target below rather # than assumed -- a recipe that silently lost it would otherwise surface as a # daemon that mysteriously never starts. # # THERE ARE NO PORTS ON THIS IMAGE, AND THAT IS THE PLATFORM RATHER THAN THE # PORT. lldpd claims each interface with socket(PF_PACKET, SOCK_RAW, # htons(ETH_P_ALL)) in priv-linux.c:189; this kernel is built `# CONFIG_PACKET # is not set', so that call answers EAFNOSUPPORT -- established directly by # 79-libnet.test's libnet/plat-af-packet from a compiled probe, and attributed # to brintos/linux#81. With no packet socket no port can be initialised, so # lldpd holds a local chassis and an empty port list. The chassis and # configuration cases therefore say whether the port is sound; the port-list # and transmission cases say whether this kernel lends it a link. A red in the # second group with the first green is the platform, and it flips by itself the # day CONFIG_PACKET is turned on -- nothing here needs changing then. # # THE CHASSIS ID IS SET BY THE CONFIGURATION, NOT DERIVED. With no ports there # is no interface MAC to derive one from, so upstream would fall back to the # hostname -- which is not something a configuration set, and the criterion asks # for what the configuration set. The bundled /etc/lldpd.conf therefore names it # with `configure system chassisid', alongside description, platform, hostname # and the tx timers. Its values are deliberately NON-DEFAULT (tx-interval 45 vs # LLDPD_TX_INTERVAL 30, tx-hold 3 vs 4, max-neighbors 16 vs LLDPD_MAX_NEIGHBORS # 32, all from src/daemon/lldpd.h) so a green here cannot be a default wearing a # configured value's clothes. # # THE STARTUP CONFIG LOADER IS SCORED APART FROM THE CONTROL SOCKET, BECAUSE # THEY CAN FAIL SEPARATELY. lldpd does not parse lldpd.conf itself: it runs # `lldpcli -c ' over the control socket, and it reaches it through # vfork(2) (lldpd_configure(), src/daemon/lldpd.c:1355). fork(2) works on this # arm; vfork(2) is a distinct entry point and is not separately established # here. So the daemon is started with the loader ENABLED and, if the # configuration did not take, this suite applies the very same file over the # control socket itself and says which of the two paths carried it. That keeps # a dark vfork from reading as "configuration does not work", which would # misfile the finding onto the port. # # NOTHING IS KILLED AND THE DAEMON IS LEFT RUNNING. kill(2) on this image # reports success and delivers no signal, SIGKILL included (brintos/linux#21), # so a daemon this suite starts outlives it. It runs under -d on a control # socket of this suite's own in $TMPDIR -- never the packaged /run path -- so # nothing here depends on /run being writable and no case scores a signal. That # red is already carried where it was found (88-dnsmasq/sigterm-stops) and a # second copy would add to the batch's fail count without adding a finding. require lldpd lldpd/present LLDPD=/usr/sbin/lldpd LLDPCLI=/usr/sbin/lldpcli CONF=/etc/lldpd.conf D=${TMPDIR:-/tmp}/bst-lldpd.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok lldpd/workdir "cannot create $D"; exit 0; } SOCK=$D/lldpd.socket # has ... : true iff EVERY needle is a substring. has() { _h=$1; shift for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done return 0 } # ---------------------------------------------------------------- inventory -- # Both programmes install to sbindir (sbin_PROGRAMS in src/daemon/Makefile.am # and src/client/Makefile.am). A name missing here means an image list picked a # subset, not that the build ran short. check lldpd/inventory-lldpd test -x "$LLDPD" check lldpd/inventory-lldpcli test -x "$LLDPCLI" # Asserting where the bare names resolve catches a packaging change that leaves # them off the PATH the runner actually has -- which would otherwise surface as # a skip and read like an absent package. check_eq lldpd/resolves-to-sbin-lldpd "$LLDPD" "$(command -v lldpd 2>/dev/null)" check_eq lldpd/resolves-to-sbin-lldpcli "$LLDPCLI" "$(command -v lldpcli 2>/dev/null)" # The bundled configuration this port adds. Upstream's do_install:append ends # with `touch ${D}${sysconfdir}/lldpd.conf', so a stock image has this file # EMPTY -- -s, not -f, is what tells the two apart, and every configuration # case below is meaningless if this is red. check lldpd/conf-bundled test -s "$CONF" # ----------------------------------------------------------------- identity -- # -v pins the series. 1.0 and 1.1 differ in the lldpcli command tree this suite # drives, so a major bump would leave the cases below asserting against a # vocabulary this port does not ship. ver=$("$LLDPD" -v 2>&1); vrc=$? check_eq lldpd/version-rc "0" "$vrc" if has "$ver" "1.0.18"; then bst_ok lldpd/version-is-1.0.18 else bst_notok lldpd/version-is-1.0.18 "want '1.0.18', got '$(_bst_1line "$ver")'"; fi # ------------------------------------------------------- start the daemon ---- # -d keeps it in the foreground; -u puts the control socket in $D; -O names the # configuration explicitly rather than trusting the compiled-in location. The # startup lldpcli loader is deliberately LEFT ENABLED -- it is one of the things # under test -- and -D lldpcli turns on the trace that says whether it ran. # # NOTHING HERE TOUCHES /run, AND -d IS LOAD-BEARING FOR THAT, not just for # foregrounding. lldpd writes its pid file inside `if (!lldpd_started_by_systemd() # && daemonize)' (lldpd.c:1853) and every failure in that block is a fatal() -- # so a backgrounded lldpd on an image whose /run is not writable dies there, # before it serves anything. Under -d the block is skipped entirely and no pid # file is opened; -u then moves the only other /run path, the control socket. # # NOT wrapped in `timeout': a wrapper would only add a second process that # equally cannot be stopped (brintos/linux#21). ( "$LLDPD" -d -D lldpcli -u "$SOCK" -O "$CONF" > "$D/lldpd.log" 2>&1 & echo $! > "$D/pid" ) # Readiness is the control socket ANSWERING, never a sleep alone: lldpd creates # the socket (ctl_create) before it is ready to serve on it, so testing for the # file would race. 45s is many times the budget for a daemon that opens no # network port. i=0 while [ $i -lt 45 ]; do "$LLDPCLI" -u "$SOCK" show chassis >/dev/null 2>&1 && break i=$((i+1)); sleep 1 done log=$(cat "$D/lldpd.log" 2>/dev/null) # It started at all -- the control that separates "the daemon never ran" from # every more interesting failure below. check lldpd/ctl-socket-created test -S "$SOCK" out=$("$LLDPCLI" -u "$SOCK" show chassis 2>&1); crc=$? if [ "$crc" -eq 0 ]; then bst_ok lldpd/daemon-answers-control-socket elif bst_execfail "$out"; then bst_unrun lldpd/daemon-answers-control-socket "$out" else # Name what the daemon said rather than asserting a cause. The shape worth # telling apart first is a privsep fatal, which would mean --disable-privsep # did not take and #58 is back in the path. why=$(grep -iE "fatal|unable|cannot|error|refus" "$D/lldpd.log" 2>/dev/null | head -3) [ -n "$why" ] || why=$(tail -3 "$D/lldpd.log" 2>/dev/null) bst_notok lldpd/daemon-answers-control-socket \ "no answer on $SOCK after ${i}s -- rc=$crc $(_bst_1line "$why")" fi # THE CONFIGURE ARM, READ OFF THE DAEMON'S OWN VOICE. The #else branch of # priv_init() logs exactly this before pinging the (absent) monitor, so a green # here is proof --disable-privsep survived into the binary. A red means the # recipe lost the flag and chroot(2) is back in the startup path -- which is # the difference between this port working and not existing. if has "$log" "no privilege separation available"; then bst_ok lldpd/built-without-privsep elif has "$log" "unable to chroot"; then bst_notok lldpd/built-without-privsep \ "privsep is COMPILED IN and chroot(2) fatally refused it (brintos/linux#58) -- the bbappend's --disable-privsep did not reach this build" else bst_notok lldpd/built-without-privsep \ "the daemon never said 'no privilege separation available' -- $(_bst_1line "$(head -5 "$D/lldpd.log")")" fi # ------------------------------------------- THE CONFIGURATION IS IN EFFECT -- # Read the configuration back over the control socket. This is the criterion's # `lldpcli show configuration', and the text writer prints one `Label: value' # per setting (display_configuration, src/client/display.c). cfg=$("$LLDPCLI" -u "$SOCK" show configuration 2>&1); cfgrc=$? check_eq lldpd/show-configuration-rc "0" "$cfgrc" # WHICH PATH CARRIED IT. If the startup loader ran, the chassis-id override is # already set; if vfork(2) is dark it is not, and the same file is applied here # over the control socket instead. Both outcomes are recorded, and the cases # after this one then judge the configuration itself either way. if has "$cfg" "brintos-lldpd-chassis"; then bst_ok lldpd/startup-config-loader else fb=$("$LLDPCLI" -u "$SOCK" -c "$CONF" 2>&1); fbrc=$? cfg=$("$LLDPCLI" -u "$SOCK" show configuration 2>&1) if has "$cfg" "brintos-lldpd-chassis"; then bst_notok lldpd/startup-config-loader \ "lldpd -O did not apply $CONF, but the SAME file applied cleanly over the control socket afterwards -- the daemon reaches lldpcli through vfork(2) (lldpd.c:1355), which is where this points" else bst_notok lldpd/startup-config-loader \ "$CONF took effect by neither path -- lldpcli -c rc=$fbrc $(_bst_1line "$fb")" fi fi # THE CRITERION'S FIRST HALF, setting by setting. Each is a value only the # bundled file can have set: three of them differ from the compiled-in default # (30, 4, 32 in src/daemon/lldpd.h), and the four strings have no default at all. # # Asserted against `-f keyvalue' rather than the text rendering, because the text # form can only be substring-matched and two of these values are substrings of # each other -- the hostname `brintos-lldpd' occurs inside the chassis id # `brintos-lldpd-chassis', so a `has' on the text output would report the # hostname present on the strength of the chassis id alone. The kv writer emits # one `dotted.key=value' per line (SEP is `.', kv_writer.c:26), so anchoring the # key on the left and the value on the right makes each case exact. The key is # matched as a SUFFIX -- `(^|\.)tx-delay=45$' -- so a change to the enclosing # tag prefix does not silently turn these green or red. cfgkv=$("$LLDPCLI" -u "$SOCK" -f keyvalue show configuration 2>&1) # kv : ok iff a line of matches # (^|.)=$ exactly. The value is regex-escaped; the key is anchored # both sides so a longer key ending in the same word cannot satisfy it. kv() { _kvh=$1; _kvid=$2; _kvk=$3; _kvv=$4 if printf '%s\n' "$_kvh" | grep -qE "(^|\.)$(printf '%s' "$_kvk")=$(printf '%s' "$_kvv" | sed 's/[].[^$*\/]/\\&/g')$"; then bst_ok "$_kvid" else bst_notok "$_kvid" \ "want '$_kvk=$_kvv' -- got '$(_bst_1line "$(printf '%s\n' "$_kvh" | grep -E "(^|\.)$_kvk=" | head -1)")'" fi } kv "$cfgkv" lldpd/config-cid-string cid-string "brintos-lldpd-chassis" kv "$cfgkv" lldpd/config-description description "brintOS wasm32-hwjs target" kv "$cfgkv" lldpd/config-platform platform "brintos-wasm32" kv "$cfgkv" lldpd/config-hostname hostname "brintos-lldpd" kv "$cfgkv" lldpd/config-tx-interval tx-delay "45" kv "$cfgkv" lldpd/config-tx-hold tx-hold "3" kv "$cfgkv" lldpd/config-max-neighbors max-neighbors "16" # ------------------------------------------------ THE CRITERION'S SECOND HALF # `show chassis'. display_chassis() prints ChassisID, SysName, SysDescr and one # Capability line per capability flag. ch=$("$LLDPCLI" -u "$SOCK" show chassis 2>&1); chrc=$? check_eq lldpd/show-chassis-rc "0" "$chrc" # THE SINGLE SENTENCE THE TICKET'S CRITERION IS BUILT AROUND: the chassis id the # configuration set, reported back by the chassis view. # # All three go through -f keyvalue for the same reason the config cases do, # and here the trap is not hypothetical: the configured hostname # `brintos-lldpd' is a PREFIX of the configured chassis id # `brintos-lldpd-chassis', so a substring test for the SysName would be # satisfied by the chassis id alone and would report a green for a TLV that was # never emitted. chkv=$("$LLDPCLI" -u "$SOCK" -f keyvalue show chassis 2>&1) # THE CHASSIS ID IS NOT UNDER A KEY CALLED `id', AND ASSERTING ONE WAS A BUG IN # THIS ASSERTION RATHER THAN IN THE PORT. display_chassis() emits the id as # tag_start(w,"id") / tag_attr(w,"type",) / tag_data(w,) # (src/client/display.c:244), and the kv writer SPECIAL-CASES an attribute named # `type': it pops the enclosing tag and pushes the attribute's VALUE in its place # (kv_writer.c:88). So the key carries the id's SUBTYPE and never the word `id' # -- `local-chassis.chassis.local=brintos-lldpd-chassis' for an id named by # `configure system chassisid', which the daemon records as # LLDP_CHASSISID_SUBTYPE_LOCAL (src/daemon/lldpd.c:1264) and lldpctl renders # `local' (src/lib/atoms/chassis.c:31). An `id=' assertion could therefore never # match whatever the daemon reported, and on the batch-1214 image it did not: # `want id=brintos-lldpd-chassis -- got ""', with SysName and SysDescr green # beside it because tag_datatag() carries no `type' attribute to consume. # # The id is asserted instead as the exact VALUE of the one line in the chassis # block whose key is a single component under `chassis.'. That keeps the # substring trap shut -- the match is whole-line, and the SysName line's value # `brintos-lldpd' does not equal `brintos-lldpd-chassis' -- while leaving the # subtype component free, so the case still holds the day this image has ports # and lldpd derives the id from an interface MAC under the key `mac' instead. # # The value is what the CONFIGURATION set rather than a default wearing its # clothes: with no cid-string lldpd falls back to the chassis NAME (lldpd.c:1266) # and the line would read `...chassis.local=brintos-lldpd'. if printf '%s\n' "$chkv" | grep -qE '(^|\.)chassis\.[^.=]+=brintos-lldpd-chassis$'; then bst_ok lldpd/chassis-id-is-configured else bst_notok lldpd/chassis-id-is-configured \ "no chassis-id line carries the configured id -- want '<...>.chassis.=brintos-lldpd-chassis', got '$(_bst_1line "$(printf '%s\n' "$chkv" | grep -E '(^|\.)chassis\.[^.=]+=' | head -3)")'" fi kv "$chkv" lldpd/chassis-sysname name "brintos-lldpd" kv "$chkv" lldpd/chassis-sysdescr descr "brintOS wasm32-hwjs target" # Capabilities. lldpd DERIVES these rather than taking them from configuration, # so what is asserted is that the capability block is reported at all -- a # chassis with no Capability line would mean the TLV set was never assembled. if has "$ch" "Capability"; then bst_ok lldpd/chassis-capabilities else bst_notok lldpd/chassis-capabilities "no Capability line in show chassis -- $(_bst_1line "$ch")"; fi # ------------------------------------------------------------------- JSON ---- # THE CRITERION'S THIRD HALF: `lldpcli -f json show neighbors' emits JSON that # PARSES. The json writer is built in unconditionally (json_writer.c is in # src/client/Makefile.am with no configure guard), so this needs no dependency # of its own -- which is why it is a fair thing to hold the port to. # # It is parsed with jq rather than pattern-matched, because "looks like JSON" is # exactly the assertion a malformed document passes. An empty neighbour list is # the EXPECTED result here (no ports -- see the header), so this case is about # well-formedness and not about content. js=$("$LLDPCLI" -u "$SOCK" -f json show neighbors 2>&1); jsrc=$? check_eq lldpd/json-neighbors-rc "0" "$jsrc" if ! command -v jq >/dev/null 2>&1; then bst_notok lldpd/json-neighbors-parses "jq is not on this image, so the document could not be parsed" else jqerr=$(printf '%s' "$js" | jq . 2>&1 >/dev/null); jqrc=$? if [ "$jqrc" -eq 0 ]; then bst_ok lldpd/json-neighbors-parses elif bst_execfail "$jqerr"; then bst_unrun lldpd/json-neighbors-parses "$jqerr" else bst_notok lldpd/json-neighbors-parses "jq refused the document: $(_bst_1line "$jqerr") -- doc was '$(_bst_1line "$js")'"; fi # It is an OBJECT with the documented top-level key, not merely well-formed: # `jq .' accepts a bare `null' just as happily. top=$(printf '%s' "$js" | jq -r 'has("lldp")' 2>/dev/null) if [ "$top" = "true" ]; then bst_ok lldpd/json-neighbors-shape else bst_notok lldpd/json-neighbors-shape "the document has no top-level 'lldp' key -- $(_bst_1line "$js")"; fi fi # The same document through the interfaces view, which is where the port list # would appear. Well-formedness only, for the same reason. jsi=$("$LLDPCLI" -u "$SOCK" -f json show interfaces 2>&1); jsirc=$? if [ "$jsirc" -eq 0 ] && command -v jq >/dev/null 2>&1 && printf '%s' "$jsi" | jq . >/dev/null 2>&1; then bst_ok lldpd/json-interfaces-parses else bst_notok lldpd/json-interfaces-parses "rc=$jsirc -- $(_bst_1line "$jsi")" fi # ------------------------------------------------- THE LINK-LAYER ARM -------- # THE PLATFORM'S HALF, scored on its own and expected RED here. lldpd claims an # interface with socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) (priv-linux.c:189) # and this kernel is built `# CONFIG_PACKET is not set', so no port can be # initialised and `show interfaces' has nothing to list. Scored so that OK is # GREEN -- the case flips by itself the day CONFIG_PACKET lands and needs no # edit here. ifs=$("$LLDPCLI" -u "$SOCK" show interfaces 2>&1) if has "$ifs" "Interface:"; then bst_ok lldpd/ports-initialised else bst_notok lldpd/ports-initialised \ "lldpd has no ports: socket(PF_PACKET, SOCK_RAW, ETH_P_ALL) is EAFNOSUPPORT on this kernel ('# CONFIG_PACKET is not set') -- brintos/linux#81 (https://brintos.io/brintos/linux/issues/81); libnet/plat-af-packet in 79-libnet.test measures the syscall directly" fi # The consequence for transmission, one level downstream and recorded where a # reader looking for "does it send frames" will find it. `show statistics' # reports per-port counters; with no ports there are none to report. st=$("$LLDPCLI" -u "$SOCK" show statistics 2>&1) if has "$st" "Transmitted"; then bst_ok lldpd/tx-statistics else bst_notok lldpd/tx-statistics \ "no transmit counters, because there is no port to transmit on -- downstream of lldpd/ports-initialised, brintos/linux#81" fi # --------------------------------------------------------------- lldpcli ----- # The client's own surface, exercised without the daemon: a bad command must be # refused. Without this, every green above could be a client that returns 0 # unconditionally. bad=$("$LLDPCLI" -u "$SOCK" show nonsuch-subcommand 2>&1); badrc=$? if [ "$badrc" -ne 0 ]; then bst_ok lldpd/lldpcli-refuses-bad-command else bst_notok lldpd/lldpcli-refuses-bad-command "exited 0 on an unknown subcommand -- $(_bst_1line "$bad")"; fi # And a socket that is not there is an error, not a hang or a silent success. nos=$("$LLDPCLI" -u "$D/no-such.socket" show chassis 2>&1); nosrc=$? if [ "$nosrc" -ne 0 ]; then bst_ok lldpd/lldpcli-refuses-absent-socket else bst_notok lldpd/lldpcli-refuses-absent-socket "exited 0 against a control socket that does not exist -- $(_bst_1line "$nos")"; fi # The daemon is deliberately NOT stopped -- see the header.