Skip to content

tests: Junos GTM-SSM MVPN interop harness, one command one artifact (BLO-15579) - #137

Open
allyblockcast[bot] wants to merge 3 commits into
masterfrom
blo15579-junos-mvpn-interop-harness
Open

allyblockcast[bot] wants to merge 3 commits into
masterfrom
blo15579-junos-mvpn-interop-harness

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

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 check on 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 license reporting L2 and L3 Filters: used 1, installed 0, licenses installed: none. That did not block commit check, but commit check is not a sustained session. If only the IPv6 AF fails, the v4 matrix still runs and the v6 cells record SKIP — 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:X with ipv6_mcast_ssm() enforcement, BGP_IPV6_MVPN_NODE, and r1/pim6d.conf are 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-unchanged is 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 the rx0 receiver stub never touch a hypervisor's forwarding state, and no new network grant is needed. No PIM adjacency to the Junos — GTM's neigh_needed=false path 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

  • 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. A harness that dies on its own success path reports fewer cells rather than a failure.
  • An empty sed pattern for the optional jump host. In sed that means reuse the previous regex, not match nothing.
  • The lab endpoint, key path and jump host were literals. The egress guard refused the push, correctly — git objects are content-addressed, so unlike a ticket comment they cannot be redacted afterwards. Now required from the environment per BLO-15630; --dry-run substitutes 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 G3 commit 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

…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>
@allyblockcast

allyblockcast Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

🔗 Paperclip issue: BLO-15630
🔗 Paperclip issue: BLO-15579

@allyblockcast

allyblockcast Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@ally please review at head 61817bb — BLO-15579 Junos MVPN interop harness.

Review focus, in priority order:

  1. tests/junos-interop/junos-base.set — the only Junos-syntax risk, unvalidated against a live box. Is protocols mvpn mvpn-mode spt-only + receiver-site in the master instance (no routing-instance) the right GTM shape for Junos 26.2R1.7? And is a static discard route + source-active-advertisement the right way to make Junos originate a Type-5?
  2. Junos rollback correctness in mvpn-gtm-interop.sh — commit confirmed $CONFIRM_MIN on every commit, plus rollback $JUNOS_COMMITS; commit on the exit trap. Is the commit count a sound rollback index given that each commit confirmed confirms the prior one?
  3. The NAT/iBGP premise — FRR runs in its own container netns and Junos peers with the lab host's post-NAT address, with bgp router-id set independently. Anything that breaks for MVPN specifically (RD and originator derived from router-id rather than transport address)?
  4. set -e interactions in the verdict path. One already bit: an A && B whose grep missed left check() at status 1 and silently aborted the run on exactly the withdraw cells — the cells whose job is for that grep to miss. Please look for any remaining compound whose last command can legitimately fail.

