@@ -722,6 +722,64 @@ def verify_eth_mac_mapping(c, log):
722722 raise Exception (f'Interface { ifname } has MAC { macs [ifname ]} , expected { expected_mac } - naming race?' )
723723 log .info ('eth0..eth7 MAC mapping verified' )
724724
725+ def verify_swapped_hwid_assignment (c , log , mac1 , mac2 ):
726+ """ eth1/eth2 were just explicitly reassigned to the opposite of their
727+ default MAC order (eth1 -> the numerically higher MAC, eth2 -> the
728+ lower one) - an ordinary, unambiguous rightful-owner rename that
729+ must take effect cleanly. This is the precondition for the next
730+ check: an existing box whose interface names do not follow
731+ ascending PCIe/MAC order, exactly like a real, already-provisioned
732+ system (its hw-id came from historical probe-order rescan, not
733+ from this sort). """
734+ log .info ('Verify the swapped eth1/eth2 hw-id assignment took effect' )
735+ c .sendline ('ip -json link show | jq -r \' .[] | select(.ifname|test("^eth[0-9]+$")) | "\(.ifname) \(.address)"\' ' )
736+ c .expect (op_mode_prompt )
737+ lines = [l .strip () for l in c .before .decode (errors = 'replace' ).splitlines () if l .strip ()]
738+
739+ macs = {}
740+ for line in lines :
741+ parts = line .split ()
742+ if len (parts ) == 2 and re .fullmatch (r'eth\d+' , parts [0 ]):
743+ macs [parts [0 ]] = parts [1 ].lower ()
744+
745+ if macs .get ('eth1' ) != mac2 :
746+ raise Exception (f'Interface eth1 has MAC { macs .get ("eth1" )} , expected { mac2 } '
747+ '- swapped hw-id assignment did not take effect' )
748+ if macs .get ('eth2' ) != mac1 :
749+ raise Exception (f'Interface eth2 has MAC { macs .get ("eth2" )} , expected { mac1 } '
750+ '- swapped hw-id assignment did not take effect' )
751+ log .info ('Swapped hw-id assignment confirmed - eth1/eth2 no longer follow ascending MAC order' )
752+
753+ def verify_pending_node_never_gets_wrong_hardware (c , log , ifname , wrong_mac ):
754+ """ Regression reported against an earlier PCIe/MAC-sorted replacement
755+ fill: with an unrelated interface fully removed in the same boot
756+ that `ifname`'s hw-id alone was cleared, there is no way to tell
757+ which of the freed candidates is genuinely `ifname`'s own
758+ hardware once its hw-id is gone - so the fix leaves `ifname`
759+ pending (safely unresolved) rather than guessing. `ifname` must
760+ never end up bound to `wrong_mac` - the OTHER freed interface's
761+ hardware - which would silently apply a setting configured on
762+ `ifname` (e.g. address) to a different physical NIC. `ifname`
763+ simply not existing (still pending) is the expected, safe
764+ outcome here, not a failure. """
765+ log .info (f'Verify { ifname } was never bound to the wrong physical NIC' )
766+ c .sendline ('ip -json link show | jq -r \' .[] | select(.ifname|test("^eth[0-9]+$")) | "\(.ifname) \(.address)"\' ' )
767+ c .expect (op_mode_prompt )
768+ lines = [l .strip () for l in c .before .decode (errors = 'replace' ).splitlines () if l .strip ()]
769+
770+ macs = {}
771+ for line in lines :
772+ parts = line .split ()
773+ if len (parts ) == 2 and re .fullmatch (r'eth\d+' , parts [0 ]):
774+ macs [parts [0 ]] = parts [1 ].lower ()
775+
776+ if macs .get (ifname ) == wrong_mac :
777+ raise Exception (f'Interface { ifname } has MAC { wrong_mac } - bound to the '
778+ "OTHER freed interface's hardware instead of its own "
779+ f'(a setting configured on { ifname } is now applied to '
780+ 'the wrong wire)' )
781+ log .info (f'{ ifname } was not bound to the wrong physical NIC' )
782+
725783def _image_update_cli_sequence (c , log , new_image_name , server_bind_host = '127.0.0.1' , use_vrf = False ):
726784 """One add-system-image/delete cycle for nested ISO over HTTP (optional Linux VRF + VyOS vrf arg)."""
727785 url = f'http://{ server_bind_host } :{ NESTED_HTTP_SERV_PORT } /{ NESTED_INNER_ISO_NAME } '
@@ -1317,15 +1375,56 @@ try:
13171375 elif args .ifnametest :
13181376 # A missing/deleted hw-id binding, or a fully deleted interface
13191377 # config, must not change the eth0..eth7 <-> MAC mapping after
1320- # the next reboot (regression check for the boot-time naming race).
1378+ # the next reboot (regression check for the boot-time naming
1379+ # race). Deliberately run as two INDEPENDENT reboots rather than
1380+ # one combined one: a pending node (hw-id cleared, node kept)
1381+ # only ever recovers its own hardware automatically when it's
1382+ # the sole candidate of its type this boot - if a different,
1383+ # unrelated interface's config were ALSO fully removed in the
1384+ # same boot, the two freed NICs become genuinely indistinguishable
1385+ # candidates and neither auto-resolves (see
1386+ # verify_pending_node_never_gets_wrong_hardware() below for why
1387+ # guessing there is unsafe). Testing each mechanism in its own
1388+ # boot is what each can actually guarantee.
13211389 log .info ('Running interface naming/hw-id persistence tests' )
13221390 del_idx , hwid_idx = random .sample (range (8 ), 2 )
1323- log .info (f'Deleting eth{ del_idx } entirely, removing hw-id only on eth{ hwid_idx } ' )
13241391
1392+ log .info (f'Deleting eth{ del_idx } entirely' )
13251393 c .sendline ('configure' )
13261394 c .expect (cfg_mode_prompt )
13271395 c .sendline (f'delete interfaces ethernet eth{ del_idx } ' )
13281396 c .expect (cfg_mode_prompt )
1397+ c .sendline ('commit' )
1398+ c .expect (cfg_mode_prompt )
1399+ c .sendline ('save' )
1400+ c .expect (cfg_mode_prompt )
1401+ c .sendline ('exit' )
1402+ c .expect (op_mode_prompt )
1403+
1404+ log .info ('Rebooting to verify the fully deleted interface backfills its own gap' )
1405+ c .sendline ('reboot now' )
1406+ waitForLogin (c , log )
1407+ loginVM (c , log )
1408+
1409+ log .info ('Collecting interface naming diagnostics' )
1410+ c .sendline ('show configuration commands | match "hw-id"' )
1411+ c .expect (op_mode_prompt )
1412+ c .sendline ('show interfaces ethernet' )
1413+ c .expect (op_mode_prompt )
1414+ c .sendline ('ip link show' )
1415+ c .expect (op_mode_prompt )
1416+ c .sendline ('show log | match "hw-id"' )
1417+ c .expect (op_mode_prompt )
1418+ c .sendline ('cat /run/vyos-net-name-resolve.json 2>/dev/null || true' )
1419+ c .expect (op_mode_prompt )
1420+ c .sendline ('show log kernel | match "eth"' )
1421+ c .expect (op_mode_prompt )
1422+
1423+ verify_eth_mac_mapping (c , log )
1424+
1425+ log .info (f"Removing hw-id only on eth{ hwid_idx } , keeping its node" )
1426+ c .sendline ('configure' )
1427+ c .expect (cfg_mode_prompt )
13291428 c .sendline (f'delete interfaces ethernet eth{ hwid_idx } hw-id' )
13301429 c .expect (cfg_mode_prompt )
13311430 c .sendline ('commit' )
@@ -1335,7 +1434,7 @@ try:
13351434 c .sendline ('exit' )
13361435 c .expect (op_mode_prompt )
13371436
1338- log .info ('Rebooting to verify interface naming survives across reboot ' )
1437+ log .info ('Rebooting to verify the pending node reclaims its own hardware ' )
13391438 c .sendline ('reboot now' )
13401439 waitForLogin (c , log )
13411440 loginVM (c , log )
@@ -1356,6 +1455,85 @@ try:
13561455
13571456 verify_eth_mac_mapping (c , log )
13581457
1458+ # Second, separate regression: a settings-bearing node must
1459+ # reclaim its OWN hardware, not whatever an ascending PCIe/MAC
1460+ # sort hands it, when an unrelated interface is fully removed in
1461+ # the same boot. The harness's own MACs are sequential
1462+ # (macbase:00..07), so a random del_idx/hwid_idx pair here would
1463+ # never expose this - a fresh install's initial bootstrap already
1464+ # sorts names and MACs together. Deterministically invert eth1's
1465+ # and eth2's hw-id first, so their names no longer follow
1466+ # ascending MAC order - exactly like any already-provisioned box,
1467+ # whose hw-id came from historical probe-order rescan rather than
1468+ # this sort.
1469+ log .info ('Simulating an existing box whose interface names do not '
1470+ 'follow PCIe/MAC order, then replacing one NIC while a '
1471+ 'different, unrelated interface is fully removed' )
1472+ mac1 = f'{ macbase } :01' .lower ()
1473+ mac2 = f'{ macbase } :02' .lower ()
1474+
1475+ c .sendline ('configure' )
1476+ c .expect (cfg_mode_prompt )
1477+ c .sendline ('delete interfaces ethernet eth1 hw-id' )
1478+ c .expect (cfg_mode_prompt )
1479+ c .sendline ('delete interfaces ethernet eth2 hw-id' )
1480+ c .expect (cfg_mode_prompt )
1481+ c .sendline (f"set interfaces ethernet eth1 hw-id '{ mac2 } '" )
1482+ c .expect (cfg_mode_prompt )
1483+ c .sendline (f"set interfaces ethernet eth2 hw-id '{ mac1 } '" )
1484+ c .expect (cfg_mode_prompt )
1485+ c .sendline ("set interfaces ethernet eth2 address '10.99.2.1/24'" )
1486+ c .expect (cfg_mode_prompt )
1487+ c .sendline ('commit' )
1488+ c .expect (cfg_mode_prompt )
1489+ c .sendline ('save' )
1490+ c .expect (cfg_mode_prompt )
1491+ c .sendline ('exit' )
1492+ c .expect (op_mode_prompt )
1493+
1494+ log .info ('Rebooting to establish the swapped hw-id assignment as '
1495+ "this box's existing state" )
1496+ c .sendline ('reboot now' )
1497+ waitForLogin (c , log )
1498+ loginVM (c , log )
1499+
1500+ verify_swapped_hwid_assignment (c , log , mac1 , mac2 )
1501+
1502+ log .info ('Fully removing eth1 while only clearing eth2\' s hw-id, '
1503+ 'keeping its address - the exact shape reported to '
1504+ 'silently bind a configured node to the wrong physical NIC' )
1505+ c .sendline ('configure' )
1506+ c .expect (cfg_mode_prompt )
1507+ c .sendline ('delete interfaces ethernet eth1' )
1508+ c .expect (cfg_mode_prompt )
1509+ c .sendline ('delete interfaces ethernet eth2 hw-id' )
1510+ c .expect (cfg_mode_prompt )
1511+ c .sendline ('commit' )
1512+ c .expect (cfg_mode_prompt )
1513+ c .sendline ('save' )
1514+ c .expect (cfg_mode_prompt )
1515+ c .sendline ('exit' )
1516+ c .expect (op_mode_prompt )
1517+
1518+ log .info ('Rebooting to check eth2 is never bound to the wrong physical NIC' )
1519+ c .sendline ('reboot now' )
1520+ waitForLogin (c , log )
1521+ loginVM (c , log )
1522+
1523+ log .info ('Collecting mac-order-mismatch diagnostics' )
1524+ c .sendline ('show configuration commands | match "hw-id"' )
1525+ c .expect (op_mode_prompt )
1526+ c .sendline ('show interfaces ethernet' )
1527+ c .expect (op_mode_prompt )
1528+ c .sendline ('ip link show' )
1529+ c .expect (op_mode_prompt )
1530+ c .sendline ('show log | match "hw-id"' )
1531+ c .expect (op_mode_prompt )
1532+ c .sendline ('cat /run/vyos-net-name-resolve.json 2>/dev/null || true' )
1533+ c .expect (op_mode_prompt )
1534+
1535+ verify_pending_node_never_gets_wrong_hardware (c , log , 'eth2' , mac2 )
1536+
13591537 elif args .raid :
13601538 # Verify RAID subsystem - by deleting a disk and re-create the array
13611539 # from scratch
0 commit comments