#!/usr/bin/env bash
# tests/dc-node-v6-verify/run-tests.sh -- guard for scripts/dc-node-v6-verify.sh
# (gate G19: are the node IPv6 statics actually UP, and does a plane CARRY v6?).
#
# TWO HALVES.
# STATIC (T1-T10): the properties a silent edit would destroy -- above all the
# ABSENCE of any v6 prefix literal, which is what makes this gate survive
# D-139's ULA->GUA carve without an edit.
# BEHAVIORAL (T11-T54): plan / node / bridges are DRIVEN against fake
# maas/ip/ping/virsh on PATH and a mktemp SYSROOT. Every FAIL and every
# REFUSE branch has its own failing fixture, because a grep cannot tell a
# gate that fails from one that merely cannot fail.
#
# WHAT THE GREEN BELOW DOES NOT PROVE: no node has ever been asserted by it. This
# is a FIXTURE green; the real gate is `plan` on the region host and `node` on a
# deployed node.
#
# Exit: 0 all pass | 1 any case failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT="$(cd "$HERE/../.." && pwd)/scripts/dc-node-v6-verify.sh"
PASS=0; FAIL=0
ok(){ echo " PASS $1"; PASS=$((PASS+1)); }
no(){ echo " FAIL $1"; FAIL=$((FAIL+1)); }
fin(){ echo; echo "dc-node-v6-verify: $PASS passed, $FAIL failed"; [ "$FAIL" -eq 0 ] || exit 1; exit 0; }
[ -f "$SCRIPT" ] && ok "T1 script present" || { no "T1 script present"; fin; }
bash -n "$SCRIPT" 2>/dev/null && ok "T2 bash -n clean" || no "T2 bash -n clean"
# ===========================================================================
# STATIC
# ===========================================================================
# T3: THE constraint. D-139 retires the VR1 ULA /48 and moves every plane to
# GUA, so any prefix literal in executed code is correct today and wrong on
# carve day. Comments may DISCUSS prefixes; code may not contain one.
CODE="$(grep -vE '^[[:space:]]*#' "$SCRIPT")"
if grep -qiE '(fd[0-9a-f]{2}:[0-9a-f]{1,4}:)|(2602:[0-9a-f]{1,4}:)|fc00::' <<<"$CODE"; then
no "T3 no v6 prefix literal in executed code (D-139 rot guard)"
grep -niE '(fd[0-9a-f]{2}:[0-9a-f]{1,4}:)|(2602:[0-9a-f]{1,4}:)|fc00::' <<<"$CODE" | sed 's/^/ /'
else ok "T3 no v6 prefix literal in executed code (D-139 rot guard)"; fi
# T4: no v4 plane literal either -- the expectation comes from MAAS, not a table.
grep -qE '10\.12\.[0-9]+\.' <<<"$CODE" \
&& no "T4 no v4 plane literal in executed code" || ok "T4 no v4 plane literal in executed code"
# T5: the three-outcome contract is declared, not just implemented.
grep -q 'EXIT: 0 PASS | 1 FAIL | 2 bad args | 3 REFUSE' "$SCRIPT" \
&& ok "T5 exit contract 0/1/2/3 documented in the header" \
|| no "T5 exit contract 0/1/2/3 documented in the header"
# T6: BREVITY CHARTER -- the header is the file's own budget, so it is asserted.
HDRLEN="$(awk 'NR>1 && /^#/{n++; next} NR>1{exit} END{print n+0}' "$SCRIPT")"
[ "$HDRLEN" -le 25 ] && ok "T6 header is $HDRLEN comment lines (<=25)" \
|| no "T6 header is $HDRLEN comment lines (>25)"
# T7/T8: site + mode validation. A bare dcN is D-119-retired everywhere else in
# this repo and must not sneak in here through a permissive case arm.
for s in dc0 vr1-dc2 ""; do
bash "$SCRIPT" node "$s" x >/dev/null 2>&1; [ $? -eq 2 ] || { BAD=1; break; }
done
[ "${BAD:-0}" -eq 0 ] && ok "T7 unknown/bare site rejected with rc=2" || no "T7 unknown/bare site rejected with rc=2"
bash "$SCRIPT" verify vr1-dc0 >/dev/null 2>&1; [ $? -eq 2 ] \
&& ok "T8 unknown mode rejected with rc=2" || no "T8 unknown mode rejected with rc=2"
# T9: node must stay root-free and rack-free -- it runs on a machine that has
# neither the repo nor privileges. sudo/systemctl/virsh in the node arm would
# make a red mean "wrong host", not "wrong address".
NODEBODY="$(awk '/^do_node\(\)/{f=1} f{print} f&&/^}/{exit}' "$SCRIPT")"
grep -qE '\b(sudo|systemctl|virsh|maas)\b' <<<"$NODEBODY" \
&& no "T9 node arm touches only unprivileged read-only tools" \
|| ok "T9 node arm touches only unprivileged read-only tools"
# T10: ping BEFORE the neighbour read. Reading the cache first would report a
# stale or empty entry and invent a contradiction that does not exist.
PING_LN="$(grep -n 'ping -6 -c' <<<"$NODEBODY" | head -1 | cut -d: -f1)"
NEI_LN="$(grep -n 'neigh show to' <<<"$NODEBODY" | head -1 | cut -d: -f1)"
[ -n "$PING_LN" ] && [ -n "$NEI_LN" ] && [ "$PING_LN" -lt "$NEI_LN" ] \
&& ok "T10 ping runs before the neighbour cache is read" \
|| no "T10 ping runs before the neighbour cache is read"
# ===========================================================================
# BEHAVIORAL
# ===========================================================================
TMP="$(mktemp -d)"; FB="$TMP/bin"; SR="$TMP/sysroot"; mkdir -p "$FB" "$SR"
trap 'rm -rf "$TMP"' EXIT
# MINBIN: a PATH carrying the script's coreutils and NOTHING ELSE, so the
# "tool is absent" cases are driven for real. /usr/bin:/bin was NOT enough --
# MEASURED on this jumphost, `ip` and `virsh` are both on it, so those two
# REFUSE cases silently ran the HOST's binaries and reported a false red.
MINBIN="$TMP/minbin"; FBIP="$TMP/ipbin"; mkdir -p "$MINBIN" "$FBIP"
for t in bash env awk grep sed cat tr uname python3 mktemp; do
p="$(command -v "$t" 2>/dev/null)" && ln -sf "$p" "$MINBIN/$t"
done
cat > "$FB/maas" <<'FAKE'
#!/usr/bin/env bash
case "$(cat "$FAKEDIR/maas.mode")" in
err) echo "profile not logged in" >&2; exit 1 ;;
junk) printf 'this is not json\n' ;;
*) cat "$FAKEDIR/machines.json" ;;
esac
FAKE
cat > "$FB/ip" <<'FAKE'
#!/usr/bin/env bash
args="$*"; W="$(cat "$FAKEDIR/addr.want")"
case "$args" in
*"link show"*)
case "$(cat "$FAKEDIR/ip.link")" in
up) printf '3: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP\\ link/ether 52:54:00:aa:bb:01\n' ;;
nocarrier) printf '3: enp1s0: <BROADCAST,MULTICAST,UP> mtu 1500 state DOWN\\ link/ether 52:54:00:aa:bb:01\n' ;;
absent) echo 'Device "enp1s0" does not exist.' >&2; exit 1 ;;
esac ;;
*"addr show"*)
case "$(cat "$FAKEDIR/ip.addr")" in
ok) printf '3: enp1s0 inet6 %s scope global \\ valid_lft forever preferred_lft forever\n' "$W" ;;
missing) : ;;
tentative) printf '3: enp1s0 inet6 %s scope global tentative \\ valid_lft forever\n' "$W" ;;
dadfailed) printf '3: enp1s0 inet6 %s scope global dadfailed \\ valid_lft forever\n' "$W" ;;
extra) printf '3: enp1s0 inet6 %s scope global \\ valid_lft forever\n' "$W"
printf '3: enp1s0 inet6 %s scope global \\ valid_lft forever\n' "$(cat "$FAKEDIR/addr.extra")" ;;
esac ;;
*"neigh show"*)
s="$(cat "$FAKEDIR/ip.neigh")"
[ "$s" = absent ] || printf '%s dev enp1s0 lladdr 52:54:00:aa:bb:02 %s\n' "$(cat "$FAKEDIR/peer")" "$s" ;;
esac
FAKE
cat > "$FB/ping" <<'FAKE'
#!/usr/bin/env bash
exit "$(cat "$FAKEDIR/ping.rc")"
FAKE
cat > "$FB/virsh" <<'FAKE'
#!/usr/bin/env bash
case "$*" in
*"net-list"*) cat "$FAKEDIR/virsh.nets" ;;
*"net-dumpxml"*)
n="${@: -1}"
[ "$(cat "$FAKEDIR/virsh.xml")" = "ok" ] || exit 1
printf "<network>\n <name>%s</name>\n <bridge name='br-%s' stp='on'/>\n</network>\n" "$n" "${n##*-}" ;;
esac
FAKE
chmod +x "$FB"/*
cp "$FB/ip" "$FBIP/ip"
# Machine fixtures are GENERATED from a prefix base, so the same green case can be
# driven with the ULA carve MAAS holds today and the GUA carve D-139 rules --
# which is the evidence that no prefix is baked in anywhere.
mk_machines() { # mk_machines <base> <shape>
python3 - "$1" "$2" <<'PY' > "$TMP/machines.json"
import json, sys
base, shape = sys.argv[1], sys.argv[2]
planes = [("enp1s0", base + ":20::/64"), ("enp3s0", base + ":21::/64")]
hosts = ["fun-crab", "wise-newt", "keen-mole"]
if shape == "lonely": hosts = hosts[:1]
out = []
for n, h in enumerate(hosts):
ifs = []
for idx, (nic, cidr) in enumerate(planes):
if shape == "diverge" and h == "keen-mole" and nic == "enp3s0": continue
if shape == "empty" and h == "keen-mole": continue
links = [{"ip_address": cidr.split("::/")[0] + "::%d" % (100 + n),
"subnet": {"cidr": cidr}}]
if shape == "dup" and h == "fun-crab" and nic == "enp1s0":
links.append({"ip_address": cidr.split("::/")[0] + "::200",
"subnet": {"cidr": cidr}})
ifs.append({"name": nic, "links": links})
ifs.append({"name": "enp2s0", "links": [{"ip_address": "10.12.4.%d" % (100 + n),
"subnet": {"cidr": "10.12.4.0/22"}}]})
out.append({"hostname": h, "system_id": "sys%d" % n,
"tag_names": ([] if shape == "notag" else ["openstack-vr1-dc0"]),
"interface_set": ifs})
json.dump(out, sys.stdout)
PY
}
run_plan() { # run_plan <want_rc> <regex> <label>
local want="$1" rx="$2" label="$3" out rc
out="$(env FAKEDIR="$TMP" PATH="$FB:$PATH" MAAS_PROFILE=fake \
bash "$SCRIPT" plan vr1-dc0 2>&1)"; rc=$?
if [ "$rc" = "$want" ] && grep -qE "$rx" <<<"$out"; then ok "$label"
else no "$label (rc=$rc want=$want)"; sed 's/^/ /' <<<"$out" | head -10; fi
}
run_node() { # run_node <spec> <want_rc> <regex> <label>
local spec="$1" want="$2" rx="$3" label="$4" out rc
out="$(env FAKEDIR="$TMP" PATH="$FB:$PATH" bash "$SCRIPT" node vr1-dc0 "$spec" 2>&1)"; rc=$?
if [ "$rc" = "$want" ] && grep -qE "$rx" <<<"$out"; then ok "$label"
else no "$label (rc=$rc want=$want)"; sed 's/^/ /' <<<"$out" | head -10; fi
}
run_br() { # run_br <want_rc> <regex> <label>
local want="$1" rx="$2" label="$3" out rc
out="$(env FAKEDIR="$TMP" DC_NODE_V6_SYSROOT="$SR" PATH="$FB:$PATH" \
bash "$SCRIPT" bridges vr1-dc0 2>&1)"; rc=$?
if [ "$rc" = "$want" ] && grep -qE "$rx" <<<"$out"; then ok "$label"
else no "$label (rc=$rc want=$want)"; sed 's/^/ /' <<<"$out" | head -10; fi
}
# --- plan ------------------------------------------------------------------
printf 'ok' > "$TMP/maas.mode"
# T11 is the baseline: if the green case cannot pass, nothing below means anything.
mk_machines "fd50:840e:74e2" green
run_plan 0 'plan \(vr1-dc0\): PASS' "T11 plan: uniform carve -> PASS (ULA fixture, the carve MAAS holds today)"
run_plan 0 'fd50:840e:74e2:20::/64' "T12 plan: prints the DERIVED prefixes, so the reader sees which carve is asserted"
run_plan 0 'fun-crab enp1s0=fd50:840e:74e2:20::/64=fd50:840e:74e2:20::100=fd50:840e:74e2:20::101=wise-newt' \
"T13 plan: SPEC line carries iface/cidr/self/peer/peerhost, peer is another machine"
# T14: the whole prefix-source argument, executed. Same script, GUA fixture,
# same green -- so D-139's carve needs no edit here.
mk_machines "2602:f3e2:f02" green
run_plan 0 '2602:f3e2:f02:21::/64=2602:f3e2:f02:21::100' "T14 plan: GUA fixture is equally green (D-139-proof)"
mk_machines "fd50:840e:74e2" green
mk_machines "fd50:840e:74e2" diverge
run_plan 1 'keen-mole v6 plane set DIFFERS' "T15 plan: an under-carved machine -> FAIL"
mk_machines "fd50:840e:74e2" empty
run_plan 1 'keen-mole carries NO v6 link' "T16 plan: a machine with zero v6 links -> FAIL"
mk_machines "fd50:840e:74e2" dup
run_plan 1 'holds 2 v6 links on plane' "T17 plan: two v6 links on one plane -> FAIL (expectation is ambiguous)"
mk_machines "fd50:840e:74e2" lonely
run_plan 3 'has only 1 machine' "T18 plan: a peerless plane -> REFUSE, not a pass"
mk_machines "fd50:840e:74e2" notag
run_plan 3 'no machine carries the tag openstack-vr1-dc0' "T19 plan: empty tag selection -> REFUSE (wrong region looks like this)"
mk_machines "fd50:840e:74e2" green
printf 'err' > "$TMP/maas.mode"
run_plan 3 "maas fake machines read' failed" "T20 plan: MAAS error -> REFUSE"
printf 'junk' > "$TMP/maas.mode"
run_plan 3 'unparseable JSON' "T21 plan: unparseable MAAS answer -> REFUSE"
printf 'ok' > "$TMP/maas.mode"
OUT="$(env FAKEDIR="$TMP" PATH="$MINBIN" MAAS_PROFILE=fake bash "$SCRIPT" plan vr1-dc0 2>&1)"; RC=$?
[ "$RC" -eq 3 ] && grep -q "MISSING TOOL is not an empty MAAS" <<<"$OUT" \
&& ok "T22 plan: absent maas CLI -> REFUSE ('could not look' is not 'nothing there')" \
|| no "T22 plan: absent maas CLI -> REFUSE ('could not look' is not 'nothing there')"
OUT="$(env FAKEDIR="$TMP" PATH="$FB:$PATH" bash "$SCRIPT" plan vr1-dc0 --profile vr1-dc0-region 2>&1)"
grep -q "maas profile 'vr1-dc0-region' (from --profile)" <<<"$OUT" \
&& ok "T23 plan: names the profile and where it came from (wrong-region is silent otherwise)" \
|| no "T23 plan: names the profile and where it came from (wrong-region is silent otherwise)"
OUT="$(env FAKEDIR="$TMP" PATH="$FB:$PATH" MAAS_PROFILE=envprof bash "$SCRIPT" plan vr1-dc0 2>&1)"
grep -q "maas profile 'envprof' (from MAAS_PROFILE env)" <<<"$OUT" \
&& ok "T24 plan: honours MAAS_PROFILE and says so" || no "T24 plan: honours MAAS_PROFILE and says so"
env FAKEDIR="$TMP" PATH="$FB:$PATH" bash "$SCRIPT" plan vr1-dc0 --bogus >/dev/null 2>&1; [ $? -eq 2 ] \
&& ok "T25 plan: unknown option rejected rc=2" || no "T25 plan: unknown option rejected rc=2"
# --- node ------------------------------------------------------------------
B="fd50:840e:74e2:20"
SPEC="enp1s0=$B::/64=$B::100=$B::101=wise-newt"
printf '%s' "$B::100/64" > "$TMP/addr.want"
printf '%s' "$B::101" > "$TMP/peer"
printf '%s' "$B::999/64" > "$TMP/addr.extra"
green_node(){ printf 'up' > "$TMP/ip.link"; printf 'ok' > "$TMP/ip.addr"
printf 'REACHABLE' > "$TMP/ip.neigh"; printf '0' > "$TMP/ping.rc"; }
green_node
run_node "$SPEC" 0 'node \(vr1-dc0\): PASS' "T26 node: address up + peer replies + neighbour REACHABLE -> PASS"
run_node "$SPEC" 0 'sole global on the NIC' "T27 node: reports the address as the SOLE global on the NIC"
printf 'missing' > "$TMP/ip.addr"
run_node "$SPEC" 1 'MAAS holds .* but the NIC does not carry it' \
"T28 node: MAAS holds the link, the NIC does not -> FAIL (the recorded lesson, executed)"
printf 'tentative' > "$TMP/ip.addr"
run_node "$SPEC" 1 'still TENTATIVE' "T29 node: DAD incomplete -> FAIL"
printf 'dadfailed' > "$TMP/ip.addr"
run_node "$SPEC" 1 'DADFAILED' "T30 node: duplicate address -> FAIL"
printf 'extra' > "$TMP/ip.addr"
run_node "$SPEC" 1 'unexpected global' "T31 node: an EXTRA global on the plane -> FAIL (the stale-prefix signature)"
green_node
printf 'absent' > "$TMP/ip.link"
run_node "$SPEC" 1 'interface ABSENT on this node' "T32 node: interface absent -> FAIL"
printf 'nocarrier' > "$TMP/ip.link"
run_node "$SPEC" 1 'no LOWER_UP' "T33 node: no carrier -> FAIL (an address on a dead link carries nothing)"
green_node
# T34-T37: the four (ping x neighbour) quadrants. This is the whole point of
# assertion (2) -- each quadrant must name a DIFFERENT next step.
printf '1' > "$TMP/ping.rc"; printf 'INCOMPLETE' > "$TMP/ip.neigh"
run_node "$SPEC" 1 'Neighbour Discovery did not resolve' \
"T34 node: no reply + INCOMPLETE -> FAIL naming ND, and points at the bridges diagnostic"
printf 'FAILED' > "$TMP/ip.neigh"
run_node "$SPEC" 1 'bridges vr1-dc0' "T35 node: no reply + FAILED -> FAIL with the same triage step"
printf 'REACHABLE' > "$TMP/ip.neigh"
run_node "$SPEC" 1 'NOT the multicast-ND class' \
"T36 node: no reply but neighbour RESOLVED -> FAIL, and explicitly NOT diagnosed as ND"
printf '0' > "$TMP/ping.rc"; printf 'INCOMPLETE' > "$TMP/ip.neigh"
run_node "$SPEC" 1 'CONTRADICTION' \
"T37 node: replies YET neighbour INCOMPLETE -> FAIL (a success one reading denies)"
printf 'absent' > "$TMP/ip.neigh"
run_node "$SPEC" 1 'CONTRADICTION' "T38 node: replies with NO neighbour entry at all -> FAIL"
green_node
printf 'WOBBLY' > "$TMP/ip.neigh"
run_node "$SPEC" 3 'is not one this gate recognises' "T39 node: unrecognised neighbour state -> REFUSE"
green_node
printf '2' > "$TMP/ping.rc"
run_node "$SPEC" 3 'the probe could not run' "T40 node: ping could not run -> REFUSE, never a pass"
green_node
run_node "enp1s0=$B::/64=$B::100=-=-" 3 'the data path cannot be evaluated' \
"T41 node: peerless plane -> REFUSE"
run_node "enp1s0=$B::/64" 2 'malformed SPEC entry' "T42 node: malformed SPEC -> rc=2"
env FAKEDIR="$TMP" PATH="$FB:$PATH" bash "$SCRIPT" node vr1-dc0 >/dev/null 2>&1; [ $? -eq 2 ] \
&& ok "T43 node: missing SPEC -> rc=2" || no "T43 node: missing SPEC -> rc=2"
OUT="$(env FAKEDIR="$TMP" PATH="$MINBIN" bash "$SCRIPT" node vr1-dc0 "$SPEC" 2>&1)"; RC=$?
[ "$RC" -eq 3 ] && grep -q "no 'ip' on this node" <<<"$OUT" \
&& ok "T44 node: absent ip(8) -> REFUSE" || no "T44 node: absent ip(8) -> REFUSE"
# T44b: ip present, ping absent. The address half must still be ASSERTED and only
# the reachability half REFUSE -- a missing prober must not blank the whole gate.
OUT="$(env FAKEDIR="$TMP" PATH="$FBIP:$MINBIN" bash "$SCRIPT" node vr1-dc0 "$SPEC" 2>&1)"; RC=$?
[ "$RC" -eq 3 ] && grep -q "reachability to .* is UNKNOWN, not absent" <<<"$OUT" \
&& grep -q "OK enp1s0" <<<"$OUT" \
&& ok "T44b node: absent ping -> address still asserted, reachability REFUSEs" \
|| no "T44b node: absent ping -> address still asserted, reachability REFUSEs"
# T45: a multi-plane SPEC must evaluate EVERY entry, not stop at the first.
printf 'missing' > "$TMP/ip.addr"
run_node "$SPEC,enp3s0=$B::/64=$B::100=$B::101=wise-newt" 1 \
'(FAIL.*does not carry it[[:space:]]*$|does not carry it)' "T45 node: every SPEC entry is evaluated"
OUT="$(env FAKEDIR="$TMP" PATH="$FB:$PATH" bash "$SCRIPT" node vr1-dc0 \
"$SPEC,enp3s0=$B::/64=$B::100=$B::101=wise-newt" 2>&1)"
[ "$(grep -c 'does not carry it' <<<"$OUT")" -eq 2 ] \
&& ok "T46 node: both entries reported, not just the first" || no "T46 node: both entries reported, not just the first"
green_node
# --- bridges ---------------------------------------------------------------
printf 'ok' > "$TMP/virsh.xml"
printf 'vr1-dc0-metal-admin\nvr1-dc0-storage\nvr0-dc0-other\n' > "$TMP/virsh.nets"
for b in admin storage; do mkdir -p "$SR/sys/class/net/br-$b/bridge"; done
printf '1\n' > "$SR/sys/class/net/br-admin/bridge/multicast_snooping"
printf '0\n' > "$SR/sys/class/net/br-admin/bridge/multicast_querier"
printf '1\n' > "$SR/sys/class/net/br-storage/bridge/multicast_snooping"
printf '1\n' > "$SR/sys/class/net/br-storage/bridge/multicast_querier"
run_br 0 'reported 2 bridge' "T47 bridges: reads the plane bridges, ignores other sites -> exit 0"
run_br 0 'br-admin +multicast_snooping=1 multicast_querier=0 +<-- SUSPECT' \
"T48 bridges: flags snooping=1/querier=0 as SUSPECT"
run_br 0 'br-storage +multicast_snooping=1 multicast_querier=1$' \
"T49 bridges: does NOT flag a bridge that has a querier"
run_br 0 'no Linux behaviour is asserted here' "T50 bridges: states plainly that it diagnoses nothing"
rm -f "$SR/sys/class/net/br-storage/bridge/multicast_querier"
run_br 3 'multicast state unreadable' "T51 bridges: unreadable sysfs -> REFUSE, never a silent 'fine'"
printf '1\n' > "$SR/sys/class/net/br-storage/bridge/multicast_querier"
printf 'vr0-dc0-other\n' > "$TMP/virsh.nets"
run_br 3 'no libvirt network named vr1-dc0-' "T52 bridges: no plane network -> REFUSE (UNKNOWN, not absent)"
printf 'vr1-dc0-metal-admin\n' > "$TMP/virsh.nets"
printf 'bad' > "$TMP/virsh.xml"
run_br 3 'did not resolve to a bridge' "T53 bridges: net with no bridge -> REFUSE"
printf 'ok' > "$TMP/virsh.xml"
OUT="$(env FAKEDIR="$TMP" DC_NODE_V6_SYSROOT="$SR" PATH="$MINBIN" bash "$SCRIPT" bridges vr1-dc0 2>&1)"; RC=$?
[ "$RC" -eq 3 ] && grep -q "no 'virsh' on this host" <<<"$OUT" \
&& ok "T54 bridges: absent virsh -> REFUSE" || no "T54 bridges: absent virsh -> REFUSE"
fin