Skip to content

Commit 35ab045

Browse files
committed
tests: topotests/ospf_topo1: negative-path ietf-ospf YANG tests
Add five negative tests that exercise the VALIDATE-phase rejection paths the config-write callbacks rely on: - test_ospf_yang_negative_missing_instance: commits targeting a control-plane-protocol name that doesn't match an FRR ospf / ospf6 instance must be rejected (resolve_instance helper). - test_ospf_yang_negative_missing_interface: commits targeting an unknown interface name under areas/area/interfaces/interface must be rejected (resolve_interface helper; closes the leafref-relaxation hole). - test_ospf_yang_negative_default_cost_on_normal_area: setting default-cost on the backbone area must be rejected (defence in depth alongside the YANG `when` clause). - test_ospf_yang_negative_v3_unsupported_interface_type: setting ospf6 interface-type=non-broadcast must be rejected (ospf6d only accepts broadcast / point-to-point / point-to-multipoint). - test_ospf_yang_area_delete_recreate_cleanup: a stub area with non-default summary and default-cost is deleted via the list-destroy path then recreated as a normal area; the previous per-leaf state must not survive. The helper _mgmt_commit_attempt issues the candidate change, applies, then aborts the candidate so the rejected entry doesn't survive into the next test's commit attempt. Tests confirm rejection by checking that the candidate value did NOT land in show running-config (more robust than parsing libyang's error string, which varies by version). Signed-off-by: Eric Parsonage <eric@eparsonage.com>
1 parent fb6dee9 commit 35ab045

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

tests/topotests/ospf_topo1/test_ospf_topo1.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,213 @@ def test_ospf_yang_area_summary_default_cost_config():
11821182
)
11831183

11841184