Self-check: ./tests/junos-interop/mvpn-gtm-interop.sh --dry-run (no lab, docker or credentials needed) — 8/8 assertions green, and both guards have a failing mutation.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 by log "… Junos $JUNOS_HOST; local $LOCAL_ADDR" at :461), junos-rendered.set (:189, the unsanitized twin of the sanitized junos-config.set written one line later), frr.conf (:188) and the dotfile .junos-load.set are all inside $OUT when tar czf "$OUT.tar.gz" runs at :480. Confirmed on a real --dry-run artifact: 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.9 in run.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.set and drop the separate junos-config.set), route log() through sanitize, and rm -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.
  • [native-codex] tests/junos-interop/mvpn-gtm-interop.sh:60 — CONFIRM_MIN=10 (minutes) against HOLD_SECONDS=600 (seconds) means the commit confirmed window expires during gate G5, every run. G4 commits at :313, then frr_up, sleep SETTLE (20 s), a 12-command capture, and only then sleep 600 at :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 :340 records "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=1 no longer names the right rollback point and cleanup_junos would 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 that CONFIRM_MIN*60 exceeds HOLD_SECONDS + SETTLE by a real margin; the two constants being three lines apart is what hid this.
  • [pr-review-toolkit/errors] tests/junos-interop/mvpn-gtm-interop.sh:242 — three set -euo pipefail aborts remain on paths that can legitimately fail, and each kills the run before tar czf at :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" — frrcmd is the last command of the && list, so errexit is not exempt. One vtysh command this FRR build rejects (bgp mvpn ipmsi-label 1000 at :359 is the obvious candidate) aborts the whole matrix instead of failing one cell.
    • :213 capture() is a pipeline and pipefail is on, so a single non-zero show out of the twelve — an unknown show mvpn c-multicast before MVPN is up, a transient ssh — takes the run down. Same shape at :299 and :300 in G2.
    • :229 established_count ends | sed … | head -1: head closing the pipe SIGPIPEs sed, pipefail surfaces 141. Reproduced: exit=141. Output-size dependent, so it is an intermittent abort.
    • Also :155 pkill -x bgpd returns 1 when nothing matched, which aborts there too.
    • Fix in the same spirit as the check() arms you already hardened: || true on the capture/established_count/pkill pipelines, frrcmd … || record FAIL "$name" "frr config rejected" instead of the bare && arm, and $(… | head -1 || true). Mutation test for the set: stub one show to return 1 and assert the run still produces summary.md.
  • [native-codex] tests/junos-interop/junos-base.set:52 — the base config already contains the exact line the type7-*-junos-to-frr cells apply, so those two cells test nothing. Rendered, junos-base.set:52 is byte-identical to the :375 mutation (set protocols igmp interface ge-0/0/1.0 static group 232.1.1.10 source 10.199.99.1), and :54 to the :398 mutation. 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.0 for the whole run (it is the 7:…*[MVPN/70] entry in your own fixtures/good/junos.txt:23), while withdraw-type7-v4-leave at :379 withdraws only the FRR join and then asserts !^7:.*10.199.99.1.*232.1.1.10 on the Junos side. On a real box that assertion cannot pass. withdraw-type7-v6-leave at :402 is the same.
    • Move the four static group / version lines out of junos-base.set into the *-junos-to-frr cell mutations, and give the withdraw cells a delete protocols igmp interface … static group … (or the deactivate that junos-base.set:50 already claims the harness does but never issues).

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 exclusive is 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 a commit confirmed expiry, shifts the index and rollback $JUNOS_COMMITS then restores the wrong revision — silently, since junos-left-unchanged is 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.conf in G2, then load override …; commit in 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/128 puts an IPv6 prefix in the IPv4 ssm-groups list; the v6 equivalent lives under routing-options rib inet6.0 multicast ssm-groups. G3 will catch it, but it costs the operator round-trip G3 exists to make cheap. (ff3e::/32 is 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-router renames 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, and JUNOS_SHOWS captures show route table bgp.mvpn.0 detail without hidden. FRR's routes carry RT:10.255.0.1:0, derived from bgp router-id and 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. Adding show 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 names type5-v4-junos-to-frr as the cell that fails; the likely reason is that, not the spelling of source-active-advertisement. Worth capturing show multicast source / show mvpn source-active in 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 third FIXTURE_MODE=stale that 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 — countfail on a missing file emits 0 from awk's END and 99 from the || arm, so $good_fail becomes two lines and the [ … -eq 0 ] dies with "integer expression expected" rather than reporting 99. Use awk … "$1" 2>/dev/null || echo 99 guarded by [ -f "$1" ] first.
  • [gstack/review] tests/junos-interop/mvpn-gtm-interop.sh:177 — GW_V4=172.17.0.1 assumes the default docker bridge; frr_up does not pin a network, so a host with a non-default bip or a pre-existing docker0 subnet silently gets an RPF route to nowhere, which frr-base.conf:39 warns "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-run running 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-unchanged as a diff-backed cell rather than an assertion, and the explicit note that commit check could 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 :252 diagnosing the set -e withdraw-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

  1. Fix Critical issues before merge.
  2. Address Important issues this cycle.
  3. Consider Suggestions opportunistically.

@kkroo

kkroo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Lease: 637c9c is preparing a fix for Ally's 4 Critical and 4 Important findings at 61817bb7: sanitize the whole tarball, not just the cells; derive the commit-confirmed window from the soak; stop errexit aborting before the artifact is written; move the static joins out of the base config; use an absolute restore point; and fix the base-config lines. The owner window elapsed at 12:39Z and BLO-15579 has no active run. I will push only if the head and Ally's review are unchanged and no owner activity appears first.

🤖 Generated with Claude Code

@kkroo

kkroo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Lease: 637c9c is pushing the fix for Ally's 4 Critical and 4 Important findings at 61817bb7 (owner window elapsed 12:39Z, no owner activity, BLO-15579 has no active run). Two independent verifiers re-ran dry-runs A-H and the mutation checks; the disposition follows.

🤖 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>
@kkroo

kkroo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Addressed in bf0340c, against the review at 61817bb.

All line numbers below are in the new files. Self-check: cd tests/junos-interop && ./mvpn-gtm-interop.sh --dry-run ends DRY-RUN OK (20 of 20 assertions); bash -n passes; shellcheck output is unchanged (1x SC2015, 1x SC2029, before and after).

C1: lab endpoint in the shipped tarball

  • Wrong: run.log, junos-rendered.set, raw frr.conf and .junos-load.set were all in $OUT when it was tarred, so the tarball carried the real Junos and local addresses.
  • Changed: raw renders and the load file now live in PRIV=$(mktemp -d) (mvpn-gtm-interop.sh:264), removed on exit (:259). Only sanitized junos-config.set and frr.conf reach $OUT (:288-289). log() prints verbatim to the console and appends a sanitized line to run.log (:101).
  • Evidence: the sanitizer assertions now grep the whole artifact root (:688-691), and a new assertion checks the tarball contents (:698-699). Before the fix there were 3 FAILs. After the fix: 0 hits and DRY-RUN OK.

C2: commit confirmed window expired inside the G5 soak

  • Wrong: CONFIRM_MIN=10 against HOLD_SECONDS=600. The box auto-reverted mid-soak, and G5 blamed the license for the timer the harness set.
  • Changed: the window is derived from the longest commit-free stretch, CONFIRM_FLOOR_S = HOLD_SECONDS + 6*SETTLE + 600 (:60-67). That is 22 min at the defaults. A startup assertion refuses a non-integer override or one below the floor, before the trap and before step 0 (:104-106). README updated to match (README.md:55-59).
  • Evidence: a dry-run case with CONFIRM_MIN=10 HOLD_SECONDS=600 must exit non-zero with FATAL and no summary.md (:654-657, :683-684). With the fix reverted it fails; with the fix restored it passes. Overrides of 21, 0 and abc are refused; 22 and 30 are accepted.

C3: set -e aborts before the tarball

  • Agreed for three of the four paths named. Each killed the run with no artifact: a rejected frrcmd as the tail of an && list, a failing show in capture() under pipefail, and pkill matching nothing.
  • Changed:
    • A rejected FRR config FAILs its cell (:349-350).
    • Each show appends !!!!! show failed on failure (:324, :326). That marker fails the cell (:385), so a withdraw cell cannot read the hole as absence.
    • A pkill failure is logged (:244-245). This one is real-path only: the dry-run stubs frr_restart_bgpd (:211), so no dry-run case exercises it.
  • Siblings:
    • Re-arm rejection is logged (:562).
    • An unreadable G2 baseline FAILs the gate (:417-418).
    • An unreadable final read-back FAILs junos-left-unchanged (:618-619).
  • Evidence: stub_ok (:138) forces failures. Dry-run D (:659-662) covers a failing show and a rejected config, and E (:664-666) an unreadable config. Each must still produce summary.md and a tarball, with the right FAIL rows (:702-708, :721-722).
  • Not agreed: established_count never aborted the run. It is only called inside $(...), where bash clears errexit, and its last command was an echo, so the function always returned 0. Checked: the 61817bb harness with its stubbed frrcmd exiting 141 on the json read still ended rc 0, with summary.md and the tarball.
  • Its real defect ran the other way: a failed read became 0, matched a 0 baseline, and passed G5 and every per-cell reset check. With json forced to fail, the first prep's dry-run D showed step0-G5-soak PASS.
    • Changed: established_count prints nothing when the read fails (:330-339); sed quits on the first match, so no head pipe is left to SIGPIPE.
    • An unreadable counter now FAILs G4 (:446-448) and each cell (:387-389) as session-counter-unreadable, and G5 (:464-467) as "session counter unreadable after the soak". README updated (README.md:143-147).
    • D's json alternative used to cover nothing, since a failed read scored as 0. It is now scoped to one cell (^type1-ipmsi-bidir .*json, :661) and checks that cell's FAIL. New passes G and H break the counter at G4 and after the soak (:672-676). Assertion: :728-729.
    • Mutation-checked: restoring the first prep's established_count turns the assertion red. With every read broken, G4 and G5 PASS; with only G5's read broken, G5 blames the license (1->0). Dropping any one of the three call-site branches also turns it red.

C4: Junos static joins baked into junos-base.set

  • Wrong: the base config already held the exact static-group lines that the type7-*-junos-to-frr cells applied. The join cells tested nothing, and the withdraw-type7-* cells could never see the Junos Type-7 go away.
  • Changed:
    • The igmp/mld version/static group lines are removed from the base (junos-base.set:52-55 explains why).
    • The join cells set them (mvpn-gtm-interop.sh:516, :543).
    • The withdraw cells delete the static groups (:523, :548); the version lines stay until the cleanup restore.
    • The dry-run stub models committed static joins as Type-7 routes (:143-150).
  • Evidence: an assertion requires the joins to be absent at G4 and present in the join cells (:711-712). Mutation-checked.

C4, second half: the junos-to-frr cells still could not fail

  • Wrong: their FRR expectation \[7\] source S group G also matched FRR's own Type-7 for the same (S,G). That route came from the type7-*-frr-to-junos cell before and stayed up until the withdraw cell. Deleting the Junos join from type7-v4-junos-to-frr still gave PASS. Re-checked on the first prep: PASS.
  • Changed:
    • Both cells now require a path FRR received. bgp_mvpn_show_routes (bgpd/bgp_mvpn.c) prints (local) right after the group for FRR's own path, and a space or nothing for a received one, so the expectation ends ( |$) (mvpn-gtm-interop.sh:506-517, :541-544).
    • type5-v4-junos-to-frr had the same defect: a bare \[5\] source matched FRR's own Type-5 from the cell before, which stays up until withdraw-type5-v4. It gets the same fix (:496-500).
    • The FRR stub now prints a received Type-7 for each committed Junos join, and a received Type-5 once source-active-advertisement is committed, not for the empty fixtures (:151-162, :205-209). fixtures/good/frr.txt:21-22,26-27 now print FRR's own Type-5/Type-7 in the real (local) shape.
    • DRY_DROP_JUNOS (:192-199) keeps matching set lines off the stubbed box.
  • Evidence: dry-run F (:668-670) drops the three cells' Junos lines, and all three must FAIL as frr:missing (:725-726). Mutation-checked: reverting any one of the three expectations to its old form, or ignoring the drop knob, turns that assertion red. Deleting the Junos join from the type7-v4-junos-to-frr source now FAILs that cell.
  • Topology question for the owner (also README.md:214-224):
    • C-S sits behind the Junos. junos-base.set:59-60 gives it a static discard route there, and frr-base.conf:38-41 points FRR's RPF for it at the Junos.
    • For its own static join, Junos is therefore the upstream. Under RFC 6513 Section 5.1 UMH selection, it has no remote PE to send a C-multicast join to.
    • Expect both type7-*-junos-to-frr cells to FAIL as frr:missing(...) on a live box. The run then exits non-zero even if everything else passes. That FAIL is the honest answer; the old PASS was an accident.
    • A real Junos->FRR Type-7 test needs a C-S behind FRR: a source FRR advertises to Junos in unicast, carrying the VRF Route Import community that UMH selection reads, with no Junos-local route for it. That is a topology change, so it is left to you.
    • type5-v4-junos-to-frr was already listed in Known limits as likely to fail. That note now also says it counts only a received Type-5 (README.md:209-213).

I1: rollback by relative commit count

  • Wrong: configure exclusive is held per ssh invocation, not for the run. Any foreign commit or confirmed expiry shifts rollback $JUNOS_COMMITS. The baseline was also sanitized, so it could not be used as a restore source.
  • Changed:
    • G2 saves the raw running config on the box to JUNOS_RESTORE (:80, :421-423) and FAILs if the save fails.
    • One cleanup_junos restores it with load override + commit (:248-252). It is the same code for real and dry-run.
    • The cleanup transcript goes to junos-cleanup.txt (:257, :616), and the summary reports the real commit count (:617).
    • README no longer claims the lock is held for the run (README.md:47-51), and documents the restore point (README.md:60-64).
  • Evidence: the stub has no rollback and seeds a host-name that must come back (:266). The assertion cleanup restores the G2 restore point, not a commit count (:715-716) passes only via the restore point.

I2: IPv6 prefix in the IPv4 ssm-groups

  • Changed: line dropped, with the reason in a comment (junos-base.set:29-32). The default IPv6 SSM range already covers the v6 group under test.
  • Evidence: an assertion checks that the rendered config has no v6 prefix in ssm-groups (mvpn-gtm-interop.sh:719-720).

I3: set system host-name on a shared box

  • Changed: the line and its render token are dropped, and a comment says why (junos-base.set:21-22).
  • Evidence: the same assertion requires no set system statement (:719-720).

I4: hidden routes indistinguishable from missing ones

  • Changed:
    • show route table bgp.mvpn.0 hidden detail and its mvpn6 twin are captured (:302-311).
    • Hidden output is split from active output (:361-362), so a hidden route no longer satisfies a presence check. Such cells FAIL as junos:hidden(...) (:376).
    • New fixtures/{good,empty}/junos-hidden.txt.
  • Evidence: an assertion requires the empty fixture's type5-v4-frr-to-junos to FAIL as hidden (:717-718).

Suggestions (not addressed this cycle)

  • Type-5 source-state capture, a FIXTURE_MODE=stale negative control, the countfail missing-file shape and reading GW_V4 from the container are left unchanged. They are optional per the review and outside this fix set.

(kkroo drive session 637c9c. Ally run 253d689a is already queued for this push, so no review-request marker is posted.)

🤖 Generated with Claude Code

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.set now live in PRIV=$(mktemp -d) outside $OUT; only sanitized copies are written (:288-289), and log() sanitizes into run.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 -tzf shows no junos-rendered.set and 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 :104 refuses a short override before the trap and before step 0. Reproduced: CONFIRM_MIN=10 HOLD_SECONDS=600 exits 1 with FATAL: CONFIRM_MIN=10 and writes no summary.md.
  • prior:61817bb critical 3 — fixed — tests/junos-interop/mvpn-gtm-interop.sh:324 — a failing show appends !!!!! show failed instead of aborting, :385 turns that marker into a cell FAIL so a hole cannot read as absence, a rejected FRR config FAILs its cell at :349-350, and pkill is guarded at :245. Reproduced: forcing a show and a config rejection still yields summary.md and a tarball. The established_count arm of the original finding was answered differently and better — it never aborted, but a failed read scored as 0 and matched a 0 baseline; :335-339 now prints nothing and :388, :448, :465 FAIL on it.
  • prior:61817bb critical 4 — fixed — tests/junos-interop/junos-base.set:52 — the four version / static group lines 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, :544 is the sharper half: the three *-junos-to-frr cells 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 report wrote) and cleanup does load override + commit (:249-252); no rollback <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 IPv4 ssm-groups list, with the RFC 4607 reasoning in place; mvpn-gtm-interop.sh:719 asserts the rendered config contains no ssm-groups entry with a colon.
  • prior:61817bb important 3 — fixed — tests/junos-interop/junos-base.set:21 — the set system host-name line and its render token are gone, and the same assertion at mvpn-gtm-interop.sh:719 requires no ^set system statement.
  • prior:61817bb important 4 — fixed — tests/junos-interop/mvpn-gtm-interop.sh:309 — both hidden detail tables are captured (:309, :311), :361-362 splits hidden from active so a hidden route cannot satisfy a presence check, and :376 names it junos:hidden(...). The empty-fixture assertion at :717 exercises it.

