From 3cd74bbab0e287f8f49ab476225411792668fb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:44:37 -0300 Subject: [PATCH] fix(mknb): avoid transient discovery addresses --- .../admin-guides/references/man8/mknb.8.rst | 2 + perl-xCAT/xCAT/NetworkUtils.pm | 33 ++- xCAT-client/pods/man8/mknb.8.pod | 2 + xCAT-server/lib/xcat/plugins/mknb.pm | 63 +++- xCAT-test/unit/mknb_xcatd_address.t | 271 ++++++++++++++++++ xCAT-test/unit/networkutils_first_address.t | 116 ++++++++ 6 files changed, 468 insertions(+), 19 deletions(-) create mode 100644 xCAT-test/unit/mknb_xcatd_address.t create mode 100644 xCAT-test/unit/networkutils_first_address.t diff --git a/docs/source/guides/admin-guides/references/man8/mknb.8.rst b/docs/source/guides/admin-guides/references/man8/mknb.8.rst index 4dd9d6c4b..3f166a58b 100644 --- a/docs/source/guides/admin-guides/references/man8/mknb.8.rst +++ b/docs/source/guides/admin-guides/references/man8/mknb.8.rst @@ -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. diff --git a/perl-xCAT/xCAT/NetworkUtils.pm b/perl-xCAT/xCAT/NetworkUtils.pm index 7e34e7d81..9a0fbef1d 100644 --- a/perl-xCAT/xCAT/NetworkUtils.pm +++ b/perl-xCAT/xCAT/NetworkUtils.pm @@ -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: =>; @@ -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; diff --git a/xCAT-client/pods/man8/mknb.8.pod b/xCAT-client/pods/man8/mknb.8.pod index 063abcbcf..c8bb98b34 100644 --- a/xCAT-client/pods/man8/mknb.8.pod +++ b/xCAT-client/pods/man8/mknb.8.pod @@ -16,6 +16,8 @@ B command yourself. If you make custom changes to the network boot root image, you will need to run B 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 uses a locally assigned C for the xcatd endpoint, or the first address reported by the operating system when C 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 diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 740cc1eea..50ba96e30 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -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); } } diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t new file mode 100644 index 000000000..f68c4362c --- /dev/null +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -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(); diff --git a/xCAT-test/unit/networkutils_first_address.t b/xCAT-test/unit/networkutils_first_address.t new file mode 100644 index 000000000..33cd1134d --- /dev/null +++ b/xCAT-test/unit/networkutils_first_address.t @@ -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: 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: 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();