2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-21 08:33:20 +00:00

Merge pull request #7598 from VersatusHPC/fix/issue-6498-mknb-xcatd-address

fix(mknb): avoid transient discovery addresses
This commit is contained in:
Daniel Hilst
2026-07-17 19:02:50 -03:00
committed by GitHub
6 changed files with 468 additions and 19 deletions
@@ -34,6 +34,8 @@ for the same architecture that the management node is. So you normally do not n
If you make custom changes to the network boot root image, you will need to run \ **mknb**\ again to regenerate the diskless image to include your changes. If you have an xCAT Hierarchical Cluster with Service Nodes having local /tftpboot directories (site.sharedtftp=0), you will need to copy the generated root image to each Service Node.
When multiple IPv4 addresses are configured for the same network, \ **mknb**\ uses a locally assigned ``site.master`` for the xcatd endpoint, or the first address reported by the operating system when ``site.master`` is not local. POWER discovery configurations also use this address for their kernel and initrd URLs.
Presently, the architectures x86_64 and ppc64 are supported. For ppc64le, use the ppc64 architecture.
+27 -6
View File
@@ -907,7 +907,8 @@ sub classful_networks_for_net_and_mask
=head3 my_hexnets
Arguments:
none
Optional: 'all' returns every IPv4 address found for each network in order.
The default behavior returns the last address found.
Returns:
Globals:
none
@@ -922,6 +923,7 @@ sub classful_networks_for_net_and_mask
#-------------------------------------------------------------------------------
sub my_hexnets
{
my $returnall = @_ && $_[-1] eq 'all';
my $rethash;
my @nets = split /\n/, `/sbin/ip addr`;
foreach (@nets)
@@ -945,7 +947,12 @@ sub my_hexnets
while ($nown <= $highn)
{
my $nowhex = sprintf("%08x", $nown);
$rethash->{ substr($nowhex, 0, $numchars) } = $curnet;
my $hexnet = substr($nowhex, 0, $numchars);
if ($returnall) {
push @{ $rethash->{$hexnet} }, $curnet;
} else {
$rethash->{$hexnet} = $curnet;
}
$nown += 1 << (32 - $maskbits - $bitstoeven);
}
}
@@ -1046,7 +1053,8 @@ sub isPingable
Return a hash ref that contains all subnet and netmask on the mn (or sn). This subroutine can be invoked on both Linux and AIX.
Arguments:
none.
Optional: 'all' returns every address found for each network in order.
The default behavior returns the last address found.
Returns:
Return a hash ref. Each entry will be: <subnet/mask>=><existing ip>;
@@ -1070,6 +1078,7 @@ For an example
#-----------------------------------------------------------------------
sub my_nets
{
my $returnall = @_ && $_[-1] eq 'all';
require xCAT::Table;
my $rethash;
my @nets;
@@ -1122,11 +1131,19 @@ sub my_nets
$nown = $nown & $curmask;
my $textnet = inet_ntoa(pack("N", $nown));
$textnet .= "/$maskbits";
$rethash->{$textnet} = $curnet;
if ($returnall) {
push @{ $rethash->{$textnet} }, $curnet;
} else {
$rethash->{$textnet} = $curnet;
}
}
else
{
$rethash->{$v6net} = $v6ip;
if ($returnall) {
push @{ $rethash->{$v6net} }, $v6ip;
} else {
$rethash->{$v6net} = $v6ip;
}
}
}
@@ -1154,7 +1171,11 @@ sub my_nets
$n .= "/$nm";
#$rethash->{$n} = $if;
$rethash->{$n} = $master;
if ($returnall) {
push @{ $rethash->{$n} }, $master;
} else {
$rethash->{$n} = $master;
}
}
}
return $rethash;
+2
View File
@@ -16,6 +16,8 @@ B<mknb> command yourself.
If you make custom changes to the network boot root image, you will need to run B<mknb> again to regenerate the diskless image to include your changes. If you have an xCAT Hierarchical Cluster with Service Nodes having local /tftpboot directories (site.sharedtftp=0), you will need to copy the generated root image to each Service Node.
When multiple IPv4 addresses are configured for the same network, B<mknb> uses a locally assigned C<site.master> for the xcatd endpoint, or the first address reported by the operating system when C<site.master> is not local. POWER discovery configurations also use this address for their kernel and initrd URLs.
Presently, the architectures x86_64 and ppc64 are supported. For ppc64le, use the ppc64 architecture.
=head1 OPTIONS
+50 -13
View File
@@ -13,6 +13,28 @@ sub handled_commands {
};
}
sub _select_network_addresses {
my ($network_addresses, $preferred_addresses) = @_;
my %preferred = map { $_ => 1 } grep { defined($_) } @{$preferred_addresses};
my %legacy;
my %selected;
foreach my $network (keys %{$network_addresses}) {
my $addresses = $network_addresses->{$network};
next unless @{$addresses};
$legacy{$network} = $addresses->[-1];
$selected{$network} = $addresses->[0];
foreach my $address (@{$addresses}) {
if (defined($address) && $preferred{$address}) {
$selected{$network} = $address;
last;
}
}
}
return (\%legacy, \%selected);
}
sub process_request {
my $request = shift;
my $callback = shift;
@@ -272,8 +294,21 @@ sub process_request {
return;
}
}
my $hexnets = xCAT::NetworkUtils->my_hexnets();
my $normnets = xCAT::NetworkUtils->my_nets();
my $hexnet_addresses = xCAT::NetworkUtils->my_hexnets('all');
my $normnet_addresses = xCAT::NetworkUtils->my_nets('all');
my @masters = xCAT::TableUtils->get_site_attribute("master");
my @master_addresses;
if ($masters[0]) {
@master_addresses = xCAT::NetworkUtils->getipaddr(
$masters[0], OnlyV4 => 1, GetAllAddresses => 1
);
}
my ($hexnets, $xcatdhexnets) = _select_network_addresses(
$hexnet_addresses, \@master_addresses
);
my ($normnets, $xcatdnormnets) = _select_network_addresses(
$normnet_addresses, \@master_addresses
);
my $consolecmdline;
if (defined($serialport) and $serialspeed) {
if ($arch =~ /ppc/) {
@@ -311,6 +346,7 @@ sub process_request {
foreach (keys %{$normnets}) {
my $net = $_;
my $nicip = $normnets->{$net};
my $xcatd_address = defined($xcatdnormnets->{$net}) ? $xcatdnormnets->{$net} : $nicip;
$net =~ s/\//_/;
if (defined($nobootnicips{$nicip})) {
if ($arch =~ /ppc/ and -r "$tftpdir/pxelinux.cfg/p/$net") {
@@ -342,10 +378,10 @@ sub process_request {
open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net");
print $cfg "#!gpxe\n";
if ($invisibletouch) {
print $cfg 'imgfetch -n kernel http://${next-server}:'.$httpport.'/tftpboot/xcat/genesis.kernel.' . "$arch xcatd=" . $normnets->{$_} . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/machyp}' . "\n";
print $cfg 'imgfetch -n kernel http://${next-server}:'.$httpport.'/tftpboot/xcat/genesis.kernel.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/machyp}' . "\n";
print $cfg 'imgfetch -n nbfs http://${next-server}:'.$httpport . "$initrd_file\n";
} else {
print $cfg 'imgfetch -n kernel http://${next-server}:'.$httpport.'/tftpboot/xcat/nbk.' . "$arch xcatd=" . $normnets->{$_} . ":$xcatdport $consolecmdline\n";
print $cfg 'imgfetch -n kernel http://${next-server}:'.$httpport.'/tftpboot/xcat/nbk.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline\n";
print $cfg 'imgfetch -n nbfs http://${next-server}:'.$httpport . "$initrd_file\n";
}
print $cfg "imgload kernel\n";
@@ -358,12 +394,12 @@ sub process_request {
print $cfg ' image=/tftpboot/xcat/genesis.kernel.' . "$arch\n";
print $cfg " label=\"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfg " initrd=$initrd_file\n";
print $cfg " append=\"xcatd=" . $normnets->{$_} . ":$xcatdport destiny=discover $consolecmdline BOOTIF=%B\"\n";
print $cfg " append=\"xcatd=" . $xcatd_address . ":$xcatdport destiny=discover $consolecmdline BOOTIF=%B\"\n";
close($cfg);
open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net.uefi");
print $cfg "#!gpxe\n";
print $cfg 'imgfetch -n kernel http://${next-server}:'.$httpport.'/tftpboot/xcat/genesis.kernel.' . "$arch\nimgload kernel\n";
print $cfg "imgargs kernel xcatd=" . $normnets->{$_} . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/mac:hexhyp}' . " destiny=discover initrd=initrd\n";
print $cfg "imgargs kernel xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/mac:hexhyp}' . " destiny=discover initrd=initrd\n";
print $cfg 'imgfetch -n initrd http://${next-server}:'.$httpport . "$initrd_file\nimgexec kernel\n";
close($cfg);
}
@@ -372,14 +408,15 @@ sub process_request {
print $cfgfile "default \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " delay=10\n";
print $cfgfile " label \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " kernel http://" . $normnets->{$_} . ":$httpport/$tftpdir/xcat/genesis.kernel.$arch\n";
print $cfgfile " initrd http://" . $normnets->{$_} . ":$httpport/$initrd_file\n";
print $cfgfile ' append "xcatd=' . $normnets->{$_} . ":$xcatdport $consolecmdline\"\n";
print $cfgfile " kernel http://" . $xcatd_address . ":$httpport/$tftpdir/xcat/genesis.kernel.$arch\n";
print $cfgfile " initrd http://" . $xcatd_address . ":$httpport/$initrd_file\n";
print $cfgfile ' append "xcatd=' . $xcatd_address . ":$xcatdport $consolecmdline\"\n";
close($cfgfile);
}
}
$dopxe = 0;
foreach (keys %{$hexnets}) {
my $xcatd_address = defined($xcatdhexnets->{$_}) ? $xcatdhexnets->{$_} : $hexnets->{$_};
$dopxe = 0;
if ($arch =~ /x86/) { #only do pxe if just x86 or x86_64 and no x86
if ($arch =~ /x86_64/) {
@@ -404,16 +441,16 @@ sub process_request {
print $cfgfile "DEFAULT xCAT\n";
print $cfgfile " LABEL xCAT\n";
print $cfgfile " KERNEL xcat/nbk.$arch\n";
print $cfgfile " APPEND initrd=$tftp_initrd xcatd=" . $hexnets->{$_} . ":$xcatdport $consolecmdline\n";
print $cfgfile " APPEND initrd=$tftp_initrd xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline\n";
close($cfgfile);
} elsif ($arch =~ /ppc/) {
open($cfgfile, ">", "$tftpdir/etc/" . lc($_));
print $cfgfile "default \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " delay=10\n";
print $cfgfile " label \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " kernel http://" . $hexnets->{$_} . ":$httpport/$tftpdir/xcat/genesis.kernel.$arch\n";
print $cfgfile " initrd http://" . $hexnets->{$_} . ":$httpport/$initrd_file\n";
print $cfgfile ' append "xcatd=' . $hexnets->{$_} . ":$xcatdport $consolecmdline\"\n";
print $cfgfile " kernel http://" . $xcatd_address . ":$httpport/$tftpdir/xcat/genesis.kernel.$arch\n";
print $cfgfile " initrd http://" . $xcatd_address . ":$httpport/$initrd_file\n";
print $cfgfile ' append "xcatd=' . $xcatd_address . ":$xcatdport $consolecmdline\"\n";
close($cfgfile);
}
}
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env perl
use strict;
use warnings;
## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoStrict, TestingAndDebugging::ProhibitNoWarnings)
use FindBin;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use Test::More;
BEGIN {
package xCAT::Utils;
$INC{'xCAT/Utils.pm'} = __FILE__;
package xCAT::TableUtils;
our ($tftpdir, $site_master);
sub getTftpDir { return $tftpdir; }
sub get_site_attribute {
my $attribute = $_[-1];
return ($site_master) if $attribute eq 'master';
return;
}
$INC{'xCAT/TableUtils.pm'} = __FILE__;
package xCAT::NetworkUtils;
our ($normnet_addresses, $hexnet_addresses, @master_addresses);
sub my_nets {
die "mknb did not request all normalized-network addresses"
unless $_[-1] eq 'all';
return $normnet_addresses;
}
sub my_hexnets {
die "mknb did not request all hexadecimal-network addresses"
unless $_[-1] eq 'all';
return $hexnet_addresses;
}
sub getipaddr { return @master_addresses; }
$INC{'xCAT/NetworkUtils.pm'} = __FILE__;
package xCAT::NodeRange;
sub import {
my $caller = caller;
no strict 'refs';
*{"${caller}::noderange"} = sub { return; };
}
$INC{'xCAT/NodeRange.pm'} = __FILE__;
}
my $source_mknb_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/mknb.pm";
if (-f $source_mknb_plugin) {
require $source_mknb_plugin;
} else {
require xCAT_plugin::mknb;
}
my ($legacy, $selected) = xCAT_plugin::mknb::_select_network_addresses(
{
'10.20.30.0/24' => [
'10.20.30.10',
'10.20.30.250',
],
},
['10.20.30.250'],
);
is_deeply(
$legacy,
{ '10.20.30.0/24' => '10.20.30.250' },
'Linux HA keeps the last address as the legacy network address',
);
is_deeply(
$selected,
{ '10.20.30.0/24' => '10.20.30.250' },
'Linux HA selects the local site.master virtual address',
);
($legacy, $selected) = xCAT_plugin::mknb::_select_network_addresses(
{
'10.30.40.0/24' => [
'10.30.40.20',
'10.30.40.100',
],
},
['192.0.2.1'],
);
is_deeply(
$legacy,
{ '10.30.40.0/24' => '10.30.40.100' },
'a service node keeps its last address as the legacy network address',
);
is_deeply(
$selected,
{ '10.30.40.0/24' => '10.30.40.20' },
'a service node ignores a remote site.master address and selects its first local address',
);
($legacy, $selected) = xCAT_plugin::mknb::_select_network_addresses(
{
'192.168.144.0/20' => [
'192.168.148.10',
'192.168.149.100',
],
'198.51.100.0/24' => [],
},
[],
);
is_deeply(
$legacy,
{ '192.168.144.0/20' => '192.168.149.100' },
'missing site.master preference preserves the last-address legacy value',
);
is_deeply(
$selected,
{ '192.168.144.0/20' => '192.168.148.10' },
'missing site.master preference falls back to the first candidate and skips empty networks',
);
sub prepare_tftpdir {
my ($root, $name, $arch) = @_;
$xCAT::TableUtils::tftpdir = "$root/$name";
make_path(
"$xCAT::TableUtils::tftpdir/xcat",
"$xCAT::TableUtils::tftpdir/etc",
);
foreach my $file (
"$xCAT::TableUtils::tftpdir/xcat/genesis.kernel.$arch",
"$xCAT::TableUtils::tftpdir/xcat/genesis.fs.$arch.gz",
) {
open(my $fh, '>', $file) or die "Unable to create $file: $!";
close($fh);
}
}
sub run_mknb {
my ($arch) = @_;
my @responses;
xCAT_plugin::mknb::process_request(
{ arg => [$arch, '--configfileonly'] },
sub { push @responses, @_; },
);
return \@responses;
}
sub read_config {
my ($path) = @_;
open(my $fh, '<', $path) or die "Unable to read $path: $!";
my $content = do { local $/; <$fh> };
close($fh);
return $content;
}
sub generation_succeeded {
my ($responses, $description) = @_;
ok(
!grep({ ref($_) eq 'HASH' && $_->{error} } @{$responses}),
$description,
);
}
sub use_reporter_address_maps {
$xCAT::NetworkUtils::normnet_addresses = {
'192.168.144.0/20' => [
'192.168.148.10',
'192.168.149.100',
],
};
$xCAT::NetworkUtils::hexnet_addresses = {
c0a89 => [
'192.168.148.10',
'192.168.149.100',
],
};
$xCAT::TableUtils::site_master = 'master.example.com';
@xCAT::NetworkUtils::master_addresses = ('203.0.113.10');
}
my $tmpdir = tempdir(CLEANUP => 1);
$::XCATROOT = "$tmpdir/xcatroot";
make_path(
"$::XCATROOT/share/xcat/netboot/genesis/x86_64",
"$::XCATROOT/share/xcat/netboot/genesis/ppc64",
);
use_reporter_address_maps();
prepare_tftpdir($tmpdir, 'tftpboot-x86', 'x86_64');
my $responses = run_mknb('x86_64');
generation_succeeded($responses, 'x86 configuration generation succeeds');
foreach my $relative_path (
'xcat/xnba/nets/192.168.144.0_20',
'pxelinux.cfg/C0A89',
) {
my $content = read_config("$xCAT::TableUtils::tftpdir/$relative_path");
like(
$content,
qr/xcatd=192\.168\.148\.10:3001/,
"$relative_path uses the first address as the xcatd endpoint",
);
unlike(
$content,
qr/xcatd=192\.168\.149\.100:3001/,
"$relative_path does not use the later floating address as the xcatd endpoint",
);
}
use_reporter_address_maps();
prepare_tftpdir($tmpdir, 'tftpboot-power', 'ppc64');
$responses = run_mknb('ppc64');
generation_succeeded($responses, 'POWER configuration generation succeeds');
foreach my $relative_path (
'pxelinux.cfg/p/192.168.144.0_20',
'etc/c0a89',
) {
my $content = read_config("$xCAT::TableUtils::tftpdir/$relative_path");
like(
$content,
qr/kernel http:\/\/192\.168\.148\.10:80\//,
"$relative_path uses the first address for the kernel URL",
);
like(
$content,
qr/initrd http:\/\/192\.168\.148\.10:80\//,
"$relative_path uses the first address for the initrd URL",
);
like(
$content,
qr/xcatd=192\.168\.148\.10:3001/,
"$relative_path uses the first address as the xcatd endpoint",
);
unlike(
$content,
qr/(?:kernel|initrd) http:\/\/192\.168\.149\.100:80\/|xcatd=192\.168\.149\.100:3001/,
"$relative_path has no functional endpoint using the later floating address",
);
}
$xCAT::NetworkUtils::normnet_addresses = {
'192.168.144.0/20' => ['192.168.148.10'],
};
$xCAT::NetworkUtils::hexnet_addresses = {
c0a89 => ['192.168.148.10'],
};
$xCAT::TableUtils::site_master = 'master.example.com';
@xCAT::NetworkUtils::master_addresses = ('203.0.113.10');
prepare_tftpdir($tmpdir, 'tftpboot-no-floating', 'x86_64');
$responses = run_mknb('x86_64');
generation_succeeded($responses, 'configuration generation without a floating address succeeds');
my $no_floating_xnba = read_config(
"$xCAT::TableUtils::tftpdir/xcat/xnba/nets/192.168.144.0_20"
);
my $no_floating_pxe = read_config(
"$xCAT::TableUtils::tftpdir/pxelinux.cfg/C0A89"
);
$xCAT::TableUtils::site_master = undef;
@xCAT::NetworkUtils::master_addresses = ();
prepare_tftpdir($tmpdir, 'tftpboot-no-floating', 'x86_64');
$responses = run_mknb('x86_64');
generation_succeeded($responses, 'configuration generation without site.master succeeds');
is(
read_config("$xCAT::TableUtils::tftpdir/xcat/xnba/nets/192.168.144.0_20"),
$no_floating_xnba,
'xNBA output is byte-identical without a site.master preference',
);
is(
read_config("$xCAT::TableUtils::tftpdir/pxelinux.cfg/C0A89"),
$no_floating_pxe,
'legacy PXE output is byte-identical without a site.master preference',
);
done_testing();
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env perl
use strict;
use warnings;
## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoWarnings)
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
BEGIN {
package NetworkUtilsTestCommand;
our $ip_addr_output = '';
package xCAT::Table;
our @network_entries;
sub new { return bless {}, shift; }
sub getAllAttribs { return @network_entries; }
$INC{'xCAT/Table.pm'} = __FILE__;
package xCAT::TableUtils;
sub get_site_attribute {
my ($attribute) = @_;
return ('192.0.2.1') if $attribute eq 'master';
return;
}
$INC{'xCAT/TableUtils.pm'} = __FILE__;
package main;
*CORE::GLOBAL::readpipe = sub {
my ($command) = @_;
return $NetworkUtilsTestCommand::ip_addr_output
if $command eq '/sbin/ip addr';
die "Unexpected command in NetworkUtils unit test: $command";
};
}
require xCAT::NetworkUtils;
sub set_ip_addr_output {
$NetworkUtilsTestCommand::ip_addr_output = join('', @_);
@xCAT::Table::network_entries = ();
}
set_ip_addr_output(
"2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500\n",
" inet 192.168.148.10/20 brd 192.168.159.255 scope global eth0\n",
" valid_lft forever preferred_lft forever\n",
" inet 192.168.149.100/20 scope global secondary eth0\n",
" valid_lft forever preferred_lft forever\n",
" inet6 2001:db8:1::10/64 scope global\n",
);
is_deeply(
xCAT::NetworkUtils->my_nets(),
{ '192.168.144.0/20' => '192.168.149.100' },
'my_nets default behavior remains last-address-wins',
);
is_deeply(
xCAT::NetworkUtils->my_nets('all'),
{
'192.168.144.0/20' => [
'192.168.148.10',
'192.168.149.100',
],
},
'my_nets all mode preserves every address in operating-system order',
);
is_deeply(
xCAT::NetworkUtils->my_hexnets(),
{ c0a89 => '192.168.149.100' },
'my_hexnets default behavior remains last-address-wins',
);
is_deeply(
xCAT::NetworkUtils->my_hexnets('all'),
{
c0a89 => [
'192.168.148.10',
'192.168.149.100',
],
},
'my_hexnets all mode preserves every address in operating-system order',
);
set_ip_addr_output(
"2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500\n",
" inet 192.168.148.10/20 brd 192.168.159.255 scope global eth0\n",
" inet6 2001:db8:1::10/64 scope global\n",
);
is_deeply(
xCAT::NetworkUtils->my_nets(),
{ '192.168.144.0/20' => '192.168.148.10' },
'my_nets default output is unchanged when there is no floating address',
);
is_deeply(
xCAT::NetworkUtils->my_nets('all'),
{ '192.168.144.0/20' => ['192.168.148.10'] },
'my_nets all mode returns one candidate when there is no floating address',
);
is_deeply(
xCAT::NetworkUtils->my_hexnets(),
{ c0a89 => '192.168.148.10' },
'my_hexnets default output is unchanged when there is no floating address',
);
is_deeply(
xCAT::NetworkUtils->my_hexnets('all'),
{ c0a89 => ['192.168.148.10'] },
'my_hexnets all mode returns one candidate when there is no floating address',
);
is_deeply(
xCAT::NetworkUtils->my_nets('all'),
{ '192.168.144.0/20' => ['192.168.148.10'] },
'Linux IPv6 addresses remain outside the all-address result',
);
done_testing();