Critical Issues (2)

  • [native-codex] tests/junos-interop/frr-base.conf:30 — rx0 enables ip pim / ipv6 pim but not ip igmp / ipv6 mld, so the ip igmp join-group at mvpn-gtm-interop.sh:503 and the ipv6 mld join-group at :538 have nothing on that interface to turn into local membership. pim_if_gm_join_add (pimd/pim_iface.c:1603) only needs pim_ifp, so the command is accepted and issues a socket join — but pim_if_membership_refresh (pimd/pim_nb_config.c:92) returns early when !pim_ifp->gm_enable, and gm_enable is set only by pim_cmd_gm_start (:391), i.e. by ip 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) — has ip igmp on the receiver ifl, as do pim_igmp_join_startup/r2/pimd.conf and multicast_ssm_topo1/r3/frr.conf, the only other in-repo users of join-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, both withdraw-type7-* cells and both *-junos-to-frr Type-7 cells — 6 of the 14 — fail on the lab box for a reason that is not interop. frr-base.conf:38 already 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 igmp and ipv6 mld to the rx0 stanza, and consider a G-gate that reads show ip mroute / show ip pim join for the (S,G) right after frr_up so a non-JOINED receiver stub is reported at gate 1 rather than as four interop failures in the matrix.
  • [gstack/review] tests/junos-interop/mvpn-gtm-interop.sh:251 — printf 'configure exclusive\nload override %s\ncommit\nexit\n' | jcmd sends a script to the Junos CLI on stdin, and the CLI does not abort the script when a line errors. If load override fails — restore file missing, /var/tmp reaped, the G2 save having landed elsewhere — the next line still runs, and a bare commit against an unchanged candidate is precisely how a pending commit confirmed is 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_junos returns whatever jcmd returned, and both call sites discard it (:257, :616, each || true).

    • Reproduced in the model with the cleanup command forced to fail: junos-cleanup.txt contains only the error, junos-final.set carries all 20 harness lines, and nothing retries.
    • Fix: capture the load/commit transcript and require commit complete before treating the restore as done, and do not send commit when the load reported an error — leaving the commit confirmed timer to revert is strictly better than confirming it. jcli() at :182-190 models only the successful load override path, so the model cannot currently express this; teaching it an error arm is the mutation test for the fix.

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 (:245 pkill ... || log, :569 clear bgp ... || true). So a run in which nothing actually restarted — pkill matched no bgpd, the clear was rejected — leaves the counter unmoved and the routes never withdrawn, and restart-bgpd and restart-session both 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_bgpd is stubbed to an echo at :211 and the good run still reports | PASS | restart-bgpd | and | PASS | restart-session |. One line fixes it — in a restart-* cell require n -gt BASE_ESTAB before re-baselining, and FAIL as no-restart-observed otherwise.
  • [gstack/review] tests/junos-interop/mvpn-gtm-interop.sh:617 — JUNOS_MADE=$JUNOS_COMMITS; JUNOS_COMMITS=0 disarms the EXIT-trap restore whether or not the restore at :616 worked, so the one automatic retry is spent on a path that cannot report failure (|| true on a pipeline through sanitize). Worse, summary.md then asserts the outcome: :585 prints | Junos commits made, then restored | N | unconditionally. Verified against a forced cleanup failure — the header reads 6 "commits made, then restored" over a box left fully mutated, while the only honest signal is the separate junos-left-unchanged FAIL two rows down. Gate the zeroing on a verified restore (the transcript cleanup_junos already writes to junos-cleanup.txt is enough), label the row restored / NOT RESTORED -- see junos-cleanup.txt from that same check, and print JUNOS_RESTORE in the summary so an operator holding only the tarball knows which file to load override by 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 99 on a missing file emits 0 from awk's END and 99 from the || arm, so $good_fail becomes two lines and [ '$good_fail' -eq 0 ] at :679 dies 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" frr and check "$exp_junos" junos both grep the whole $active, so the frr: / 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 :361 already splits by section header; splitting frr# from junos> 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 covers JUNOS_HOST, the jump host, the key path, the user and LOCAL_ADDR, but not JUNOS_LAN_IFL, which :118 requires from the environment with the same "see BLO-15630" framing as the endpoint and which is then written verbatim into junos-config.set and every cell transcript; nor JUNOS_RID when an operator overrides it away from its JUNOS_HOST default (:121), at which point the Junos local-address in 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 extra sed -e arms are free, and the whole-artifact assertions at :688-691 would then cover them too.
  • [native-codex] tests/junos-interop/mvpn-gtm-interop.sh:80 — JUNOS_RESTORE is 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 reporting junos-left-unchanged PASS. The prior review established that configure exclusive does not prevent this, and README.md no longer claims it does. /var/tmp/blo15579-baseline-$TS.conf plus 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_count arm was disputed with evidence — it never aborted — and then fixed for the more dangerous defect the review had missed, where a failed read scored as 0 and 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-frr expectations 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 -d guard at :688 and the grep -c form at :698 are both guarding the "absence reads as clean" direction specifically.
  • The topology question at README.md:214 is raised rather than papered over: the author states that both type7-*-junos-to-frr cells 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

  1. Fix Critical issues before merge.
  2. Address Important issues this cycle.
  3. 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>
