diff --git a/xCAT-probe/lib/perl/probe_utils.pm b/xCAT-probe/lib/perl/probe_utils.pm index 218678f340..d6731c44e4 100644 --- a/xCAT-probe/lib/perl/probe_utils.pm +++ b/xCAT-probe/lib/perl/probe_utils.pm @@ -160,10 +160,16 @@ sub get_os { sub _netplan_get { my $key = shift; - open(my $fh, '-|', 'netplan', 'get', $key) or return undef; + my $pid = open(my $fh, '-|'); + return unless defined $pid; + if ($pid == 0) { + open(STDERR, '>', '/dev/null'); + exec 'netplan', 'get', $key; + exit 1; + } my $val = <$fh>; close $fh; - return undef if $?; + return if $?; chomp $val if defined $val; return $val; } @@ -177,30 +183,113 @@ sub _command_available { return 0; } -sub _netplan_has_static_ip { - my ($nic, $ip) = @_; +sub _networkd_config_dirs { + return ('/run/systemd/network', '/etc/systemd/network'); +} + +sub _networkd_name_matches { + my ($names, $nic) = @_; - return 0 unless _command_available('netplan'); + foreach my $name (split /\s+/, $names) { + return 1 if $name eq $nic; + } - (my $escaped_nic = $nic) =~ s/\./\\./g; - for my $devtype (qw(ethernets bonds bridges vlans)) { - my $dev_obj = _netplan_get("$devtype.$escaped_nic"); - next unless defined $dev_obj; - next if ($dev_obj eq 'null' || $dev_obj eq ''); + return 0; +} - my $addresses = _netplan_get("$devtype.$escaped_nic.addresses"); - next unless defined $addresses; - next if ($addresses eq 'null' || $addresses eq ''); - next unless $addresses =~ /(?:^|[\s\[,])\Q$ip\E(?:\/\d+)?(?:$|[\s\],])/m; +sub _networkd_address_matches { + my ($addresses, $ip) = @_; - my $dhcp_val = _netplan_get("$devtype.$escaped_nic.dhcp4"); - return 0 if defined $dhcp_val && $dhcp_val =~ /true/i; - return 1; + foreach my $address (split /\s+/, $addresses) { + return 1 if $address =~ /^\Q$ip\E(?:\/\d+)?$/; + } + + return 0; +} + +sub _networkd_file_has_static_ip { + my ($file, $nic, $ip) = @_; + my $section = ''; + my $matched_nic = 0; + my $matched_ip = 0; + my $dhcp4 = 0; + + open(my $fh, '<', $file) or return 0; + while (my $line = <$fh>) { + chomp $line; + $line =~ s/^\s+|\s+$//g; + next if $line eq '' || $line =~ /^[#;]/; + $line =~ s/\s*[#;].*$//; + + if ($line =~ /^\[([^\]]+)\]$/) { + $section = lc $1; + next; + } + + if ($section eq 'match' && $line =~ /^Name\s*=\s*(.+)$/i) { + $matched_nic = 1 if _networkd_name_matches($1, $nic); + next; + } + + if ($section eq 'network' && $line =~ /^Address\s*=\s*(.+)$/i) { + $matched_ip = 1 if _networkd_address_matches($1, $ip); + next; + } + + if ($section eq 'network' && $line =~ /^DHCP\s*=\s*(.+)$/i) { + $dhcp4 = 1 if $1 =~ /^(?:yes|true|ipv4|both)$/i; + } + } + close $fh; + + return $matched_nic && $matched_ip && !$dhcp4; +} + +sub _networkd_has_static_ip { + my ($nic, $ip) = @_; + + foreach my $dir (_networkd_config_dirs()) { + next unless -d $dir; + opendir(my $dh, $dir) or next; + foreach my $file (sort grep { /\.network$/ } readdir($dh)) { + return 1 if _networkd_file_has_static_ip("$dir/$file", $nic, $ip); + } + closedir $dh; } return 0; } +sub _netplan_has_static_ip { + my ($nic, $ip) = @_; + my $netplan_get_supported = 0; + + if (_command_available('netplan')) { + (my $escaped_nic = $nic) =~ s/\./\\./g; + for my $devtype (qw(ethernets bonds bridges vlans)) { + my $dev_obj = _netplan_get("$devtype.$escaped_nic"); + $netplan_get_supported = 1 if defined $dev_obj; + next unless defined $dev_obj; + next if ($dev_obj eq 'null' || $dev_obj eq ''); + + my $addresses = _netplan_get("$devtype.$escaped_nic.addresses"); + next unless defined $addresses; + next if ($addresses eq 'null' || $addresses eq ''); + next unless $addresses =~ /(?:^|[\s\[,"'])\Q$ip\E(?:\/\d+)?(?:$|[\s\],"'])/m; + + my $dhcp_val = _netplan_get("$devtype.$escaped_nic.dhcp4"); + return 0 if defined $dhcp_val && $dhcp_val =~ /true/i; + return 1; + } + } + + return 0 if $netplan_get_supported; + + # Older netplan releases can render networkd files but do not support + # "netplan get", so use the generated files as a conservative fallback. + return _networkd_has_static_ip($nic, $ip); +} + sub dhcp_query_reply_mac { my ($reply, $node, $ip) = @_; diff --git a/xCAT-test/unit/probe_utils_netplan.t b/xCAT-test/unit/probe_utils_netplan.t index 1f1a8c1e24..acd493417e 100644 --- a/xCAT-test/unit/probe_utils_netplan.t +++ b/xCAT-test/unit/probe_utils_netplan.t @@ -5,6 +5,7 @@ use warnings; use FindBin; use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; +use File::Temp qw(tempdir); use Test::More; require probe_utils; @@ -14,8 +15,11 @@ my %netplan = ( 'ethernets.eth0.addresses' => "- 10.0.0.2/24\n", 'ethernets.eth0.dhcp4' => 'false', 'ethernets.eth1' => 'renderer: networkd', - 'ethernets.eth1.addresses' => "- 10.0.0.3/24\n", - 'ethernets.eth1.dhcp4' => 'true', + 'ethernets.eth1.addresses' => "- \"10.0.0.3/24\"\n", + 'ethernets.eth1.dhcp4' => 'false', + 'ethernets.eth4' => 'renderer: networkd', + 'ethernets.eth4.addresses' => "- 10.0.0.4/24\n", + 'ethernets.eth4.dhcp4' => 'true', 'vlans.bond0\.123' => 'renderer: networkd', 'vlans.bond0\.123.addresses' => "- 10.0.123.5/24\n", ); @@ -27,8 +31,89 @@ my %netplan = ( ok(probe_utils::_netplan_has_static_ip('eth0', '10.0.0.2'), 'static netplan address is detected'); ok(!probe_utils::_netplan_has_static_ip('eth0', '10.0.0.99'), 'wrong address is not treated as static'); - ok(!probe_utils::_netplan_has_static_ip('eth1', '10.0.0.3'), 'dhcp4 true is not treated as static'); + ok(probe_utils::_netplan_has_static_ip('eth1', '10.0.0.3'), 'quoted netplan address is detected'); + ok(!probe_utils::_netplan_has_static_ip('eth4', '10.0.0.4'), 'dhcp4 true is not treated as static'); ok(probe_utils::_netplan_has_static_ip('bond0.123', '10.0.123.5'), 'dotted VLAN interface is escaped for netplan get'); } +sub write_file { + my ($file, $contents) = @_; + + open(my $fh, '>', $file) or die "Unable to write $file: $!"; + print $fh $contents; + close $fh; +} + +my $networkd_dir = tempdir(CLEANUP => 1); +my $fake_bin = tempdir(CLEANUP => 1); + +write_file("$networkd_dir/10-netplan-eth2.network", <<'EOF'); +[Match] +Name=eth2 + +[Network] +Address=10.0.2.5/24 +EOF + +write_file("$networkd_dir/10-netplan-eth3.network", <<'EOF'); +[Match] +Name=eth3 + +[Network] +Address=10.0.3.5/24 +DHCP=ipv4 +EOF + +write_file("$networkd_dir/10-netplan-eth20.network", <<'EOF'); +[Match] +Name=eth20 + +[Network] +Address=10.0.20.5/24 +EOF + +my $fake_netplan = "$fake_bin/netplan"; +write_file($fake_netplan, <<'EOF'); +#!/bin/sh +echo "netplan get is not supported" >&2 +exit 1 +EOF +chmod oct('755'), $fake_netplan; + +{ + no warnings 'redefine'; + local *probe_utils::_networkd_config_dirs = sub { return ($networkd_dir); }; + + ok(probe_utils::_networkd_has_static_ip('eth2', '10.0.2.5'), 'networkd fallback detects generated static address'); + ok(!probe_utils::_networkd_has_static_ip('eth2', '10.0.2.99'), 'networkd fallback rejects wrong address'); + ok(!probe_utils::_networkd_has_static_ip('eth3', '10.0.3.5'), 'networkd fallback rejects DHCP-enabled IPv4 config'); + ok(!probe_utils::_networkd_has_static_ip('eth2', '10.0.20.5'), 'networkd fallback requires exact interface match'); +} + +{ + no warnings 'redefine'; + local *probe_utils::_command_available = sub { return $_[0] eq 'netplan' ? 1 : 0; }; + local *probe_utils::_netplan_get = sub { return; }; + local *probe_utils::_networkd_config_dirs = sub { return ($networkd_dir); }; + + ok(probe_utils::_netplan_has_static_ip('eth2', '10.0.2.5'), 'old netplan without get falls back to generated networkd config'); +} + +{ + no warnings 'redefine'; + local $ENV{PATH} = "$fake_bin:$ENV{PATH}"; + local *probe_utils::_networkd_config_dirs = sub { return ($networkd_dir); }; + + ok(probe_utils::_netplan_has_static_ip('eth2', '10.0.2.5'), 'unsupported netplan get command uses generated networkd fallback'); +} + +{ + no warnings 'redefine'; + local *probe_utils::_command_available = sub { return $_[0] eq 'netplan' ? 1 : 0; }; + local *probe_utils::_netplan_get = sub { return 'null'; }; + local *probe_utils::_networkd_config_dirs = sub { return ($networkd_dir); }; + + ok(!probe_utils::_netplan_has_static_ip('eth2', '10.0.2.5'), 'supported netplan get remains authoritative when no netplan key matches'); +} + done_testing();