@@ -747,6 +747,64 @@ def verify_eth_mac_mapping(c, log):
747747 raise Exception (f'Interface { ifname } has MAC { macs [ifname ]} , expected { expected_mac } - naming race?' )
748748 log .info ('eth0..eth7 MAC mapping verified' )
749749
750+ def verify_swapped_hwid_assignment (c , log , mac1 , mac2 ):
751+ """ eth1/eth2 were just explicitly reassigned to the opposite of their
752+ default MAC order (eth1 -> the numerically higher MAC, eth2 -> the
753+ lower one) - an ordinary, unambiguous rightful-owner rename that
754+ must take effect cleanly. This is the precondition for the next
755+ check: an existing box whose interface names do not follow
756+ ascending PCIe/MAC order, exactly like a real, already-provisioned
757+ system (its hw-id came from historical probe-order rescan, not
758+ from this sort). """
759+ log .info ('Verify the swapped eth1/eth2 hw-id assignment took effect' )
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 ('eth1' ) != mac2 :
771+ raise Exception (f'Interface eth1 has MAC { macs .get ("eth1" )} , expected { mac2 } '
772+ '- swapped hw-id assignment did not take effect' )
773+ if macs .get ('eth2' ) != mac1 :
774+ raise Exception (f'Interface eth2 has MAC { macs .get ("eth2" )} , expected { mac1 } '
775+ '- swapped hw-id assignment did not take effect' )
776+ log .info ('Swapped hw-id assignment confirmed - eth1/eth2 no longer follow ascending MAC order' )
777+
778+ def verify_pending_node_never_gets_wrong_hardware (c , log , ifname , wrong_mac ):
779+ """ Regression reported against an earlier PCIe/MAC-sorted replacement
780+ fill: with an unrelated interface fully removed in the same boot
781+ that `ifname`'s hw-id alone was cleared, there is no way to tell
782+ which of the freed candidates is genuinely `ifname`'s own
783+ hardware once its hw-id is gone - so the fix leaves `ifname`
784+ pending (safely unresolved) rather than guessing. `ifname` must
785+ never end up bound to `wrong_mac` - the OTHER freed interface's
786+ hardware - which would silently apply a setting configured on
787+ `ifname` (e.g. address) to a different physical NIC. `ifname`
788+ simply not existing (still pending) is the expected, safe
789+ outcome here, not a failure. """
790+ log .info (f'Verify { ifname } was never bound to the wrong physical NIC' )
791+ c .sendline ('ip -json link show | jq -r \' .[] | select(.ifname|test("^eth[0-9]+$")) | "\(.ifname) \(.address)"\' ' )
792+ c .expect (op_mode_prompt )
793+ lines = [l .strip () for l in c .before .decode (errors = 'replace' ).splitlines () if l .strip ()]
794+
795+ macs = {}
796+ for line in lines :
797+ parts = line .split ()
798+ if len (parts ) == 2 and re .fullmatch (r'eth\d+' , parts [0 ]):
799+ macs [parts [0 ]] = parts [1 ].lower ()
800+
801+ if macs .get (ifname ) == wrong_mac :
802+ raise Exception (f'Interface { ifname } has MAC { wrong_mac } - bound to the '
803+ "OTHER freed interface's hardware instead of its own "
804+ f'(a setting configured on { ifname } is now applied to '
805+ 'the wrong wire)' )
806+ log .info (f'{ ifname } was not bound to the wrong physical NIC' )
807+
750808def _image_update_cli_sequence (c , log , new_image_name , server_bind_host = '127.0.0.1' , use_vrf = False ):
751809 """One add-system-image/delete cycle for nested ISO over HTTP (optional Linux VRF + VyOS vrf arg)."""
752810 url = f'http://{ server_bind_host } :{ NESTED_HTTP_SERV_PORT } /{ NESTED_INNER_ISO_NAME } '
@@ -1342,15 +1400,56 @@ try:
13421400 elif args .ifnametest :
13431401 # A missing/deleted hw-id binding, or a fully deleted interface
13441402 # config, must not change the eth0..eth7 <-> MAC mapping after
1345- # the next reboot (regression check for the boot-time naming race).
1403+ # the next reboot (regression check for the boot-time naming
1404+ # race). Deliberately run as two INDEPENDENT reboots rather than
1405+ # one combined one: a pending node (hw-id cleared, node kept)
1406+ # only ever recovers its own hardware automatically when it's
1407+ # the sole candidate of its type this boot - if a different,
1408+ # unrelated interface's config were ALSO fully removed in the
1409+ # same boot, the two freed NICs become genuinely indistinguishable
1410+ # candidates and neither auto-resolves (see
1411+ # verify_pending_node_never_gets_wrong_hardware() below for why
1412+ # guessing there is unsafe). Testing each mechanism in its own
1413+ # boot is what each can actually guarantee.
13461414 log .info ('Running interface naming/hw-id persistence tests' )
13471415 del_idx , hwid_idx = random .sample (range (8 ), 2 )
1348- log .info (f'Deleting eth{ del_idx } entirely, removing hw-id only on eth{ hwid_idx } ' )
13491416
1417+ log .info (f'Deleting eth{ del_idx } entirely' )
13501418 c .sendline ('configure' )
13511419 c .expect (cfg_mode_prompt )
13521420 c .sendline (f'delete interfaces ethernet eth{ del_idx } ' )
13531421 c .expect (cfg_mode_prompt )
1422+ c .sendline ('commit' )
1423+ c .expect (cfg_mode_prompt )
1424+ c .sendline ('save' )
1425+ c .expect (cfg_mode_prompt )
1426+ c .sendline ('exit' )
1427+ c .expect (op_mode_prompt )
1428+
1429+ log .info ('Rebooting to verify the fully deleted interface backfills its own gap' )
1430+ c .sendline ('reboot now' )
1431+ waitForLogin (c , log )
1432+ loginVM (c , log )
1433+
1434+ log .info ('Collecting interface naming diagnostics' )
1435+ c .sendline ('show configuration commands | match "hw-id"' )
1436+ c .expect (op_mode_prompt )
1437+ c .sendline ('show interfaces ethernet' )
1438+ c .expect (op_mode_prompt )
1439+ c .sendline ('ip link show' )
1440+ c .expect (op_mode_prompt )
1441+ c .sendline ('show log | match "hw-id"' )
1442+ c .expect (op_mode_prompt )
1443+ c .sendline ('cat /run/vyos-net-name-resolve.json 2>/dev/null || true' )
1444+ c .expect (op_mode_prompt )
1445+ c .sendline ('show log kernel | match "eth"' )
1446+ c .expect (op_mode_prompt )
1447+
1448+ verify_eth_mac_mapping (c , log )
1449+
1450+ log .info (f"Removing hw-id only on eth{ hwid_idx } , keeping its node" )
1451+ c .sendline ('configure' )
1452+ c .expect (cfg_mode_prompt )
13541453 c .sendline (f'delete interfaces ethernet eth{ hwid_idx } hw-id' )
13551454 c .expect (cfg_mode_prompt )
13561455 c .sendline ('commit' )
@@ -1360,7 +1459,7 @@ try:
13601459 c .sendline ('exit' )
13611460 c .expect (op_mode_prompt )
13621461
1363- log .info ('Rebooting to verify interface naming survives across reboot ' )
1462+ log .info ('Rebooting to verify the pending node reclaims its own hardware ' )
13641463 c .sendline ('reboot now' )
13651464 waitForLogin (c , log )
13661465 loginVM (c , log )
@@ -1381,6 +1480,85 @@ try:
13811480
13821481 verify_eth_mac_mapping (c , log )
13831482
1483+ # Second, separate regression: a settings-bearing node must
1484+ # reclaim its OWN hardware, not whatever an ascending PCIe/MAC
1485+ # sort hands it, when an unrelated interface is fully removed in
1486+ # the same boot. The harness's own MACs are sequential
1487+ # (macbase:00..07), so a random del_idx/hwid_idx pair here would
1488+ # never expose this - a fresh install's initial bootstrap already
1489+ # sorts names and MACs together. Deterministically invert eth1's
1490+ # and eth2's hw-id first, so their names no longer follow
1491+ # ascending MAC order - exactly like any already-provisioned box,
1492+ # whose hw-id came from historical probe-order rescan rather than
1493+ # this sort.
1494+ log .info ('Simulating an existing box whose interface names do not '
1495+ 'follow PCIe/MAC order, then replacing one NIC while a '
1496+ 'different, unrelated interface is fully removed' )
1497+ mac1 = f'{ macbase } :01' .lower ()
1498+ mac2 = f'{ macbase } :02' .lower ()
1499+
1500+ c .sendline ('configure' )
1501+ c .expect (cfg_mode_prompt )
1502+ c .sendline ('delete interfaces ethernet eth1 hw-id' )
1503+ c .expect (cfg_mode_prompt )
1504+ c .sendline ('delete interfaces ethernet eth2 hw-id' )
1505+ c .expect (cfg_mode_prompt )
1506+ c .sendline (f"set interfaces ethernet eth1 hw-id '{ mac2 } '" )
1507+ c .expect (cfg_mode_prompt )
1508+ c .sendline (f"set interfaces ethernet eth2 hw-id '{ mac1 } '" )
1509+ c .expect (cfg_mode_prompt )
1510+ c .sendline ("set interfaces ethernet eth2 address '10.99.2.1/24'" )
1511+ c .expect (cfg_mode_prompt )
1512+ c .sendline ('commit' )
1513+ c .expect (cfg_mode_prompt )
1514+ c .sendline ('save' )
1515+ c .expect (cfg_mode_prompt )
1516+ c .sendline ('exit' )
1517+ c .expect (op_mode_prompt )
1518+
1519+ log .info ('Rebooting to establish the swapped hw-id assignment as '
1520+ "this box's existing state" )
1521+ c .sendline ('reboot now' )
1522+ waitForLogin (c , log )
1523+ loginVM (c , log )
1524+
1525+ verify_swapped_hwid_assignment (c , log , mac1 , mac2 )
1526+
1527+ log .info ('Fully removing eth1 while only clearing eth2\' s hw-id, '
1528+ 'keeping its address - the exact shape reported to '
1529+ 'silently bind a configured node to the wrong physical NIC' )
1530+ c .sendline ('configure' )
1531+ c .expect (cfg_mode_prompt )
1532+ c .sendline ('delete interfaces ethernet eth1' )
1533+ c .expect (cfg_mode_prompt )
1534+ c .sendline ('delete interfaces ethernet eth2 hw-id' )
1535+ c .expect (cfg_mode_prompt )
1536+ c .sendline ('commit' )
1537+ c .expect (cfg_mode_prompt )
1538+ c .sendline ('save' )
1539+ c .expect (cfg_mode_prompt )
1540+ c .sendline ('exit' )
1541+ c .expect (op_mode_prompt )
1542+
1543+ log .info ('Rebooting to check eth2 is never bound to the wrong physical NIC' )
1544+ c .sendline ('reboot now' )
1545+ waitForLogin (c , log )
1546+ loginVM (c , log )
1547+
1548+ log .info ('Collecting mac-order-mismatch diagnostics' )
1549+ c .sendline ('show configuration commands | match "hw-id"' )
1550+ c .expect (op_mode_prompt )
1551+ c .sendline ('show interfaces ethernet' )
1552+ c .expect (op_mode_prompt )
1553+ c .sendline ('ip link show' )
1554+ c .expect (op_mode_prompt )
1555+ c .sendline ('show log | match "hw-id"' )
1556+ c .expect (op_mode_prompt )
1557+ c .sendline ('cat /run/vyos-net-name-resolve.json 2>/dev/null || true' )
1558+ c .expect (op_mode_prompt )
1559+
1560+ verify_pending_node_never_gets_wrong_hardware (c , log , 'eth2' , mac2 )
1561+
13841562 elif args .raid :
13851563 # Verify RAID subsystem - by deleting a disk and re-create the array
13861564 # from scratch
0 commit comments