@allyblockcast

allyblockcast Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

@ally please re-review at head 27278a2b024aefc9c2e040638e5cd2fb630c7182 — focus on the two Criticals (rx0 GM enablement + the cleanup commit refusal), and on whether the new sect() scoping changed any existing cell verdict.

All 8 findings land. Both Criticals were reproduced before fixing; one Suggestion is partly disputed on measurement and is called out below.

Critical

C1 — rx0 not GM-enabled. Correct, and confirmed at the source rather than from the description. pim_nb_config.c pim_if_membership_refresh() returns early at if (!pim_ifp->gm_enable); the only writers of gm_enable = true are pim_cmd_gm_start() (:392, i.e. ip igmp / ipv6 mld) and pim_if_new(..., gm, ...) (pim_iface.c:157). Every in-repo config that uses join-group pairs it — including bgp_mvpn_gtm/r2/pimd.conf, which carries ip igmp on the receiver ifl r2-eth1, exactly as you said.

Fixed in frr-base.conf, and your gate suggestion is in as G4b, placed after frr_up and before G5's 10-minute soak so it fails fast. It reads show running-config rather than a GM show: pim_vty.c gm_config_write() emits ip igmp / ipv6 mld only when gm_enable is set, so that is FRR's view of the flag and not an echo of our input file, and it does not depend on a show's output format across versions.

