. "$BST_LIB" # BST-TIMEOUT: 300 # vblade 25 -- the virtual EtherDrive (R) blade, an ATA-over-Ethernet target # from meta-networking recipes-daemons. Two programs plus its integration: # /usr/sbin/vblade (the target), /usr/sbin/vbladed (the daemonising wrapper), # /etc/vblade.conf (the export table) and /etc/init.d/vblade (the sysv script # that reads it). vblade-doc adds vblade.8. # # AoE IS A RAW LAYER-2 PROTOCOL AND THIS KERNEL HAS NO AF_PACKET. dial() in # linux.c opens socket(PF_PACKET, SOCK_RAW, htons(0x88a2)) and that is the very # first thing it does; wasm32_defconfig carries `# CONFIG_PACKET is not set`, so # the call returns EAFNOSUPPORT and vblade reports "got bad socket: Address # family not supported by protocol" and exits 1. That is brintos/linux#81, open. # Everything past the bind -- AoE discovery, the Config query, and the ATA read # and write commands -- is therefore skipped, and the skip rests on a PROBE that # confirms the refusal from two independent directions rather than on a comment: # /proc/net/packet is absent (the kernel exports no PACKET family) AND vblade's # own perror names the socket() call. vblade/probe-packet-agrees asserts the two # answers MATCH, so the day CONFIG_PACKET lands this suite takes its live arm # instead of silently going on skipping. # # WHAT IS ASSERTED FOR REAL IS EVERYTHING BEFORE THE SOCKET, AND IT IS MOST OF # THE PROGRAM. main() opens the backing file, probes its size, applies -o and -l # and validates every argument BEFORE it calls dial(), so the usage text, the # argument validation, the -m mask parse, the open path, the sector arithmetic # and the exit codes are all exercised on the real binary, on target. The # vbladed wrapper's `dirname $0` sibling resolution and $* passthrough are # asserted against a stub sibling, and the shipped vblade.conf column order is # asserted by driving the shipped init script's own parser and reading the argv # it builds -- including the pid files it writes and the stop that reaps them. # # EVERY vblade FAILURE PATH EXITS 1. vblade.8 documents no exit codes at all; # the codes asserted here are the ones in aoe.c, where every reject is exit(1) # and a dial() failure is `return 1` from main. # # THE INIT-SCRIPT CASES RUN AGAINST A COPY UNDER $TMPDIR, not the shipped paths. # The rootfs is read-only at browser runtime, and /var/log and /var/lock do not # exist on this image at all (they are populate-volatiles' job and this image # carries no sysvinit), so a test that wrote there would pass locally and fail in # a browser. The one case that runs the SHIPPED script is `start` against the # SHIPPED all-comment conf, whose empty-table branch writes nothing. require vblade vblade/present W=${TMPDIR:-/tmp}/bst-vblade.$$ rm -rf "$W" mkdir -p "$W/bin" "$W/etc" "$W/run" "$W/log" "$W/lock/subsys" || { bst_notok vblade/workdir "cannot create $W"; exit 0; } bst_ok vblade/workdir USG='shelf slot netif filename' say() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want '$2' in: $(_bst_1line "$3")" ;; esac } nosay() { case "$3" in *"$2"*) bst_notok "$1" "unexpected '$2' in: $(_bst_1line "$3")" ;; *) bst_ok "$1" ;; esac } # rc : exit status must equal . rc() { _id=$1; _want=$2; shift 2 _o=$("$@" 2>&1); _r=$? if [ "$_r" -eq "$_want" ]; then bst_ok "$_id" elif bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "rc=$_r want=$_want out:'$(_bst_1line "$_o")'"; fi } # rcsay : both the status and the message. rcsay() { _id=$1; _want=$2; _n=$3; shift 3 _o=$("$@" 2>&1); _r=$? case "$_o" in *"$_n"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_r" -eq "$_want" ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "rc=$_r want=$_want needle:'$_n' out:'$(_bst_1line "$_o")'"; fi } # ------------------------------------------------------------------ presence --- if command -v vbladed >/dev/null 2>&1; then bst_ok vblade/present-vbladed else bst_notok vblade/present-vbladed "vbladed not installed"; fi for pair in 'conf /etc/vblade.conf' 'init /etc/init.d/vblade' 'man /usr/share/man/man8/vblade.8'; do n=${pair% *}; f=${pair#* } if [ -s "$f" ]; then bst_ok "vblade/ships-$n"; else bst_notok "vblade/ships-$n" "$f missing or empty"; fi done if [ -x /etc/init.d/vblade ]; then bst_ok vblade/init-executable else bst_notok vblade/init-executable "/etc/init.d/vblade not executable"; fi say vblade/man-title '.TH vblade 8' "$(cat /usr/share/man/man8/vblade.8 2>&1)" say vblade/man-body 'virtual EtherDrive' "$(cat /usr/share/man/man8/vblade.8 2>&1)" say vblade/man-synopsis 'vblade [ -m mac[,mac...] ] shelf slot netif filename' \ "$(cat /usr/share/man/man8/vblade.8 2>&1)" # ------------------------------------------------------- fixtures (sectors) --- # 1 MiB == 2048 sectors. The 511/512-byte pair pins the sector boundary: main # divides the byte size by 512 and rejects a 0-sector export. dd if=/dev/zero of="$W/big" bs=1024 count=1024 2>/dev/null dd if=/dev/zero of="$W/s511" bs=1 count=511 2>/dev/null dd if=/dev/zero of="$W/s512" bs=1 count=512 2>/dev/null if [ "$(wc -c < "$W/big")" = 1048576 ]; then bst_ok vblade/fixture-1mib else bst_notok vblade/fixture-1mib "big is $(wc -c < "$W/big") bytes"; fi # ------------------------------------------------------ usage and validation --- # The usage line is printed to stderr by usage(), which exit(1)s. Its needle is # the tail of the format string in aoe.c, so a usage string that silently # changes shape is caught rather than merely "some output". rcsay vblade/usage-noargs 1 "$USG" vblade rcsay vblade/usage-1arg 1 "$USG" vblade 0 rcsay vblade/usage-2args 1 "$USG" vblade 0 0 rcsay vblade/usage-3args 1 "$USG" vblade 0 0 lo rcsay vblade/usage-5args 1 "$USG" vblade 0 0 lo "$W/big" extra rcsay vblade/usage-prefix 1 'usage: ' vblade rcsay vblade/usage-badopt 1 "$USG" vblade -X rcsay vblade/badopt-names-it 1 "invalid option" vblade -X rcsay vblade/reject-bufcnt-0 1 "$USG" vblade -b 0 0 0 lo "$W/big" rcsay vblade/reject-bufcnt-neg 1 "$USG" vblade -b -1 0 0 lo "$W/big" rcsay vblade/reject-offset-nan 1 "$USG" vblade -o zz 0 0 lo "$W/big" rcsay vblade/reject-offset-neg 1 "$USG" vblade -o -1 0 0 lo "$W/big" rcsay vblade/reject-length-0 1 "$USG" vblade -l 0 0 0 lo "$W/big" rcsay vblade/reject-length-nan 1 "$USG" vblade -l zz 0 0 lo "$W/big" # ----------------------------------------------------------- option parsing --- # Each accepted flag must carry the run PAST getopt and into open(2): the needle # is the open failure, and the usage text must be absent. A flag silently # rejected would print usage instead, which is what nosay pins. for pair in '-r r' '-d d' '-s s'; do f=${pair% *}; t=${pair#* } o=$(vblade "$f" 0 0 lo "$W/nosuch" 2>&1) say "vblade/accept-$t" 'open: No such file or directory' "$o" nosay "vblade/accept-$t-not-usage" "$USG" "$o" done o=$(vblade -b 8 0 0 lo "$W/nosuch" 2>&1) say vblade/accept-b 'open: No such file or directory' "$o" nosay vblade/accept-b-not-usage "$USG" "$o" o=$(vblade -o 1 0 0 lo "$W/nosuch" 2>&1) say vblade/accept-o 'open: No such file or directory' "$o" nosay vblade/accept-o-not-usage "$USG" "$o" o=$(vblade -l 1 0 0 lo "$W/nosuch" 2>&1) say vblade/accept-l 'open: No such file or directory' "$o" nosay vblade/accept-l-not-usage "$USG" "$o" # --------------------------------------------------------------- -m masks --- # setmask() splits on commas and calls parseether() on each; a mac that runs out # of characters is reported by name and skipped, the rest are kept. vblade.8 # says a mac may be given with or without colons -- parseether steps over a ':' # if it is there, so both spellings must parse silently. o=$(vblade -m zz 0 0 lo "$W/nosuch" 2>&1) say vblade/mask-bad-named 'ignoring mask zz, parseether failure' "$o" say vblade/mask-bad-goes-on 'open: No such file or directory' "$o" o=$(vblade -m 00:11:22:33:44:55 0 0 lo "$W/nosuch" 2>&1) nosay vblade/mask-colons 'parseether failure' "$o" say vblade/mask-colons-goes-on 'open: No such file or directory' "$o" o=$(vblade -m 001122334455 0 0 lo "$W/nosuch" 2>&1) nosay vblade/mask-nocolons 'parseether failure' "$o" o=$(vblade -m 00:11:22:33:44:55,66:77:88:99:aa:bb 0 0 lo "$W/nosuch" 2>&1) nosay vblade/mask-list 'parseether failure' "$o" o=$(vblade -m 00:11:22:33:44:55,zz 0 0 lo "$W/nosuch" 2>&1) say vblade/mask-list-partial 'ignoring mask zz, parseether failure' "$o" nosay vblade/mask-list-partial-only-bad 'ignoring mask 00:11:22:33:44:55' "$o" # ------------------------------------------------------- backing-file open --- # open(argv[3]) runs BEFORE the socket, so these are real assertions on this # image rather than casualties of the missing AF_PACKET. rcsay vblade/open-missing 1 'open: No such file or directory' vblade 0 0 lo "$W/nosuch" rcsay vblade/open-isdir 1 'open: Is a directory' vblade 0 0 lo "$W" # -r is the only thing that changes the open MODE (O_RDONLY instead of O_RDWR), # and a directory is the one subject that tells the two apart on this image: # EISDIR for the read-write open above, accepted for the read-only one here. nosay vblade/readonly-opens-dir 'Is a directory' "$(vblade -r 0 0 lo "$W" 2>&1)" # ---------------------------------------------------------- size arithmetic --- # getsize() falls back to fstat(2) for a regular file (BLKGETSIZE64 needs a block # device and this kernel is built CONFIG_BLOCK=n), then main divides by 512. rcsay vblade/size-511-rejected 1 '0-sector file size is too small' vblade 0 0 lo "$W/s511" rcsay vblade/size-offset-big 1 'Offset 4096 too large for 2048-sector export' \ vblade -o 4096 0 0 lo "$W/big" rcsay vblade/size-offset-equal 1 'Offset 2048 too large for 2048-sector export' \ vblade -o 2048 0 0 lo "$W/big" rcsay vblade/size-length-big 1 'too big - exceeds size of file!' \ vblade -l 99999 0 0 lo "$W/big" # ------------------------------------------------------- the AF_PACKET probe --- # Two independent answers, and the case asserts they AGREE. /proc/net/packet is # created by net/packet/af_packet.c's module init, so its presence is the # kernel's own statement about the family; "got bad socket" is vblade's perror # on the socket(2) call itself, which is the first thing dial() does. if [ -e /proc/net/packet ]; then PKT=yes; else PKT=no; fi if [ "$PKT" = no ]; then o=$(vblade 0 0 lo "$W/big" 2>&1); r=$? case "$o" in *'got bad socket'*) SOCK=refused ;; *) SOCK=other ;; esac if [ "$SOCK" = refused ] && [ "$r" -eq 1 ]; then bst_ok vblade/probe-packet-agrees else bst_notok vblade/probe-packet-agrees \ "/proc/net/packet absent but vblade rc=$r out:'$(_bst_1line "$o")'"; fi say vblade/probe-socket-eafnosupport 'Address family not supported by protocol' "$o" # A 512-byte file clears the sector probe, so this run reaches dial() too: # it is the far edge of what runs, and it fails at the socket, not the size. rcsay vblade/size-512-reaches-dial 1 'got bad socket' vblade 0 0 lo "$W/s512" # An interface name that does not exist gets the SAME message: socket(2) fails # before getindx() ever looks the name up. Asserted so the day the family # lands, this case goes red and the interface arm gets written. rcsay vblade/probe-badif-same 1 'got bad socket' vblade 0 0 nosuchif0 "$W/big" B='AF_PACKET absent: CONFIG_PACKET unset in wasm32_defconfig, socket(PF_PACKET,SOCK_RAW) = EAFNOSUPPORT (brintos/linux#81, open)' bst_skip vblade/aoe-bind "$B" bst_skip vblade/aoe-pid-line "$B" bst_skip vblade/aoe-discovery "$B" bst_skip vblade/aoe-config "$B" bst_skip vblade/aoe-read "$B" bst_skip vblade/aoe-write "$B" else # The family is live: vblade must bind, and announce the export it serves. vblade 0 0 lo "$W/big" > "$W/serve.out" 2>&1 & vbpid=$! t0=$(date +%s); while [ $(( $(date +%s) - t0 )) -lt 5 ]; do sleep 1; done o=$(cat "$W/serve.out" 2>&1) if kill -0 "$vbpid" 2>/dev/null; then bst_ok vblade/probe-packet-agrees else bst_notok vblade/probe-packet-agrees \ "/proc/net/packet present but vblade exited: $(_bst_1line "$o")"; fi say vblade/aoe-bind '2048 sectors' "$o" say vblade/aoe-pid-line 'pid ' "$o" nosay vblade/probe-socket-eafnosupport 'got bad socket' "$o" kill "$vbpid" 2>/dev/null I='AF_PACKET is available but this image carries no AoE initiator to drive the target' bst_skip vblade/aoe-discovery "$I" bst_skip vblade/aoe-config "$I" bst_skip vblade/aoe-read "$I" bst_skip vblade/aoe-write "$I" bst_skip vblade/size-512-reaches-dial "the family is live; the socket no longer bounds this run" bst_skip vblade/probe-badif-same "the family is live; getindx() now decides this case" fi # ------------------------------------------------------------------ vbladed --- # vbladed is `sh -c "`dirname $0`/vblade $* < /dev/null 2>&1 | logger -t vbladed" &` # -- it resolves its SIBLING vblade rather than searching PATH, forwards its # argument vector verbatim, and returns without waiting. The sibling resolution # and the passthrough are asserted against a stub placed next to a copy, which is # the only way to read the argv the real wrapper would have built. say vblade/vbladed-is-script '/bin/sh' "$(head -1 /usr/sbin/vbladed 2>&1)" cp /usr/sbin/vbladed "$W/bin/vbladed" 2>/dev/null && chmod +x "$W/bin/vbladed" 2>/dev/null printf '#!/bin/sh\nprintf "%%s\\n" "$*" > %s/argv\n' "$W" > "$W/bin/vblade" chmod +x "$W/bin/vblade" o=$("$W/bin/vbladed" 7 3 lo /some/file 2>&1); r=$? t0=$(date +%s); while [ $(( $(date +%s) - t0 )) -lt 3 ]; do sleep 1; done if [ "$r" -eq 0 ]; then bst_ok vblade/vbladed-returns else bst_notok vblade/vbladed-returns "rc=$r out:'$(_bst_1line "$o")'"; fi check_eq vblade/vbladed-sibling-argv '7 3 lo /some/file' "$(cat "$W/argv" 2>/dev/null)" # The real wrapper, given a target it cannot serve, still returns 0 and leaves # nothing behind -- daemonisation is the parent's contract, not the child's fate. rc vblade/vbladed-detaches 0 vbladed 0 0 lo "$W/big" t0=$(date +%s); while [ $(( $(date +%s) - t0 )) -lt 3 ]; do sleep 1; done if [ "$PKT" = no ]; then left=$(pidof vblade 2>/dev/null) if [ -z "$left" ]; then bst_ok vblade/vbladed-no-orphan else bst_notok vblade/vbladed-no-orphan "vblade still running: $left"; fi else for p in $(pidof vblade 2>/dev/null); do kill "$p" 2>/dev/null; done bst_skip vblade/vbladed-no-orphan "the family is live, so the child is expected to stay up" fi # -------------------------------------------------------- vblade.conf parse --- # The shipped conf is the export table /etc/init.d/vblade reads. Its header # documents the column order, and the init script maps those columns onto # vblade's own argument order -- which is NOT the same order. That mapping is # the thing worth asserting, so it is driven through the shipped parser. say vblade/conf-documents-columns \ 'network_device shelf slot file/disk/partition mac[,mac[,mac]]' \ "$(cat /etc/vblade.conf 2>&1)" check_eq vblade/conf-ships-no-exports 0 "$(grep -vc '^#\|^$' /etc/vblade.conf 2>/dev/null)" # The shipped script, the shipped conf: the empty-table branch, which writes # nothing and so is safe against a read-only rootfs. rcsay vblade/init-start-empty-table 0 'empty vblade.conf?' sh /etc/init.d/vblade start # A copy with every fixed root redirected under $TMPDIR, driven against a stub # named vblade (the script hardcodes prog=vblade) that records its argv and # stays alive so the pid handling has a live process to manage. sed -e "s#/etc/#$W/etc/#g" -e "s#/var/run/#$W/run/#g" \ -e "s#/var/log/#$W/log/#g" -e "s#/var/lock/#$W/lock/#g" \ /etc/init.d/vblade > "$W/vbinit" printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> %s/spawned\nsleep 25\n' "$W" > "$W/bin/vblade" chmod +x "$W/bin/vblade" initrun() { PATH="$W/bin:$PATH" sh "$W/vbinit" "$@" 2>&1; } settle() { t0=$(date +%s); while [ $(( $(date +%s) - t0 )) -lt 2 ]; do sleep 1; done; } printf '# a comment line, skipped\nlo 7 3 /tmp/disk1 00:11:22:33:44:55\nlo 8 4 /tmp/disk2\n' \ > "$W/etc/vblade.conf" : > "$W/spawned" o=$(initrun start); r=$?; settle sp=$(cat "$W/spawned" 2>/dev/null) if [ "$r" -eq 0 ]; then bst_ok vblade/init-start-rc else bst_notok vblade/init-start-rc "rc=$r out:'$(_bst_1line "$o")'"; fi # conf `netif shelf slot file mac` -> vblade `-m mac shelf slot netif file`. say vblade/conf-maps-with-mac '-m 00:11:22:33:44:55 7 3 lo /tmp/disk1' "$sp" say vblade/conf-maps-without-mac '8 4 lo /tmp/disk2' "$sp" nosay vblade/conf-skips-comments '# a comment' "$sp" check_eq vblade/conf-spawns-two 2 "$(printf '%s\n' "$sp" | grep -c 'lo /tmp/disk')" say vblade/init-reports-export 'e7.3@lo' "$o" # pid handling: one pid file per export, each holding a LIVE pid. pf=$(ls "$W/run/vblade" 2>/dev/null | tr '\n' ' ') say vblade/init-pidfile-named 'lo-e7.3.pid' "$pf" say vblade/init-pidfile-named-2 'lo-e8.4.pid' "$pf" up=0; down=0 for f in "$W/run/vblade"/*.pid; do [ -f "$f" ] || continue p=$(cat "$f" 2>/dev/null) if kill -0 "$p" 2>/dev/null; then up=$((up+1)); else down=$((down+1)); fi done check_eq vblade/init-pids-live "2 0" "$up $down" o=$(initrun stop); r=$?; settle if [ "$r" -eq 0 ]; then bst_ok vblade/init-stop-rc else bst_notok vblade/init-stop-rc "rc=$r out:'$(_bst_1line "$o")'"; fi check_eq vblade/init-stop-clears-pidfiles '' "$(ls "$W/run/vblade" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')" if [ -z "$(pidof vblade 2>/dev/null)" ]; then bst_ok vblade/init-stop-reaps else bst_notok vblade/init-stop-reaps "still running: $(pidof vblade 2>/dev/null)"; fi # A tab-separated conf line parses the same: the parser normalises tabs and # double spaces to single spaces before `read` splits it. printf 'lo\t9\t5\t/tmp/disk3\n' > "$W/etc/vblade.conf" : > "$W/spawned" initrun start >/dev/null 2>&1; settle check_eq vblade/conf-tab-separated '9 5 lo /tmp/disk3' "$(cat "$W/spawned" 2>/dev/null)" initrun stop >/dev/null 2>&1; settle # An all-comment table takes the empty branch and spawns nothing. printf '# only a comment\n' > "$W/etc/vblade.conf" : > "$W/spawned" o=$(initrun start); settle say vblade/init-empty-branch 'empty vblade.conf?' "$o" check_eq vblade/init-empty-spawns-nothing '' "$(cat "$W/spawned" 2>/dev/null)" initrun stop >/dev/null 2>&1 for p in $(pidof vblade 2>/dev/null); do kill "$p" 2>/dev/null; done rm -rf "$W"