Newer
Older
openstack-caracal-dc-dc / tests / dc-node-v6-verify / run-tests.sh
#!/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