|
16 | 16 | import os |
17 | 17 | import sys |
18 | 18 | import json |
| 19 | +import time |
19 | 20 | import pytest |
20 | 21 | import functools |
21 | 22 |
|
@@ -134,6 +135,92 @@ def _bgp_check_conditional_static_routes_from_r2(): |
134 | 135 | _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) |
135 | 136 | assert result is None, "R1 SHOULD receive 172.16.255.2/32 from R2" |
136 | 137 |
|
| 138 | + # Once the conditional advertisement has been observed on R1, confirm the |
| 139 | + # route remains stable across at least one conditional-advertisement scanner |
| 140 | + # cycle on R2 (configured at 5s in r2/bgpd.conf). A regression on R2 that |
| 141 | + # makes the scanner advertise the prefix and then a downstream code path |
| 142 | + # withdraw it again every cycle (for example, mpath bookkeeping incorrectly |
| 143 | + # flagging BGP_PATH_MULTIPATH_CHG on the still-best path, triggering |
| 144 | + # group_announce_route() through deny-all route-map out) shows up here as |
| 145 | + # either: |
| 146 | + # (a) R1 losing 172.16.255.2/32 during the wait, or |
| 147 | + # (b) R1's per-prefix BGP dest version moving forward, because the route |
| 148 | + # was removed and re-added in the RIB at least once. |
| 149 | + # Either condition causes the assertion below to fail deterministically, |
| 150 | + # instead of leaving this test as a probabilistic poller that catches the |
| 151 | + # flap only when its `wait=1` poll lands in the ~100ms window where the |
| 152 | + # route happens to be present. |
| 153 | + def _r1_prefix_version(): |
| 154 | + output = json.loads(r1.vtysh_cmd("show bgp ipv4 unicast json")) |
| 155 | + paths = (output.get("routes") or {}).get("172.16.255.2/32") |
| 156 | + return paths[0].get("version") if paths else None |
| 157 | + |
| 158 | + def _r2_cond_adv_timer_remain(): |
| 159 | + # `bgpTimerUntilConditionalAdvertisementsSec` is sourced from |
| 160 | + # `event_timer_remain_second(bgp->t_condition_check)` in bgpd; it |
| 161 | + # counts down each second toward 0 and then jumps back up to |
| 162 | + # `bgp_conditional-advertisement timer` (5s in r2/bgpd.conf) when the |
| 163 | + # scanner re-arms its own timer at the top of |
| 164 | + # `bgp_conditional_adv_timer()`. |
| 165 | + output = json.loads(r2.vtysh_cmd("show bgp neighbors 192.168.1.1 json")) |
| 166 | + return ( |
| 167 | + output.get("192.168.1.1", {}) |
| 168 | + .get("bgpTimerUntilConditionalAdvertisementsSec") |
| 169 | + ) |
| 170 | + |
| 171 | + initial_version = _r1_prefix_version() |
| 172 | + assert initial_version is not None, ( |
| 173 | + "R1 lost 172.16.255.2/32 immediately after observing it; " |
| 174 | + "R2 is flapping the conditional advertisement" |
| 175 | + ) |
| 176 | + |
| 177 | + initial_timer = _r2_cond_adv_timer_remain() |
| 178 | + assert initial_timer is not None, ( |
| 179 | + "R2 is not reporting bgpTimerUntilConditionalAdvertisementsSec toward " |
| 180 | + "192.168.1.1; the conditional-advertisement scanner does not appear " |
| 181 | + "to be scheduled" |
| 182 | + ) |
| 183 | + |
| 184 | + # Wait for one full scanner cycle to complete on R2 by watching the |
| 185 | + # remaining-time value tick down and then jump back up. Using R2's own |
| 186 | + # running state is more reliable than a fixed sleep: the test waits exactly |
| 187 | + # as long as R2's scanner says it needs, and no longer. |
| 188 | + cycle_deadline = time.time() + 25 # >= 4 cycles, well above any single 5s window |
| 189 | + previous_timer = initial_timer |
| 190 | + cycle_fired = False |
| 191 | + while time.time() < cycle_deadline: |
| 192 | + time.sleep(0.5) |
| 193 | + current_timer = _r2_cond_adv_timer_remain() |
| 194 | + if current_timer is None: |
| 195 | + # The scanner field disappeared mid-test; treat as failure below. |
| 196 | + break |
| 197 | + if current_timer > previous_timer: |
| 198 | + # Timer wrapped back up -> the scanner just fired one cycle. |
| 199 | + cycle_fired = True |
| 200 | + break |
| 201 | + previous_timer = current_timer |
| 202 | + |
| 203 | + assert cycle_fired, ( |
| 204 | + "R2's conditional-advertisement scanner did not fire within 25s " |
| 205 | + "(initial bgpTimerUntilConditionalAdvertisementsSec={}, last={}); " |
| 206 | + "scanner appears stuck".format(initial_timer, previous_timer) |
| 207 | + ) |
| 208 | + |
| 209 | + final_version = _r1_prefix_version() |
| 210 | + assert final_version is not None, ( |
| 211 | + "R1 lost 172.16.255.2/32 during a conditional-advertisement scanner " |
| 212 | + "cycle; R2 is flapping the conditional advertisement " |
| 213 | + "(advertise/withdraw cycle)" |
| 214 | + ) |
| 215 | + assert final_version == initial_version, ( |
| 216 | + "R1's 172.16.255.2/32 per-prefix BGP dest version moved from {} to {} " |
| 217 | + "across a single R2 conditional-advertisement scanner cycle; the route " |
| 218 | + "was withdrawn and re-added at least once while the exist-map condition " |
| 219 | + "was still met (conditional advertisement is flapping on R2)".format( |
| 220 | + initial_version, final_version |
| 221 | + ) |
| 222 | + ) |
| 223 | + |
137 | 224 | step("Disable session between R2 and R3 again") |
138 | 225 | r3.vtysh_cmd( |
139 | 226 | """ |
|
0 commit comments