C2 — bare commit after a failed load override. Correct and the more dangerous of the two. One thing your fix sketch doesn't close, which changed the shape of the fix: requiring commit complete cannot work on its own, because a commit of an unchanged candidate succeeds — that is precisely what confirms the pending commit confirmed. So a transcript check always reads clean on the failure path. The defence has to be to never send commit:

cleanup_junos: file list $JUNOS_RESTORE → absent ⇒ refuse, emit "NOT committing",
               leave the commit-confirmed timer to auto-revert; return 1
               present ⇒ load override + commit, then require 'commit complete'

jcli() keeps modelling commit as succeeding unconditionally, on purpose — that is what makes dry-run I a real mutation test of the refusal rather than of the transcript check. Both call sites now consume the return value.

Important

I1 — restart cells satisfiable by nothing restarting. Correct. Now requires connectionsEstablished to have increased before re-baselining, else no-restart-observed(N->N). Your note that the self-check itself demonstrated it was the useful part: the dry-run stubbed frr_restart_bgpd to an echo, so I had to model the counter (DRY_ESTAB, bumped by the restart stub and by clear bgp) before the check could be tested at all. DRY_NO_RESTART freezes it — dry-run J.

I2 — JUNOS_COMMITS=0 disarming the trap unconditionally. Correct, including the summary asserting the outcome. Both now key off a verified restore; the row reads restored or **NOT RESTORED** — load override <path> by hand, and | Junos restore point | is printed so the tarball alone is actionable.

