@@ -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+
11851392def test_ospf_convergence ():
11861393 "Test OSPF daemon convergence"
11871394 tgen = get_topogen ()
0 commit comments