mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
Make the interop harness report link loss and record what it built
build-images.sh resolved each ref with a bare rev-parse, so an annotated tag recorded its tag object's SHA in refs.env and the image label: v0.5.1 showed as46601931rather than the commitc5aeef39that was built. Peel each ref to its commit. The harness asserted connectivity only in its ping phases. Phase 7, the five-minute mesh-size warmup, pinged nothing, so a link lost and re-established there passed the run; the v0.5.2 gate's three link losses all fell in it and were found only by reading the logs. Phase 7 now checks on every poll round, warmup included, that each node still lists all its direct peers, and attributes a lost link to its pair in the failure list. An isolated unreadable peer-list round is counted and tolerated; two in a row, or an unreadable final round, fail the node. The global log checks move to a final phase so they cover the whole run, and the generated config sets handlers::mmp to debug so their "MMP link teardown" pattern can match. The Phase 6 per-pair summary now says the pair was healthy through Phase 6, which is what it observed. Builds before v0.5.2 log a link rekey responder's self-cutover with the same "Rekey cutover complete (initiator)" line an initiator logs, so in a mixed mesh the cutover count cannot show direction. None of its uses needs direction; say so where it is counted and reported, instead of labelling it an initiator count.
This commit is contained in:
@@ -123,7 +123,8 @@ per node-spec into `generated-configs/docker-compose.generated.yml`.
|
|||||||
with its ref + short SHA.
|
with its ref + short SHA.
|
||||||
5. Remove the temp worktree (done per-ref so peak disk stays at one worktree).
|
5. Remove the temp worktree (done per-ref so peak disk stays at one worktree).
|
||||||
|
|
||||||
It also writes `.build/refs.env`, recording each slot's ref and SHA. The
|
It also writes `.build/refs.env`, recording each slot's ref and the short SHA
|
||||||
|
of the commit it resolves to (an annotated tag is peeled to its commit). The
|
||||||
driver reads it to know which pairs are mixed-version. (If absent, it falls
|
driver reads it to know which pairs are mixed-version. (If absent, it falls
|
||||||
back to the image labels.)
|
back to the image labels.)
|
||||||
|
|
||||||
@@ -244,19 +245,20 @@ working copy at all.
|
|||||||
|
|
||||||
## How to read the output
|
## How to read the output
|
||||||
|
|
||||||
The driver runs eight phases (0 to 7), plus 1b and 5b when data-plane
|
The driver runs nine phases (0 to 8), plus 1b and 5b when data-plane
|
||||||
streams are on:
|
streams are on:
|
||||||
|
|
||||||
| Phase | Check |
|
| Phase | Check |
|
||||||
| ----- | ---------------------------------------------------------------- |
|
| ----- | ---------------------------------------------------------------- |
|
||||||
| 0 | Bring up the mesh (+ optional netem). |
|
| 0 | Bring up the mesh (+ optional netem). |
|
||||||
| 1 | All nodes reach N-1 authenticated peers; all directed pairs ping over `fips0` (the definitive FSP-session check). |
|
| 1 | All nodes reach N-1 authenticated peers; all directed pairs ping over `fips0` (the definitive FSP-session check). |
|
||||||
| 2 | First FMP rekey cutover completes within the timeout. |
|
| 2 | First FMP rekey cutover completes within the timeout (the count is role-blind: builds before v0.5.2 log a responder's cutover with the same line). |
|
||||||
| 3 | All pairs still ping after the first rekey. |
|
| 3 | All pairs still ping after the first rekey. |
|
||||||
| 4 | Wait out a second rekey cycle. |
|
| 4 | Wait out a second rekey cycle. |
|
||||||
| 5 | All pairs still ping after the second rekey. |
|
| 5 | All pairs still ping after the second rekey. |
|
||||||
| 6 | Per-node / per-pair interop log analysis. |
|
| 6 | Per-node / per-pair interop log analysis. |
|
||||||
| 7 | Every node's mesh-size estimate within ±25% of N after warmup. |
|
| 7 | Every node's mesh-size estimate within ±25% of N after warmup, and every node lists all its direct peers in every poll round, warmup included (one isolated round in which a node cannot be asked is tolerated). |
|
||||||
|
| 8 | Whole-run log health (the global negative checks below). |
|
||||||
|
|
||||||
When data-plane streams are on (`--topology`, or `FIPS_INTEROP_STREAMS`),
|
When data-plane streams are on (`--topology`, or `FIPS_INTEROP_STREAMS`),
|
||||||
Phase 1b measures stream loss over a quiet control window and Phase 5b
|
Phase 1b measures stream loss over a quiet control window and Phase 5b
|
||||||
@@ -270,12 +272,16 @@ many reps Phase 5b abstained and in how many it re-measured.
|
|||||||
|
|
||||||
Phase 6 is the interop-specific part. It reports:
|
Phase 6 is the interop-specific part. It reports:
|
||||||
|
|
||||||
- **Global health** — panics, `ERROR` lines, `unknown FMP version` drops,
|
|
||||||
link teardowns, decrypt failures, handshake failures, rekey-msg2 failures.
|
|
||||||
Any non-zero count is broken down per node, attributed to a specific build.
|
|
||||||
- **Rekey machinery exercised** — both FMP and FSP rekey cutovers fired.
|
- **Rekey machinery exercised** — both FMP and FSP rekey cutovers fired.
|
||||||
- **Per-pair interop summary** — each unordered pair, classified
|
- **Per-pair interop summary** — each unordered pair, classified
|
||||||
same-version vs MIXED, with whether it stayed healthy through the run.
|
same-version vs MIXED, with whether it stayed healthy through Phase 6.
|
||||||
|
|
||||||
|
Phase 8 runs the global health checks last, so they cover the whole run,
|
||||||
|
the Phase 7 warmup included: panics, `ERROR` lines, `unknown FMP version`
|
||||||
|
drops, link teardowns (`MMP link teardown`, which the generated config
|
||||||
|
makes visible by setting `fips::node::handlers::mmp` to debug), decrypt
|
||||||
|
failures, handshake failures, rekey-msg2 failures. Any non-zero count is
|
||||||
|
broken down per node, attributed to a specific build.
|
||||||
|
|
||||||
The final verdict lists every failure attributed to a specific
|
The final verdict lists every failure attributed to a specific
|
||||||
`x[ref@sha] <-> y[ref@sha]` pair or build, then states the attribution:
|
`x[ref@sha] <-> y[ref@sha]` pair or build, then states the attribution:
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ build_one() {
|
|||||||
local slot="$1"
|
local slot="$1"
|
||||||
local ref="$2"
|
local ref="$2"
|
||||||
local sha
|
local sha
|
||||||
sha="$(git -C "$REPO_ROOT" rev-parse --short "$ref")"
|
# Peel to the commit: an annotated tag's own name resolves to the tag
|
||||||
|
# object, which is not what was built.
|
||||||
|
sha="$(git -C "$REPO_ROOT" rev-parse --short "${ref}^{commit}")"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Building slot '$slot' ref='$ref' sha=$sha ==="
|
echo "=== Building slot '$slot' ref='$ref' sha=$sha ==="
|
||||||
@@ -224,7 +226,7 @@ MANIFEST="$WORK_BASE/refs.env"
|
|||||||
for i in 0 1 2; do
|
for i in 0 1 2; do
|
||||||
slot="${SLOTS[$i]}"
|
slot="${SLOTS[$i]}"
|
||||||
ref="${REFS[$i]}"
|
ref="${REFS[$i]}"
|
||||||
sha="$(git -C "$REPO_ROOT" rev-parse --short "$ref")"
|
sha="$(git -C "$REPO_ROOT" rev-parse --short "${ref}^{commit}")"
|
||||||
upper="$(echo "$slot" | tr '[:lower:]' '[:upper:]')"
|
upper="$(echo "$slot" | tr '[:lower:]' '[:upper:]')"
|
||||||
echo "INTEROP_REF_${upper}=$ref"
|
echo "INTEROP_REF_${upper}=$ref"
|
||||||
echo "INTEROP_SHA_${upper}=$sha"
|
echo "INTEROP_SHA_${upper}=$sha"
|
||||||
|
|||||||
@@ -307,7 +307,10 @@ COMPOSE_FILE="$OUT_DIR/docker-compose.generated.yml"
|
|||||||
echo " - net.ipv6.conf.all.disable_ipv6=0"
|
echo " - net.ipv6.conf.all.disable_ipv6=0"
|
||||||
echo " restart: \"no\""
|
echo " restart: \"no\""
|
||||||
echo " environment:"
|
echo " environment:"
|
||||||
echo " - RUST_LOG=info,fips::node::handlers::rekey=debug,fips::node::handlers::handshake=debug"
|
# handlers::mmp at debug so the driver's "MMP link teardown" scan, which
|
||||||
|
# catches a peer removed at any point of the run, can match: the line is
|
||||||
|
# a debug! in that module.
|
||||||
|
echo " - RUST_LOG=info,fips::node::handlers::rekey=debug,fips::node::handlers::handshake=debug,fips::node::handlers::mmp=debug"
|
||||||
echo ""
|
echo ""
|
||||||
echo "services:"
|
echo "services:"
|
||||||
for nid in "${NODE_IDS[@]}"; do
|
for nid in "${NODE_IDS[@]}"; do
|
||||||
|
|||||||
+186
-61
@@ -41,6 +41,11 @@
|
|||||||
# Phase 5b), and MESH-SIZE estimate convergence across versions
|
# Phase 5b), and MESH-SIZE estimate convergence across versions
|
||||||
# (Phase 7).
|
# (Phase 7).
|
||||||
#
|
#
|
||||||
|
# A link lost at any point of a run turns it red: the ping phases cover
|
||||||
|
# Phases 1 to 5, Phase 7 checks every node's direct peers on each poll
|
||||||
|
# round of its warmup and settle loops, and Phase 8 scans the whole run's
|
||||||
|
# logs for peer removals and the other global failure signatures.
|
||||||
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# FIPS_INTEROP_NETEM tc-netem arg string applied to every container's
|
# FIPS_INTEROP_NETEM tc-netem arg string applied to every container's
|
||||||
# eth0, e.g. "delay 10ms 5ms 25% loss 1%". Unset =
|
# eth0, e.g. "delay 10ms 5ms 25% loss 1%". Unset =
|
||||||
@@ -537,7 +542,14 @@ count_log_pattern() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Count completed FMP initiator rekey cutovers across all node logs.
|
# Count completed FMP rekey cutovers across all node logs.
|
||||||
|
#
|
||||||
|
# The line says "(initiator)", but it does not show direction in a mixed
|
||||||
|
# mesh: builds before v0.5.2 also log it as a responder, on the self-cutover
|
||||||
|
# one tick after sending msg2. So this counts cutovers of either role, and
|
||||||
|
# every caller reports it as a role-blind count. None needs direction: the
|
||||||
|
# control window wants any cutover, and the rekey phases want proof that
|
||||||
|
# rekeys completed.
|
||||||
#
|
#
|
||||||
# The pattern is a literal argument so the log-string guard can check it
|
# The pattern is a literal argument so the log-string guard can check it
|
||||||
# against the daemon source. Prints the count, or `unreadable:<ctr>` with
|
# against the daemon source. Prints the count, or `unreadable:<ctr>` with
|
||||||
@@ -597,6 +609,114 @@ mesh_estimate() {
|
|||||||
|| echo null
|
|| echo null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The direct neighbours <node> does not list in `fipsctl show peers`, as
|
||||||
|
# space-separated node ids (empty when all are listed). Prints `unreadable`
|
||||||
|
# with status 1 when the node cannot be asked or its answer cannot be
|
||||||
|
# parsed, so a dead reader is never taken for a full peer list.
|
||||||
|
missing_peers() {
|
||||||
|
local n="$1" out listed m missing=""
|
||||||
|
if ! out="$(docker exec "${CONTAINER[$n]}" fipsctl show peers 2>/dev/null)" \
|
||||||
|
|| ! listed="$(python3 -c 'import sys,json; print(" ".join(p["npub"] for p in json.load(sys.stdin)["peers"]))' <<<"$out" 2>/dev/null)"; then
|
||||||
|
echo "unreadable"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for m in "${NODES[@]}"; do
|
||||||
|
pair_is_direct "$n" "$m" || continue
|
||||||
|
case " $listed " in
|
||||||
|
*" ${NPUB_OF[$m]} "*) ;;
|
||||||
|
*) missing+=" $m" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo "${missing# }"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# One link-continuity round over every node: record each direct neighbour
|
||||||
|
# a node has stopped listing, and each node that could not be asked. A
|
||||||
|
# peer drops out of `show peers` only when its link is removed (a stale
|
||||||
|
# but live link stays listed), so a miss here is a lost link, not loss
|
||||||
|
# noise. Phase 7 runs no pings, so without this a link lost during its
|
||||||
|
# warmup would pass the run.
|
||||||
|
#
|
||||||
|
# A node that cannot be asked is a different matter: one failed
|
||||||
|
# `docker exec` says nothing about its links. An isolated unreadable round
|
||||||
|
# is counted and reported but tolerated; two in a row, or an unreadable
|
||||||
|
# final round, fail the node, because then the harness has stopped
|
||||||
|
# observing it. A link lost and re-established inside the unread gap
|
||||||
|
# still leaves an "MMP link teardown" line for Phase 8 to find.
|
||||||
|
declare -A LINK_LOST_AT LINK_LOST_ROUNDS LINK_UNREADABLE LINK_UNREAD_RUN LINK_UNREAD_WORST
|
||||||
|
LINK_ROUNDS=0
|
||||||
|
check_links_round() {
|
||||||
|
local n m missing at=$(( SECONDS - MESH_UP_AT ))
|
||||||
|
LINK_ROUNDS=$((LINK_ROUNDS + 1))
|
||||||
|
for n in "${NODES[@]}"; do
|
||||||
|
if ! missing="$(missing_peers "$n")"; then
|
||||||
|
LINK_UNREADABLE[$n]=$(( ${LINK_UNREADABLE[$n]:-0} + 1 ))
|
||||||
|
LINK_UNREAD_RUN[$n]=$(( ${LINK_UNREAD_RUN[$n]:-0} + 1 ))
|
||||||
|
if [ "${LINK_UNREAD_RUN[$n]}" -gt "${LINK_UNREAD_WORST[$n]:-0}" ]; then
|
||||||
|
LINK_UNREAD_WORST[$n]="${LINK_UNREAD_RUN[$n]}"
|
||||||
|
fi
|
||||||
|
echo " UNREADABLE $n: show peers could not be read (${at}s after mesh start)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
LINK_UNREAD_RUN[$n]=0
|
||||||
|
for m in $missing; do
|
||||||
|
if [ -z "${LINK_LOST_AT[$n|$m]:-}" ]; then
|
||||||
|
LINK_LOST_AT[$n|$m]="$at"
|
||||||
|
echo " LINK LOST $n no longer lists direct peer $m (${at}s after mesh start)"
|
||||||
|
fi
|
||||||
|
LINK_LOST_ROUNDS[$n|$m]=$(( ${LINK_LOST_ROUNDS[$n|$m]:-0} + 1 ))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global negative checks — these must be zero on EVERY node regardless
|
||||||
|
# of version pairing. A non-zero count is attributed to the node and,
|
||||||
|
# where the count is asymmetric across versions, flagged as interop.
|
||||||
|
# Every pattern must be live at the level generate-configs.sh sets.
|
||||||
|
declare -A GLOBAL_PATTERNS=(
|
||||||
|
["PANIC|panicked"]="panics"
|
||||||
|
["ERROR"]="error-level log lines"
|
||||||
|
["unknown FMP version|Unknown FMP version"]="unknown-FMP-version drops"
|
||||||
|
["MMP link teardown"]="MMP link teardowns (a peer removed)"
|
||||||
|
["Excessive decryption failures"]="excessive-decryption-failure removals"
|
||||||
|
["Session AEAD decryption failed"]="FSP AEAD decrypt failures"
|
||||||
|
["Rekey msg2 processing failed"]="rekey msg2 failures"
|
||||||
|
["Handshake failed|handshake failed"]="handshake failures"
|
||||||
|
)
|
||||||
|
|
||||||
|
scan_global_patterns() {
|
||||||
|
local pat desc total n c s u
|
||||||
|
for pat in "${!GLOBAL_PATTERNS[@]}"; do
|
||||||
|
desc="${GLOBAL_PATTERNS[$pat]}"
|
||||||
|
if ! total="$(count_log_pattern "$pat")"; then
|
||||||
|
echo " FAIL $desc: node logs unreadable ($total), zero not established"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
INTEROP_FAILURES+=("[log] $desc: node logs unreadable ($total)")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$total" -eq 0 ]; then
|
||||||
|
echo " PASS $desc: 0"
|
||||||
|
PASSED=$((PASSED + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL $desc: $total (expected 0)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
# Per-node breakdown so the count can be attributed to a build.
|
||||||
|
for n in "${NODES[@]}"; do
|
||||||
|
c="$(count_node_pattern "$n" "$pat")"
|
||||||
|
if [ "$c" -gt 0 ]; then
|
||||||
|
s="${SLOT_OF[$n]}"
|
||||||
|
u="$(echo "$s" | tr '[:lower:]' '[:upper:]')"
|
||||||
|
echo " $n [$u ${SLOT_REF[$s]}@${SLOT_SHA[$s]}]: $c"
|
||||||
|
INTEROP_FAILURES+=("[log] node $n ($u ${SLOT_REF[$s]}@${SLOT_SHA[$s]}): $c x '$desc'")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ── Data-plane continuity streams ────────────────────────────────────
|
# ── Data-plane continuity streams ────────────────────────────────────
|
||||||
#
|
#
|
||||||
# A sustained ping6 stream over the overlay (<npub>.fips) is data-plane
|
# A sustained ping6 stream over the overlay (<npub>.fips) is data-plane
|
||||||
@@ -876,7 +996,7 @@ if ! fmp_cutovers="$(count_log_pattern 'Rekey cutover complete \(initiator\), K-
|
|||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
INTEROP_FAILURES+=("[log] FMP rekey cutovers: node logs unreadable ($fmp_cutovers)")
|
INTEROP_FAILURES+=("[log] FMP rekey cutovers: node logs unreadable ($fmp_cutovers)")
|
||||||
elif [ "$fmp_cutovers" -ge 1 ]; then
|
elif [ "$fmp_cutovers" -ge 1 ]; then
|
||||||
echo " PASS FMP rekey initiator cutovers: $fmp_cutovers"
|
echo " PASS FMP rekey cutovers (either role; see fmp_cutover_count): $fmp_cutovers"
|
||||||
PASSED=$((PASSED + 1))
|
PASSED=$((PASSED + 1))
|
||||||
else
|
else
|
||||||
echo " FAIL no FMP rekey cutover observed within ${FIRST_REKEY_TIMEOUT}s"
|
echo " FAIL no FMP rekey cutover observed within ${FIRST_REKEY_TIMEOUT}s"
|
||||||
@@ -983,49 +1103,9 @@ PASSED=0; FAILED=0
|
|||||||
# at least one cutover before the final assertions.
|
# at least one cutover before the final assertions.
|
||||||
wait_for_log_pattern_count "FSP rekey cutover complete" 1 "$REKEY_SETTLE" || true
|
wait_for_log_pattern_count "FSP rekey cutover complete" 1 "$REKEY_SETTLE" || true
|
||||||
|
|
||||||
# Global negative checks — these must be zero on EVERY node regardless
|
# The global negative checks (panics, errors, link teardowns, ...) run
|
||||||
# of version pairing. A non-zero count is attributed to the node and,
|
# after Phase 7 instead, so they cover the whole run including the
|
||||||
# where the count is asymmetric across versions, flagged as interop.
|
# mesh-size warmup; see Phase 8.
|
||||||
echo ""
|
|
||||||
echo " -- Global health (all $NUM_NODES nodes) --"
|
|
||||||
|
|
||||||
declare -A GLOBAL_PATTERNS=(
|
|
||||||
["PANIC|panicked"]="panics"
|
|
||||||
["ERROR"]="error-level log lines"
|
|
||||||
["unknown FMP version|Unknown FMP version"]="unknown-FMP-version drops"
|
|
||||||
["MMP link teardown"]="MMP link teardowns"
|
|
||||||
["Excessive decryption failures"]="excessive-decryption-failure removals"
|
|
||||||
["Session AEAD decryption failed"]="FSP AEAD decrypt failures"
|
|
||||||
["Rekey msg2 processing failed"]="rekey msg2 failures"
|
|
||||||
["Handshake failed|handshake failed"]="handshake failures"
|
|
||||||
)
|
|
||||||
|
|
||||||
for pat in "${!GLOBAL_PATTERNS[@]}"; do
|
|
||||||
desc="${GLOBAL_PATTERNS[$pat]}"
|
|
||||||
if ! total="$(count_log_pattern "$pat")"; then
|
|
||||||
echo " FAIL $desc: node logs unreadable ($total), zero not established"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
INTEROP_FAILURES+=("[log] $desc: node logs unreadable ($total)")
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [ "$total" -eq 0 ]; then
|
|
||||||
echo " PASS $desc: 0"
|
|
||||||
PASSED=$((PASSED + 1))
|
|
||||||
else
|
|
||||||
echo " FAIL $desc: $total (expected 0)"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
# Per-node breakdown so the count can be attributed to a build.
|
|
||||||
for n in "${NODES[@]}"; do
|
|
||||||
c="$(count_node_pattern "$n" "$pat")"
|
|
||||||
if [ "$c" -gt 0 ]; then
|
|
||||||
s="${SLOT_OF[$n]}"
|
|
||||||
u="$(echo "$s" | tr '[:lower:]' '[:upper:]')"
|
|
||||||
echo " $n [$u ${SLOT_REF[$s]}@${SLOT_SHA[$s]}]: $c"
|
|
||||||
INTEROP_FAILURES+=("[log] node $n ($u ${SLOT_REF[$s]}@${SLOT_SHA[$s]}): $c x '$desc'")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Positive checks — the rekey machinery actually exercised both layers.
|
# Positive checks — the rekey machinery actually exercised both layers.
|
||||||
echo ""
|
echo ""
|
||||||
@@ -1036,7 +1116,7 @@ if ! fmp_total="$(count_log_pattern 'Rekey cutover complete \(initiator\), K-bit
|
|||||||
echo " FAIL FMP rekey cutovers: node logs unreadable ($fmp_total), not established"
|
echo " FAIL FMP rekey cutovers: node logs unreadable ($fmp_total), not established"
|
||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
elif [ "$fmp_total" -ge 1 ]; then
|
elif [ "$fmp_total" -ge 1 ]; then
|
||||||
echo " PASS FMP rekey cutovers across mesh: $fmp_total"
|
echo " PASS FMP rekey cutovers across mesh (either role): $fmp_total"
|
||||||
PASSED=$((PASSED + 1))
|
PASSED=$((PASSED + 1))
|
||||||
else
|
else
|
||||||
echo " FAIL FMP rekey cutovers: $fmp_total (expected >= 1)"
|
echo " FAIL FMP rekey cutovers: $fmp_total (expected >= 1)"
|
||||||
@@ -1056,8 +1136,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Per-pair summary: classify each unordered pair and report whether it
|
# Per-pair summary: classify each unordered pair and report whether it
|
||||||
# stayed healthy through the run. "Healthy" = no connectivity failure
|
# stayed healthy through Phases 1 to 6. "Healthy" = no connectivity
|
||||||
# recorded for either direction of the pair.
|
# failure recorded for either direction of the pair. Phase 7's link
|
||||||
|
# checks and Phase 8's log scan run later and record their own failures.
|
||||||
echo ""
|
echo ""
|
||||||
echo " -- Per-pair interop summary --"
|
echo " -- Per-pair interop summary --"
|
||||||
for p in "${PAIRS[@]}"; do
|
for p in "${PAIRS[@]}"; do
|
||||||
@@ -1074,10 +1155,10 @@ for p in "${PAIRS[@]}"; do
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
if [ "$pair_failed" -eq 0 ]; then
|
if [ "$pair_failed" -eq 0 ]; then
|
||||||
echo " PASS $kind pair $label: stayed healthy"
|
echo " PASS $kind pair $label: healthy through Phase 6"
|
||||||
PASSED=$((PASSED + 1))
|
PASSED=$((PASSED + 1))
|
||||||
else
|
else
|
||||||
echo " FAIL $kind pair $label: connectivity failed during the run"
|
echo " FAIL $kind pair $label: connectivity failed by Phase 6"
|
||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -1109,21 +1190,28 @@ declare -A MS_EST MS_OK MS_INBAND_SINCE
|
|||||||
ms_accept_after=$(( MESH_UP_AT + MESH_SIZE_WARMUP ))
|
ms_accept_after=$(( MESH_UP_AT + MESH_SIZE_WARMUP ))
|
||||||
echo " band [$ms_lo, $ms_hi], warmup ends $(( ms_accept_after - SECONDS ))s from now, then ${MESH_SIZE_SETTLE}s settled, poll up to ${MESH_SIZE_TIMEOUT}s beyond that"
|
echo " band [$ms_lo, $ms_hi], warmup ends $(( ms_accept_after - SECONDS ))s from now, then ${MESH_SIZE_SETTLE}s settled, poll up to ${MESH_SIZE_TIMEOUT}s beyond that"
|
||||||
|
|
||||||
# Poll through the warmup as well. Nothing here is asserted on — it is
|
# Poll through the warmup as well. The estimates are not asserted on here —
|
||||||
# the trajectory the phase has never recorded, and it is what an
|
# they are the trajectory the phase has never recorded, and what an
|
||||||
# undercount that never recovers would show up in.
|
# undercount that never recovers would show up in — but every round
|
||||||
|
# checks that each node still lists all its direct peers.
|
||||||
|
ms_next_print=$SECONDS
|
||||||
while [ "$SECONDS" -lt "$ms_accept_after" ]; do
|
while [ "$SECONDS" -lt "$ms_accept_after" ]; do
|
||||||
ms_line=""
|
check_links_round
|
||||||
for n in "${NODES[@]}"; do
|
if [ "$SECONDS" -ge "$ms_next_print" ]; then
|
||||||
MS_EST[$n]="$(mesh_estimate "$n")"
|
ms_line=""
|
||||||
ms_line+=" $n=${MS_EST[$n]}"
|
for n in "${NODES[@]}"; do
|
||||||
done
|
MS_EST[$n]="$(mesh_estimate "$n")"
|
||||||
echo " warmup, $(( ms_accept_after - SECONDS ))s to go:$ms_line"
|
ms_line+=" $n=${MS_EST[$n]}"
|
||||||
sleep 30
|
done
|
||||||
|
echo " warmup, $(( ms_accept_after - SECONDS ))s to go:$ms_line"
|
||||||
|
ms_next_print=$(( SECONDS + 30 ))
|
||||||
|
fi
|
||||||
|
sleep "$MESH_SIZE_POLL"
|
||||||
done
|
done
|
||||||
|
|
||||||
ms_deadline=$(( SECONDS + MESH_SIZE_SETTLE + MESH_SIZE_TIMEOUT ))
|
ms_deadline=$(( SECONDS + MESH_SIZE_SETTLE + MESH_SIZE_TIMEOUT ))
|
||||||
while :; do
|
while :; do
|
||||||
|
check_links_round
|
||||||
all_ok=1
|
all_ok=1
|
||||||
for n in "${NODES[@]}"; do
|
for n in "${NODES[@]}"; do
|
||||||
est="$(mesh_estimate "$n")"
|
est="$(mesh_estimate "$n")"
|
||||||
@@ -1165,7 +1253,44 @@ done
|
|||||||
# that the nodes agreed. Print the spread so nobody has to infer it.
|
# that the nodes agreed. Print the spread so nobody has to infer it.
|
||||||
ms_spread="$(printf '%s\n' "${MS_EST[@]}" | sort -n | awk '/^[0-9]+$/{ if (lo=="") lo=$1; hi=$1 } END{ if (lo=="") print "no numeric estimates"; else if (lo==hi) print "all nodes agree on " lo; else print "nodes disagree: " lo " to " hi }')"
|
ms_spread="$(printf '%s\n' "${MS_EST[@]}" | sort -n | awk '/^[0-9]+$/{ if (lo=="") lo=$1; hi=$1 } END{ if (lo=="") print "no numeric estimates"; else if (lo==hi) print "all nodes agree on " lo; else print "nodes disagree: " lo " to " hi }')"
|
||||||
echo " NOTE: final estimates — $ms_spread (agreement is reported, not asserted)"
|
echo " NOTE: final estimates — $ms_spread (agreement is reported, not asserted)"
|
||||||
phase_result "Mesh-size estimate convergence"
|
|
||||||
|
# Link continuity across Phase 7, one check per node.
|
||||||
|
for n in "${NODES[@]}"; do
|
||||||
|
s="${SLOT_OF[$n]}"; u="$(echo "$s" | tr '[:lower:]' '[:upper:]')"
|
||||||
|
node_ok=1
|
||||||
|
if [ "${LINK_UNREAD_WORST[$n]:-0}" -ge 2 ] || [ "${LINK_UNREAD_RUN[$n]:-0}" -ge 1 ]; then
|
||||||
|
node_ok=0
|
||||||
|
echo " FAIL $n [$u]: show peers unreadable in ${LINK_UNREADABLE[$n]} of $LINK_ROUNDS rounds (longest run ${LINK_UNREAD_WORST[$n]}, final round $([ "${LINK_UNREAD_RUN[$n]:-0}" -ge 1 ] && echo unreadable || echo read)); link continuity not established"
|
||||||
|
INTEROP_FAILURES+=("[link] node $n ($u ${SLOT_REF[$s]}@${SLOT_SHA[$s]}): show peers unreadable in ${LINK_UNREADABLE[$n]} of $LINK_ROUNDS Phase 7 rounds")
|
||||||
|
elif [ -n "${LINK_UNREADABLE[$n]:-}" ]; then
|
||||||
|
echo " NOTE $n [$u]: show peers unreadable in ${LINK_UNREADABLE[$n]} isolated round(s) of $LINK_ROUNDS; tolerated"
|
||||||
|
fi
|
||||||
|
for m in "${NODES[@]}"; do
|
||||||
|
[ -n "${LINK_LOST_AT[$n|$m]:-}" ] || continue
|
||||||
|
node_ok=0
|
||||||
|
echo " FAIL $n [$u]: lost direct peer $m (first ${LINK_LOST_AT[$n|$m]}s after mesh start, missing in ${LINK_LOST_ROUNDS[$n|$m]} of $LINK_ROUNDS rounds)"
|
||||||
|
# NOTE: keep "$kind pair" contiguous — interop-stress.sh greps it.
|
||||||
|
INTEROP_FAILURES+=("[link] $(pair_kind "$n" "$m") pair $(pair_label "$n" "$m") [$(hop_label "$n" "$m")]: $n lost $m during Phase 7 (first ${LINK_LOST_AT[$n|$m]}s after mesh start)")
|
||||||
|
done
|
||||||
|
if [ "$node_ok" -eq 1 ]; then
|
||||||
|
echo " PASS $n [$u]: listed every direct peer in every readable round of $LINK_ROUNDS Phase 7 rounds"
|
||||||
|
PASSED=$((PASSED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
phase_result "Mesh-size estimate convergence and link continuity"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Phase 8: whole-run log health ────────────────────────────────────
|
||||||
|
#
|
||||||
|
# The global negative checks, run last so they cover every phase. A
|
||||||
|
# link lost and re-established inside the Phase 7 warmup leaves an
|
||||||
|
# "MMP link teardown" line even if no poll round caught it missing.
|
||||||
|
echo "Phase 8: Whole-run log health (all $NUM_NODES nodes)"
|
||||||
|
PASSED=0; FAILED=0
|
||||||
|
scan_global_patterns
|
||||||
|
phase_result "Whole-run log health"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Summary ──────────────────────────────────────────────────────────
|
# ── Summary ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user