Skip to content

Commit 5f8f1ce

Browse files
committed
tests: topology test for OSPFv2 shutdown
Test the new OSPFv2 shutdown feature. The topology tests the following cases: 1. Regular OSPFv2 instance shutdown 2. OSPFv2 instance shutdown with graceful restart The difference between the cases is that when graceful restart is enabled the OSPFv2 instance sends a grace LSA in all OSPF enabled interfaces so the helper routers keep the routes until the instance is not shutdown anymore. Signed-off-by: Rafael Zalamena <rzalamena@opensourcerouting.org>
1 parent d747c2f commit 5f8f1ce

5 files changed

Lines changed: 390 additions & 0 deletions

File tree

tests/topotests/ospf_shutdown/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
interface r1-eth0
2+
ip address 192.168.1.1/24
3+
ip ospf area 0
4+
ip ospf hello-interval 2
5+
ip ospf dead-interval 8
6+
exit
7+
!
8+
interface r1-eth1
9+
ip address 192.168.2.1/24
10+
ip ospf area 0
11+
ip ospf hello-interval 2
12+
ip ospf dead-interval 8
13+
exit
14+
!
15+
interface lo
16+
ip address 10.254.254.1/32
17+
exit
18+
!
19+
router ospf
20+
redistribute connected
21+
capability opaque
22+
exit
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface r2-eth0
2+
ip address 192.168.1.2/24
3+
ip ospf area 0
4+
ip ospf hello-interval 2
5+
ip ospf dead-interval 8
6+
exit
7+
!
8+
interface lo
9+
ip address 10.254.254.2/32
10+
exit
11+
!
12+
router ospf
13+
redistribute connected
14+
capability opaque
15+
exit
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface r3-eth0
2+
ip address 192.168.2.2/24
3+
ip ospf area 0
4+
ip ospf hello-interval 2
5+
ip ospf dead-interval 8
6+
exit
7+
!
8+
interface lo
9+
ip address 10.254.254.3/32
10+
exit
11+
!
12+
router ospf
13+
redistribute connected
14+
capability opaque
15+
exit
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
#!/usr/bin/env python
2+
# SPDX-License-Identifier: ISC
3+
4+
#
5+
# test_ospf_shutdown.py
6+
# Part of NetDEF Topology Tests
7+
#
8+
# Copyright (c) 2026 by
9+
# Network Device Education Foundation, Inc. ("NetDEF")
10+
#
11+
12+
"""
13+
test_ospf_shutdown.py: test OSPFv2 instance shutdown feature.
14+
"""
15+
16+
import os
17+
import sys
18+
from functools import partial
19+
import pytest
20+
21+
# Save the Current Working Directory to find configuration files.
22+
CWD = os.path.dirname(os.path.realpath(__file__))
23+
sys.path.append(os.path.join(CWD, "../"))
24+
25+
# pylint: disable=C0413
26+
# Import topogen and topotest helpers
27+
from lib import topotest
28+
29+
# Required to instantiate the topology builder class.
30+
from lib.topogen import Topogen, get_topogen
31+
32+
pytestmark = [pytest.mark.ospfd]
33+
34+
35+
def setup_module(mod):
36+
topodef = {
37+
"s1": ("r1", "r2"),
38+
"s2": ("r1", "r3")
39+
}
40+
41+
tgen = Topogen(topodef, mod.__name__)
42+
tgen.start_topology()
43+
44+
router_list = tgen.routers()
45+
for rname, router in router_list.items():
46+
router.load_frr_config(f"{CWD}/{rname}/frr.conf")
47+
48+
tgen.start_router()
49+
50+
51+
def teardown_module():
52+
"Teardown the pytest environment"
53+
tgen = get_topogen()
54+
tgen.stop_topology()
55+
56+
57+
def expect_ospf_neighbor(router, neighbor):
58+
tgen = get_topogen()
59+
60+
expected = {
61+
"neighbors": {
62+
neighbor: [{
63+
"converged": "Full"
64+
}]
65+
}
66+
}
67+
test_func = partial(
68+
topotest.router_json_cmp,
69+
tgen.gears[router],
70+
"show ip ospf neighbor json",
71+
expected)
72+
_, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
73+
assert result is None, f"Router {router} failed to converge"
74+
75+
76+
def test_ospf_convergence():
77+
tgen = get_topogen()
78+
if tgen.routers_have_failure():
79+
pytest.skip(tgen.errors)
80+
81+
expect_ospf_neighbor("r1", "10.254.254.2")
82+
expect_ospf_neighbor("r1", "10.254.254.3")
83+
expect_ospf_neighbor("r2", "10.254.254.1")
84+
expect_ospf_neighbor("r3", "10.254.254.1")
85+
86+
87+
def expect_ospf_neighbor_gone(router, neighbor):
88+
tgen = get_topogen()
89+
90+
expected = {
91+
"neighbors": {
92+
neighbor: None
93+
}
94+
}
95+
test_func = partial(
96+
topotest.router_json_cmp,
97+
tgen.gears[router],
98+
"show ip ospf neighbor json",
99+
expected)
100+
_, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
101+
assert result is None, f"Neighbor {neighbor} still present in {router}"
102+
103+
104+
def expect_ospf_lsa(router, expected_lsa):
105+
tgen = get_topogen()
106+
107+
test_func = partial(
108+
topotest.router_json_cmp,
109+
tgen.gears[router],
110+
"show ip ospf database json",
111+
expected_lsa)
112+
_, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
113+
assert result is None, f"LSA {expected_lsa} not in {router}"
114+
115+
116+
def expect_ospf_route(router, expected_route):
117+
tgen = get_topogen()
118+
119+
test_func = partial(
120+
topotest.router_json_cmp,
121+
tgen.gears[router],
122+
"show ip ospf route json",
123+
expected_route)
124+
_, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
125+
assert result is None, f"Route {expected_route} not in {router}"
126+
127+
128+
def test_ospf_shutdown():
129+
tgen = get_topogen()
130+
if tgen.routers_have_failure():
131+
pytest.skip(tgen.errors)
132+
133+
#
134+
# Test OSPF instance shutdown by checking the neighbor session
135+
# state gone.
136+
#
137+
tgen.gears["r1"].vtysh_cmd("""
138+
configure terminal
139+
router ospf
140+
shutdown
141+
""")
142+
143+
expect_ospf_neighbor_gone("r1", "10.254.254.2")
144+
expect_ospf_neighbor_gone("r1", "10.254.254.3")
145+
expect_ospf_neighbor_gone("r2", "10.254.254.1")
146+
expect_ospf_neighbor_gone("r3", "10.254.254.1")
147+
148+
#
149+
# Don't expect grace LSA when graceful-restart is disabled
150+
#
151+
expect_ospf_lsa("r2", {
152+
"areas": {
153+
"0.0.0.0": {
154+
"linkLocalOpaqueLsa": None
155+
}
156+
}
157+
})
158+
159+
expect_ospf_lsa("r3", {
160+
"areas": {
161+
"0.0.0.0": {
162+
"linkLocalOpaqueLsa": None
163+
}
164+
}
165+
})
166+
167+
#
168+
# Don't expect external routes available
169+
#
170+
expect_ospf_route("r1", {
171+
"10.254.254.2/32": None,
172+
"10.254.254.3/32": None,
173+
})
174+
175+
expect_ospf_route("r2", {
176+
"10.254.254.1/32": None,
177+
"10.254.254.3/32": None,
178+
})
179+
180+
expect_ospf_route("r3", {
181+
"10.254.254.1/32": None,
182+
"10.254.254.2/32": None,
183+
})
184+
185+
#
186+
# Test OSPF instance by checking the neighbor session
187+
# state presence.
188+
#
189+
tgen.gears["r1"].vtysh_cmd("""
190+
configure terminal
191+
router ospf
192+
no shutdown
193+
""")
194+
195+
expect_ospf_neighbor("r1", "10.254.254.2")
196+
expect_ospf_neighbor("r1", "10.254.254.3")
197+
expect_ospf_neighbor("r2", "10.254.254.1")
198+
expect_ospf_neighbor("r3", "10.254.254.1")
199+
200+
#
201+
# Expect external routes available
202+
#
203+
expect_ospf_route("r1", {
204+
"10.254.254.2/32": {"routeType": "N E2"},
205+
"10.254.254.3/32": {"routeType": "N E2"},
206+
})
207+
208+
expect_ospf_route("r2", {
209+
"10.254.254.1/32": {"routeType": "N E2"},
210+
"10.254.254.3/32": {"routeType": "N E2"},
211+
})
212+
213+
expect_ospf_route("r3", {
214+
"10.254.254.1/32": {"routeType": "N E2"},
215+
"10.254.254.2/32": {"routeType": "N E2"},
216+
})
217+
218+
219+
def test_ospf_shutdown_graceful_restart():
220+
tgen = get_topogen()
221+
if tgen.routers_have_failure():
222+
pytest.skip(tgen.errors)
223+
224+
#
225+
# Configure graceful restart
226+
#
227+
tgen.gears["r1"].vtysh_cmd("""
228+
configure terminal
229+
router ospf
230+
graceful-restart
231+
""")
232+
tgen.gears["r2"].vtysh_cmd("""
233+
configure terminal
234+
router ospf
235+
graceful-restart helper enable
236+
""")
237+
tgen.gears["r3"].vtysh_cmd("""
238+
configure terminal
239+
router ospf
240+
graceful-restart helper enable
241+
""")
242+
243+
#
244+
# Test OSPF instance shutdown by checking the neighbor session
245+
# state gone.
246+
#
247+
tgen.gears["r1"].vtysh_cmd("""
248+
configure terminal
249+
router ospf
250+
shutdown
251+
""")
252+
253+
expect_ospf_neighbor_gone("r1", "10.254.254.2")
254+
expect_ospf_neighbor_gone("r1", "10.254.254.3")
255+
256+
#
257+
# Expect the grace LSA in the helper routers database
258+
#
259+
expect_ospf_lsa("r2", {
260+
"areas": {
261+
"0.0.0.0": {
262+
"linkLocalOpaqueLsa": [
263+
{
264+
"lsId": "3.0.0.0",
265+
"advertisedRouter":"10.254.254.1"
266+
}
267+
]
268+
}
269+
}
270+
})
271+
272+
expect_ospf_lsa("r3", {
273+
"areas": {
274+
"0.0.0.0": {
275+
"linkLocalOpaqueLsa": [
276+
{
277+
"lsId": "3.0.0.0",
278+
"advertisedRouter":"10.254.254.1"
279+
}
280+
]
281+
}
282+
}
283+
})
284+
285+
#
286+
# Expect external routes are *still* available in helper routers
287+
#
288+
expect_ospf_route("r2", {
289+
"10.254.254.1/32": {"routeType": "N E2"},
290+
"10.254.254.3/32": {"routeType": "N E2"},
291+
})
292+
293+
expect_ospf_route("r3", {
294+
"10.254.254.1/32": {"routeType": "N E2"},
295+
"10.254.254.2/32": {"routeType": "N E2"},
296+
})
297+
298+
#
299+
# Test OSPF instance by checking the neighbor session
300+
# state presence.
301+
#
302+
tgen.gears["r1"].vtysh_cmd("""
303+
configure terminal
304+
router ospf
305+
no shutdown
306+
""")
307+
308+
expect_ospf_neighbor("r1", "10.254.254.2")
309+
expect_ospf_neighbor("r1", "10.254.254.3")
310+
expect_ospf_neighbor("r2", "10.254.254.1")
311+
expect_ospf_neighbor("r3", "10.254.254.1")
312+
313+
#
314+
# Expect external routes are regenerated by r1
315+
#
316+
expect_ospf_route("r2", {
317+
"10.254.254.1/32": {"routeType": "N E2"},
318+
"10.254.254.3/32": {"routeType": "N E2"},
319+
})
320+
321+
expect_ospf_route("r3", {
322+
"10.254.254.1/32": {"routeType": "N E2"},
323+
"10.254.254.2/32": {"routeType": "N E2"},
324+
})
325+
326+
327+
def test_memory_leak():
328+
"Run the memory leak test and report results."
329+
tgen = get_topogen()
330+
if not tgen.is_memleak_enabled():
331+
pytest.skip("Memory leak test/report is disabled")
332+
333+
tgen.report_memory_leaks()
334+
335+
336+
if __name__ == "__main__":
337+
args = ["-s"] + sys.argv[1:]
338+
sys.exit(pytest.main(args))

0 commit comments

Comments
 (0)