mirror of
https://github.com/webmin/webmin.git
synced 2026-06-10 14:50:33 +01:00
ⓘ Prefer Netplan when Debian has Netplan YAML config, otherwise select the existing NetworkManager backend for Debian systems with saved NM connection profiles, with regression tests for backend selection. https://github.com/webmin/webmin/issues/2559
236 lines
6.9 KiB
Perl
236 lines
6.9 KiB
Perl
#!/usr/bin/perl
|
|
use strict;
|
|
use warnings;
|
|
use Test::More;
|
|
use Cwd qw(abs_path);
|
|
use File::Basename qw(dirname);
|
|
use File::Path qw(make_path);
|
|
use File::Temp qw(tempdir);
|
|
|
|
my $root = abs_path(dirname(__FILE__)."/../..") or die "rootdir: $!";
|
|
my $tmp = tempdir(CLEANUP => 1);
|
|
my %file_cache;
|
|
my @commands;
|
|
my %command_status;
|
|
my %command_output;
|
|
|
|
sub write_text
|
|
{
|
|
my ($file, $text) = @_;
|
|
open(my $fh, ">", $file) || die "write $file: $!";
|
|
print $fh $text;
|
|
close($fh) || die "close $file: $!";
|
|
delete($file_cache{$file});
|
|
}
|
|
|
|
sub read_text
|
|
{
|
|
my ($file) = @_;
|
|
open(my $fh, "<", $file) || die "read $file: $!";
|
|
local $/ = undef;
|
|
my $text = <$fh>;
|
|
close($fh) || die "close $file: $!";
|
|
return $text;
|
|
}
|
|
|
|
sub read_file_lines
|
|
{
|
|
my ($file) = @_;
|
|
if (!exists($file_cache{$file})) {
|
|
open(my $fh, "<", $file) || die "read_file_lines $file: $!";
|
|
my @lines = <$fh>;
|
|
close($fh) || die "close $file: $!";
|
|
chomp(@lines);
|
|
$file_cache{$file} = \@lines;
|
|
}
|
|
return $file_cache{$file};
|
|
}
|
|
|
|
sub flush_file_lines
|
|
{
|
|
my ($file) = @_;
|
|
open(my $fh, ">", $file) || die "flush $file: $!";
|
|
foreach my $line (@{$file_cache{$file}}) {
|
|
print $fh $line, "\n";
|
|
}
|
|
close($fh) || die "close $file: $!";
|
|
}
|
|
|
|
sub lock_file { return 1; }
|
|
sub unlock_file { return 1; }
|
|
sub error { die join("", @_), "\n"; }
|
|
sub unflush_file_lines { delete($file_cache{$_[0]}); }
|
|
sub has_command { return $_[0] eq "netplan" ? "/usr/sbin/netplan" : undef; }
|
|
sub execute_command_logged
|
|
{
|
|
my ($cmd, undef, $stdout, $stderr) = @_;
|
|
push(@commands, $cmd);
|
|
my $out = $command_output{$cmd} || "";
|
|
$$stdout = $out if (ref($stdout));
|
|
$$stderr = $out if (ref($stderr) && $stderr ne $stdout);
|
|
return $command_status{$cmd} || 0;
|
|
}
|
|
sub backquote_logged
|
|
{
|
|
my ($cmd) = @_;
|
|
push(@commands, $cmd);
|
|
$? = $command_status{$cmd} || 0;
|
|
return $command_output{$cmd} || "";
|
|
}
|
|
sub check_ipaddress { return $_[0] =~ /^\d+\.\d+\.\d+\.\d+$/; }
|
|
sub check_ip6address { return $_[0] =~ /:/; }
|
|
sub check_ipaddress_any { return &check_ipaddress($_[0]) || &check_ip6address($_[0]); }
|
|
sub mask_to_prefix { return $_[0] eq "255.255.255.0" ? 24 : $_[0]; }
|
|
sub prefix_to_mask { return $_[0] == 24 ? "255.255.255.0" : $_[0]; }
|
|
sub indexof
|
|
{
|
|
my ($needle, @haystack) = @_;
|
|
for(my $i=0; $i<@haystack; $i++) {
|
|
return $i if ($haystack[$i] eq $needle);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
unshift(@INC, "$root/net", $root);
|
|
do "$root/net/net-detect.pl" || die "net-detect.pl: $@ $!";
|
|
|
|
my $detect_root = tempdir(CLEANUP => 1);
|
|
my $detect_netplan = "$detect_root/netplan";
|
|
my $detect_no_netplan = "$detect_root/no-netplan";
|
|
my $detect_nm = "$detect_root/NetworkManager/system-connections";
|
|
my $detect_nm_empty = "$detect_root/NetworkManager-empty/system-connections";
|
|
make_path($detect_netplan, $detect_nm, $detect_nm_empty);
|
|
write_text("$detect_nm/eth0.nmconnection", "");
|
|
|
|
is(main::net_auto_backend("debian-linux", $detect_netplan, $detect_nm_empty),
|
|
"netplan", "Debian uses Netplan when the config directory exists");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm),
|
|
"nm", "Debian uses NetworkManager when only nmconnection files exist");
|
|
is(main::net_auto_backend("redhat-linux", $detect_no_netplan, $detect_nm),
|
|
"nm", "Red Hat still uses NetworkManager when nmconnection files exist");
|
|
write_text("$detect_netplan/50-cloud-init.yaml", "");
|
|
is(main::net_auto_backend("debian-linux", $detect_netplan, $detect_nm),
|
|
"netplan", "Debian prefers Netplan over NetworkManager when YAML exists");
|
|
unlink("$detect_netplan/50-cloud-init.yaml");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty),
|
|
undef, "Debian falls back when no Netplan or NetworkManager config exists");
|
|
|
|
do "$root/net/netplan-lib.pl" || die "netplan-lib.pl: $@ $!";
|
|
|
|
{
|
|
no warnings 'once';
|
|
$main::netplan_dir = $tmp;
|
|
}
|
|
|
|
my $netplan = "$tmp/50-cloud-init.yaml";
|
|
write_text($netplan, <<'YAML');
|
|
network:
|
|
version: 2
|
|
ethernets:
|
|
eth0:
|
|
dhcp4: true
|
|
nameservers:
|
|
addresses:
|
|
- 1.1.1.1
|
|
search:
|
|
- example.com
|
|
eth1:
|
|
dhcp4: true
|
|
YAML
|
|
|
|
my @boot = main::boot_interfaces();
|
|
is(scalar(grep { !defined($_->{'virtual'}) || $_->{'virtual'} eq '' } @boot),
|
|
2, "parsed two boot interfaces");
|
|
my ($eth0) = grep { $_->{'fullname'} eq "eth0" } @boot;
|
|
$eth0->{'nameserver'} = [ "127.0.0.1", "2001:4860:4860::8888" ];
|
|
$eth0->{'search'} = [ "example.com" ];
|
|
main::save_interface($eth0, \@boot);
|
|
|
|
my $saved = read_text($netplan);
|
|
like($saved, qr/^ eth0:\n dhcp4: true\n nameservers:\n addresses: \[127\.0\.0\.1, '2001:4860:4860::8888'\]\n search: \[example\.com\]/m,
|
|
"save_interface preserves existing two-space Netplan hierarchy");
|
|
like($saved, qr/^ eth1:\n dhcp4: true/m,
|
|
"untouched sibling interface keeps matching indentation");
|
|
unlike($saved, qr/^ eth0:/m,
|
|
"rewritten interface is not moved to an eight-space sibling indent");
|
|
|
|
write_text($netplan, <<'YAML');
|
|
network:
|
|
version: 2
|
|
ethernets:
|
|
eth0:
|
|
dhcp4: true
|
|
nameservers:
|
|
addresses:
|
|
- 1.1.1.1
|
|
eth1:
|
|
dhcp4: true
|
|
YAML
|
|
@boot = main::boot_interfaces();
|
|
my ($need_apply, $generated_resolv) =
|
|
main::os_save_dns_config({ 'nameserver' => [ "127.0.0.1" ],
|
|
'domain' => [ "example.com" ] });
|
|
ok($need_apply, "DNS changes request a netplan apply");
|
|
ok($generated_resolv, "Netplan DNS save reports resolv.conf as generated");
|
|
$saved = read_text($netplan);
|
|
like($saved, qr/^ eth0:\n dhcp4: true\n nameservers:\n addresses: \[127\.0\.0\.1\]\n search: \[example\.com\]/m,
|
|
"os_save_dns_config updates existing nameservers block");
|
|
unlike($saved, qr/eth1:\n(?:.*\n)*?\s+nameservers:/,
|
|
"os_save_dns_config does not add nameservers to every interface");
|
|
|
|
@commands = ( );
|
|
%command_status = (
|
|
"(cd / && /usr/sbin/netplan generate)" => 1,
|
|
);
|
|
%command_output = (
|
|
"(cd / && /usr/sbin/netplan generate)" => "bad yaml\n",
|
|
);
|
|
is(main::apply_network(), "bad yaml\n",
|
|
"apply_network returns validation errors");
|
|
is_deeply(\@commands, [ "(cd / && /usr/sbin/netplan generate)" ],
|
|
"apply_network skips apply when generate fails");
|
|
|
|
@commands = ( );
|
|
%command_status = ( );
|
|
%command_output = ( );
|
|
is(main::apply_network(), undef, "apply_network applies after validation");
|
|
is_deeply(\@commands,
|
|
[ "(cd / && /usr/sbin/netplan generate)",
|
|
"(cd / && /usr/sbin/netplan apply)" ],
|
|
"apply_network validates before applying");
|
|
|
|
do "$root/net/nm-lib.pl" || die "nm-lib.pl: $@ $!";
|
|
my $nmfile = "$tmp/eth0.nmconnection";
|
|
write_text($nmfile, <<'NM');
|
|
[connection]
|
|
id=eth0
|
|
uuid=11111111-2222-3333-4444-555555555555
|
|
type=ethernet
|
|
interface-name=eth0
|
|
|
|
[ipv4]
|
|
method=auto
|
|
|
|
[ipv6]
|
|
method=disabled
|
|
NM
|
|
my $nmcfg = main::read_nm_config($nmfile);
|
|
my $nmiface = {
|
|
'name' => 'eth0',
|
|
'fullname' => 'eth0',
|
|
'file' => $nmfile,
|
|
'cfg' => $nmcfg,
|
|
'edit' => 1,
|
|
'up' => 1,
|
|
'dhcp' => 1,
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'nameserver' => [ "2001:4860:4860::8888" ],
|
|
};
|
|
@commands = ( );
|
|
main::save_interface($nmiface, [ $nmiface ]);
|
|
like(join("\n", @commands), qr/ipv6\.dns/,
|
|
"NetworkManager save_interface writes IPv6 nameservers");
|
|
|
|
done_testing();
|