1185+
def _mgmt_commit_attempt(router, set_cmd):
1186+
"""Run a mgmt set-config + commit and return the vty output.
1187+
1188+
The caller verifies the rejection by confirming the candidate value
1189+
did NOT land in `show running-config` (more robust than parsing the
1190+
error string, which varies by libyang version). `mgmt commit abort`
1191+
follows the apply so a rejected candidate doesn't survive into the
1192+
next test's commit attempt.
1193+
"""
1194+
return router.vtysh_cmd(
1195+
"configure terminal file-lock\n"
1196+
"{}\n"
1197+
"mgmt commit apply\n"
1198+
"mgmt commit abort".format(set_cmd),
1199+
isjson=False,
1200+
)
1201+
1202+
1203+
def test_ospf_yang_negative_missing_instance():
1204+
"""Reject YANG config that targets an OSPF instance the daemon doesn't have.
1205+
1206+
The branch's resolve_instance helper rejects at NB_EV_VALIDATE when no
1207+
FRR-side ospf / ospf6 instance matches the control-plane-protocol name
1208+
key. Confirm a commit against name='ghost' does not land.
1209+
"""
1210+
tgen = get_topogen()
1211+
if tgen.routers_have_failure():
1212+
pytest.skip("skipped because of router(s) failure")
1213+
1214+
r1 = tgen.gears["r1"]
1215+
1216+
ghost_v2 = (
1217+
"/ietf-routing:routing/control-plane-protocols/"
1218+
"control-plane-protocol[type='ietf-ospf:ospfv2'][name='ghost']/"
1219+
"ietf-ospf:ospf/explicit-router-id"
1220+
)
1221+
_mgmt_commit_attempt(
1222+
r1,
1223+
"mgmt set-config {} 9.9.9.9".format(ghost_v2),
1224+
)
1225+
# The default instance is unchanged; the ghost one was never created.
1226+
running = r1.vtysh_cmd("show running-config ospfd")
1227+
assert "router-id 9.9.9.9" not in running, (
1228+
"ghost router-id must not have landed, got:\n" + running
1229+
)
1230+
1231+
ghost_v3 = (
1232+
"/ietf-routing:routing/control-plane-protocols/"
1233+
"control-plane-protocol[type='ietf-ospf:ospfv3'][name='ghost']/"
1234+
"ietf-ospf:ospf/explicit-router-id"
1235+
)
1236+
_mgmt_commit_attempt(
1237+
r1,
1238+
"mgmt set-config {} 9.9.9.9".format(ghost_v3),
1239+
)
1240+
running = r1.vtysh_cmd("show running-config ospf6d")
1241+
assert "router-id 9.9.9.9" not in running, (
1242+
"ghost ospf6 router-id must not have landed, got:\n" + running
1243+
)
1244+
1245+
1246+
def test_ospf_yang_negative_missing_interface():
1247+
"""Reject per-interface YANG config that names a non-existent interface.
1248+
1249+
frr-deviations-ietf-routing-ospf relaxes the RFC 9129 interface-name
1250+
leafref, so libyang no longer rejects names that aren't in
1251+
/ietf-interfaces. The branch's resolve_interface helper restores that
1252+
rejection inside the callback at NB_EV_VALIDATE.
1253+
"""
1254+
tgen = get_topogen()
1255+
if tgen.routers_have_failure():
1256+
pytest.skip("skipped because of router(s) failure")
1257+
1258+
r1 = tgen.gears["r1"]
1259+
1260+
bogus_v2 = (
1261+
_yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0")
1262+
+ "/interfaces/interface[name='r1-eth42']/cost"
1263+
)
1264+
_mgmt_commit_attempt(
1265+
r1,
1266+
"mgmt set-config {} 7".format(bogus_v2),
1267+
)
1268+
running = r1.vtysh_cmd("show running-config ospfd")
1269+
assert "r1-eth42" not in running, (
1270+
"interface r1-eth42 must not appear in running-config, got:\n" + running
1271+
)
1272+
1273+
bogus_v3 = (
1274+
_yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0")
1275+
+ "/interfaces/interface[name='r1-eth42']/cost"
1276+
)
1277+
_mgmt_commit_attempt(
1278+
r1,
1279+
"mgmt set-config {} 7".format(bogus_v3),
1280+
)
1281+
running = r1.vtysh_cmd("show running-config ospf6d")
1282+
assert "r1-eth42" not in running, (
1283+
"interface r1-eth42 must not appear in v3 running-config, got:\n" + running
1284+
)
1285+
1286+
1287+
def test_ospf_yang_negative_default_cost_on_normal_area():
1288+
"""Reject default-cost on a non-stub / non-NSSA area.
1289+
1290+
RFC 9129's `when` clause restricts default-cost to stub or NSSA areas;
1291+
the callback also rejects at VALIDATE as a defence-in-depth measure.
1292+
Area 0 is the backbone (normal); setting default-cost on it must fail.
1293+
"""
1294+
tgen = get_topogen()
1295+
if tgen.routers_have_failure():
1296+
pytest.skip("skipped because of router(s) failure")
1297+
1298+
r1 = tgen.gears["r1"]
1299+
1300+
area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0")
1301+
_mgmt_commit_attempt(
1302+
r1,
1303+
"mgmt set-config {}/default-cost 99".format(area_path),
1304+
)
1305+
running = r1.vtysh_cmd("show running-config ospfd")
1306+
assert "default-cost 99" not in running, (
1307+
"default-cost 99 must not appear on the backbone area, got:\n" + running
1308+
)
1309+
1310+
1311+
def test_ospf_yang_negative_v3_unsupported_interface_type():
1312+
"""Reject ospf6 interface-type enum values that ospf6d doesn't accept.
1313+
1314+
RFC 9129 declares non-broadcast and hybrid; ospf6d only supports
1315+
broadcast, point-to-point and point-to-multipoint. The callback must
1316+
reject the unsupported enum values at VALIDATE.
1317+
"""
1318+
tgen = get_topogen()
1319+
if tgen.routers_have_failure():
1320+
pytest.skip("skipped because of router(s) failure")
1321+
1322+
r1 = tgen.gears["r1"]
1323+
1324+
iface_v3 = (
1325+
_yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0")
1326+
+ "/interfaces/interface[name='r1-eth1']"
1327+
)
1328+
_mgmt_commit_attempt(
1329+
r1,
1330+
"mgmt set-config {}/interface-type non-broadcast".format(iface_v3),
1331+
)
1332+
running = r1.vtysh_cmd("show running-config ospf6d")
1333+
assert "non-broadcast" not in running, (
1334+
"unsupported interface-type must not appear in v3 running-config, "
1335+
"got:\n" + running
1336+
)
1337+
1338+
1339+
def test_ospf_yang_area_delete_recreate_cleanup():
1340+
"""Delete then recreate an area; per-leaf state must reset cleanly.
1341+
1342+
Sets a non-default summary and default-cost on a stub area, then deletes
1343+
the area via the list-destroy path, then recreates it as a normal area,
1344+
and confirms the previous stub / default-cost state is gone. Catches
1345+
regressions where destroy callbacks leave stale per-leaf state.
1346+
"""
1347+
tgen = get_topogen()
1348+
if tgen.routers_have_failure():
1349+
pytest.skip("skipped because of router(s) failure")
1350+
1351+
r1 = tgen.gears["r1"]
1352+
area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.51")
1353+
1354+
# Set area-type=stub + summary=false + default-cost=77 in one commit.
1355+
# libyang materialises the area list entry implicitly when a child leaf
1356+
# is set; no separate list-create step is needed.
1357+
r1.vtysh_cmd(
1358+
"configure terminal file-lock\n"
1359+
"mgmt set-config {}/area-type stub-area\n"
1360+
"mgmt set-config {}/summary false\n"
1361+
"mgmt set-config {}/default-cost 77\n"
1362+
"mgmt commit apply".format(area_path, area_path, area_path)
1363+
)
1364+
running = r1.vtysh_cmd("show running-config ospfd")
1365+
assert "area 0.0.0.51 stub no-summary" in running, running
1366+
assert "area 0.0.0.51 default-cost 77" in running, running
1367+
1368+
_clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.51")
1369+
running = r1.vtysh_cmd("show running-config ospfd")
1370+
assert "0.0.0.51" not in running, (
1371+
"area 0.0.0.51 must be gone after delete, got:\n" + running
1372+
)
1373+
1374+
# Recreate by setting only area-type=normal-area; previous stub /
1375+
# default-cost must not bleed through.
1376+
r1.vtysh_cmd(
1377+
"configure terminal file-lock\n"
1378+
"mgmt set-config {}/area-type normal-area\n"
1379+
"mgmt commit apply".format(area_path)
1380+
)
1381+
running = r1.vtysh_cmd("show running-config ospfd")
1382+
assert "area 0.0.0.51 stub" not in running, (
1383+
"stub setting must not survive delete + recreate, got:\n" + running
1384+
)
1385+
assert "area 0.0.0.51 default-cost" not in running, (
1386+
"default-cost must not survive delete + recreate, got:\n" + running
1387+
)
1388+
1389+
_clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.51")
1390+
1391+
11851392
def test_ospf_convergence():
11861393
"Test OSPF daemon convergence"
11871394
tgen = get_topogen()

0 commit comments

Comments
 (0)