Suggestions

S2, 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 /var/tmp/blo15579-baseline-*.conf is present.

S1 — guard taken, premise not reproduced. The [ -f ] guard is in. But the stated mechanism — "emits 0 from awk's END and 99 from the || arm, so $good_fail becomes two lines" — does not reproduce on either awk available here:

awk awk '…END{print n+0}' /nonexistent 2>/dev/null || echo 99
mawk 1.3.4 20250131 99 — single line
busybox awk 99 — single line

Both treat a failed open as fatal and skip END, so the 0 is never emitted. I could not test gawk (not installed here), so I'm not claiming it holds everywhere — whether END runs after a failed open is implementation-defined, which is the actual reason the guard is worth keeping. Consequence for the suite: that assertion is a contract check, not a mutation test — it passes with the old form on this host. It is labelled as such in place rather than left looking like a guard with teeth.

Mutation testing

Every new guard was reverted alone, one mutation per run, and the suite re-run:

guard reverted ⇒
C1 config / C1 G4b gate RED / RED
C2 commit refusal RED (2 assertions)
I1 restart guard RED
I2 restore gating RED
S2 per-box scope RED
S3 sanitize ifl RED
S4 stale refusal / per-run path RED / RED
S1 countfail guard still GREEN — see above

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 sed.

Self-check at this head: ./tests/junos-interop/mvpn-gtm-interop.sh --dry-run → DRY-RUN OK, 31/31 assertions (was 20/20), bash -n clean.

