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