From 1e5de1e476ea4d7b712f133786aeadfc16cb52cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:42:50 -0300 Subject: [PATCH 01/10] fix(dhcp): register the InfiniBand identity of a node that boots over IPoIB A node that discovery finds over ethernet is known by its ethernet mac. When that node boots over IPoIB, the request carries the InfiniBand identity of the adapter and not the ethernet mac. dhcpd finds no host entry for that identity and answers nothing, thus the node does not boot and the log gives no reason. dhcp.pm already gives hardware type 32 to a node whose mac attribute holds an 8 or 9 byte fabric address, but that needs the fabric address before the node boots, and discovery records the ethernet mac. A Mellanox adapter makes its port GUID from the ethernet mac, by the insertion of 03:00 in the middle. Thus the InfiniBand identity of the node is already known. Write it as a second host entry with the -xcat-ib suffix and hardware type 32, so a request over either fabric finds the node and receives the same address. Remove that entry with the node. Write the second entry only for a node whose network an IPoIB interface serves. A cluster with no InfiniBand keeps the host entries that it has today. This is for the ISC backend. makedhcp returns into the Kea code before this routine when Kea is the backend, and a Kea server does not answer an IPoIB client, thus there is nothing there for a second entry to answer. Recovered from the lenovobuild branch. Reimplemented against master: the original writes the second entry for every ethernet node, which makes two host entries for each node of a cluster that has no InfiniBand. --- xCAT-server/lib/xcat/plugins/dhcp.pm | 265 +++++++++++++++++++++++---- 1 file changed, 229 insertions(+), 36 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index c0874ce9f..414fab81d 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -243,21 +243,37 @@ sub _static_host_statements return $statements; } +sub _node_host_statements +{ + my ($node, $statements) = @_; + + return 'ddns-hostname \"' . $node . '\"; send host-name \"' + . $node . '\";' . ($statements || ''); +} + sub _add_isc_static_host { - my ($node, $hostname, $mac, $ip, $statements) = @_; + my ($node, $hostname, $mac, $hardwaretype, $mgtifname, $ip, $statements, + $has_infiniband_identity) = @_; return unless $ip && $ip ne 'DENIED'; + $mac = normalize_mac($mac) || $mac; _delete_isc_static_host($node); my $host_statements = _static_host_statements($statements); + my $hardware_kind = $hardwaretype == 32 ? 'infiniband' : 'ethernet'; push @dhcpconf, "#xCAT host declaration for $node aka host $hostname start\n"; push @dhcpconf, "host $hostname {\n"; - push @dhcpconf, " hardware ethernet $mac;\n"; + push @dhcpconf, " hardware $hardware_kind $mac;\n"; push @dhcpconf, " fixed-address $ip;\n"; push @dhcpconf, " $host_statements\n" if $host_statements; - push @dhcpconf, "} #xCAT host declaration for $node aka host $hostname end\n"; + push @dhcpconf, "}\n"; + push @dhcpconf, _infiniband_twin_static_lines( + $hostname, $mac, $hardwaretype, $mgtifname, $ip, + $host_statements, $has_infiniband_identity + ); + push @dhcpconf, "#xCAT host declaration for $node aka host $hostname end\n"; $restartdhcp = 1; } @@ -473,6 +489,7 @@ sub delnode if ($ent and $ent->{mac}) { my @macs = split(/\|/, $ent->{mac}); + my $has_infiniband_identity = _infiniband_identity_present(@macs); my $mace; my $count = 0; foreach $mace (@macs) @@ -487,12 +504,16 @@ sub delnode } #Default to hostname equal to nodename unless ($mac) { next; } #Skip corrupt format - if (!grep /:/, $mac) { + my $normalized_mac = normalize_mac($mac); + if ($normalized_mac) { + $mac = $normalized_mac; + } elsif (!grep /:/, $mac) { $mac = lc($mac); $mac =~ s/(\w{2})/$1:/g; $mac =~ s/:$//; } my $hostname = $hname; + my $hardwaretype = length($mac) == 23 || length($mac) == 26 ? 32 : 1; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); if ($client_nethash{$node}{mgtifname} =~ /hf/) { @@ -514,14 +535,21 @@ sub delnode print $omshell "remove\n"; print $omshell "close\n"; - if ($mac) + my ($ibnamecommands, $ibaddresscommands) = + _infiniband_twin_delete_commands( + $hostname, $mac, $hardwaretype, + $client_nethash{$node}{mgtifname}, + _omapi_pre_create_cleanup_supported(), + $has_infiniband_identity + ); + print $omshell $ibnamecommands if ($ibnamecommands); + + my $hardwarecommands = + _hardware_address_delete_commands($mac, $hardwaretype); + if ($hardwarecommands) { - print $omshell "new host\n"; - print $omshell "set hardware-address = " . $mac - . "\n"; #find and destroy mac conflict - print $omshell "open\n"; - print $omshell "remove\n"; - print $omshell "close\n"; + print $omshell $hardwarecommands; + print $omshell $ibaddresscommands if ($ibaddresscommands); } if ($inetn and _omapi_ip_lookup_supported()) { @@ -632,6 +660,166 @@ sub addnode6 { } +sub _infiniband_twin_mac +{ + my $mac = shift; + + $mac = normalize_mac($mac); + return unless defined($mac) and length($mac) == 17; + return substr($mac, 0, 8) . ":03:00" . substr($mac, 8); +} + +sub _infiniband_identity_present +{ + foreach my $entry (@_) { + my ($address) = split(/!/, $entry); + next unless defined($address); + $address =~ s/[:-]//g; + return 1 if $address =~ /\A(?:[[:xdigit:]]{16}|[[:xdigit:]]{18})\z/; + } + return 0; +} + +sub _infiniband_twin_identity +{ + my ($hostname, $mac, $hardwaretype, $mgtifname, + $has_infiniband_identity) = @_; + my $ibmac = _infiniband_twin_mac($mac); + + return if $hardwaretype != 1 + || !$ibmac + || !defined($mgtifname) + || $mgtifname !~ /^ib/ + || $has_infiniband_identity; + return ("$hostname-xcat-ib", $ibmac); +} + +sub _infiniband_twin_create_commands +{ + my ($hostname, $mac, $hardwaretype, $mgtifname, $ip, $hoststatements, + $cleanup_supported, $has_infiniband_identity) = @_; + my ($ibhostname, $ibmac) = _infiniband_twin_identity( + $hostname, $mac, $hardwaretype, $mgtifname, + $has_infiniband_identity + ); + + return unless $ibmac; + + my $commands = ''; + if ($cleanup_supported) { + $commands .= "new host\n" + . "set name = \"$ibhostname\"\n" + . "open\n" + . "remove\n" + . "close\n"; + } + $commands .= "new host\n" + . "set name = \"$ibhostname\"\n" + . "set hardware-address = $ibmac\n" + . "set dhcp-client-identifier = $ibmac\n" + . "set hardware-type = 32\n"; + + if ($ip eq "DENIED") { + $commands .= "set statements = \"deny booting;\"\n"; + } else { + $commands .= "set ip-address = $ip\n" if ($ip); + $commands .= "set statements = \"$hoststatements\"\n" if ($hoststatements); + } + + return $commands . "create\nclose\n"; +} + +sub _infiniband_twin_static_lines +{ + my ($hostname, $mac, $hardwaretype, $mgtifname, $ip, $hoststatements, + $has_infiniband_identity) = @_; + my ($ibhostname, $ibmac) = _infiniband_twin_identity( + $hostname, $mac, $hardwaretype, $mgtifname, + $has_infiniband_identity + ); + + return unless $ibmac && $ip && $ip ne 'DENIED'; + + my @lines = ( + "host $ibhostname {\n", + " hardware infiniband $ibmac;\n", + " fixed-address $ip;\n", + ); + push @lines, " $hoststatements\n" if $hoststatements; + push @lines, "}\n"; + return @lines; +} + +sub _infiniband_twin_delete_commands +{ + my ($hostname, $mac, $hardwaretype, $mgtifname, $cleanup_supported, + $has_infiniband_identity) = @_; + my $ibmac = _infiniband_twin_mac($mac); + return unless $ibmac; + if (!$cleanup_supported) { + my (undef, $current_ibmac) = _infiniband_twin_identity( + $hostname, $mac, $hardwaretype, $mgtifname, + $has_infiniband_identity + ); + return unless $current_ibmac; + } + my $ibhostname = "$hostname-xcat-ib"; + + my $namecommands = "new host\n" + . "set name = \"$ibhostname\"\n" + . "open\n" + . "remove\n" + . "close\n"; + my $addresscommands; + + if ($cleanup_supported) { + $addresscommands = "new host\n" + . "set hardware-address = $ibmac\n" + . "set hardware-type = 32\n" + . "open\n" + . "remove\n" + . "close\n"; + } + + return ($namecommands, $addresscommands); +} + +sub _hardware_address_delete_commands +{ + my ($mac, $hardwaretype) = @_; + return unless $mac; + + my $commands = "new host\n" + . "set hardware-address = $mac\n"; + $commands .= "set hardware-type = $hardwaretype\n" + if $hardwaretype != 1; + + return $commands . "open\nremove\nclose\n"; +} + +sub _infiniband_twin_update_commands +{ + my ($hostname, $mac, $hardwaretype, $mgtifname, $ip, $hoststatements, + $cleanup_supported, $has_infiniband_identity) = @_; + my ($namecommands, $addresscommands); + + if ($cleanup_supported) { + ($namecommands, $addresscommands) = + _infiniband_twin_delete_commands( + $hostname, $mac, $hardwaretype, $mgtifname, + $cleanup_supported, $has_infiniband_identity + ); + undef $addresscommands if $has_infiniband_identity; + } + my $createcommands = _infiniband_twin_create_commands( + $hostname, $mac, $hardwaretype, $mgtifname, $ip, + $hoststatements, 0, $has_infiniband_identity + ); + + return ($namecommands, $addresscommands, $createcommands); +} + + sub addnode { @@ -755,6 +943,7 @@ sub addnode } my @macs = split(/\|/, $ent->{mac}); + my $has_infiniband_identity = _infiniband_identity_present(@macs); my $mace; my $deflstaments = $lstatements; my $count = 0; @@ -780,6 +969,7 @@ sub addnode ); next; } + $mac = normalize_mac($mac); my $ip = getipaddr($hname, OnlyV4 => 1); if ($hname eq '*NOIP*') { $hname = $node . "-noip" . $mac; @@ -919,11 +1109,6 @@ sub addnode } else { - if (!grep /:/, $mac) { - $mac = lc($mac); - $mac =~ s/(\w{2})/$1:/g; - $mac =~ s/:$//; - } my $hostname = $hname; my $hardwaretype = 1; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); @@ -945,19 +1130,30 @@ sub addnode if (_isc_static_host_fallback()) { if ($ip ne "DENIED") { - if ($lstatements) { - $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";' . $lstatements; - } else { - $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";'; - } + $lstatements = _node_host_statements($node, $lstatements); } else { $lstatements = "deny booting;"; } - _add_isc_static_host($node, $hostname, $mac, $ip, $lstatements); + _add_isc_static_host( + $node, $hostname, $mac, $hardwaretype, + $client_nethash{$node}{mgtifname}, $ip, $lstatements, + $has_infiniband_identity + ); $count = $count + 2; next; } + if ($ip ne "DENIED") { + $lstatements = _node_host_statements($node, $lstatements); + } + my ($ibnamecommands, $ibaddresscommands, $ibcreatecommands) = + _infiniband_twin_update_commands( + $hostname, $mac, $hardwaretype, + $client_nethash{$node}{mgtifname}, $ip, $lstatements, + _omapi_pre_create_cleanup_supported(), + $has_infiniband_identity + ); + #syslog("local4|err", "Setting $node ($hname|$ip) to " . $mac); if (_omapi_pre_create_cleanup_supported()) { print $omshell "new host\n"; @@ -966,6 +1162,7 @@ sub addnode print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; + print $omshell $ibnamecommands if ($ibnamecommands); } if ($ip and $ip ne 'DENIED' and _omapi_ip_lookup_supported()) { print $omshell "new host\n"; @@ -975,12 +1172,9 @@ sub addnode print $omshell "close\n"; } if (_omapi_pre_create_cleanup_supported()) { - print $omshell "new host\n"; - print $omshell "set hardware-address = " . $mac - . "\n"; #find and destroy mac conflict - print $omshell "open\n"; - print $omshell "remove\n"; - print $omshell "close\n"; + print $omshell + _hardware_address_delete_commands($mac, $hardwaretype); + print $omshell $ibaddresscommands if ($ibaddresscommands); } print $omshell "new host\n"; print $omshell "set name = \"$hostname\"\n"; @@ -997,18 +1191,12 @@ sub addnode if ($ip) { print $omshell "set ip-address = $ip\n"; } - if ($lstatements) - { - $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";' . $lstatements; - - } else { - $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";'; - } print $omshell "set statements = \"$lstatements\"\n"; } print $omshell "create\n"; print $omshell "close\n"; + print $omshell $ibcreatecommands if ($ibcreatecommands); unless ($::XCATSITEVALS{externaldhcpservers}) { unless (grep /#definition for host $node aka host $hostname/, @dhcpconf) { @@ -3311,7 +3499,7 @@ sub kea_xnba_client_classes_for_nodes ); } -sub kea_normalize_mac +sub normalize_mac { my ($mac) = @_; @@ -3321,6 +3509,11 @@ sub kea_normalize_mac return lc($mac); } +sub kea_normalize_mac +{ + return normalize_mac(shift); +} + sub kea_node_reservations6 { my ( $backend, $config, $node ) = @_; From c045dd3ed3721109c33a4590ebfe7c4e87e7d374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:42:51 -0300 Subject: [PATCH 02/10] test(dhcp): pin the InfiniBand identity of a node that boots over IPoIB Drive the routine that makes the InfiniBand address and make sure that it gives the port GUID of the two adapters that this was validated with, and that it gives nothing for an address that is not a six byte ethernet mac. Pin that the second entry waits for an ethernet node whose network an IPoIB interface serves, that it carries hardware type 32 and the derived address, that it does not take the place of the entry of the node, and that removing the node removes it by name and by address. --- xCAT-test/unit/dhcp_infiniband_twin.t | 287 ++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 xCAT-test/unit/dhcp_infiniband_twin.t diff --git a/xCAT-test/unit/dhcp_infiniband_twin.t b/xCAT-test/unit/dhcp_infiniband_twin.t new file mode 100644 index 000000000..81cf13336 --- /dev/null +++ b/xCAT-test/unit/dhcp_infiniband_twin.t @@ -0,0 +1,287 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-server/lib"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use Test::More; + +$ENV{XCATCFG} ||= 'SQLite:/tmp'; + +my $source_dhcp_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm"; +if ( -f $source_dhcp_plugin ) { + require $source_dhcp_plugin; +} else { + require xCAT_plugin::dhcp; +} + +sub twin { return xCAT_plugin::dhcp::_infiniband_twin_mac( $_[0] ); } + +is( + xCAT_plugin::dhcp::normalize_mac('B8-3F-D2-4A-68-AA'), + 'b8:3f:d2:4a:68:aa', + 'the shared MAC normalizer canonicalizes case and separators', +); +is( twin('b8:3f:d2:4a:68:aa'), 'b8:3f:d2:03:00:4a:68:aa', + 'the port GUID of the first adapter is derived from its mac' ); +is( twin('b8:3f:d2:4a:68:b2'), 'b8:3f:d2:03:00:4a:68:b2', + 'the port GUID of the second adapter is derived from its mac' ); +is( twin('b8-3f-d2-4a-68-aa'), 'b8:3f:d2:03:00:4a:68:aa', + 'a dash-separated mac produces a canonical InfiniBand identity' ); +is( length( twin('b8:3f:d2:4a:68:aa') ), 23, + 'the derived address is eight bytes' ); + +foreach my $other ( 'b8:3f:d2:03:00:4a:68:aa', '00:11:22:33:44', '', 'notamac' ) { + is( twin($other), undef, "'$other' gives no derived address" ); +} +is( twin(undef), undef, 'no mac gives no derived address' ); +ok( + xCAT_plugin::dhcp::_infiniband_identity_present( + 'b8:3f:d2:4a:68:aa!node01', + 'b8:3f:d2:03:00:4a:68:aa!node01-ib', + ), + 'an explicit InfiniBand identity is detected in a node mac list', +); +ok( + xCAT_plugin::dhcp::_infiniband_identity_present( + 'b83fd203004a68aa!node01-ib', + ), + 'a colonless InfiniBand identity is detected in a node mac list', +); +ok( + !xCAT_plugin::dhcp::_infiniband_identity_present( + 'b8:3f:d2:4a:68:aa!node01', + ), + 'an Ethernet-only mac list needs the derived identity', +); + +is( + xCAT_plugin::dhcp::_node_host_statements('node01', ''), + 'ddns-hostname \"node01\"; send host-name \"node01\";', + 'the default host statements identify the node', +); +is( + xCAT_plugin::dhcp::_node_host_statements( + 'node01', 'filename = \"bootfile\";' + ), + 'ddns-hostname \"node01\"; send host-name \"node01\";filename = \"bootfile\";', + 'node identity is prepended to existing host statements', +); + +my $create_commands = <<'OMAPI'; +new host +set name = "node01-xcat-ib" +open +remove +close +new host +set name = "node01-xcat-ib" +set hardware-address = b8:3f:d2:03:00:4a:68:aa +set dhcp-client-identifier = b8:3f:d2:03:00:4a:68:aa +set hardware-type = 32 +set ip-address = 192.0.2.10 +set statements = "ddns-hostname \"node01\"; send host-name \"node01\";" +create +close +OMAPI + +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 1, 0 + ), + $create_commands, + 'an Ethernet identity on an IPoIB network creates the InfiniBand twin', +); + +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', 'DENIED', '', 1, 0 + ), + join( '', ( split( /^/m, $create_commands ) )[ 0 .. 9 ] ) + . "set statements = \"deny booting;\"\ncreate\nclose\n", + 'a denied Ethernet identity creates a denied InfiniBand twin', +); + +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 32, 'ib0', '192.0.2.10', '', 1, 1 + ), + undef, + 'an existing InfiniBand identity does not create another twin', +); +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'eth0', '192.0.2.10', '', 1, 0 + ), + undef, + 'an Ethernet network does not create an InfiniBand twin', +); + +my $create_without_cleanup = <<'OMAPI'; +new host +set name = "node01-xcat-ib" +set hardware-address = b8:3f:d2:03:00:4a:68:aa +set dhcp-client-identifier = b8:3f:d2:03:00:4a:68:aa +set hardware-type = 32 +set ip-address = 192.0.2.10 +set statements = "ddns-hostname \"node01\"; send host-name \"node01\";" +create +close +OMAPI +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 0, 0 + ), + $create_without_cleanup, + 'the Ubuntu-limited OMAPI path creates the twin without a failed-open cleanup', +); +my $create_without_address = $create_without_cleanup; +$create_without_address =~ s/^set ip-address = .*\n//m; +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', undef, + 'ddns-hostname \"node01\"; send host-name \"node01\";', 0, 0 + ), + $create_without_address, + 'an unresolved IP omits only the twin address', +); +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', '', 1, 1 + ), + undef, + 'an explicit InfiniBand identity suppresses the derived twin', +); + +is_deeply( + [ xCAT_plugin::dhcp::_infiniband_twin_static_lines( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'option host-name "node01";', 0 + ) ], + [ + "host node01-xcat-ib {\n", + " hardware infiniband b8:3f:d2:03:00:4a:68:aa;\n", + " fixed-address 192.0.2.10;\n", + " option host-name \"node01\";\n", + "}\n", + ], + 'the static-host fallback declares the derived InfiniBand identity', +); + +my $delete_name_commands = <<'OMAPI'; +new host +set name = "node01-xcat-ib" +open +remove +close +OMAPI +my $delete_address_commands = <<'OMAPI'; +new host +set hardware-address = b8:3f:d2:03:00:4a:68:aa +set hardware-type = 32 +open +remove +close +OMAPI + +is( + xCAT_plugin::dhcp::_hardware_address_delete_commands( + 'b8:3f:d2:4a:68:aa', 1 + ), + "new host\nset hardware-address = b8:3f:d2:4a:68:aa\nopen\nremove\nclose\n", + 'Ethernet cleanup keeps the legacy hardware-address lookup', +); +is( + xCAT_plugin::dhcp::_hardware_address_delete_commands( + 'b8:3f:d2:03:00:4a:68:aa', 32 + ), + $delete_address_commands, + 'InfiniBand cleanup includes its OMAPI hardware type', +); + +my @delete_commands = xCAT_plugin::dhcp::_infiniband_twin_delete_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', 1, 0 +); +is_deeply( + \@delete_commands, + [ $delete_name_commands, $delete_address_commands ], + 'removing a node produces separate name and address cleanup commands', +); +my @moved_network_delete_commands = + xCAT_plugin::dhcp::_infiniband_twin_delete_commands( + 'node01', 'b8-3f-d2-4a-68-aa', 1, 'eth0', 1, 1 + ); +is_deeply( + \@moved_network_delete_commands, + [ $delete_name_commands, $delete_address_commands ], + 'removing a node cleans up its old twin after network metadata changes', +); +my @limited_delete_commands = + xCAT_plugin::dhcp::_infiniband_twin_delete_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', 0, 0 + ); +is_deeply( + \@limited_delete_commands, + [ $delete_name_commands, undef ], + 'the Ubuntu-limited OMAPI path still removes the twin by name', +); +my @limited_ethernet_delete_commands = + xCAT_plugin::dhcp::_infiniband_twin_delete_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'eth0', 0, 0 + ); +is_deeply( + \@limited_ethernet_delete_commands, + [], + 'the Ubuntu-limited OMAPI path avoids a failed-open cleanup on Ethernet', +); + +my @moved_network_update_commands = + xCAT_plugin::dhcp::_infiniband_twin_update_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'eth0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 1, 0 + ); +is_deeply( + \@moved_network_update_commands, + [ $delete_name_commands, $delete_address_commands, undef ], + 're-registering a node on Ethernet removes its former InfiniBand twin', +); + +my @infiniband_update_commands = + xCAT_plugin::dhcp::_infiniband_twin_update_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 1, 0 + ); +is_deeply( + \@infiniband_update_commands, + [ $delete_name_commands, $delete_address_commands, $create_without_cleanup ], + 're-registering an IPoIB node replaces its generated twin', +); + +my @limited_update_commands = + xCAT_plugin::dhcp::_infiniband_twin_update_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 0, 0 + ); +is_deeply( + \@limited_update_commands, + [ undef, undef, $create_without_cleanup ], + 'the limited OMAPI path does not attempt a failed-open cleanup', +); + +my @explicit_identity_update_commands = + xCAT_plugin::dhcp::_infiniband_twin_update_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 1, 1 + ); +is_deeply( + \@explicit_identity_update_commands, + [ $delete_name_commands, undef, undef ], + 'updating an explicit InfiniBand identity does not remove it by address', +); + +done_testing(); From 498240ea26839a5007d507fe6d46c7aa3c41a879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:28:45 -0300 Subject: [PATCH 03/10] fix(dhcp): keep static host markers standalone Keep the end marker on its own line so deleting one generated host cannot consume the declaration that follows it. --- xCAT-server/lib/xcat/plugins/dhcp.pm | 55 ++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 414fab81d..c5d9f4f3c 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -214,22 +214,41 @@ sub _isc_static_host_fallback sub _delete_isc_static_host { - my $node = shift; + my ($node, $config, $hostname) = @_; + $config ||= \@dhcpconf; + + my $start_marker = defined($hostname) + ? qr/^#xCAT host declaration for \Q$node\E aka host \Q$hostname\E start$/ + : qr/^#xCAT host declaration for \Q$node\E aka host .* start$/; + my $end_marker = defined($hostname) + ? qr/^(?:\}\s*)?#xCAT host declaration for \Q$node\E aka host \Q$hostname\E end$/ + : qr/^(?:\}\s*)?#xCAT host declaration for \Q$node\E aka host .* end$/; my @updated; my $skip = 0; - foreach my $line (@dhcpconf) { - if ($line =~ /^#xCAT host declaration for \Q$node\E\b.* start$/) { + foreach my $line (@{$config}) { + if ($line =~ $start_marker) { $skip = 1; next; } - if ($skip && $line =~ /^#xCAT host declaration for \Q$node\E\b.* end$/) { + if ($skip && $line =~ $end_marker) { $skip = 0; next; } push @updated, $line unless $skip; } - @dhcpconf = @updated; + @{$config} = @updated; +} + +sub _begin_isc_static_host_update +{ + my ($node, $enabled, $config) = @_; + + return 0 unless $enabled; + $config ||= \@dhcpconf; + my $line_count = scalar(@{$config}); + _delete_isc_static_host($node, $config); + return scalar(@{$config}) != $line_count; } sub _static_host_statements @@ -254,26 +273,27 @@ sub _node_host_statements sub _add_isc_static_host { my ($node, $hostname, $mac, $hardwaretype, $mgtifname, $ip, $statements, - $has_infiniband_identity) = @_; + $has_infiniband_identity, $config) = @_; + $config ||= \@dhcpconf; return unless $ip && $ip ne 'DENIED'; $mac = normalize_mac($mac) || $mac; - _delete_isc_static_host($node); + _delete_isc_static_host($node, $config, $hostname); my $host_statements = _static_host_statements($statements); my $hardware_kind = $hardwaretype == 32 ? 'infiniband' : 'ethernet'; - push @dhcpconf, "#xCAT host declaration for $node aka host $hostname start\n"; - push @dhcpconf, "host $hostname {\n"; - push @dhcpconf, " hardware $hardware_kind $mac;\n"; - push @dhcpconf, " fixed-address $ip;\n"; - push @dhcpconf, " $host_statements\n" if $host_statements; - push @dhcpconf, "}\n"; - push @dhcpconf, _infiniband_twin_static_lines( + push @{$config}, "#xCAT host declaration for $node aka host $hostname start\n"; + push @{$config}, "host $hostname {\n"; + push @{$config}, " hardware $hardware_kind $mac;\n"; + push @{$config}, " fixed-address $ip;\n"; + push @{$config}, " $host_statements\n" if $host_statements; + push @{$config}, "}\n"; + push @{$config}, _infiniband_twin_static_lines( $hostname, $mac, $hardwaretype, $mgtifname, $ip, $host_statements, $has_infiniband_identity ); - push @dhcpconf, "#xCAT host declaration for $node aka host $hostname end\n"; + push @{$config}, "#xCAT host declaration for $node aka host $hostname end\n"; $restartdhcp = 1; } @@ -944,6 +964,9 @@ sub addnode my @macs = split(/\|/, $ent->{mac}); my $has_infiniband_identity = _infiniband_identity_present(@macs); + my $static_host_fallback = _isc_static_host_fallback(); + $restartdhcp = 1 + if _begin_isc_static_host_update($node, $static_host_fallback); my $mace; my $deflstaments = $lstatements; my $count = 0; @@ -1128,7 +1151,7 @@ sub addnode $hardwaretype = 32; } - if (_isc_static_host_fallback()) { + if ($static_host_fallback) { if ($ip ne "DENIED") { $lstatements = _node_host_statements($node, $lstatements); } else { From 1be564aef7a78a776aa0d0901284b9d0aab66e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:29:46 -0300 Subject: [PATCH 04/10] test(dhcp): preserve adjacent static hosts --- xCAT-test/unit/dhcp_static_host_markers.t | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 xCAT-test/unit/dhcp_static_host_markers.t diff --git a/xCAT-test/unit/dhcp_static_host_markers.t b/xCAT-test/unit/dhcp_static_host_markers.t new file mode 100644 index 000000000..5a87dabc3 --- /dev/null +++ b/xCAT-test/unit/dhcp_static_host_markers.t @@ -0,0 +1,157 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-server/lib"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use Test::More; + +$ENV{XCATCFG} ||= 'SQLite:/tmp'; + +my $source_dhcp_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm"; +if ( -f $source_dhcp_plugin ) { + require $source_dhcp_plugin; +} else { + require xCAT_plugin::dhcp; +} + +my @config; + +my @infiniband_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node00', 'node00', 'b8:3f:d2:03:00:4a:68:aa', 32, + 'ib0', '192.0.2.10', '', 1, \@infiniband_config, +); +like( + join( '', @infiniband_config ), + qr/^\s*hardware infiniband b8:3f:d2:03:00:4a:68:aa;$/m, + 'the static-host fallback keeps an explicit InfiniBand hardware type', +); +unlike( + join( '', @infiniband_config ), + qr/^\s*hardware ethernet b8:3f:d2:03:00:4a:68:aa;$/m, + 'the static-host fallback does not label an InfiniBand identity as Ethernet', +); + +my @dashed_mac_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node00', 'node00', 'B8-3F-D2-4A-68-AA', 1, + 'eth0', '192.0.2.11', '', 0, \@dashed_mac_config, +); +like( + join( '', @dashed_mac_config ), + qr/^\s*hardware ethernet b8:3f:d2:4a:68:aa;$/m, + 'the static-host fallback emits a valid canonical dashed MAC', +); + +xCAT_plugin::dhcp::_add_isc_static_host( + 'node01', 'node01', '00:11:22:33:44:55', 1, + 'eth0', '192.0.2.1', '', 0, \@config, +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node02', 'node02', '00:11:22:33:44:66', 1, + 'eth0', '192.0.2.2', '', 0, \@config, +); + +like( + join( '', @config ), + qr/^#xCAT host declaration for node01 aka host node01 end$/m, + 'the static host end marker occupies its own line', +); + +xCAT_plugin::dhcp::_delete_isc_static_host('node01', \@config); +my $remaining = join( '', @config ); +unlike($remaining, qr/\bnode01\b/, 'the selected static host is removed'); +like($remaining, qr/\bnode02\b/, 'the following static host is preserved'); + +my @legacy_config = ( + "#xCAT host declaration for node01 aka host node01 start\n", + "host node01 {\n", + " hardware ethernet 00:11:22:33:44:55;\n", + " fixed-address 192.0.2.1;\n", + "} #xCAT host declaration for node01 aka host node01 end\n", + "#xCAT host declaration for node02 aka host node02 start\n", + "host node02 {\n", + " hardware ethernet 00:11:22:33:44:66;\n", + " fixed-address 192.0.2.2;\n", + "} #xCAT host declaration for node02 aka host node02 end\n", +); +xCAT_plugin::dhcp::_delete_isc_static_host('node01', \@legacy_config); +my $legacy_remaining = join( '', @legacy_config ); +unlike($legacy_remaining, qr/\bnode01\b/, + 'a static host written by an older xCAT release is removed'); +like($legacy_remaining, qr/\bnode02\b/, + 'deleting an old-format host preserves the following host'); + +my @multi_host_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node03', 'node03', '00:11:22:33:44:77', 1, + 'eth0', '192.0.2.3', '', 0, \@multi_host_config, +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node03', 'node03-ib', '00:11:22:33:44:88', 32, + 'ib0', '192.0.2.3', '', 1, \@multi_host_config, +); +my $multi_host = join( '', @multi_host_config ); +like($multi_host, qr/^host node03 \{$/m, + 'adding a second identity preserves the first host for a node'); +like($multi_host, qr/^host node03-ib \{$/m, + 'adding a second identity records its own host for the node'); + +my @prefix_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node01', 'node01', '00:11:22:33:44:99', 1, + 'eth0', '192.0.2.1', '', 0, \@prefix_config, +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node01-ib', 'node01-ib', '00:11:22:33:44:aa', 1, + 'eth0', '192.0.2.4', '', 0, \@prefix_config, +); +xCAT_plugin::dhcp::_delete_isc_static_host('node01', \@prefix_config); +my $prefix_remaining = join( '', @prefix_config ); +unlike($prefix_remaining, qr/^host node01 \{$/m, + 'node-wide deletion removes the exact node'); +like($prefix_remaining, qr/^host node01-ib \{$/m, + 'node-wide deletion preserves a node with the same prefix'); + +my @replacement_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node04', 'node04-old', '00:11:22:33:44:bb', 1, + 'eth0', '192.0.2.5', '', 0, \@replacement_config, +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node04', 'node04-old-ib', '00:11:22:33:44:cc', 32, + 'ib0', '192.0.2.5', '', 1, \@replacement_config, +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node05', 'node05', '00:11:22:33:44:dd', 1, + 'eth0', '192.0.2.6', '', 0, \@replacement_config, +); +ok( + xCAT_plugin::dhcp::_begin_isc_static_host_update( + 'node04', 1, \@replacement_config, + ), + 'removing stale identities marks the static configuration as changed', +); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node04', 'node04-current', '00:11:22:33:44:ee', 1, + 'eth0', '192.0.2.5', '', 0, \@replacement_config, +); +my $replacement = join( '', @replacement_config ); +unlike($replacement, qr/^host node04-old(?:-ib)? \{$/m, + 're-registering a node removes identities that are no longer present'); +like($replacement, qr/^host node04-current \{$/m, + 're-registering a node writes its current identity'); +like($replacement, qr/^host node05 \{$/m, + 're-registering a node preserves other nodes'); +ok( + !xCAT_plugin::dhcp::_begin_isc_static_host_update( + 'node04', 0, \@replacement_config, + ), + 'a disabled static update leaves the configuration unchanged', +); + +done_testing(); From e392621593f0f36ac7f62799cebd87d20b1ded51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:21:29 -0300 Subject: [PATCH 05/10] fix(dhcp): align fabric identity handling --- xCAT-server/lib/xcat/plugins/dhcp.pm | 57 +++++++++++++++++++--------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index c5d9f4f3c..274e556cd 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -270,6 +270,14 @@ sub _node_host_statements . $node . '\";' . ($statements || ''); } +sub _noip_hostname +{ + my ($node, $mac) = @_; + my $hostname = $node . '-noip' . $mac; + $hostname =~ s/://g; + return $hostname; +} + sub _add_isc_static_host { my ($node, $hostname, $mac, $hardwaretype, $mgtifname, $ip, $statements, @@ -533,8 +541,10 @@ sub delnode $mac =~ s/:$//; } my $hostname = $hname; - my $hardwaretype = length($mac) == 23 || length($mac) == 26 ? 32 : 1; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); + my $hardwaretype = _hardware_type_for( + $mac, $client_nethash{$node}{mgtifname} + ); if ($client_nethash{$node}{mgtifname} =~ /hf/) { if (scalar(@macs) > 1) { @@ -693,13 +703,30 @@ sub _infiniband_identity_present { foreach my $entry (@_) { my ($address) = split(/!/, $entry); - next unless defined($address); - $address =~ s/[:-]//g; - return 1 if $address =~ /\A(?:[[:xdigit:]]{16}|[[:xdigit:]]{18})\z/; + $address = normalize_mac($address); + return 1 + if defined($address) + && (length($address) == 23 || length($address) == 26); } return 0; } +sub _hardware_type_for +{ + my ($mac, $mgtifname) = @_; + + return 37 if defined($mgtifname) && $mgtifname =~ /hf/; + $mac = normalize_mac($mac) || $mac || ''; + return 32 if length($mac) == 23 || length($mac) == 26; + return 1; +} + +sub _is_infiniband_interface +{ + my $mgtifname = shift; + return defined($mgtifname) && $mgtifname =~ /(?:^|!)ib[^!]*$/; +} + sub _infiniband_twin_identity { my ($hostname, $mac, $hardwaretype, $mgtifname, @@ -708,8 +735,7 @@ sub _infiniband_twin_identity return if $hardwaretype != 1 || !$ibmac - || !defined($mgtifname) - || $mgtifname !~ /^ib/ + || !_is_infiniband_interface($mgtifname) || $has_infiniband_identity; return ("$hostname-xcat-ib", $ibmac); } @@ -726,7 +752,7 @@ sub _infiniband_twin_create_commands return unless $ibmac; my $commands = ''; - if ($cleanup_supported) { + if ($cleanup_supported && _is_infiniband_interface($mgtifname)) { $commands .= "new host\n" . "set name = \"$ibhostname\"\n" . "open\n" @@ -792,7 +818,7 @@ sub _infiniband_twin_delete_commands . "close\n"; my $addresscommands; - if ($cleanup_supported) { + if ($cleanup_supported && _is_infiniband_interface($mgtifname)) { $addresscommands = "new host\n" . "set hardware-address = $ibmac\n" . "set hardware-type = 32\n" @@ -992,11 +1018,11 @@ sub addnode ); next; } + my $noip_mac = $mac; $mac = normalize_mac($mac); my $ip = getipaddr($hname, OnlyV4 => 1); if ($hname eq '*NOIP*') { - $hname = $node . "-noip" . $mac; - $hname =~ s/://g; + $hname = _noip_hostname($node, $noip_mac); $ip = 'DENIED'; # } #if 'guess_next_server', inherit from the network provided value... see how this pans out @@ -1133,11 +1159,12 @@ sub addnode else { my $hostname = $hname; - my $hardwaretype = 1; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); - if ($client_nethash{$node}{mgtifname} =~ /hf/) + my $hardwaretype = _hardware_type_for( + $mac, $client_nethash{$node}{mgtifname} + ); + if ($hardwaretype == 37) { - $hardwaretype = 37; if (scalar(@macs) > 1) { if ($hname !~ /^(.*)-hf(.*)$/) { $hostname = $hname . "-hf" . $count; @@ -1145,10 +1172,6 @@ sub addnode $hostname = $1 . "-hf" . $count; } } - } elsif (length($mac) == 23 || length($mac) == 26) { # 8 or 9 bytes of mac address - # Currently the only thing that has 8 or 9 bytes is an infiniband - # or infiniband like device, which is type 32 (0x20). - $hardwaretype = 32; } if ($static_host_fallback) { From e66a014564022bba4ea70fcb28206dffbe647336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:21:35 -0300 Subject: [PATCH 06/10] test(dhcp): cover fabric identity compatibility --- xCAT-test/unit/dhcp_infiniband_twin.t | 54 ++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/xCAT-test/unit/dhcp_infiniband_twin.t b/xCAT-test/unit/dhcp_infiniband_twin.t index 81cf13336..8d8824d1f 100644 --- a/xCAT-test/unit/dhcp_infiniband_twin.t +++ b/xCAT-test/unit/dhcp_infiniband_twin.t @@ -46,10 +46,16 @@ ok( 'an explicit InfiniBand identity is detected in a node mac list', ); ok( - xCAT_plugin::dhcp::_infiniband_identity_present( + !xCAT_plugin::dhcp::_infiniband_identity_present( 'b83fd203004a68aa!node01-ib', ), - 'a colonless InfiniBand identity is detected in a node mac list', + 'a colonless address rejected by addnode is not treated as an identity', +); +ok( + xCAT_plugin::dhcp::_infiniband_identity_present( + 'b8-3f-d2-03-00-4a-68-aa!node01-ib', + ), + 'a valid dashed InfiniBand identity is detected in a node mac list', ); ok( !xCAT_plugin::dhcp::_infiniband_identity_present( @@ -57,6 +63,23 @@ ok( ), 'an Ethernet-only mac list needs the derived identity', ); +is( + xCAT_plugin::dhcp::_hardware_type_for('b8:3f:d2:4a:68:aa', 'eth0'), + 1, + 'an Ethernet interface uses the Ethernet hardware type', +); +is( + xCAT_plugin::dhcp::_hardware_type_for( + 'b8:3f:d2:03:00:4a:68:aa', 'ib0' + ), + 32, + 'an eight-byte fabric address uses the InfiniBand hardware type', +); +is( + xCAT_plugin::dhcp::_hardware_type_for('b8:3f:d2:4a:68:aa', 'hf0'), + 37, + 'an HFI interface uses the HFI hardware type', +); is( xCAT_plugin::dhcp::_node_host_statements('node01', ''), @@ -70,6 +93,16 @@ is( 'ddns-hostname \"node01\"; send host-name \"node01\";filename = \"bootfile\";', 'node identity is prepended to existing host statements', ); +is( + xCAT_plugin::dhcp::_noip_hostname('node01', 'B8:3F:D2:4A:68:AA'), + 'node01-noipB83FD24A68AA', + 'the denied-host name preserves the legacy MAC case', +); +is( + xCAT_plugin::dhcp::_noip_hostname('node01', 'B8-3F-D2-4A-68-AA'), + 'node01-noipB8-3F-D2-4A-68-AA', + 'the denied-host name preserves legacy dash separators', +); my $create_commands = <<'OMAPI'; new host @@ -97,6 +130,15 @@ is( 'an Ethernet identity on an IPoIB network creates the InfiniBand twin', ); +is( + xCAT_plugin::dhcp::_infiniband_twin_create_commands( + 'node01', 'b8:3f:d2:4a:68:aa', 1, '!service!ib0', '192.0.2.10', + 'ddns-hostname \"node01\"; send host-name \"node01\";', 1, 0 + ), + $create_commands, + 'a relayed IPoIB interface creates the InfiniBand twin', +); + is( xCAT_plugin::dhcp::_infiniband_twin_create_commands( 'node01', 'b8:3f:d2:4a:68:aa', 1, 'ib0', 'DENIED', '', 1, 0 @@ -218,8 +260,8 @@ my @moved_network_delete_commands = ); is_deeply( \@moved_network_delete_commands, - [ $delete_name_commands, $delete_address_commands ], - 'removing a node cleans up its old twin after network metadata changes', + [ $delete_name_commands, undef ], + 'removing a moved node cleans up its old twin by name only', ); my @limited_delete_commands = xCAT_plugin::dhcp::_infiniband_twin_delete_commands( @@ -247,8 +289,8 @@ my @moved_network_update_commands = ); is_deeply( \@moved_network_update_commands, - [ $delete_name_commands, $delete_address_commands, undef ], - 're-registering a node on Ethernet removes its former InfiniBand twin', + [ $delete_name_commands, undef, undef ], + 're-registering a node on Ethernet removes its former twin by name only', ); my @infiniband_update_commands = From 39db381d50ad71345af05a88f064240f208c6ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:21:41 -0300 Subject: [PATCH 07/10] fix(dhcp): defer static host replacement --- xCAT-server/lib/xcat/plugins/dhcp.pm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 274e556cd..32c5b914a 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -281,11 +281,16 @@ sub _noip_hostname sub _add_isc_static_host { my ($node, $hostname, $mac, $hardwaretype, $mgtifname, $ip, $statements, - $has_infiniband_identity, $config) = @_; + $has_infiniband_identity, $config, $update_started) = @_; $config ||= \@dhcpconf; return unless $ip && $ip ne 'DENIED'; + if ($update_started && !${$update_started}) { + $restartdhcp = 1 if _begin_isc_static_host_update($node, 1, $config); + ${$update_started} = 1; + } + $mac = normalize_mac($mac) || $mac; _delete_isc_static_host($node, $config, $hostname); @@ -991,8 +996,7 @@ sub addnode my @macs = split(/\|/, $ent->{mac}); my $has_infiniband_identity = _infiniband_identity_present(@macs); my $static_host_fallback = _isc_static_host_fallback(); - $restartdhcp = 1 - if _begin_isc_static_host_update($node, $static_host_fallback); + my $static_host_update_started = 0; my $mace; my $deflstaments = $lstatements; my $count = 0; @@ -1183,7 +1187,8 @@ sub addnode _add_isc_static_host( $node, $hostname, $mac, $hardwaretype, $client_nethash{$node}{mgtifname}, $ip, $lstatements, - $has_infiniband_identity + $has_infiniband_identity, undef, + \$static_host_update_started ); $count = $count + 2; next; From 7b9fef4df3d1f0b1ce5e2fe1033856fefcaa8def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:21:41 -0300 Subject: [PATCH 08/10] test(dhcp): preserve hosts on failed replacement --- xCAT-test/unit/dhcp_static_host_markers.t | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/xCAT-test/unit/dhcp_static_host_markers.t b/xCAT-test/unit/dhcp_static_host_markers.t index 5a87dabc3..8409a1965 100644 --- a/xCAT-test/unit/dhcp_static_host_markers.t +++ b/xCAT-test/unit/dhcp_static_host_markers.t @@ -154,4 +154,31 @@ ok( 'a disabled static update leaves the configuration unchanged', ); +my @lazy_config; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node06', 'node06-old', '00:11:22:33:44:11', 1, + 'eth0', '192.0.2.7', '', 0, \@lazy_config, +); +my $before_failed_update = join( '', @lazy_config ); +my $update_started = 0; +xCAT_plugin::dhcp::_add_isc_static_host( + 'node06', 'node06-current', '00:11:22:33:44:22', 1, + 'eth0', undef, '', 0, \@lazy_config, \$update_started, +); +is(join( '', @lazy_config ), $before_failed_update, + 'an unresolved replacement preserves the existing static host'); +is($update_started, 0, + 'an unresolved replacement does not begin the static update'); +xCAT_plugin::dhcp::_add_isc_static_host( + 'node06', 'node06-current', '00:11:22:33:44:22', 1, + 'eth0', '192.0.2.8', '', 0, \@lazy_config, \$update_started, +); +my $lazy_replacement = join( '', @lazy_config ); +unlike($lazy_replacement, qr/^host node06-old \{$/m, + 'the first valid replacement removes stale node identities'); +like($lazy_replacement, qr/^host node06-current \{$/m, + 'the first valid replacement writes the current identity'); +is($update_started, 1, + 'the successful replacement records that cleanup has started'); + done_testing(); From f7afab4bc2665c2d6406955f6a959eaff880f784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:53:42 -0300 Subject: [PATCH 09/10] fix(dhcp): recognise an InfiniBand interface in any position The mgtifname of a network can name more than one interface, separated by !. The test for an InfiniBand interface ends at the end of the value, thus it recognises eth0!ib0 but not ib0!eth0, and a node on such a network receives no second host entry and no address over IPoIB. Accept the name in any position. --- xCAT-server/lib/xcat/plugins/dhcp.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 32c5b914a..bac6f933b 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -729,7 +729,7 @@ sub _hardware_type_for sub _is_infiniband_interface { my $mgtifname = shift; - return defined($mgtifname) && $mgtifname =~ /(?:^|!)ib[^!]*$/; + return defined($mgtifname) && $mgtifname =~ /(?:^|!)ib[^!]*(?:!|$)/; } sub _infiniband_twin_identity From 5473c5f43973abd6e9676a56b8d87be6a50a89bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:53:42 -0300 Subject: [PATCH 10/10] test(dhcp): pin which interface names name an InfiniBand network Drive the routine with an InfiniBand name first, last and alone, and with a name that has none, so a test that only reads the last name does not return. --- xCAT-test/unit/dhcp_infiniband_twin.t | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xCAT-test/unit/dhcp_infiniband_twin.t b/xCAT-test/unit/dhcp_infiniband_twin.t index 8d8824d1f..ea4d79e8a 100644 --- a/xCAT-test/unit/dhcp_infiniband_twin.t +++ b/xCAT-test/unit/dhcp_infiniband_twin.t @@ -326,4 +326,18 @@ is_deeply( 'updating an explicit InfiniBand identity does not remove it by address', ); +# The mgtifname of a network can name more than one interface, separated by !. +# The InfiniBand interface is not always the last one, so a test that only +# matches the last name leaves a node on ib0!eth0 without its second entry. +foreach my $ifname (qw(ib0 ib0.8001 eth0!ib0 ib0!eth0 ib0!ib1 bond0!ib2)) { + ok( xCAT_plugin::dhcp::_is_infiniband_interface($ifname), + "'$ifname' is served by an InfiniBand interface" ); +} +foreach my $ifname (qw(eth0 eth0!eth1 bond0 hf0)) { + ok( !xCAT_plugin::dhcp::_is_infiniband_interface($ifname), + "'$ifname' is not served by an InfiniBand interface" ); +} +ok( !xCAT_plugin::dhcp::_is_infiniband_interface(undef), + 'a network with no interface name is not served by InfiniBand' ); + done_testing();