From 02f963cbf3b6d207635899024d9455157a06ae4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:56:27 -0300 Subject: [PATCH 1/2] fix(dhcp): only dispatch makedhcp to the service nodes serving the nodes makedhcp on a noderange sends the request to every service node running a dhcp server, whether or not it serves any of the named nodes. On a cluster with many service nodes each one does the work and reports on nodes that are not its responsibility, for example warning that a node it does not serve has no mac address. Map the named nodes to their service nodes and skip the rest. Regenerating the networks with -n still reaches every dhcp server, since a dynamic range is not tied to a node, and if none of the named nodes can be mapped the request goes to all of them as before rather than risk leaving one out. The node to service node mapping reuses getSNformattedhash, which the disjointdhcps path in this same function already uses. Co-authored-by: Jarrod Johnson <10814490+jjohnson42@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/dhcp.pm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index a360c8be1..48aa8f697 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -1458,10 +1458,22 @@ sub preprocess_request push @requests, $reqcopy; } + # A request naming nodes only concerns the service nodes that serve + # them. Regenerating the networks still reaches every dhcp server, + # since a dynamic range is not tied to a node. If none of the named + # nodes can be mapped to a service node, keep sending to all of them + # rather than risk leaving one out. + my %servingsn = (); + unless ($opt{n}) { + my $sn_hash = xCAT::ServiceNodeUtils->getSNformattedhash(\@nodes, "xcat", "MN"); + foreach my $sn (keys %$sn_hash) { $servingsn{$sn} = 1; } + } + foreach my $s (@snlist) { if (scalar @nodes == 1 and $nodes[0] eq $s) { next; } next if ($issn && exists($iphash{$s})); + next if (keys(%servingsn) && !exists($servingsn{$s})); my $reqcopy = {%$req}; $reqcopy->{'_xcatdest'} = $s; From 158c16a138a90762a252f3ce79129e0c1a91291f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:56:27 -0300 Subject: [PATCH 2/2] test(dhcp): cover the service node dispatch scope for makedhcp Assert that a node-targeted makedhcp maps its nodes to their service nodes and skips the ones serving none of them, that regenerating the networks with -n stays exempt because a dynamic range is not tied to a node, and that an unmapped noderange still reaches every service node instead of none. Also pin the two conditions the loop already had, so restricting the fan-out does not quietly drop the self dispatch guards. --- xCAT-test/unit/dhcp_sn_dispatch_scope.t | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 xCAT-test/unit/dhcp_sn_dispatch_scope.t diff --git a/xCAT-test/unit/dhcp_sn_dispatch_scope.t b/xCAT-test/unit/dhcp_sn_dispatch_scope.t new file mode 100644 index 000000000..07299611d --- /dev/null +++ b/xCAT-test/unit/dhcp_sn_dispatch_scope.t @@ -0,0 +1,58 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use Test::More; + +my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); +my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server/lib/xcat/plugins/dhcp.pm' ); + +plan skip_all => "$plugin not found" unless -r $plugin; + +open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; +my $source = do { local $/; <$fh> }; +close($fh); + +# The dispatch loop that fans a node-targeted makedhcp out to service nodes. +my ($loop) = $source =~ m{ + ( my \s+ %servingsn .*? foreach \s+ my \s+ \$s \s+ \(\@snlist\) .*? \n \s* \} \n ) +}sx; + +ok( $loop, 'the service node dispatch loop was located' ) + or BAIL_OUT('dhcp.pm no longer matches the expected dispatch shape'); + +like( + $loop, + qr/getSNformattedhash\(\\\@nodes/, + 'the named nodes are mapped to their service nodes' +); +like( + $loop, + qr/next \s+ if \s+ \(keys\(%servingsn\) \s* && \s* !exists\(\$servingsn\{\$s\}\)\)/x, + 'a service node serving none of the named nodes is skipped' +); + +# Regenerating the networks must still reach every dhcp server, because a +# dynamic range does not belong to any node. +like( + $loop, + qr/unless \s* \(\$opt\{n\}\)/x, + 'network regeneration is exempt from the node based restriction' +); + +# Empty mapping must fall back to the previous fan-out rather than silently +# dispatching to nothing. +my ($guard) = $loop =~ /(next\s+if\s+\(keys\(%servingsn\)[^\n]*)/; +like( + $guard, + qr/keys\(%servingsn\)\s*&&/, + 'an unmapped noderange still reaches every service node' +); + +# The pre-existing conditions in the loop must survive. +like( $loop, qr/scalar \@nodes == 1 and \$nodes\[0\] eq \$s/, 'the self dispatch guard is retained' ); +like( $loop, qr/\$issn && exists\(\$iphash\{\$s\}\)/, 'the service node self skip is retained' ); + +done_testing();