mirror of
https://github.com/webmin/webmin.git
synced 2026-07-25 10:50:31 +01:00
ⓘ Create and detect VLAN devices before applying live configuration, parse live VLANs with their physical parent instead of a virtual alias, normalize persisted dotted VLAN names when the parent device exists, consistently target the real VLAN device across the ip, ifup and vconfig paths, skip recreating existing devices or activating absent disabled ones, and add regression coverage. https://github.com/webmin/webmin/pull/2785
968 lines
30 KiB
Perl
968 lines
30 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";
|
|
my $detect_ifupdown = "$detect_root/interfaces";
|
|
my $detect_ifupdown_empty = "$detect_root/interfaces-empty";
|
|
my $detect_ifupdown_dir = "$detect_root/interfaces.d";
|
|
my $detect_dhcpcd = "$detect_root/dhcpcd.conf";
|
|
make_path($detect_netplan, $detect_nm, $detect_nm_empty);
|
|
write_text("$detect_nm/eth0.nmconnection", "");
|
|
make_path($detect_ifupdown_dir);
|
|
write_text($detect_ifupdown, "source $detect_ifupdown_dir/*\n");
|
|
write_text($detect_ifupdown_empty, "# empty\n");
|
|
write_text($detect_dhcpcd, "# default dhcpcd configuration\nhostname\n");
|
|
|
|
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,
|
|
$detect_ifupdown_empty, $detect_dhcpcd, 0),
|
|
undef, "Debian falls back when no Netplan or NetworkManager config exists");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown_empty, $detect_dhcpcd, 0),
|
|
undef, "Debian does not use inactive default dhcpcd config");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown_empty, $detect_dhcpcd, 1),
|
|
"dhcpcd", "Debian uses active dhcpcd service with default config");
|
|
write_text($detect_dhcpcd, "interface eth0\n");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown_empty, $detect_dhcpcd, 0),
|
|
"dhcpcd", "Debian uses dhcpcd only as a final configured backend");
|
|
write_text($detect_ifupdown_empty, "auto lo\niface lo inet loopback\n");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown_empty, $detect_dhcpcd),
|
|
"dhcpcd", "Debian loopback-only ifupdown config does not block dhcpcd");
|
|
write_text("$detect_ifupdown_dir/eth0", "iface eth0 inet dhcp\n");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown, $detect_dhcpcd),
|
|
undef, "Debian does not prefer dhcpcd over ifupdown iface stanzas");
|
|
write_text("$detect_ifupdown_dir/eth0", "iface eth0 inet dhcp # uplink\n");
|
|
is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty,
|
|
$detect_ifupdown, $detect_dhcpcd),
|
|
undef, "Debian ifupdown detection tolerates inline iface comments");
|
|
|
|
do "$root/net/netplan-lib.pl" || die "netplan-lib.pl: $@ $!";
|
|
|
|
{
|
|
no warnings 'once';
|
|
$main::netplan_dir = $tmp;
|
|
}
|
|
|
|
is(main::linux_nsswitch_hosts_line("hosts: files dns\n",
|
|
"files dns"),
|
|
"hosts: files dns\n",
|
|
"Linux DNS save preserves nsswitch hosts spacing");
|
|
is(main::linux_nsswitch_hosts_line("hosts:\tfiles dns # local policy\n",
|
|
"files mdns4 dns"),
|
|
"hosts:\tfiles mdns4 dns # local policy\n",
|
|
"Linux DNS save preserves nsswitch hosts comments");
|
|
|
|
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");
|
|
|
|
do "$root/net/dhcpcd-lib.pl" || die "dhcpcd-lib.pl: $@ $!";
|
|
my $dhcpcd = "$tmp/dhcpcd.conf";
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
# global option
|
|
hostname
|
|
|
|
interface eth0
|
|
static ip_address=192.168.1.10/24
|
|
static routers=192.168.1.1
|
|
static routes=10.10.0.0/16 192.168.1.254 10.20.30.0/24 192.168.1.253
|
|
static domain_name_servers=1.1.1.1 2001:4860:4860::8888
|
|
static domain_search=example.com
|
|
static ip6_address=2001:db8::10/64
|
|
mtu 1400
|
|
noipv6rs
|
|
|
|
interface wlan0
|
|
# DHCP by default
|
|
DHCPCD
|
|
|
|
{
|
|
no warnings 'once';
|
|
$main::dhcpcd_config = $dhcpcd;
|
|
$main::dhcpcd_synthesize_implicit = 0;
|
|
}
|
|
|
|
@boot = main::boot_interfaces();
|
|
is(scalar(grep { $_->{'virtual'} eq '' } @boot), 2,
|
|
"dhcpcd parses two real interfaces");
|
|
my ($dh0) = grep { $_->{'fullname'} eq "eth0" } @boot;
|
|
is($dh0->{'address'}, "192.168.1.10", "dhcpcd parses static IPv4");
|
|
is($dh0->{'netmask'}, "255.255.255.0", "dhcpcd parses IPv4 prefix");
|
|
is($dh0->{'gateway'}, "192.168.1.1", "dhcpcd parses router");
|
|
is_deeply($dh0->{'routes'},
|
|
[ "10.10.0.0/16,192.168.1.254",
|
|
"10.20.30.0/24,192.168.1.253" ],
|
|
"dhcpcd parses static routes");
|
|
is_deeply($dh0->{'nameserver'},
|
|
[ "1.1.1.1", "2001:4860:4860::8888" ],
|
|
"dhcpcd parses nameservers");
|
|
is_deeply($dh0->{'address6'}, [ "2001:db8::10" ],
|
|
"dhcpcd parses static IPv6");
|
|
my ($wlan0) = grep { $_->{'fullname'} eq "wlan0" } @boot;
|
|
ok($wlan0->{'dhcp'}, "dhcpcd treats no static address as DHCP");
|
|
|
|
$dh0->{'address'} = "192.168.1.20";
|
|
$dh0->{'gateway'} = "192.168.1.254";
|
|
$dh0->{'routes'} = [ "172.16.0.0/12,192.168.1.253" ];
|
|
$dh0->{'nameserver'} = [ "9.9.9.9" ];
|
|
$dh0->{'search'} = [ "example.net" ];
|
|
main::save_interface($dh0, \@boot);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/interface eth0\nnoipv6rs\nstatic ip_address=192\.168\.1\.20\/24\nstatic routers=192\.168\.1\.254\nstatic routes=172\.16\.0\.0\/12 192\.168\.1\.253\nstatic domain_name_servers=9\.9\.9\.9\nstatic domain_search=example\.net\nstatic ip6_address=2001:db8::10\/64\nmtu 1400/s,
|
|
"dhcpcd save_interface rewrites managed values and preserves extras");
|
|
like($saved, qr/interface wlan0\n# DHCP by default/s,
|
|
"dhcpcd save_interface preserves sibling DHCP block");
|
|
|
|
my ($dhwlan0) = grep { $_->{'fullname'} eq "wlan0" } main::boot_interfaces();
|
|
push(@boot, { 'name' => 'wlan0',
|
|
'fullname' => 'wlan0:0',
|
|
'virtual' => 0,
|
|
'address' => '192.168.2.10',
|
|
'netmask' => '255.255.255.0' });
|
|
main::save_interface($dhwlan0, \@boot);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/interface wlan0\nstatic ip_address=192\.168\.2\.10\/24/s,
|
|
"dhcpcd writes virtual static address for DHCP parent interface");
|
|
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
interface enp0s5
|
|
static ip_address=10.211.55.20/24
|
|
static domain_name_servers=1.1.1.1 8.8.8.8
|
|
DHCPCD
|
|
my ($dhneed, $dhgenerated) = main::os_save_dns_config(
|
|
{ 'nameserver' => [ "1.1.1.1", "8.8.1.1" ],
|
|
'domain' => [ "example.test" ] });
|
|
ok($dhneed, "dhcpcd DNS save requests dhcpcd apply");
|
|
ok($dhgenerated, "dhcpcd DNS save reports resolv.conf as generated");
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/static domain_name_servers=1\.1\.1\.1 8\.8\.1\.1\nstatic domain_search=example\.test/,
|
|
"dhcpcd DNS save updates interface DNS settings");
|
|
|
|
@commands = ( );
|
|
($dhneed, $dhgenerated) = main::os_save_dns_config(
|
|
{ 'nameserver' => [ "1.1.1.1", "8.8.1.1" ],
|
|
'domain' => [ "example.test" ] });
|
|
ok(!$dhneed, "dhcpcd DNS save skips apply when unchanged");
|
|
ok($dhgenerated, "dhcpcd unchanged DNS still suppresses resolv.conf rewrite");
|
|
is_deeply(\@commands, [ ], "dhcpcd unchanged DNS does not rewrite config");
|
|
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
# default dhcpcd configuration
|
|
hostname
|
|
DHCPCD
|
|
{
|
|
no warnings 'once';
|
|
$main::dhcpcd_synthesize_implicit = 1;
|
|
$main::dhcpcd_test_active_interfaces = [
|
|
{ 'name' => 'lo', 'fullname' => 'lo', 'virtual' => '',
|
|
'up' => 1 },
|
|
{ 'name' => 'enp0s5', 'fullname' => 'enp0s5',
|
|
'virtual' => '', 'up' => 1 },
|
|
{ 'name' => 'wlan0', 'fullname' => 'wlan0',
|
|
'virtual' => '', 'up' => 1 },
|
|
];
|
|
}
|
|
@boot = main::boot_interfaces();
|
|
is_deeply([ map { $_->{'fullname'} } @boot ], [ "enp0s5", "wlan0" ],
|
|
"dhcpcd synthesizes implicit DHCP boot interfaces");
|
|
ok((grep { $_->{'fullname'} eq "enp0s5" && $_->{'dhcp'} &&
|
|
$_->{'implicit'} } @boot),
|
|
"dhcpcd marks synthesized interface as implicit DHCP");
|
|
|
|
my ($implicit) = grep { $_->{'fullname'} eq "enp0s5" } @boot;
|
|
main::delete_interface($implicit);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/^denyinterfaces enp0s5\n# default dhcpcd configuration/m,
|
|
"dhcpcd delete of implicit interface writes denyinterfaces");
|
|
|
|
@boot = main::boot_interfaces();
|
|
is_deeply([ map { $_->{'fullname'} } @boot ], [ "wlan0" ],
|
|
"dhcpcd does not synthesize denied interfaces");
|
|
|
|
my $newif = { 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5',
|
|
'virtual' => '',
|
|
'dhcp' => 1,
|
|
'address6' => [ ],
|
|
'netmask6' => [ ] };
|
|
main::save_interface($newif, \@boot);
|
|
$saved = read_text($dhcpcd);
|
|
unlike($saved, qr/^denyinterfaces/m,
|
|
"dhcpcd save removes denyinterfaces for managed interface");
|
|
like($saved, qr/interface enp0s5\n(?:\n|$)/,
|
|
"dhcpcd save creates explicit DHCP interface block");
|
|
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
denyinterfaces eth0 wlan0
|
|
interface eth0
|
|
static ip_address=192.168.1.30/24
|
|
DHCPCD
|
|
{
|
|
no warnings 'once';
|
|
$main::dhcpcd_synthesize_implicit = 0;
|
|
}
|
|
@boot = main::boot_interfaces();
|
|
my ($denied_eth0) = grep { $_->{'fullname'} eq "eth0" } @boot;
|
|
$denied_eth0->{'address'} = "192.168.1.31";
|
|
main::save_interface($denied_eth0, \@boot);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/^denyinterfaces wlan0\ninterface eth0\nstatic ip_address=192\.168\.1\.31\/24/m,
|
|
"dhcpcd save removes one denyinterfaces word without shifting block replacement");
|
|
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
denyinterfaces enp0s6 wlan0
|
|
interface enp0s6
|
|
static ip_address=10.211.55.21/24
|
|
DHCPCD
|
|
@boot = main::boot_interfaces();
|
|
my ($removed_enp0s6) = grep { $_->{'fullname'} eq "enp0s6" } @boot;
|
|
main::delete_interface($removed_enp0s6);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/^denyinterfaces wlan0\n/m,
|
|
"dhcpcd delete of explicit interface removes stale denyinterfaces word");
|
|
unlike($saved, qr/^interface enp0s6/m,
|
|
"dhcpcd delete of explicit interface removes its block entirely");
|
|
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
allowinterfaces eth0
|
|
interface eth0
|
|
DHCPCD
|
|
@boot = main::boot_interfaces();
|
|
my $allowed_new = { 'name' => 'wlan0',
|
|
'fullname' => 'wlan0',
|
|
'virtual' => '',
|
|
'dhcp' => 1,
|
|
'address6' => [ ],
|
|
'netmask6' => [ ] };
|
|
main::save_interface($allowed_new, \@boot);
|
|
$saved = read_text($dhcpcd);
|
|
like($saved, qr/^allowinterfaces eth0 wlan0\ninterface eth0\n\ninterface wlan0/m,
|
|
"dhcpcd save adds new explicit interface to existing allowinterfaces");
|
|
|
|
{
|
|
no warnings 'once';
|
|
$main::dhcpcd_test_active_interfaces = [
|
|
{ 'name' => 'enp0s5', 'fullname' => 'enp0s5',
|
|
'virtual' => '', 'up' => 1 },
|
|
];
|
|
}
|
|
@commands = ( );
|
|
like(main::apply_interface({ 'name' => 'enp0s6',
|
|
'fullname' => 'enp0s6',
|
|
'virtual' => '' }),
|
|
qr/Cannot find device "enp0s6"/,
|
|
"dhcpcd apply reports missing real device");
|
|
is_deeply(\@commands, [ ], "dhcpcd apply skips restart for missing device");
|
|
|
|
@commands = ( );
|
|
is(main::apply_interface({ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5',
|
|
'virtual' => '' }),
|
|
undef, "dhcpcd apply restarts service for existing device");
|
|
is_deeply(\@commands, [ "/etc/init.d/dhcpcd restart 2>&1 </dev/null" ],
|
|
"dhcpcd apply runs restart command for existing device");
|
|
|
|
@commands = ( );
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
interface enp0s5
|
|
static ip_address=10.211.55.20/24
|
|
DHCPCD
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
$main::dhcpcd_synthesize_implicit = 0;
|
|
$main::dhcpcd_test_active_interfaces = [
|
|
{ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5',
|
|
'virtual' => '',
|
|
'address' => '10.211.55.20',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 },
|
|
{ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:1',
|
|
'virtual' => 1,
|
|
'address' => '10.211.55.21',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 },
|
|
];
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
is(main::apply_network(), undef,
|
|
"dhcpcd global apply removes virtual alias missing from config");
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip addr del 10\\.211\\.55\\.21\\/24 dev enp0s5 2>&1",
|
|
"/etc/init.d/dhcpcd restart 2>&1 </dev/null" ],
|
|
"dhcpcd global apply removes live virtual address before restart");
|
|
|
|
@commands = ( );
|
|
write_text($dhcpcd, <<'DHCPCD');
|
|
interface enp0s5
|
|
static ip_address=10.211.55.20/24
|
|
static ip_address=10.211.55.23/24
|
|
DHCPCD
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
$main::dhcpcd_synthesize_implicit = 0;
|
|
$main::dhcpcd_test_active_interfaces = [
|
|
{ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5',
|
|
'virtual' => '',
|
|
'address' => '10.211.55.20',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 },
|
|
{ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:0',
|
|
'virtual' => 0,
|
|
'address' => '10.211.55.24',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 },
|
|
{ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:1',
|
|
'virtual' => 1,
|
|
'address' => '10.211.55.23',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 },
|
|
];
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
is(main::delete_active_interface($main::dhcpcd_test_active_interfaces->[2]),
|
|
undef,
|
|
"dhcpcd active delete removes matching boot virtual alias");
|
|
}
|
|
$saved = read_text($dhcpcd);
|
|
unlike($saved, qr/static ip_address=10\.211\.55\.23\/24/,
|
|
"dhcpcd active delete removes alias from config");
|
|
is_deeply(\@commands,
|
|
[ "ip addr del 10\\.211\\.55\\.23\\/24 dev enp0s5 2>&1" ],
|
|
"dhcpcd active delete drops only the selected live virtual alias");
|
|
do "$root/net/linux-lib.pl" || die "linux-lib.pl: $@ $!";
|
|
|
|
my $ip_vlan_output = <<'IPADDR';
|
|
5: br-lan.10@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
|
|
link/ether 02:00:00:00:00:10 brd ff:ff:ff:ff:ff:ff
|
|
inet 10.0.0.2/24 brd 10.0.0.255 scope global br-lan.10
|
|
6: eth0.10.30@eth0.10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
|
|
inet 10.0.30.1/24 brd 10.0.30.255 scope global eth0.10.30
|
|
IPADDR
|
|
my @parsed_vlan;
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
local *main::clean_language = sub { };
|
|
local *main::reset_environment = sub { };
|
|
local *main::open_execute_command = sub {
|
|
open(main::IFC, "<", \$ip_vlan_output) || die "open scalar: $!";
|
|
return 1;
|
|
};
|
|
@parsed_vlan = main::active_interfaces(1);
|
|
}
|
|
is_deeply([ map { [ $_->{'fullname'}, $_->{'name'},
|
|
$_->{'vlanid'}, $_->{'physical'},
|
|
$_->{'vlan'}, $_->{'virtual'} ] } @parsed_vlan ],
|
|
[ [ 'br-lan.10', 'br-lan.10', '10', 'br-lan', 1, '' ],
|
|
[ 'eth0.10.30', 'eth0.10.30', '30', 'eth0.10', 1, '' ] ],
|
|
"Linux active VLAN parsing supports parent names without digits");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
main::deactivate_interface($parsed_vlan[0]);
|
|
}
|
|
is_deeply(\@commands,
|
|
[ 'ip addr del 10\.0\.0\.2\/24 dev br\-lan\.10 2>&1',
|
|
"ip link set dev br\\-lan\\.10 down 2>&1" ],
|
|
"Linux active VLAN is treated as a real link when deactivated");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
main::activate_interface({ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:1',
|
|
'virtual' => 1,
|
|
'address' => '10.211.55.25',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 0 });
|
|
}
|
|
is_deeply(\@commands, [ ],
|
|
"Linux active virtual interface stays absent when created down");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ({ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:1',
|
|
'virtual' => 1,
|
|
'address' => '10.211.55.25',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
};
|
|
main::activate_interface({ 'name' => 'enp0s5',
|
|
'fullname' => 'enp0s5:1',
|
|
'virtual' => 1,
|
|
'address' => '10.211.55.25',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 0 });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip addr del 10\\.211\\.55\\.25\\/24 dev enp0s5 2>&1" ],
|
|
"Linux active virtual interface is removed when saved down");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ 'name' => 'bond0',
|
|
'fullname' => 'bond0',
|
|
'virtual' => '',
|
|
'bond' => 1,
|
|
'partner' => 'eth0 eth1',
|
|
'mode' => '1',
|
|
'primary' => 'eth0',
|
|
'miimon' => '100',
|
|
'updelay' => '200',
|
|
'downdelay' => '200',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link add bond0 type bond mode active\\-backup miimon 100 updelay 200 downdelay 200 2>&1",
|
|
"ip link set dev eth0 down 2>&1",
|
|
"ip link set dev eth0 master bond0 2>&1",
|
|
"ip link set dev eth0 up 2>&1",
|
|
"ip link set dev eth1 down 2>&1",
|
|
"ip link set dev eth1 master bond0 2>&1",
|
|
"ip link set dev eth1 up 2>&1",
|
|
"ip link set dev bond0 type bond primary eth0 2>&1",
|
|
"ip link set dev bond0 up 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev bond0 2>&1" ],
|
|
"Linux active bond interface is created before assigning an address");
|
|
|
|
my $vlan = { 'name' => 'auto',
|
|
'fullname' => 'eth0.10',
|
|
'virtual' => '',
|
|
'vlan' => 1,
|
|
'physical' => 'eth0',
|
|
'vlanid' => '10',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 };
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ %$vlan });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link add link eth0 name eth0\\.10 type vlan id 10 2>&1",
|
|
"ip link set dev eth0\\.10 up 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev eth0\\.10 2>&1" ],
|
|
"Linux active VLAN interface is created before assigning an address");
|
|
|
|
my $persisted_vlan = { 'name' => 'br-lan.10',
|
|
'fullname' => 'br-lan.10',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 };
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ({ 'name' => 'br-lan',
|
|
'fullname' => 'br-lan',
|
|
'virtual' => '',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ %$persisted_vlan });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link add link br\\-lan name br\\-lan\\.10 type vlan id 10 2>&1",
|
|
"ip link set dev br\\-lan\\.10 up 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev br\\-lan\\.10 2>&1" ],
|
|
"Linux persisted dotted VLAN is normalized before activation");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
main::activate_interface({ 'name' => 'lan.10',
|
|
'fullname' => 'lan.10',
|
|
'virtual' => '',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link set dev lan\\.10 up 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev lan\\.10 2>&1" ],
|
|
"Linux dotted interface without a live parent is not made a VLAN");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local $main::gconfig{'os_type'} = 'redhat-linux';
|
|
local $main::gconfig{'os_version'} = 13;
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ({ 'name' => 'eth0.10',
|
|
'fullname' => 'eth0.10',
|
|
'virtual' => '',
|
|
'vlan' => 1,
|
|
'physical' => 'eth0',
|
|
'vlanid' => '10',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
};
|
|
local *main::system_logged = sub {
|
|
push(@commands, $_[0]);
|
|
$? = 0;
|
|
return 0;
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ %$vlan, 'address' => '10.0.0.3' });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip addr del 10\\.0\\.0\\.2/24 dev eth0\\.10 >/dev/null 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.3/24 dev eth0\\.10 2>&1" ],
|
|
"Linux active VLAN interface is updated without being recreated");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local $main::gconfig{'os_type'} = 'debian-linux';
|
|
local $main::gconfig{'os_version'} = 12;
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifup" ? "/sbin/ifup" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
main::activate_interface({ %$vlan,
|
|
'ether' => '02:00:00:00:00:10',
|
|
'mtu' => '1400',
|
|
'address6' => [ '2001:db8::2' ],
|
|
'netmask6' => [ '64' ] });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link add link eth0 name eth0\\.10 type vlan id 10 2>&1",
|
|
"ip link set dev eth0\\.10 up 2>&1",
|
|
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev eth0\\.10 2>&1",
|
|
"ip link set dev eth0\\.10 address 02\\:00\\:00\\:00\\:00\\:10 2>&1",
|
|
"ip link set dev eth0\\.10 mtu 1400 2>&1",
|
|
"ip -6 addr add 2001\\:db8\\:\\:2/64 dev eth0\\.10 2>&1" ],
|
|
"Linux active VLAN settings use the created device with ifup present");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ({ 'name' => 'eth0.10',
|
|
'fullname' => 'eth0.10',
|
|
'virtual' => '',
|
|
'vlan' => 1,
|
|
'physical' => 'eth0',
|
|
'vlanid' => '10',
|
|
'address' => '10.0.0.2',
|
|
'netmask' => '255.255.255.0',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 1 });
|
|
};
|
|
main::activate_interface({ %$vlan, 'up' => 0 });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "ip link set dev eth0\\.10 down 2>&1" ],
|
|
"Linux active VLAN is brought down without running an empty command");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local $main::gconfig{'os_type'} = 'debian-linux';
|
|
local $main::gconfig{'os_version'} = 12;
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ip" ? "/sbin/ip" :
|
|
$_[0] eq "ifup" ? "/sbin/ifup" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
main::activate_interface({ %$vlan,
|
|
'ether' => '02:00:00:00:00:10',
|
|
'mtu' => '1400',
|
|
'address6' => [ '2001:db8::2' ],
|
|
'netmask6' => [ '64' ],
|
|
'up' => 0 });
|
|
}
|
|
is_deeply(\@commands, [ ],
|
|
"Linux absent disabled VLAN does not receive live link settings");
|
|
|
|
@commands = ( );
|
|
my @active_include_empty;
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local $main::gconfig{'os_type'} = 'suse-linux';
|
|
local $main::gconfig{'os_version'} = 15;
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ifconfig" ? "/sbin/ifconfig" :
|
|
$_[0] eq "vconfig" ? "/sbin/vconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
push(@active_include_empty, @_ ? $_[0] : undef);
|
|
return @_ && $_[0] ?
|
|
({ 'name' => 'eth0.10',
|
|
'fullname' => 'eth0.10',
|
|
'virtual' => '',
|
|
'address6' => [ ],
|
|
'netmask6' => [ ],
|
|
'up' => 0 }) : ( );
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ %$vlan });
|
|
}
|
|
is_deeply(\@active_include_empty, [ 1 ],
|
|
"Linux activation checks for interfaces without IPv4");
|
|
is_deeply(\@commands,
|
|
[ "cd / ; ifconfig eth0.10 10\\.0\\.0\\.2 netmask 255\\.255\\.255\\.0 up 2>&1" ],
|
|
"Linux existing addressless VLAN is configured without being recreated");
|
|
|
|
@commands = ( );
|
|
{
|
|
no warnings 'redefine';
|
|
no warnings 'once';
|
|
local $main::gconfig{'os_type'} = 'suse-linux';
|
|
local $main::gconfig{'os_version'} = 15;
|
|
local *main::has_command = sub {
|
|
return $_[0] eq "ifconfig" ? "/sbin/ifconfig" :
|
|
$_[0] eq "vconfig" ? "/sbin/vconfig" : undef;
|
|
};
|
|
local *main::active_interfaces = sub {
|
|
return ( );
|
|
};
|
|
local *main::backquote_command = sub {
|
|
return "";
|
|
};
|
|
main::activate_interface({ %$vlan });
|
|
}
|
|
is_deeply(\@commands,
|
|
[ "vconfig add eth0 10 2>&1",
|
|
"cd / ; ifconfig eth0.10 10\\.0\\.0\\.2 netmask 255\\.255\\.255\\.0 up 2>&1" ],
|
|
"Linux VLAN interface falls back to vconfig without ip");
|
|
|
|
done_testing();
|