Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 106 additions & 17 deletions xCAT-probe/lib/perl/probe_utils.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) = @_;

Expand Down
91 changes: 88 additions & 3 deletions xCAT-test/unit/probe_utils_netplan.t
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
);
Expand All @@ -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();