. "$BST_LIB" # BST-TIMEOUT: 600 # net-snmp 5.9.4 -- the SNMP agent (snmpd), the trap receiver (snmptrapd), the # command-line managers (snmpget/snmpwalk/snmpset/...) and the MIB parser # (snmptranslate). meta-networking recipes-protocols/net-snmp. # # THE AGENT IS STARTED FIRST, BEFORE ANY OTHER CASE, AND THAT ORDER IS THE # RESULT OF A MEASUREMENT RATHER THAN A PREFERENCE. Every binary in this package # is a 35-51 MB static wasm module, and the guest stages each exec by compiling # it; snmpd at 51 MB is the largest thing this image asks the loader to take. A # first cut of this suite ran the packaging and MIB-parser cases first and the # agent could then not be staged at all -- measured, in the shell's own words: # `snmpd: Cannot allocate memory` (brintos/linux#44), and on a second run the # guest wedged outright on `wasm32_user_exec_load trapped`. Started first, from # a fresh guest, snmpd loads every time. So: the agent goes up, and everything # cheap happens while it is resident. # # FOR THE SAME REASON EVERY MANAGER CALL IS BATCHED. SNMP allows many varbinds # per PDU and snmptranslate takes many OIDs per invocation, so a dozen # assertions are read out of ONE exec instead of a dozen. Twenty execs of one # binary was measured clean (peak host RSS 3206 MiB); sixty across several # wedged the guest. The cost is per DISTINCT binary -- each is a fresh 35-51 MB # module the host compiles, ~300 MiB it does not give back -- so if you add a # case, read it out of an exec that already runs. # # WHAT THIS SUITE DELIBERATELY DOES NOT DO IS SLOW ITSELF DOWN. An earlier cut # spaced the first use of each binary 20-40 s apart, to keep host growth under # the 2048 MiB/min bound brintos_local_verify reads as the hardwarejs#20 EINTR # livelock. It does not work, and the measurement is the reason it is gone: # unpaced 1/3 boots completed, paced-40 2/3, and unpaced with the harness's rate # bound raised to 4096 MiB/min 3/3 -- while pacing took the suite from ~130 s to # ~600 s. The growth is real, bounded (peak 5.9-9.5 GB against a 32 GB cap) and # explained by thirteen module compilations plus the runtime's own background # allocation; it is the RATE bound that does not fit this package, and waiting # only lengthens the window it is measured over. Run the gate with # `--rss-rate 4096`; see docs/brintos-test-plan.md entry 333. # # UDP, NOT TCP, AND THAT IS THE PROTOCOL'S OWN CHOICE (RFC 3417 names 161/162), # so the bind is asserted from /proc/net/udp -- which this kernel does export, # measured here, and which no suite on this image had read before. # # `sleep N` does not sleep on this image (brintos/linux#48, # linux-in-the-browser#120), and `timeout` did not fire before the # brintos/linux#77 fix (the reason brintos-selftest bounds suites by polling), # so every wait spins against /proc/uptime and no case is wrapped in `timeout`: # a wrapper here would add a process that cannot be stopped rather than a bound. # What DOES bound the managers is net-snmp's own `-t 2 -r 1`, which is a # select(2) timeout the kernel honours. require snmptranslate net-snmp/present VER=5.9.4 PORT=16161; PHEX=3F21 # printf '%04X' 16161 -- /proc/net/udp is uppercase hex TPORT=16162 D=/tmp/.bst-net-snmp rm -rf "$D"; mkdir -p "$D/persist"; cd "$D" || exit 0 # Every net-snmp process here keeps its persistent state (engineID, USM users) # under $D rather than /var/lib/net-snmp, so a re-run starts from nothing and # nothing this suite creates outlives it. SNMP_PERSISTENT_DIR="$D/persist"; export SNMP_PERSISTENT_DIR RO=bstpublic RW=bstprivate V3USER=bstv3 LOC="BST loopback wasm32-hwjs" up() { cut -d' ' -f1 /proc/uptime; } spin() { _i=0; while [ $_i -lt 2000 ]; do _i=$((_i + 1)); done; } # waitfor -- spin until the file carries the needle. waitfor() { _w=$1; _f=$2; _n=$3 _s=$(up); _s=${_s%.*} while :; do grep -q "$_n" "$_f" 2>/dev/null && return 0 _c=$(up); _c=${_c%.*} [ $((_c - _s)) -ge "$_w" ] && return 1 spin done } # 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 } # said ... -- one case per assertion over text ALREADY # captured, so a dozen cases cost no execs at all. This is what makes the # batching above readable. said() { _i=$1; _t=$2; shift 2 if has "$_t" "$@"; then bst_ok "$_i" else bst_notok "$_i" "want:'$*' got:'$(_bst_1line "$_t")'"; fi } # line -- the first line of containing . line() { printf '%s\n' "$1" | grep "$2" | head -1; } # --------------------------------------------------------------------------- # part 1: the agent, started before anything else (see the header) # --------------------------------------------------------------------------- # Written here rather than reusing /etc/snmp/snmpd.conf: that file is upstream's # demo configuration, reads the host's real MIB tree and does not name our port. # # sysLocation is set HERE and sysContact is deliberately NOT: net-snmp makes an # object read-only exactly when the configuration pins it, so this split gives # the suite one object it must refuse to write and one it must accept. cat > snmpd.conf < "$D/snmpd.out" 2>&1 & AGENT_PID=$! # READINESS IS THE AGENT'S OWN BANNER: snmpd logs "NET-SNMP version ..." after # every transport is open, so no request below can race the bind. waitfor 40 "$D/snmpd.log" "NET-SNMP version" UP=$? # The agent's exit status, collected only when it never announced itself -- the # evidence a log cannot give when a syscall traps instead of returning ENOSYS. AGENT_RC="" [ "$UP" -eq 0 ] || { wait "$AGENT_PID" 2>/dev/null; AGENT_RC=$?; } agent_said() { tail -3 "$D/snmpd.log" 2>/dev/null | tr '\n' ' '; cat "$D/snmpd.out" 2>/dev/null; } # gate_of -- name the gap the agent actually hit, # from what IT said and how it DIED, never from which gap we expect. The gaps # land independently and a suite that hardcodes today's answer reports the wrong # issue on the first day one of them changes. gate_of() { _line=$1; _rc=$2 case "$_line" in *"Cannot allocate memory"*) echo "the agent could not be staged at all -- brintos/linux#44 (a 51 MB exec image under a 1 GiB guest): $(_bst_1line "$_line")"; return ;; *"Address already in use"*|*"Error opening specified endpoint"*) echo "udp/$PORT was not free or could not be bound: $(_bst_1line "$_line")"; return ;; *"Function not implemented"*) echo "a syscall this arm does not wire answered ENOSYS -- $(_bst_1line "$_line")"; return ;; *"Resource temporarily unavailable"*) echo "a resource was refused (EAGAIN) -- if this is a clone, brintos/linux-in-the-browser#85: $(_bst_1line "$_line")"; return ;; esac if [ -n "$_rc" ] && [ "$_rc" -gt 128 ]; then echo "the agent was killed by signal $((_rc - 128)) with no error of its own -- a trapping syscall rather than one that answers ENOSYS; last lines before it: '$(_bst_1line "$_line")'" elif [ -z "$_line" ]; then echo "the agent exited (rc=${_rc:-unknown}) or never announced itself, and printed nothing -- gate not identified" else echo "gate not identified -- rc=${_rc:-unknown}, agent said: $(_bst_1line "$_line")" fi } GATE="" if [ "$UP" -eq 0 ]; then bst_ok net-snmp/agent-starts else GATE=$(gate_of "$(agent_said)" "$AGENT_RC") bst_notok net-snmp/agent-starts "$GATE" fi # agate -- every case past the start is meaningless without an agent. # Report each as the SAME gap rather than as an independent failure, so a reader # counts one defect and not forty. agate() { if [ -n "$GATE" ]; then bst_notok "$1" "no agent: $GATE"; return 0; fi return 1 } # The banner carries the version this image built. Note the tarball calls itself # 5.9.4.pre2 -- meta-networking packages that as 5.9.4, so the assertion is on # the prefix and not on an exact string. if agate net-snmp/agent-logs-version; then :; else said net-snmp/agent-logs-version "$(cat "$D/snmpd.log" 2>/dev/null)" "NET-SNMP version $VER" fi # The agent said it was up; this asserts the KERNEL agrees a UDP socket is bound # to the port it was told to serve. Separate from the gate because the two can # disagree: an agent that opened no transport still logs its banner, and that is # the shape of a transport-layer regression. if agate net-snmp/agent-binds-udp; then :; else if [ -r /proc/net/udp ]; then if awk -v p=":$PHEX" 'index($2, p) { hit = 1 } END { exit !hit }' /proc/net/udp; then bst_ok net-snmp/agent-binds-udp else bst_notok net-snmp/agent-binds-udp "no UDP socket on port $PORT ($PHEX) in /proc/net/udp" fi else bst_notok net-snmp/agent-binds-udp "this kernel does not export /proc/net/udp, so the bind could not be read from it" fi fi # --------------------------------------------------------------------------- # part 2: the ticket's named criterion -- one GET, ten assertions # --------------------------------------------------------------------------- # Ten varbinds in ONE PDU. SNMP allows it, and it is what keeps this suite # inside the exec budget the header describes. It also exercises something a # stream of single-varbind gets never would: an agent that answered only the # first binding would pass every one-varbind case and fail here. G1="" if agate net-snmp/get-sysdescr; then for c in get-sysuptime get-sysname get-syslocation get-sysservices \ get-sysobjectid get-syscontact-default get-no-such-object \ get-no-such-instance get-ten-varbinds; do agate "net-snmp/$c"; done else G1=$(snmpget -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT \ SNMPv2-MIB::sysDescr.0 SNMPv2-MIB::sysUpTime.0 SNMPv2-MIB::sysName.0 \ SNMPv2-MIB::sysLocation.0 SNMPv2-MIB::sysServices.0 \ SNMPv2-MIB::sysObjectID.0 SNMPv2-MIB::sysContact.0 \ SNMPv2-MIB::snmpInPkts.0 .1.3.6.1.2.1.1.99.0 SNMPv2-MIB::sysDescr.1 2>&1) # sysDescr is the agent's own description of the machine; on Linux it is built # from uname(2), so "Linux" in it is the kernel identifying itself through SNMP. said net-snmp/get-sysdescr "$(line "$G1" sysDescr.0)" "sysDescr.0 = STRING:" "Linux" said net-snmp/get-sysuptime "$(line "$G1" sysUpTime)" "Timeticks:" said net-snmp/get-sysname "$(line "$G1" sysName.0)" "sysName.0 = STRING:" # sysLocation and sysServices come from OUR config file, so these two are what # prove the agent read it. said net-snmp/get-syslocation "$(line "$G1" sysLocation.0)" "$LOC" said net-snmp/get-sysservices "$(line "$G1" sysServices.0)" "INTEGER: 72" # sysObjectID identifies the AGENT: net-snmp's enterprise arc 8072, .3.2.10 for # the Linux agent. said net-snmp/get-sysobjectid "$(line "$G1" sysObjectID.0)" "netSnmpAgentOIDs.10" # sysContact is NOT pinned in the config, so it still carries the compiled-in # default -- which is what makes it writable further down. said net-snmp/get-syscontact-default "$(line "$G1" sysContact.0)" "sysContact.0 = STRING:" # The two v2c EXCEPTIONS, which are not errors: an OID under no registered # subtree is noSuchObject, a missing instance of a real object is # noSuchInstance. Telling them apart is the whole point of v2c's exceptions, # and both travel in the same reply as the eight good bindings above. said net-snmp/get-no-such-object "$G1" "No Such Object available on this agent at this OID" said net-snmp/get-no-such-instance "$G1" "No Such Instance currently exists at this OID" # ...and the reply really did carry all ten. N1=$(printf '%s\n' "$G1" | grep -c ' = ') if [ "${N1:-0}" -ge 10 ]; then bst_ok net-snmp/get-ten-varbinds else bst_notok net-snmp/get-ten-varbinds "the reply carried ${N1:-0} of 10 varbinds -- $(_bst_1line "$G1")"; fi fi # A second read, numeric this time: -On asks the manager to print OIDs rather # than names, which is the path that does NOT consult the MIB tree. It also # gives the two counters a second sample without a third exec. G2="" if agate net-snmp/get-numeric-oid; then for c in sysuptime-advances snmp-stats-advance; do agate "net-snmp/$c"; done else G2=$(snmpget -v2c -c "$RO" -t 2 -r 1 -On 127.0.0.1:$PORT \ .1.3.6.1.2.1.1.3.0 .1.3.6.1.2.1.11.1.0 2>&1) said net-snmp/get-numeric-oid "$G2" ".1.3.6.1.2.1.1.3.0 = Timeticks:" # sysUpTime is a clock, so the later sample must be LARGER. This is the case # that proves the agent is running rather than replaying: a canned answer # cannot advance. T1=$(printf '%s\n' "$G1" | grep sysUpTime | sed 's/.*(\([0-9]*\)).*/\1/') T2=$(printf '%s\n' "$G2" | grep '1.3.6.1.2.1.1.3.0' | sed 's/.*(\([0-9]*\)).*/\1/') if [ -n "$T1" ] && [ -n "$T2" ] && [ "$T2" -gt "$T1" ] 2>/dev/null; then bst_ok net-snmp/sysuptime-advances else bst_notok net-snmp/sysuptime-advances "sysUpTime went '$T1' -> '$T2'; the later sample must exceed the earlier" fi # snmpInPkts counts the requests THIS suite has sent, so it must advance too -- # a second clock, driven by us rather than by time. P1=$(printf '%s\n' "$G1" | grep snmpInPkts | sed 's/.*Counter32: //') P2=$(printf '%s\n' "$G2" | grep '1.3.6.1.2.1.11.1.0' | sed 's/.*Counter32: //') if [ -n "$P1" ] && [ -n "$P2" ] && [ "$P2" -gt "$P1" ] 2>/dev/null; then bst_ok net-snmp/snmp-stats-advance else bst_notok net-snmp/snmp-stats-advance "snmpInPkts went '$P1' -> '$P2' across two requests" fi fi # GETNEXT is the walk primitive: asked for the group, it must answer the first # object IN it. if agate net-snmp/getnext-first-of-system; then :; else said net-snmp/getnext-first-of-system \ "$(snmpgetnext -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT SNMPv2-MIB::system 2>&1)" \ "SNMPv2-MIB::sysDescr.0 = STRING:" fi # --------------------------------------------------------------------------- # part 3: the ticket's named criterion -- a walk of the system subtree # --------------------------------------------------------------------------- if agate net-snmp/walk-system; then for c in walk-system-count walk-ends-cleanly walk-sysortable; do agate "net-snmp/$c"; done else W=$(snmpwalk -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT SNMPv2-MIB::system 2>&1) said net-snmp/walk-system "$W" \ "sysDescr.0" "sysObjectID.0" "sysUpTime" "sysContact.0" "sysName.0" \ "sysLocation.0" "sysServices.0" # The system group is 8 scalars plus sysORTable's rows. A walk that returned # three of them would satisfy the substring case above on its own, so the # COUNT is asserted separately. WN=$(printf '%s\n' "$W" | grep -c '^SNMPv2-MIB::') if [ "${WN:-0}" -ge 8 ]; then bst_ok net-snmp/walk-system-count else bst_notok net-snmp/walk-system-count "the walk returned ${WN:-0} rows, want at least 8"; fi # A walk ends when the agent answers outside the subtree and the client stops. # One that ended in a timeout would print rows AND fail, which is what this # separates out. if printf '%s\n' "$W" | grep -qi 'Timeout\|No Response'; then bst_notok net-snmp/walk-ends-cleanly "$(_bst_1line "$W")" else bst_ok net-snmp/walk-ends-cleanly fi # sysORTable is the agent's own register of the MIB modules it loaded, so a # non-empty one is the agent saying which parts of the tree it can serve. said net-snmp/walk-sysortable "$W" "sysORID.1 = OID:" fi # GETBULK is v2c's own PDU and takes a different path through the agent than the # GETNEXT loop above: one request, many varbinds in the reply. if agate net-snmp/bulkwalk-system; then :; else said net-snmp/bulkwalk-system \ "$(snmpbulkwalk -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT SNMPv2-MIB::system 2>&1)" \ "sysDescr.0" "sysLocation.0" fi if agate net-snmp/bulkget-repeaters; then :; else B=$(snmpbulkget -v2c -c "$RO" -t 2 -r 1 -Cn0 -Cr5 127.0.0.1:$PORT \ SNMPv2-MIB::system 2>&1) BN=$(printf '%s\n' "$B" | grep -c ' = ') if [ "${BN:-0}" -ge 5 ]; then bst_ok net-snmp/bulkget-repeaters else bst_notok net-snmp/bulkget-repeaters "max-repetitions 5 returned ${BN:-0} varbinds -- $(_bst_1line "$B")"; fi fi # A whole-table render, which is snmptable walking the columns and joining them # by index. sysORTable is the one table every agent has. if agate net-snmp/snmptable-sysortable; then :; else said net-snmp/snmptable-sysortable \ "$(snmptable -v2c -c "$RO" -t 2 -r 1 -Ci 127.0.0.1:$PORT SNMPv2-MIB::sysORTable 2>&1)" \ "sysORID" "sysORDescr" fi # snmpstatus is a manager in its own right: one multi-varbind get rendered as a # one-line summary of the device. if agate net-snmp/snmpstatus; then :; else said net-snmp/snmpstatus \ "$(snmpstatus -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT 2>&1)" "Linux" fi # --------------------------------------------------------------------------- # part 4: SET, and the two ways it is refused # --------------------------------------------------------------------------- # sysContact is not pinned in snmpd.conf, so the agent accepts a write. This is # the only case that changes agent state, and the reply itself carries the new # value -- so the acceptance and the value are one exec. if agate net-snmp/set-syscontact; then agate net-snmp/set-syscontact-readback; else S=$(snmpset -v2c -c "$RW" -t 2 -r 1 127.0.0.1:$PORT \ SNMPv2-MIB::sysContact.0 s "bst@example.invalid" 2>&1); SRC=$? if [ "$SRC" -eq 0 ]; then bst_ok net-snmp/set-syscontact else bst_notok net-snmp/set-syscontact "rc=$SRC $(_bst_1line "$S")"; fi said net-snmp/set-syscontact-readback "$S" "bst@example.invalid" fi # sysLocation IS pinned in snmpd.conf, and net-snmp's rule is that a # configuration file makes the object read-only. The refusal must be the SNMP # error notWritable, not a transport failure. if agate net-snmp/set-pinned-refused; then :; else S=$(snmpset -v2c -c "$RW" -t 2 -r 1 127.0.0.1:$PORT \ SNMPv2-MIB::sysLocation.0 s "somewhere else" 2>&1); SRC=$? if [ "$SRC" -ne 0 ] && has "$S" "notWritable"; then bst_ok net-snmp/set-pinned-refused else bst_notok net-snmp/set-pinned-refused "rc=$SRC want a notWritable error -- $(_bst_1line "$S")" fi fi # The READ-ONLY community must not write, which is a different rule from the one # above: here the object is writable and the requester is not. if agate net-snmp/set-rocommunity-refused; then :; else S=$(snmpset -v2c -c "$RO" -t 2 -r 1 127.0.0.1:$PORT \ SNMPv2-MIB::sysContact.0 s "not allowed" 2>&1); SRC=$? if [ "$SRC" -ne 0 ]; then bst_ok net-snmp/set-rocommunity-refused else bst_notok net-snmp/set-rocommunity-refused "the read-only community was allowed to write -- $(_bst_1line "$S")"; fi fi # A community the agent does not know gets NO answer -- SNMP does not say "wrong # password", it stays silent and the manager times out. That silence is the # access control working, and it is also the one case that proves net-snmp's own # select(2) retry timer fires on this kernel: nothing here is wrapped in # `timeout`, so if -t 1 -r 0 did not expire this case would hang the suite. if agate net-snmp/wrong-community-times-out; then :; else S=$(snmpget -v2c -c wrong-community -t 1 -r 0 127.0.0.1:$PORT \ SNMPv2-MIB::sysDescr.0 2>&1); SRC=$? if [ "$SRC" -ne 0 ] && has "$S" "Timeout"; then bst_ok net-snmp/wrong-community-times-out else bst_notok net-snmp/wrong-community-times-out "rc=$SRC $(_bst_1line "$S")" fi fi # --------------------------------------------------------------------------- # part 5: the other two protocol versions # --------------------------------------------------------------------------- # v1 has no exceptions and no GETBULK; its errors live in the PDU header. The # agent must still answer a v1 get... if agate net-snmp/v1-get; then :; else said net-snmp/v1-get \ "$(snmpget -v1 -c "$RO" -t 2 -r 1 127.0.0.1:$PORT SNMPv2-MIB::sysDescr.0 2>&1)" \ "sysDescr.0 = STRING:" fi # ...and answer a missing OID with noSuchName in the header rather than with a # varbind exception. That difference IS SNMPv1, stated as a case. if agate net-snmp/v1-nosuchname; then :; else S=$(snmpget -v1 -c "$RO" -t 2 -r 1 127.0.0.1:$PORT .1.3.6.1.2.1.1.99.0 2>&1); SRC=$? if [ "$SRC" -ne 0 ] && has "$S" "noSuchName"; then bst_ok net-snmp/v1-nosuchname else bst_notok net-snmp/v1-nosuchname "rc=$SRC $(_bst_1line "$S")"; fi fi # SNMPv3 / USM is where openssl earns net-snmp's DEPENDS: the agent creates the # user from the createUser line at start-up, authNoPriv signs every PDU with # HMAC-MD5 and authPriv encrypts it with DES. A red here is a statement about # the crypto, not about SNMP. if agate net-snmp/v3-authnopriv; then :; else said net-snmp/v3-authnopriv \ "$(snmpget -v3 -u "$V3USER" -l authNoPriv -a MD5 -A bstauthpass -t 2 -r 1 \ 127.0.0.1:$PORT SNMPv2-MIB::sysDescr.0 2>&1)" "sysDescr.0 = STRING:" fi if agate net-snmp/v3-authpriv; then :; else said net-snmp/v3-authpriv \ "$(snmpget -v3 -u "$V3USER" -l authPriv -a MD5 -A bstauthpass -x DES -X bstprivpass \ -t 2 -r 1 127.0.0.1:$PORT SNMPv2-MIB::sysDescr.0 2>&1)" "sysDescr.0 = STRING:" fi # A wrong passphrase must NOT authenticate. Without this case the two above # would pass equally well against an agent that ignored the signature. if agate net-snmp/v3-wrong-auth-refused; then :; else S=$(snmpget -v3 -u "$V3USER" -l authNoPriv -a MD5 -A wrongpassphrase -t 2 -r 1 \ 127.0.0.1:$PORT SNMPv2-MIB::sysDescr.0 2>&1); SRC=$? if [ "$SRC" -ne 0 ]; then bst_ok net-snmp/v3-wrong-auth-refused else bst_notok net-snmp/v3-wrong-auth-refused "a wrong passphrase was accepted -- $(_bst_1line "$S")"; fi fi # --------------------------------------------------------------------------- # part 6: the trap receiver, which is the other direction # --------------------------------------------------------------------------- # Everything above is request/response with the agent responding. A TRAP travels # the other way and through a different program: snmptrapd binds its own UDP # port and snmptrap sends it an unacknowledged notification. cat > snmptrapd.conf < "$D/snmptrapd.out" 2>&1 & TRAPD_PID=$! waitfor 40 "$D/snmptrapd.log" "NET-SNMP version" TRAPD_UP=$? if [ "$TRAPD_UP" -eq 0 ]; then bst_ok net-snmp/trapd-starts else bst_notok net-snmp/trapd-starts "$(_bst_1line "$(tail -3 "$D/snmptrapd.log" 2>/dev/null; cat "$D/snmptrapd.out" 2>/dev/null)")" fi if [ "$TRAPD_UP" -ne 0 ]; then for c in trap-sent trap-received trap-oid-preserved; do bst_notok "net-snmp/$c" "no trap receiver: see net-snmp/trapd-starts" done else # An SNMPv2c trap: uptime, the trap OID, and one varbind of our own. It is # UNACKNOWLEDGED, so snmptrap exiting 0 says only that it sent -- the proof is # what the receiver logged, which is why they are separate cases. S=$(snmptrap -v2c -c "$RO" 127.0.0.1:$TPORT '' SNMPv2-MIB::coldStart \ SNMPv2-MIB::sysName.0 s "bst-trap-witness" 2>&1); SRC=$? if [ "$SRC" -eq 0 ]; then bst_ok net-snmp/trap-sent else bst_notok net-snmp/trap-sent "rc=$SRC $(_bst_1line "$S")"; fi waitfor 20 "$D/snmptrapd.log" "bst-trap-witness" if [ $? -eq 0 ]; then bst_ok net-snmp/trap-received else bst_notok net-snmp/trap-received "the receiver's log never carried the varbind this suite sent -- $(_bst_1line "$(tail -3 "$D/snmptrapd.log" 2>/dev/null)")"; fi # The trap's identity has to survive too: a receiver that logged the varbind # but lost the notification OID would still pass the case above. said net-snmp/trap-oid-preserved "$(cat "$D/snmptrapd.log" 2>/dev/null)" "coldStart" fi # --------------------------------------------------------------------------- # part 7: the MIB parser, offline -- no agent is involved in any case below # --------------------------------------------------------------------------- # snmptranslate takes many OIDs per invocation, so the whole name/number # correspondence is two execs rather than eight. said net-snmp/translate-version "$(snmptranslate -V 2>&1)" "$VER" TN=$(snmptranslate -On SNMPv2-MIB::sysDescr.0 IF-MIB::ifTable IP-MIB::ipForwarding.0 \ SNMPv2-SMI::enterprises HOST-RESOURCES-MIB::hrSystemUptime.0 2>&1) said net-snmp/translate-name-to-oid "$TN" ".1.3.6.1.2.1.1.1.0" said net-snmp/translate-ifmib "$TN" ".1.3.6.1.2.1.2.2" said net-snmp/translate-ipmib "$TN" ".1.3.6.1.2.1.4.1.0" said net-snmp/translate-enterprises "$TN" ".1.3.6.1.4.1" said net-snmp/translate-hostmib "$TN" ".1.3.6.1.2.1.25.1.1.0" TO=$(snmptranslate .1.3.6.1.2.1.1.3.0 .1.3.6.1.2.1.2.2.1.10 .1.3.6.1.2.1.1.1.99.99 2>&1) # .1.3.6.1.2.1.1.3.0 answers DISMAN-EVENT-MIB::sysUpTimeInstance, NOT # SNMPv2-MIB::sysUpTime.0, and that is correct: two MIBs register that same OID # and only DISMAN-EVENT-MIB names the .0 INSTANCE as an object of its own, so # the parser prefers the exact node over the parent-plus-suffix. Asserting the # name a reader would guess is how this case failed on its first run -- the # lookup was right and the expectation was not. said net-snmp/translate-oid-to-name "$TO" "DISMAN-EVENT-MIB::sysUpTimeInstance" said net-snmp/translate-ifinoctets "$TO" "IF-MIB::ifInOctets" # A numeric OID with no name still resolves as far as the tree goes and prints # the remainder numerically -- the parser's partial-match path. said net-snmp/translate-partial "$TO" "SNMPv2-MIB::sysDescr" said net-snmp/translate-full-path "$(snmptranslate -Of .1.3.6.1.2.1.1.1.0 2>&1)" \ ".iso.org.dod.internet.mgmt.mib-2.system.sysDescr.0" # -Td prints the object's own MIB definition -- SYNTAX, MAX-ACCESS, STATUS, # DESCRIPTION. A parser that merely tokenised the file could produce the OID # above; only one that built the object can produce this. said net-snmp/translate-object-definition "$(snmptranslate -Td SNMPv2-MIB::sysDescr 2>&1)" \ "SYNTAX" "DisplayString" "read-only" "current" # -Tp renders the subtree as a tree; a correct render of the system group names # its scalars and descends into sysORTable. said net-snmp/translate-tree "$(snmptranslate -Tp SNMPv2-MIB::system 2>&1)" \ "sysDescr" "sysObjectID" "sysORTable" "sysORIndex" # An OID with no definition in any loaded MIB is an ERROR, not a silent pass -- # it is how a manager finds out its MIB set is incomplete. TU=$(snmptranslate NO-SUCH-MIB::noSuchObject 2>&1); TU_RC=$? if [ "$TU_RC" -ne 0 ] && has "$TU" "Unknown"; then bst_ok net-snmp/translate-unknown-rejected else bst_notok net-snmp/translate-unknown-rejected "rc=$TU_RC $(_bst_1line "$TU")" fi # THE WHOLE BUNDLED MIB SET, PARSED IN ONE GO. -m ALL loads every .txt under # /usr/share/snmp/mibs; this asserts the parser gets through all of them and # still answers, which is a far stronger statement about the shipped -mibs # package than any single lookup. Parse diagnostics go to stderr and are # tolerated (upstream's own set is not warning-free); a module it cannot OPEN is # not. MA=$(snmptranslate -m ALL -On SNMPv2-MIB::sysDescr.0 2>&1) if has "$MA" ".1.3.6.1.2.1.1.1.0" && ! has "$MA" "Cannot find module"; then bst_ok net-snmp/translate-all-mibs else bst_notok net-snmp/translate-all-mibs "$(_bst_1line "$MA")" fi # --------------------------------------------------------------------------- # part 8: packaging -- no exec of a net-snmp binary, so it costs nothing # --------------------------------------------------------------------------- inv="" for f in /usr/sbin/snmpd /usr/sbin/snmptrapd \ /usr/bin/snmpget /usr/bin/snmpgetnext /usr/bin/snmpwalk \ /usr/bin/snmpbulkwalk /usr/bin/snmpbulkget /usr/bin/snmpset \ /usr/bin/snmptranslate /usr/bin/snmpstatus /usr/bin/snmptable \ /usr/bin/snmptrap /usr/bin/snmpusm /usr/bin/snmpvacm \ /usr/bin/snmpdelta /usr/bin/snmptest /usr/bin/snmpdf \ /usr/bin/encode_keychange \ /etc/snmp/snmpd.conf /etc/snmp/snmptrapd.conf; do [ -e "$f" ] || inv="$inv $f" done check_eq net-snmp/inventory "" "$inv" # The MIB text files are the parser's whole input; without them every case in # part 7 would be vacuous. mib="" for m in SNMPv2-MIB.txt IF-MIB.txt IP-MIB.txt TCP-MIB.txt UDP-MIB.txt \ HOST-RESOURCES-MIB.txt UCD-SNMP-MIB.txt SNMPv2-SMI.txt; do [ -e "/usr/share/snmp/mibs/$m" ] || mib="$mib $m" done check_eq net-snmp/mibs-present "" "$mib" # THIS DISTRO HAS NO SHARED LIBRARIES, so net-snmp's five libnetsnmp*.so # packages are empty and the bbappend drops the RDEPENDS that named them. The # assertion is therefore that there is NO .so -- the opposite of what upstream's # packaging implies, and the thing that would change if the distro ever grew # them. check_eq net-snmp/no-shared-libs "" "$(ls /usr/lib/libnetsnmp*.so* 2>/dev/null)" # The bbappend drops upstream's init script deliberately: an agent is started by # whoever wants one, and a boot-time listener on udp/161 would make this suite's # own bind assertion pass while its own agent failed. check_eq net-snmp/no-init-script "" "$(ls /etc/init.d/snmpd 2>/dev/null)" # Nothing in the agent is an unresolved-symbol trapping stub -- wasmld emits one # for a symbol it could not resolve and it traps on first call. Scoped to snmpd # alone: `strings` over all 34 binaries is 1.2 GB of reads in a 1 GiB guest, and # the agent is the binary that reaches the most of the library. check_eq net-snmp/no-trap-stubs "" \ "$(strings /usr/sbin/snmpd 2>/dev/null | grep signature_mismatch)" # --------------------------------------------------------------------------- # part 9: stop the agent # --------------------------------------------------------------------------- # The ticket asks for a clean exit. snmpd has no in-band shutdown for a v2c # manager -- SNMP has no verb for "quit" -- so the only stop available is a # signal, and this suite was written expecting that to fail: brintos/linux#21 # says kill(2) reports success and delivers nothing, SIGKILL included, and # 88-mosquitto and 88-lighttpd each carry a red saying exactly that. # # IT WORKS HERE. Measured 2026-08-19: snmpd is gone within the poll after a # single SIGTERM, on every run. So #21 is not the blanket "no signal is ever # delivered" its neighbours' comments imply -- whatever it covers, a SIGTERM to # a foreground snmpd is outside it. The red below still names #21, because that # is the first thing to suspect if this case ever regresses; it is a hypothesis # for a future failure, not a description of today. if agate net-snmp/sigterm-stops; then agate net-snmp/releases-port; agate net-snmp/agent-alive-before-stop agate net-snmp/sigterm-handler-ran else # ESTABLISH THAT THERE IS SOMETHING TO STOP. Without this, an agent that had # already died of its own accord somewhere above would make the stop case pass # for the opposite of the right reason -- the classic vacuous green, and the # one this whole section is exposed to because its assertion is an ABSENCE. if kill -0 "$AGENT_PID" 2>/dev/null; then bst_ok net-snmp/agent-alive-before-stop else bst_notok net-snmp/agent-alive-before-stop "the agent was already gone before SIGTERM was sent, so the stop case below proves nothing" fi kill -TERM "$AGENT_PID" 2>/dev/null _s=$(up); _s=${_s%.*}; gone=0 while :; do kill -0 "$AGENT_PID" 2>/dev/null || { gone=1; break; } _n=$(up); _n=${_n%.*} [ $((_n - _s)) -ge 15 ] && break spin done if [ "$gone" -eq 1 ]; then bst_ok net-snmp/sigterm-stops else bst_notok net-snmp/sigterm-stops "the agent survived SIGTERM for 15s -- it exits on one today, so suspect brintos/linux#21 (kill(2) reporting success and delivering nothing) before suspecting snmpd" fi # THE PROCESS BEING GONE IS NOT YET PROOF THE SIGNAL ARRIVED. snmpd's SIGTERM # handler is SnmpdShutDown(), and the first thing it does is log "Received TERM # or STOP signal... shutting down..." -- so that line in the agent's own log is # the handler saying it ran, which no other route to a dead process can forge. # Without this case the one above would be satisfied by an agent that died of # something else in the same window, and the conclusion drawn from it about # brintos/linux#21 would be an inference rather than a measurement. if grep -q "Received TERM or STOP signal" "$D/snmpd.log" 2>/dev/null; then bst_ok net-snmp/sigterm-handler-ran else bst_notok net-snmp/sigterm-handler-ran "the agent is gone but never logged SnmpdShutDown's line, so the signal is not what stopped it -- $(_bst_1line "$(tail -3 "$D/snmpd.log" 2>/dev/null)")" fi # Whether or not the process went, the port it held is what a next agent needs # back -- asserted separately so the two failures are distinguishable. if [ -r /proc/net/udp ] && awk -v p=":$PHEX" 'index($2, p) { hit = 1 } END { exit !hit }' /proc/net/udp; then bst_notok net-snmp/releases-port "udp/$PORT is still bound after the agent was asked to stop" else bst_ok net-snmp/releases-port fi fi # The agent must not have logged an error of its own while serving. A red here # with every case above green means the agent worked and complained. if agate net-snmp/no-agent-errors; then :; else check_eq net-snmp/no-agent-errors "" \ "$(_bst_1line "$(grep -i 'error\|fatal' "$D/snmpd.log" 2>/dev/null | tail -3)")" fi