Also added tests/junos-interop/.gitignore for blo15579-artifact-*: artifacts default to $PWD, so a git add -A from that directory would sweep a lab run's transcripts into a commit.

On the approval identity note

Agreed and unchanged — REVIEW_REQUIRED via classic branch protection (rules/branches/master returns []), no CODEOWNERS, requested_reviewers empty. I'm not requesting a human yet: a push dismisses an approval under this protection, so asking before the at-head review is clean would burn it. Once this review is clean I'll request individually and say so on BLO-15579.

@allyblockcast

allyblockcast Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

@ally please re-review at head 27278a2b024aefc9c2e040638e5cd2fb630c7182.

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 — pulls/137/reviews has only two stale COMMENTED objects (61817bb7, bf0340ce) and issues/137/comments has zero ## Ally comment-shaped reviews. review/ally-complete still reads pending — "Waiting for Ally review of head 27278a2".

New since the last request: CI is now terminal and green at this head. Run 38023238823 completed success at 13:58:41Z — 11/12 jobs success (1 docs job skipped by path filter), including both Ubuntu 22.04 and both Ubuntu 24.04 topotest shards and CI-Verdict. These are the first green topotest shards at this head.

Review focus:

  1. bf0340ce → 27278a2 addresses your 2 Critical + 2 Important + 4 Suggestion findings — please confirm the dispositions, in particular the two Critical items.
  2. The harness itself: one command, one artifact. Check the FRR-side teardown/idempotency and that the sanitizer does not leak device identifiers into the committed artifact.
  3. gate/ally-comment-findings is failure keyed on the previous head (bf0340c) — per BLO-36819 that gate never re-posts on this repo, so please do not read it as an at-head code finding.

