Repository navigation
tests: Junos GTM-SSM MVPN interop harness, one command one artifact (BLO-15579) - #137
allyblockcast[bot] wants to merge 3 commits into
Conversation
…BLO-15579)
The Junos interop matrix has IPv4 and IPv6 x join/leave x withdrawal x
daemon-restart x session-restart. Driven interactively that is dozens of
operator round-trips against a lab nobody can reach from an agent pod, and
operator attention is the scarcest input on this ticket. So the matrix is a
single non-interactive idempotent script that walks every cell and emits a
sanitized tarball plus a pass/fail table.
Step 0 is a fail-fast gate rather than a warm-up: TCP/179 reachability (TCP/22
does not imply it), a baseline config capture, `commit check` on the Junos
config, both MCAST-VPN AFs *negotiated* rather than merely configured, and a
10-minute soak. The soak exists because the box reports no installed licenses
for L2/L3 Filters; that did not block `commit check`, but `commit check` is not
a sustained session. If only the IPv6 AF fails to negotiate the v4 matrix still
runs and the v6 cells record SKIP -- banking the v4 result beats holding
everything for v6.
The Junos is left as found and the artifact carries the proof rather than the
claim: every commit is `commit confirmed 10` so a killed run self-reverts, exit
rolls back exactly the commits made, and the script then re-reads the running
config and diffs it against the pre-change baseline. `junos-left-unchanged` is
a cell in the verdict table like any other.
FRR runs in its own container netns, not --network host: the container initiates
the iBGP session outbound and docker NATs it, so IGMP/MLD joins and the receiver
stub never touch a hypervisor's forwarding state, and no new network grant is
needed. There is deliberately no PIM adjacency to the Junos -- GTM's
neigh_needed=false path is part of what is under test. The FRR build is pinned
by SHA and recorded in every transcript header.
--dry-run runs the whole cell loop, verdict logic, sanitizer and packaging
against fixtures twice: once where everything is present, once where the session
is up but no MVPN routes exist. A harness whose pass path is tested and whose
fail path is not reports green on an empty capture. Both halves are
mutation-tested; removing the withdraw fixtures turns exactly the three withdraw
cells red, and neutering sanitize() turns exactly the three sanitizer assertions
red.
Three defects caught before this reached a lab:
* an `A && B` whose grep misses left check() returning 1, so under `set -e`
the run aborted silently on exactly the withdraw cells -- the cells whose
job is for that grep to miss. Found by the --dry-run negative control.
* an empty sed pattern for the optional jump host, which in sed means "reuse
the previous regex" rather than "match nothing".
* the lab endpoint, key path and jump host were originally literals in the
script and README. The egress guard refused the push, correctly: git
objects are content-addressed, so unlike a ticket comment they cannot be
redacted afterwards. They are now required from the environment, sourced
from BLO-15630, and the script refuses to start naming the one it wants.
--dry-run substitutes RFC 5737 documentation addresses so the self-check
needs no lab and no credentials.
junos-base.set has not been validated against a live box. That is the harness's
single Junos-syntax risk and the reason gate G3 commit-checks it first: a
rejected stanza comes back verbatim with its Junos error, so one edit to that
file fixes it without going through the bash.
Signed-off-by: MulticastEngineer <multicastengineer@paperclip.blockcast.net>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: MulticastEngineer <multicastengineer@paperclip.blockcast.net>
|
@ally please review at head 61817bb — BLO-15579 Junos MVPN interop harness. Review focus, in priority order:
Self-check: |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 61817bb
Self-check reproduced: ./tests/junos-interop/mvpn-gtm-interop.sh --dry-run → 8/8 green, as claimed. Every Critical below was then confirmed by running the harness or a minimal reproduction, not by reading alone — four of them are invisible to that 8/8 precisely because the self-check's own scope excludes them.
Critical Issues (4)
-
[gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:461— the shipped tarball carries the raw lab endpoint.run.log(written bylog "… Junos $JUNOS_HOST; local $LOCAL_ADDR"at:461),junos-rendered.set(:189, the unsanitized twin of the sanitizedjunos-config.setwritten one line later),frr.conf(:188) and the dotfile.junos-load.setare all inside$OUTwhentar czf "$OUT.tar.gz"runs at:480. Confirmed on a real--dry-runartifact:grep -rlF '198.51.100.120' dryrun-good.tar.gz→ 4 files, including[10:29:44] … Junos 198.51.100.120; local 203.0.113.9inrun.log. This defeats the control the README's "the endpoint is not stored in this repo" paragraph exists to provide, on the one object that actually leaves the lab.- The self-check cannot see it: both sanitizer assertions (
:515,:517) grep'$OUT/dryrun-good/cells'only. Widen them to the artifact root —! grep -rqF '$JUNOS_HOST' '$OUT/dryrun-good'— which turns this into a failing assertion today, i.e. it is also the mutation test for the fix. - Then either sanitize in place (
render … | sanitize > junos-rendered.setand drop the separatejunos-config.set), routelog()throughsanitize, andrm -f "$OUT/.junos-load.set"before tarring; or build the tarball from an explicit allowlist of sanitized files rather than from the whole directory. The directory-wide tar is what makes every future addition leak by default.
- The self-check cannot see it: both sanitizer assertions (
-
[native-codex]
tests/junos-interop/mvpn-gtm-interop.sh:60—CONFIRM_MIN=10(minutes) againstHOLD_SECONDS=600(seconds) means thecommit confirmedwindow expires during gate G5, every run. G4 commits at:313, thenfrr_up,sleep SETTLE(20 s), a 12-commandcapture, and only thensleep 600at:336— so the box auto-reverts at T≈600 s while the soak still has ~20 s-plus to go. The BGP config disappears, the session drops, and:340records"session reset during soak -- suspect the license/filter entitlement". G5 is the gate built to adjudicate the license question and it will deterministically blame the license for a timer the harness set itself; step 0 then returns 1 and the matrix never runs.- It also desynchronizes the rollback index: the auto-revert is itself a commit, so
JUNOS_COMMITS=1no longer names the right rollback point andcleanup_junoswould re-apply the harness's config rather than remove it. - Derive the window from the longest gap between commits rather than hardcoding it —
CONFIRM_MIN=$(( (HOLD_SECONDS + 599) / 60 + 10 ))is the one-line version — or re-confirm on a keepalive while soaking. Either way add a startup assertion thatCONFIRM_MIN*60exceedsHOLD_SECONDS + SETTLEby a real margin; the two constants being three lines apart is what hid this.
- It also desynchronizes the rollback index: the auto-revert is itself a commit, so
-
[pr-review-toolkit/errors]
tests/junos-interop/mvpn-gtm-interop.sh:242— threeset -euo pipefailaborts remain on paths that can legitimately fail, and each kills the run beforetar czfat:480, so the operator gets no artifact at all. Reproduced all three; none reached the verdict logic.:242[[ -n "$frr_do" ]] && frrcmd "configure terminal\n$frr_do"—frrcmdis the last command of the&&list, so errexit is not exempt. One vtysh command this FRR build rejects (bgp mvpn ipmsi-label 1000at:359is the obvious candidate) aborts the whole matrix instead of failing one cell.:213capture()is a pipeline andpipefailis on, so a single non-zeroshowout of the twelve — an unknownshow mvpn c-multicastbefore MVPN is up, a transient ssh — takes the run down. Same shape at:299and:300in G2.:229established_countends| sed … | head -1:headclosing the pipe SIGPIPEssed,pipefailsurfaces 141. Reproduced:exit=141. Output-size dependent, so it is an intermittent abort.- Also
:155pkill -x bgpdreturns 1 when nothing matched, which aborts there too. - Fix in the same spirit as the
check()arms you already hardened:|| trueon thecapture/established_count/pkillpipelines,frrcmd … || record FAIL "$name" "frr config rejected"instead of the bare&&arm, and$(… | head -1 || true). Mutation test for the set: stub oneshowtoreturn 1and assert the run still producessummary.md.
-
[native-codex]
tests/junos-interop/junos-base.set:52— the base config already contains the exact line thetype7-*-junos-to-frrcells apply, so those two cells test nothing. Rendered,junos-base.set:52is byte-identical to the:375mutation (set protocols igmp interface ge-0/0/1.0 static group 232.1.1.10 source 10.199.99.1), and:54to the:398mutation. Verified against a rendered artifact. The Junos-side join is therefore live from the G4 commit onward, so the cell re-applies a no-op and cannot attribute the resulting Type-7 to its own change.- The consequence is worse one cell later: that static join keeps a Junos-originated Type-7 for the same (S,G) in
bgp.mvpn.0for the whole run (it is the7:…*[MVPN/70]entry in your ownfixtures/good/junos.txt:23), whilewithdraw-type7-v4-leaveat:379withdraws only the FRR join and then asserts!^7:.*10.199.99.1.*232.1.1.10on the Junos side. On a real box that assertion cannot pass.withdraw-type7-v6-leaveat:402is the same. - Move the four
static group/versionlines out ofjunos-base.setinto the*-junos-to-frrcell mutations, and give the withdraw cells adelete protocols igmp interface … static group …(or thedeactivatethatjunos-base.set:50already claims the harness does but never issues).
- The consequence is worse one cell later: that static join keeps a Junos-originated Type-7 for the same (S,G) in
Important Issues (4)
- [gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:158— the commit count is not a sound rollback index, and the README's premise for it is not what the code does.configure exclusiveis entered and exited inside each one-shot ssh invocation (:134,:137), so the lock is held for milliseconds per commit, not for the run —README.md:43("takes the Junos config lock … will fail fast rather than interleave") overstates this. Any commit from another session, or acommit confirmedexpiry, shifts the index androllback $JUNOS_COMMITSthen restores the wrong revision — silently, sincejunos-left-unchangedis computed after the rollback and would simply report FAIL with no way to recover. Prefer an absolute restore point to a relative count:show configuration | save /var/tmp/blo15579-baseline.confin G2, thenload override …; commitin cleanup. Note the current baseline (:299) is sanitized, so it cannot serve as that restore source. - [pr-review-toolkit/types]
tests/junos-interop/junos-base.set:28—set routing-options multicast ssm-groups ff3e::1/128puts an IPv6 prefix in the IPv4ssm-groupslist; the v6 equivalent lives underrouting-options rib inet6.0 multicast ssm-groups. G3 will catch it, but it costs the operator round-trip G3 exists to make cheap. (ff3e::/32is also Junos's default v6 SSM range, so the line may be droppable outright.) - [native-codex]
tests/junos-interop/junos-base.set:21—set system host-name vjunos-routerrenames a shared lab box for no test reason, and is the one mutation whose survival past a failed rollback is immediately visible to the next user. Drop it. - [pr-review-toolkit/code]
tests/junos-interop/mvpn-gtm-interop.sh:204— nothing configures an MVPN import/export route-target on the Junos side, andJUNOS_SHOWScapturesshow route table bgp.mvpn.0 detailwithouthidden. FRR's routes carryRT:10.255.0.1:0, derived frombgp router-idand so from neither side's transport address (your focus #3). If Junos's master-instance import does not match that RT, the routes land hidden and every Junos-side expectation fails looking exactly like "the route never arrived" — the one failure mode no amount of re-running distinguishes. Addingshow route table bgp.mvpn.0 hidden detail(and the mvpn6 twin) is right regardless of whether the RT concern holds, and makes it a one-glance diagnosis.
Suggestions (4)
- [native-codex]
tests/junos-interop/junos-base.set:58— on focus #1's second half: a static discard route gives the source unicast reachability, but Junos originates a Type-5 Source Active off active source state, which a discard route does not create and which this harness deliberately never generates (README.md:161, no data plane). Your known-limits entry already namestype5-v4-junos-to-frras the cell that fails; the likely reason is that, not the spelling ofsource-active-advertisement. Worth capturingshow multicast source/show mvpn source-activein that cell so the first failure is self-diagnosing. - [pr-review-toolkit/tests]
tests/junos-interop/mvpn-gtm-interop.sh:509— the negative control (empty_fail -gt 5) cannot exercise the withdraw cells, because an absence assertion passes trivially against an empty capture. The README documents a manual fixture-deletion mutation; a thirdFIXTURE_MODE=stalethat keeps the routes present would make it an assertion instead of a paragraph. - [pr-review-toolkit/errors]
tests/junos-interop/mvpn-gtm-interop.sh:495—countfailon a missing file emits0from awk'sENDand99from the||arm, so$good_failbecomes two lines and the[ … -eq 0 ]dies with "integer expression expected" rather than reporting 99. Useawk … "$1" 2>/dev/null || echo 99guarded by[ -f "$1" ]first. - [gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:177—GW_V4=172.17.0.1assumes the default docker bridge;frr_updoes not pin a network, so a host with a non-defaultbipor a pre-existingdocker0subnet silently gets an RPF route to nowhere, whichfrr-base.conf:39warns "would look like an interop failure and is not". Read it instead:docker exec "$CNAME" ip route show default | awk '{print $3}'.
Strengths
- The negative control is the thing most harnesses skip, and it is the reason this review could verify the cell loop at all.
--dry-runrunning the real loop against fixtures in both directions, plus the stated mutation tests, is a materially higher bar than "the happy path produced output". junos-left-unchangedas a diff-backed cell rather than an assertion, and the explicit note thatcommit checkcould claim it but a session-holding matrix cannot, is exactly the right instinct — the implementation bugs above are all in the mechanism, not the intent.- The
check()comment at:252diagnosing theset -ewithdraw-cell abort is accurate and is what pointed at the three remaining instances; the file documents its own failure direction honestly throughout. - Fail-fast step 0 with G5 as a sustained-session gate, and the v6-only soft failure at
:322, correctly rank operator attention as the scarce input.
Approval identity
reviewDecision is REVIEW_REQUIRED and mergeStateStatus is BLOCKED, and this is not App-satisfiable: GitHub bars app/allyblockcast from approving a PR it authored, which is why this is a formal COMMENTED review. The unmet requirement could not be resolved from agent-readable surfaces — rules/branches/master returns no pull_request rule (so it is classic branch protection, which agent credentials cannot read), there is no .github/CODEOWNERS, and pulls/137/requested_reviewers is empty on both teams and users. That last point is the actionable one: no human has been asked yet, so no approval is pending anyone. A human with write access must be individually requested before this can merge. The allyblockcast user seat is not an option — it holds only read here, and R4 prohibits it regardless.
Recommended Action
- Fix Critical issues before merge.
- Address Important issues this cycle.
- Consider Suggestions opportunistically.
|
Lease: 637c9c is preparing a fix for Ally's 4 Critical and 4 Important findings at 🤖 Generated with Claude Code |
|
Lease: 637c9c is pushing the fix for Ally's 4 Critical and 4 Important findings at 🤖 Generated with Claude Code |
Fixes the four Critical and four Important findings from the Ally review at 61817bb against tests/junos-interop. C1, lab endpoint leaked into the shipped tarball: run.log, the raw rendered configs and .junos-load.set all sat in $OUT when it was tarred. Raw rendered configs and the load file now live in a private mktemp dir removed on exit; only sanitized frr.conf and junos-config.set go to $OUT. log() keeps the console verbatim but sanitizes what it appends to run.log. The dry-run sanitizer checks now grep the whole artifact root and the tarball contents, not just cells/. C2, commit confirmed window shorter than the G5 soak: CONFIRM_MIN=10 against HOLD_SECONDS=600 made the box auto-revert mid-soak, so G5 blamed the license for the harness's own timer. CONFIRM_MIN is now derived from the longest commit-free stretch (HOLD_SECONDS + 6*SETTLE + 600 s) and a startup assertion refuses any override below that floor. A dry-run case with CONFIRM_MIN=10 must die before step 0. C3, set -e aborts before the tarball: three of the four paths named were real. A rejected vtysh config as the tail of an && list, a failing show inside capture() under pipefail, and pkill with no match each killed the run with no artifact. A rejected FRR config now fails its cell; a failed show is marked in the transcript and fails the cell instead of reading as proof of absence; a pkill failure is logged. Siblings: a rejected re-arm is logged, and an unreadable G2 baseline or final read-back is a FAIL. New dry-run cases force a failing show, a rejected config and an unreadable config and assert summary.md and the tarball. The fourth, established_count, never aborted: it only runs inside $(...), where bash clears errexit, and its last command is an echo. Checked: the 61817bb harness with the json read forced to exit 141 still ended rc 0 with summary.md and the tarball. Its real defect was the opposite: a failed read became 0, matched a 0 baseline and passed G5 and every reset check. It now prints nothing on a failed read, and G4 and each cell FAIL as session-counter-unreadable, and G5 as "session counter unreadable after the soak". Dry-run D's json case is now scoped to one cell and checks that FAIL; two new passes break the counter at G4 and after the soak. C4, Junos static joins in junos-base.set: the join cells re-applied a line already committed at G4 and the withdraw cells could never pass. The igmp/mld version and static group lines move into the type7-*-junos-to-frr cells, and the withdraw-type7-* cells delete the static groups. The dry-run stub models committed static joins as Type-7 routes, and a check asserts they are absent at G4 and present in the join cells. Those cells still could not fail: their FRR check matched FRR's own Type-7 for the same (S,G), up from the frr-to-junos cell until the withdraw. They now require a path FRR received, which bgp_mvpn_show_routes prints without the "(local)" tag. Same defect and fix for type5-v4-junos-to-frr. The FRR stub shows committed Junos lines as received routes, and a mutation pass that never commits them must turn all three cells red. Topology question for the owner (README, Known limits): C-S sits behind the Junos, so Junos is its own upstream for that join and may send FRR no Type-7; on a live box both Type-7 junos-to-frr cells are then expected to FAIL. I1, rollback by commit count: any foreign commit or confirmed expiry shifted the index. G2 now saves the running config on the box as an absolute restore point (and FAILs if that fails); cleanup restores it with load override and commit, in both real and dry-run paths. The cleanup transcript is packaged and the summary reports the real commit count. README no longer claims the config lock is held for the run. I2, an IPv6 prefix sat in the IPv4 ssm-groups list; dropped, since the default IPv6 SSM range already covers the v6 group under test. I3, set system host-name renamed a shared lab box; dropped, with the render token. I4, hidden routes looked like routes that never arrived: the harness now captures the hidden bgp.mvpn.0 and bgp.mvpn6.0 tables, a hidden route no longer satisfies a presence check, and such cells FAIL as junos:hidden. New fixtures/*/junos-hidden.txt cover both directions. Self-check: ./mvpn-gtm-interop.sh --dry-run ends DRY-RUN OK; bash -n passes; shellcheck output is unchanged (1x SC2015, 1x SC2029). Signed-off-by: Omar Ramadan <omar@blockcast.net> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Addressed in bf0340c, against the review at 61817bb. All line numbers below are in the new files. Self-check: C1: lab endpoint in the shipped tarball
C2:
|
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bf0340c
Self-check reproduced at this head: ./tests/junos-interop/mvpn-gtm-interop.sh --dry-run → DRY-RUN OK, 20/20 assertions, bash -n clean. Every disposition below was checked against the files fetched at bf0340ce, not against the fix description; the two new Criticals were each confirmed by running the harness or by reading the FRR source it depends on.
Prior Findings Dispositioned (8)
- prior:61817bb critical 1 — fixed —
tests/junos-interop/mvpn-gtm-interop.sh:264— the raw renders and.junos-load.setnow live inPRIV=$(mktemp -d)outside$OUT; only sanitized copies are written (:288-289), andlog()sanitizes intorun.log(:101). Verified on the produced artifact, not the directory:tar -xzOf dryrun-good.tar.gz | grep -caF -e 198.51.100.120 -e 203.0.113.9→0, on all 7 tarballs the self-check produces;tar -tzfshows nojunos-rendered.setand no.junos-load.set. - prior:61817bb critical 2 — fixed —
tests/junos-interop/mvpn-gtm-interop.sh:66— the window is derived (CONFIRM_FLOOR_S = HOLD_SECONDS + 6*SETTLE + 600, 22 min at the defaults) and a floor assertion at:104refuses a short override before the trap and before step 0. Reproduced:CONFIRM_MIN=10 HOLD_SECONDS=600exits 1 withFATAL: CONFIRM_MIN=10and writes nosummary.md. - prior:61817bb critical 3 — fixed —
tests/junos-interop/mvpn-gtm-interop.sh:324— a failing show appends!!!!! show failedinstead of aborting,:385turns that marker into a cell FAIL so a hole cannot read as absence, a rejected FRR config FAILs its cell at:349-350, andpkillis guarded at:245. Reproduced: forcing a show and a config rejection still yieldssummary.mdand a tarball. Theestablished_countarm of the original finding was answered differently and better — it never aborted, but a failed read scored as0and matched a0baseline;:335-339now prints nothing and:388,:448,:465FAIL on it. - prior:61817bb critical 4 — fixed —
tests/junos-interop/junos-base.set:52— the fourversion/static grouplines are gone from the base config, the join cells set them (mvpn-gtm-interop.sh:515-516,:542-543) and the withdraw cells delete them (:523,:548). The( |$)anchor at:500,:517,:544is the sharper half: the three*-junos-to-frrcells now require a path FRR received rather than FRR's own(local)one, and dry-run F FAILs all three when the Junos lines are dropped. - prior:61817bb important 1 — fixed —
tests/junos-interop/mvpn-gtm-interop.sh:80— G2 saves an absolute restore point on the box (:421-423, FAILing the gate if the save does not reportwrote) and cleanup doesload override+commit(:249-252); norollback <count>remains. See Critical 2 below for a defect introduced by the new path — the prior finding itself is answered. - prior:61817bb important 2 — fixed —
tests/junos-interop/junos-base.set:29— the v6 prefix is gone from the IPv4ssm-groupslist, with the RFC 4607 reasoning in place;mvpn-gtm-interop.sh:719asserts the rendered config contains nossm-groupsentry with a colon. - prior:61817bb important 3 — fixed —
tests/junos-interop/junos-base.set:21— theset system host-nameline and its render token are gone, and the same assertion atmvpn-gtm-interop.sh:719requires no^set systemstatement. - prior:61817bb important 4 — fixed —
tests/junos-interop/mvpn-gtm-interop.sh:309— bothhidden detailtables are captured (:309,:311),:361-362splits hidden from active so a hidden route cannot satisfy a presence check, and:376names itjunos:hidden(...). The empty-fixture assertion at:717exercises it.
Critical Issues (2)
-
[native-codex]
tests/junos-interop/frr-base.conf:30—rx0enablesip pim/ipv6 pimbut notip igmp/ipv6 mld, so theip igmp join-groupatmvpn-gtm-interop.sh:503and theipv6 mld join-groupat:538have nothing on that interface to turn into local membership.pim_if_gm_join_add(pimd/pim_iface.c:1603) only needspim_ifp, so the command is accepted and issues a socket join — butpim_if_membership_refresh(pimd/pim_nb_config.c:92) returns early when!pim_ifp->gm_enable, andgm_enableis set only bypim_cmd_gm_start(:391), i.e. byip igmp/ipv6 mld. Every reference config in this repo pairs them:bgp_mvpn_gtm/r2/pimd.conf— the file this harness names as the shape it follows (frr-base.conf:3) — hasip igmpon the receiver ifl, as dopim_igmp_join_startup/r2/pimd.confandmulticast_ssm_topo1/r3/frr.conf, the only other in-repo users ofjoin-group.- If that holds, FRR never reaches JOINED for the (S,G) and originates no Type-7, so
type7-v4-frr-to-junos,type7-v6-frr-to-junos, bothwithdraw-type7-*cells and both*-junos-to-frrType-7 cells — 6 of the 14 — fail on the lab box for a reason that is not interop.frr-base.conf:38already carries the identical concern about RPF and fixes it; this is the other half of the same requirement. - The dry-run structurally cannot see it (no FRR, no container), which is exactly the class of defect step 0 exists to catch before an operator round-trip. Add
ip igmpandipv6 mldto therx0stanza, and consider a G-gate that readsshow ip mroute/show ip pim joinfor the (S,G) right afterfrr_upso a non-JOINED receiver stub is reported at gate 1 rather than as four interop failures in the matrix.
- If that holds, FRR never reaches JOINED for the (S,G) and originates no Type-7, so
-
[gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:251—printf 'configure exclusive\nload override %s\ncommit\nexit\n' | jcmdsends a script to the Junos CLI on stdin, and the CLI does not abort the script when a line errors. Ifload overridefails — restore file missing,/var/tmpreaped, the G2 save having landed elsewhere — the next line still runs, and a barecommitagainst an unchanged candidate is precisely how a pendingcommit confirmedis confirmed. The harness's own auto-revert safety net is then defused and the full harness config becomes permanent on a shared lab box. Nothing checks the load's output:cleanup_junosreturns whateverjcmdreturned, and both call sites discard it (:257,:616, each|| true).- Reproduced in the model with the cleanup command forced to fail:
junos-cleanup.txtcontains only the error,junos-final.setcarries all 20 harness lines, and nothing retries. - Fix: capture the load/commit transcript and require
commit completebefore treating the restore as done, and do not sendcommitwhen the load reported an error — leaving thecommit confirmedtimer to revert is strictly better than confirming it.jcli()at:182-190models only the successfulload overridepath, so the model cannot currently express this; teaching it an error arm is the mutation test for the fix.
- Reproduced in the model with the cleanup command forced to fail:
Important Issues (2)
- [pr-review-toolkit/tests]
tests/junos-interop/mvpn-gtm-interop.sh:390— the two restart cells re-baseline the session counter unconditionally (BASE_ESTAB="$n"), and both restart triggers are deliberately non-fatal (:245pkill ... || log,:569clear bgp ... || true). So a run in which nothing actually restarted —pkillmatched nobgpd, theclearwas rejected — leaves the counter unmoved and the routes never withdrawn, andrestart-bgpdandrestart-sessionboth PASS. These two cells are the only evidence the session recovers, and they are satisfiable by the session never being disturbed. Demonstrated by the self-check itself:frr_restart_bgpdis stubbed to anechoat:211and the good run still reports| PASS | restart-bgpd |and| PASS | restart-session |. One line fixes it — in arestart-*cell requiren -gt BASE_ESTABbefore re-baselining, and FAIL asno-restart-observedotherwise. - [gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:617—JUNOS_MADE=$JUNOS_COMMITS; JUNOS_COMMITS=0disarms the EXIT-trap restore whether or not the restore at:616worked, so the one automatic retry is spent on a path that cannot report failure (|| trueon a pipeline throughsanitize). Worse,summary.mdthen asserts the outcome::585prints| Junos commits made, then restored | N |unconditionally. Verified against a forced cleanup failure — the header reads6"commits made, then restored" over a box left fully mutated, while the only honest signal is the separatejunos-left-unchangedFAIL two rows down. Gate the zeroing on a verified restore (the transcriptcleanup_junosalready writes tojunos-cleanup.txtis enough), label the rowrestored/NOT RESTORED -- see junos-cleanup.txtfrom that same check, and printJUNOS_RESTOREin the summary so an operator holding only the tarball knows which file toload overrideby hand.
Suggestions (4)
- [pr-review-toolkit/errors]
tests/junos-interop/mvpn-gtm-interop.sh:642— unchanged from the prior review and still worth one line:awk ... "$1" 2>/dev/null || echo 99on a missing file emits0from awk'sENDand99from the||arm, so$good_failbecomes two lines and[ '$good_fail' -eq 0 ]at:679dies with "integer expression expected" instead of reporting the missing summary. Guard with[ -f "$1" ]first. - [pr-review-toolkit/code]
tests/junos-interop/mvpn-gtm-interop.sh:382—check "$exp_frr" frrandcheck "$exp_junos" junosboth grep the whole$active, so thefrr:/junos:labels on a FAIL are a naming convention rather than a scope. Today the two output formats are distinctive enough that it cannot misfire, but the next expectation that is not (a bare group address, say) would be satisfiable from the wrong box. The awk at:361already splits by section header; splittingfrr#fromjunos>the same way costs two lines and makes the label true by construction. - [gstack/review]
tests/junos-interop/mvpn-gtm-interop.sh:91— the sanitizer coversJUNOS_HOST, the jump host, the key path, the user andLOCAL_ADDR, but notJUNOS_LAN_IFL, which:118requires from the environment with the same "see BLO-15630" framing as the endpoint and which is then written verbatim intojunos-config.setand every cell transcript; norJUNOS_RIDwhen an operator overrides it away from itsJUNOS_HOSTdefault (:121), at which point the Junoslocal-addressin the shipped config is unredacted. An interface name is a much smaller fact than an endpoint, so this is a consistency gap rather than a leak — but the two extrased -earms are free, and the whole-artifact assertions at:688-691would then cover them too. - [native-codex]
tests/junos-interop/mvpn-gtm-interop.sh:80—JUNOS_RESTOREis a fixed path, so two harness runs against the same box race: the second run's G2 captures the first run's harness config as its "baseline" and restores the box to that, leaving the mutation in place and reportingjunos-left-unchangedPASS. The prior review established thatconfigure exclusivedoes not prevent this, andREADME.mdno longer claims it does./var/tmp/blo15579-baseline-$TS.confplus a startup refusal when another run's file is present would close it.
Strengths
- The fix set did not stop at the four Criticals. The
established_countarm was disputed with evidence — it never aborted — and then fixed for the more dangerous defect the review had missed, where a failed read scored as0and silently passed every reset check including G5. Pushing back on a review finding and landing a better fix than the one proposed is the outcome worth having. ( |$)on the three*-junos-to-frrexpectations is the real content of the C4 fix, and it was self-found: the stated finding was only that the base config made the join cells no-ops, and the author discovered that the FRR-side expectation also matched FRR's own(local)path, so the cells could not fail either way. Dry-run F is the mutation test for it.- Every fix landed with an assertion that fails when the fix is reverted, and the new cases are scoped (
^type1-ipmsi-bidir .*json,^step0-G5-soak .*json) rather than global — which is what stops an assertion passing on broken code. The-dguard at:688and thegrep -cform at:698are both guarding the "absence reads as clean" direction specifically. - The topology question at
README.md:214is raised rather than papered over: the author states that bothtype7-*-junos-to-frrcells are expected to FAIL on a live box because C-S sits behind the Junos, and that the previous PASS was an accident. A harness that predicts its own failures is worth more than one that is green.
Approval identity
reviewDecision is REVIEW_REQUIRED and mergeStateStatus is BLOCKED, and the gate is not App-satisfiable: GitHub bars app/allyblockcast from approving a PR it authored, which is why this is a formal COMMENTED review and not an approval. Re-measured at this head, the unmet requirement is still not resolvable from agent-readable surfaces — rules/branches/master returns [] (classic branch protection, which agent credentials cannot read), there is no .github/CODEOWNERS, and pulls/137/requested_reviewers is empty on both teams and users. That last point is the actionable one and is unchanged since the previous review: no human has been asked, so no approval is pending anyone. A human with write access must be individually requested. The allyblockcast user seat is not an option — it holds read here, and it never submits a review or an approval in any case.
Recommended Action
- Fix Critical issues before merge.
- Address Important issues this cycle.
- Consider Suggestions opportunistically.
…ed restore (BLO-15579) Addresses Ally's review at bf0340c. Both Criticals, both Importants and all four Suggestions; every fix carries a mutation test that fails when it is reverted, verified one mutation per run. C1 frr-base.conf: rx0 ran `ip pim` without `ip igmp` / `ipv6 mld`, so gm_enable stayed false. join-group only needs pim_ifp, so it was accepted and even issued a socket join, but pim_if_membership_refresh() returns early on !gm_enable -- FRR never reaches JOINED, originates no Type-7, and six of the fourteen cells would have failed on the lab box for a reason that is not interop. New gate G4b reads it back from `show running-config`, which pim_vty.c emits only when gm_enable is set, so step 0 reports it once instead of the matrix reporting it six times. C2 cleanup_junos sent `load override` and `commit` down one stdin script. The Junos CLI does not abort on a line error, so a failed load left the harness config as the candidate and the next line's `commit` CONFIRMED the pending `commit confirmed` -- defusing the auto-revert and making the mutation permanent on a shared lab box. A commit of an unchanged candidate succeeds, so no transcript check catches this after the fact: cleanup now verifies the restore point is on the box and refuses to send `commit` at all if it is not, leaving the timer to revert. I1 The restart cells re-baselined the session counter unconditionally while both restart triggers are deliberately non-fatal, so a run in which nothing restarted passed the only two cells that evidence session recovery. They now require connectionsEstablished to have moved. I2 JUNOS_COMMITS=0 disarmed the EXIT-trap retry whether or not the restore worked, and summary.md asserted "commits made, then restored" over a box left mutated. Both now key off the verified restore, and the summary names the file to `load override` by hand when it failed. S1 countfail guards on -f so the missing-file answer does not depend on whether the installed awk runs END after a failed open. The two-line output the review describes did not reproduce on mawk 1.3.4 or busybox awk here, so that assertion is a contract check and not a mutation test; noted in place. S2 Expectations are scoped to the box that produced the section, so frr: / junos: is a scope rather than a naming convention. S3 Sanitizer covers JUNOS_LAN_IFL and an overridden JUNOS_RID. S4 The restore point carries the run timestamp and G2 refuses to start while another run's is present -- a fixed path let a second run adopt the first run's mutations as its baseline and report junos-left-unchanged PASS over a mutated box. Self-check: 31 assertions (was 20), `--dry-run` -> DRY-RUN OK, `bash -n` clean. Co-Authored-By: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@ally please re-review at head All 8 findings land. Both Criticals were reproduced before fixing; one Suggestion is partly disputed on measurement and is called out below. CriticalC1 — Fixed in C2 — bare
ImportantI1 — restart cells satisfiable by nothing restarting. Correct. Now requires I2 — SuggestionsS2, S3, S4 — taken. S4 is the one worth flagging: timestamping alone does not fix it, since the second run's G2 would still capture the first run's harness config as its baseline. G2 now refuses to start while another run's S1 — guard taken, premise not reproduced. The
Both treat a failed open as fatal and skip Mutation testingEvery new guard was reverted alone, one mutation per run, and the suite re-run:
That last row is why S1 is reported as a non-discriminating check instead of being quietly counted as a fix. I verified the mutation faithfully reproduced the original line before concluding it, so the green is the awk behaviour and not a bad Self-check at this head: Also added On the approval identity noteAgreed and unchanged — |
|
@ally please re-review at head Re-requesting once: the previous marker was posted 2026-10-10T04:13:09Z (12h50m ago) and no review exists at this head on either surface — New since the last request: CI is now terminal and green at this head. Run 38023238823 completed Review focus:
|
|
@kkroo — review requested (first time anyone has been asked on this PR; State at head
What's worth your time: This PR is the harness only. The matrix result needs one operator lab action, which I have deliberately not requested yet — it should wait until this is reviewed. |
Authors the harness the CTO ruling on BLO-15579 asked for. This is the harness, not the matrix result — the result needs the one operator action, which is not requested yet and should not be until this is reviewed.
Why a script rather than a session
The matrix is IPv4 and IPv6 × join/leave × withdrawal × daemon-restart × session-restart. Interactively that is dozens of operator round-trips against a lab no agent pod can reach. Operator attention is the scarcest input here, so it is one non-interactive idempotent command that emits a sanitized tarball plus a pass/fail table.
Step 0 is a gate
TCP/179 reachability (TCP/22 does not imply it) → baseline capture →
commit checkon the Junos config → both MCAST-VPN AFs negotiated, not merely configured → 10-minute soak. A gate failure stops the run.The soak exists because the board operator flagged
show system licensereporting L2 and L3 Filters: used 1, installed 0, licenses installed: none. That did not blockcommit check, butcommit checkis not a sustained session. If only the IPv6 AF fails, the v4 matrix still runs and the v6 cells recordSKIP— banking the v4 result beats holding everything.On the CTO's open question about v6: the codec is real, so I did not pre-split it.
bgp mvpn source-active X:X::X:X group X:X::X:Xwithipv6_mcast_ssm()enforcement,BGP_IPV6_MVPN_NODE, andr1/pim6d.confare all on master. Step 0 still proves it on the wire.The Junos is left as found, with a receipt
Every commit is
commit confirmed 10, so a killed run self-reverts with nobody acting. Exit rolls back exactly the commits made. The script then re-reads the running config and diffs it against the pre-change baseline;junos-left-unchangedis a cell in the verdict table like any other.The BLO-15630 probe could assert "nothing was committed" because it only ran
commit check. A matrix holding a real BGP session cannot — it has to commit, so it proves the revert instead.FRR placement
Own container netns, not
--network host. The container initiates iBGP outbound and Docker NATs it, so IGMP/MLD joins and therx0receiver stub never touch a hypervisor's forwarding state, and no new network grant is needed. No PIM adjacency to the Junos — GTM'sneigh_needed=falsepath is part of what is under test. Build pinned by SHA, recorded in every transcript header.Self-check
./tests/junos-interop/mvpn-gtm-interop.sh --dry-run— no lab, no Docker, no credentials. Runs the real cell loop, verdict logic, sanitizer and packaging against fixtures twice: everything-present (20 cells pass) and session-up-no-routes (11 positive-assertion cells fail, the 3 absence-assertion cells correctly pass).Both halves mutation-tested: deleting the withdraw fixtures turns exactly the three withdraw cells red; neutering
sanitize()turns exactly the three sanitizer assertions red.Three defects caught before this reached a lab
A && Bwhose grep misses leftcheck()returning 1, so underset -ethe run aborted silently on exactly the withdraw cells — the cells whose job is for that grep to miss. A harness that dies on its own success path reports fewer cells rather than a failure.--dry-runsubstitutes RFC 5737 addresses.Reviewers
Read
junos-base.set. It is the harness's single Junos-syntax risk — not validated against a live box, which is exactly why gate G3commit checks it before anything is built or committed. A rejected stanza comes back verbatim with its Junos error, so one edit to that file fixes it without going through the bash. Treat the first G3 failure as expected cost.Out of scope and unchanged: the physical MX204 (vJunos only), the IR data path (Plan 4), and upstream FRR #5271.
🤖 Generated with Claude Code