diff --git a/xCAT-server/lib/xcat/plugins/hosts.pm b/xCAT-server/lib/xcat/plugins/hosts.pm index d84b5d155..596b73ac0 100644 --- a/xCAT-server/lib/xcat/plugins/hosts.pm +++ b/xCAT-server/lib/xcat/plugins/hosts.pm @@ -13,6 +13,8 @@ use Getopt::Long; use Fcntl ':flock'; my @hosts; #Hold /etc/hosts data to be written back +my %host_indexes_by_ip; +my %host_indexes_by_node; my $LONGNAME; my $OTHERNAMESFIRST; my $ADDNAMES; @@ -27,6 +29,119 @@ sub handled_commands return { makehosts => "hosts", }; } +sub _host_line_index_values +{ + my $line = shift; + return unless defined($line); + + my ($ip) = $line =~ /^(\S+)\s/; + return unless defined($ip); + + my @nodes; + if ($ip =~ /^\d+\.\d+\.\d+\.\d+$/) + { + my ($primary_name) = $line =~ /^\S+\s+(\S+)/; + if (defined($primary_name)) + { + # Preserve matching a short node against an FQDN-first entry. + my $offset = 0; + while ((my $dot = index($primary_name, '.', $offset)) >= 0) + { + push @nodes, substr($primary_name, 0, $dot); + $offset = $dot + 1; + } + push @nodes, $primary_name; + } + } + + return ($ip, \@nodes); +} + +sub _index_host_line +{ + my $idx = shift; + my ($ip, $nodes) = _host_line_index_values($hosts[$idx]); + return unless defined($ip); + + $host_indexes_by_ip{$ip}{$idx} = 1; + foreach my $node (@{$nodes}) + { + $host_indexes_by_node{$node}{$idx} = 1; + } +} + +sub _unindex_host_line +{ + my $idx = shift; + my ($ip, $nodes) = _host_line_index_values($hosts[$idx]); + return unless defined($ip); + + if (exists($host_indexes_by_ip{$ip})) + { + delete $host_indexes_by_ip{$ip}{$idx}; + delete $host_indexes_by_ip{$ip} unless keys %{ $host_indexes_by_ip{$ip} }; + } + foreach my $node (@{$nodes}) + { + if (exists($host_indexes_by_node{$node})) + { + delete $host_indexes_by_node{$node}{$idx}; + delete $host_indexes_by_node{$node} unless keys %{ $host_indexes_by_node{$node} }; + } + } +} + +sub _rebuild_host_indexes +{ + %host_indexes_by_ip = (); + %host_indexes_by_node = (); + foreach my $idx (0 .. $#hosts) + { + _index_host_line($idx); + } +} + +sub _set_host_lines +{ + my $lines = shift; + @hosts = @{$lines}; + _rebuild_host_indexes(); + return \@hosts; +} + +sub _set_host_line +{ + my ($idx, $line) = @_; + _unindex_host_line($idx); + $hosts[$idx] = $line; + _index_host_line($idx); +} + +sub _push_host_line +{ + my $line = shift; + push @hosts, $line; + _index_host_line($#hosts); +} + +sub _matching_host_indexes +{ + my ($node, $ip) = @_; + my %matches; + + if (exists($host_indexes_by_ip{$ip})) + { + $matches{$_} = 1 foreach keys %{ $host_indexes_by_ip{$ip} }; + } + if (exists($host_indexes_by_node{$node})) + { + $matches{$_} = 1 foreach keys %{ $host_indexes_by_node{$node} }; + } + + my @indexes = sort { $a <=> $b } keys %matches; + return @indexes; +} + sub delnode { my $node = shift; @@ -39,16 +154,10 @@ sub delnode my $othernames = shift; my $domain = shift; - my $idx = 0; - while ($idx <= $#hosts) + foreach my $idx (_matching_host_indexes($node, $ip)) { - if (($ip and $hosts[$idx] =~ /^${ip}\s/) - or $hosts[$idx] =~ /^\d+\.\d+\.\d+\.\d+\s+${node}[\s\.\r]/) - { - $hosts[$idx] = ""; - } - $idx++; + _set_host_line($idx, ""); } } @@ -66,47 +175,40 @@ sub addnode my $othernames = shift; my $domain = shift; my $nics = shift; - my $idx = 0; - my $foundone = 0; # if this ip was already added then just update the entry - while ($idx <= $#hosts) + my @matches = _matching_host_indexes($node, $ip); + if (@matches) { - if ($hosts[$idx] =~ /^${ip}\s/ - or $hosts[$idx] =~ /^\d+\.\d+\.\d+\.\d+\s+${node}[\s\.\r]/) + my $idx = shift @matches; + my $line; + if ($nics) { - if ($foundone) - { - $hosts[$idx] = ""; - } - else - { - # we found a matching entry in the hosts list - if ($nics) { + # we're processing the nics table and we found an + # existing entry for this ip so just add this + # node name as an alias for the existing entry + my $existing_line = $hosts[$idx]; + chomp($existing_line); + my ($hip, $hnode, $hdom, $hother) = split(/ /, $existing_line); - # we're processing the nics table and we found an - # existing entry for this ip so just add this - # node name as an alias for the existing entry - chomp($hosts[$idx]); - my ($hip, $hnode, $hdom, $hother) = split(/ /, $hosts[$idx]); - - $hosts[$idx] = build_line($callback, $ip, $hnode, $domain, $othernames); - } else { - - # otherwise just try to completely update the existing - # entry - $hosts[$idx] = build_line($callback, $ip, $node, $domain, $othernames); - } - } - $foundone = 1; + $line = build_line($callback, $ip, $hnode, $domain, $othernames); } - $idx++; + else + { + # otherwise just try to completely update the existing entry + $line = build_line($callback, $ip, $node, $domain, $othernames); + } + _set_host_line($idx, $line); + foreach my $duplicate_idx (@matches) + { + _set_host_line($duplicate_idx, ""); + } + return; } - if ($foundone) { return; } my $line = build_line($callback, $ip, $node, $domain, $othernames); if ($line) { - push @hosts, $line; + _push_host_line($line); } } @@ -423,17 +525,17 @@ sub process_request my $bakname = "/etc/hosts.xcatbak"; copy("/etc/hosts", $bakname); - @hosts = (); + my @host_lines; if ($REMOVE) { # add the localhost entry if trying to create the /etc/hosts from scratch if ($^O =~ /^aix/i) { - push @hosts, "127.0.0.1 loopback localhost\n"; + push @host_lines, "127.0.0.1 loopback localhost\n"; } else { - push @hosts, "127.0.0.1 localhost\n"; + push @host_lines, "127.0.0.1 localhost\n"; } } else @@ -447,11 +549,12 @@ sub process_request { while (<$rconf>) { - push @hosts, $_; + push @host_lines, $_; } close($rconf); } } + _set_host_lines(\@host_lines); if ($req->{node}) { diff --git a/xCAT-test/unit/makehosts_index.t b/xCAT-test/unit/makehosts_index.t new file mode 100644 index 000000000..a5196da29 --- /dev/null +++ b/xCAT-test/unit/makehosts_index.t @@ -0,0 +1,164 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +BEGIN { + package xCAT::Table; + $INC{'xCAT/Table.pm'} = __FILE__; + + package xCAT::TableUtils; + $INC{'xCAT/TableUtils.pm'} = __FILE__; + + package xCAT::Utils; + $INC{'xCAT/Utils.pm'} = __FILE__; + + package xCAT::NetworkUtils; + $INC{'xCAT/NetworkUtils.pm'} = __FILE__; + + package xCAT::MsgUtils; + $INC{'xCAT/MsgUtils.pm'} = __FILE__; +} + +my $source_hosts_plugin = + "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/hosts.pm"; +require $source_hosts_plugin; + +sub set_host_lines +{ + return xCAT_plugin::hosts::_set_host_lines([@_]); +} + +{ + my $lines = set_host_lines( + "127.0.0.1 localhost\n", + "# retained comment\n", + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01', '10.0.0.1', 'alias1 alias2', 'cluster.test' + ); + + is_deeply( + $lines, + [ + "127.0.0.1 localhost\n", + "# retained comment\n", + "10.0.0.1 node01 node01.cluster.test alias1 alias2\n", + ], + 'new host is appended without changing unrelated lines' + ); +} + +{ + my $lines = set_host_lines( + "10.0.0.1 node01 node01.cluster.test oldalias\n", + "10.0.0.2 other other.cluster.test\n", + "10.0.0.3 node01 node01.other.test\n", + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01', '10.0.0.2', 'newalias', 'cluster.test' + ); + + is_deeply( + $lines, + [ + "10.0.0.2 node01 node01.cluster.test newalias\n", + '', + '', + ], + 'first IP or primary-name match is updated and later duplicates are removed' + ); +} + +{ + my $lines = set_host_lines( + "10.0.0.1 node01.cluster.test node01\n", + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01', '10.0.0.9', '', 'cluster.test' + ); + + is( + $lines->[0], + "10.0.0.9 node01 node01.cluster.test \n", + 'short node matches an existing FQDN-first entry' + ); +} + +{ + my $lines = set_host_lines( + "10.0.0.1 node01 node01.cluster.test\n", + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01-bmc', '10.0.0.1', 'bmc-alias', 'cluster.test', 1 + ); + is( + $lines->[0], + "10.0.0.1 node01 node01.cluster.test bmc-alias\n", + 'NIC entry keeps the existing primary node when matching by IP' + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01', '10.0.0.9', '', 'cluster.test' + ); + is( + $lines->[0], + "10.0.0.9 node01 node01.cluster.test \n", + 'updated line remains indexed for the next host change' + ); +} + +{ + my $lines = set_host_lines( + "10.0.0.1 node01 node01.cluster.test\n", + "10.0.0.2 node02 node02.cluster.test\n", + "10.0.0.3 node01 node01.other.test\n", + ); + + xCAT_plugin::hosts::delnode('node01', '10.0.0.2', '', 'cluster.test'); + is_deeply( + $lines, + ['', '', ''], + 'delete removes every IP or primary-name match' + ); + + xCAT_plugin::hosts::addnode( + undef, 'node01', '10.0.0.2', '', 'cluster.test' + ); + is_deeply( + $lines, + ['', '', '', "10.0.0.2 node01 node01.cluster.test \n"], + 'deleted lines are removed from the indexes before a later append' + ); +} + +{ + my $lines = set_host_lines( + "fe80::1 node01 node01.cluster.test\n", + "10.0.0.1 \n", + ); + + xCAT_plugin::hosts::addnode( + undef, 'node02', 'fe80::1', '', 'cluster.test' + ); + xCAT_plugin::hosts::addnode( + undef, 'node03', '10.0.0.1', '', 'cluster.test' + ); + + is_deeply( + $lines, + [ + "fe80::1 node02 node02.cluster.test \n", + "10.0.0.1 node03 node03.cluster.test \n", + ], + 'IP index handles IPv6 addresses and entries without a primary name' + ); +} + +done_testing();