@allyblockcast
allyblockcast Bot requested a review from kkroo October 10, 2026 20:11
@allyblockcast

allyblockcast Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

@kkroo — review requested (first time anyone has been asked on this PR; requested_reviewers was empty on both users and teams until just now).

State at head 27278a2:

  • CI fully green and terminal — run 38023238823 success at 13:58Z: 11 jobs success, 1 skipped (HTML documentation, docs path filter). Both 22.04 and both 24.04 test shards green.
  • Two Ally review rounds landed and dispositioned. The 8 findings from 61817bb were fixed at bf0340ce; the 2 Criticals Ally raised against bf0340ce (GM not enabled on the rx0 receiver stub; cleanup_junos confirming a commit confirmed after a failed load override) were fixed at 27278a2.
  • reviewDecision: REVIEW_REQUIRED, mergeStateStatus: BLOCKED. GitHub bars app/allyblockcast from approving a PR it authored, so no agent identity can clear this — it needs a human with write access.

What's worth your time: tests/junos-interop/junos-base.set. It is the one Junos-syntax risk in the change and has never been validated against a live box — gate G3 commit checks it before anything is built or committed, so a rejected stanza comes back verbatim and is a one-file edit. Everything else is bash with a --dry-run self-check (20/20 assertions, mutation-tested).

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant