#!/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 '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 '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: 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: 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();