X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=src%2FPVE%2FINotify.pm;h=0d82a522b63f105ad271eaaae7490d40ab6529e4;hp=330085066f29f461329494084d7a654c0149e7a3;hb=d37a71867233a09803e825f0249a1c7df8be25a0;hpb=c27ef07ff57a0143ef9200ed8ee53401d46f727a diff --git a/src/PVE/INotify.pm b/src/PVE/INotify.pm index 3300850..0d82a52 100644 --- a/src/PVE/INotify.pm +++ b/src/PVE/INotify.pm @@ -1,25 +1,29 @@ package PVE::INotify; # todo: maybe we do not need update_file() ? - use strict; use warnings; -use POSIX; -use IO::File; -use IO::Dir; -use File::stat; -use File::Basename; +use Clone qw(clone); +use Digest::SHA; +use Encode qw(encode decode); use Fcntl qw(:DEFAULT :flock); -use PVE::SafeSyslog; +use File::Basename; +use File::stat; +use IO::Dir; +use IO::File; +use JSON; +use Linux::Inotify2; +use POSIX; + use PVE::Exception qw(raise_param_exc); +use PVE::JSONSchema; use PVE::Network; -use PVE::Tools; use PVE::ProcFSTools; -use Clone qw(clone); -use Linux::Inotify2; +use PVE::SafeSyslog; +use PVE::Tools; + use base 'Exporter'; -use JSON; our @EXPORT_OK = qw(read_file write_file register_file); @@ -56,13 +60,11 @@ sub ccache_compute_diff { my $diff = ''; - open (TMP, "diff -b -N -u '$filename' '$shadow'|"); - - while (my $line = ) { - $diff .= $line; - } - - close (TMP); + my $cmd = ['/usr/bin/diff', '-b', '-N', '-u', $filename, $shadow]; + PVE::Tools::run_command($cmd, noerr => 1, outfunc => sub { + my ($line) = @_; + $diff .= decode('UTF-8', $line) . "\n"; + }); $diff = undef if !$diff; @@ -83,15 +85,15 @@ sub ccache_info { $cp->{$k} = $v; } $ccache->{$filename} = $cp; - } + } return ($ccache->{$filename}, $filename); } } - + $filename = $ccachemap->{$filename} if defined ($ccachemap->{$filename}); die "file '$filename' not added :ERROR" if !defined ($ccache->{$filename}); - + return ($ccache->{$filename}, $filename); } @@ -134,7 +136,7 @@ sub write_file { if (!rename($tmpname, $realname)) { my $msg = "close (rename) atomic file '$filename' failed: $!\n"; unlink $tmpname; - die $msg; + die $msg; } my $diff; @@ -167,7 +169,7 @@ sub update_file { my $code = sub { $fd = IO::File->new ($filename, "r"); - + my $new = &$update($filename, $fd, $data, @args); if (defined($new)) { @@ -215,9 +217,9 @@ sub read_file { my $parser; my ($ccinfo, $filename) = ccache_info($fileid); - + $parser = $ccinfo->{parser}; - + my $fd; my $shadow; @@ -237,7 +239,7 @@ sub read_file { if (!$fd) { $ccinfo->{version} = undef; - $ccinfo->{data} = undef; + $ccinfo->{data} = undef; $ccinfo->{diff} = undef; return undef if !$acp; } @@ -259,7 +261,7 @@ sub read_file { $ret->{data} = $ccinfo->{data}; } $ret->{changes} = $ccinfo->{diff}; - + return $full ? $ret : $ret->{data}; } @@ -292,7 +294,7 @@ sub read_file { $ret->{changes} = $ccinfo->{diff}; return $full ? $ret : $ret->{data}; -} +} sub parse_ccache_options { my ($ccinfo, %options) = @_; @@ -311,7 +313,7 @@ sub parse_ccache_options { # noclone flag for large read-only data chunks like aplinfo $ccinfo->{$opt} = $v; } elsif ($opt eq 'always_call_parser') { - # when set, we call parser even when the file does not exists. + # when set, we call parser even when the file does not exist. # this allows the parser to return some default $ccinfo->{$opt} = $v; } else { @@ -336,7 +338,7 @@ sub register_file { $ccinfo->{update} = $update; parse_ccache_options($ccinfo, %options); - + if ($options{shadow}) { $shadowfiles->{$filename} = $options{shadow}; } @@ -352,7 +354,7 @@ sub register_regex { my $uid = "$dir/$regex"; die "regular expression '$uid' already added :ERROR" if defined ($ccacheregex->{$uid}); - + my $ccinfo = {}; $ccinfo->{dir} = $dir; @@ -416,7 +418,7 @@ sub inotify_init { foreach my $uid (keys %$ccacheregex) { my $ccinfo = $ccacheregex->{$uid}; - $dirhash->{$ccinfo->{dir}}->{_regex} = 1; + $dirhash->{$ccinfo->{dir}}->{_regex} = 1; } $inotify_pid = $$; @@ -446,7 +448,7 @@ sub inotify_init { syslog ('err', "got 'unmount' event on '$name' - disabling inotify"); $inotify = undef; } - if ($e->IN_IGNORED) { + if ($e->IN_IGNORED) { syslog ('err', "got 'ignored' event on '$name' - disabling inotify"); $inotify = undef; } @@ -479,7 +481,7 @@ sub inotify_init { next if $dir ne $ccinfo->{dir}; my $re = $ccinfo->{regex}; if (my $fd = IO::Dir->new ($dir)) { - while (defined(my $de = $fd->read)) { + while (defined(my $de = $fd->read)) { if ($de =~ m/^$re$/) { my $fn = "$dir/$de"; $versions->{$fn}++; # init with version @@ -534,10 +536,58 @@ sub write_etc_hostname { return $hostname; } -register_file('hostname', "/etc/hostname", - \&read_etc_hostname, +register_file('hostname', "/etc/hostname", + \&read_etc_hostname, \&write_etc_hostname); +sub read_etc_hosts { + my ($filename, $fh) = @_; + + my $raw = ''; + my $data = ''; + + while (my $line = <$fh>) { + $raw .= $line; + if ($line =~ m/^\s*#/) { + $line = decode('UTF-8', $line); + } + $data .= $line; + } + + return { + digest => Digest::SHA::sha1_hex($raw), + data => $data, + } +} + +sub write_etc_hosts { + my ($filename, $fh, $hosts, @args) = @_; + + # check validity of ips/names + for my $line (split("\n", $hosts)) { + next if $line =~ m/^\s*#/; # comments + next if $line =~ m/^\s*$/; # whitespace/empty lines + + my ($ip, @names) = split(/\s+/, $line); + + raise_param_exc({ 'data' => "Invalid IP '$ip'" }) + if $ip !~ m/^$PVE::Tools::IPRE$/; + + for my $name (@names) { + raise_param_exc({ 'data' => "Invalid Hostname '$name'" }) + if $name !~ m/^[.\-a-zA-Z0-9]+$/; + } + } + + die "write failed: $!" if !print $fh encode('UTF-8', $hosts); + + return $hosts; +} + +register_file('etchosts', "/etc/hosts", + \&read_etc_hosts, + \&write_etc_hosts); + sub read_etc_resolv_conf { my ($filename, $fh) = @_; @@ -580,12 +630,12 @@ sub update_etc_resolv_conf { next if $line =~ m/^(search|domain|nameserver)\s+/; $data .= $line } - + return $data; } -register_file('resolvconf', "/etc/resolv.conf", - \&read_etc_resolv_conf, undef, +register_file('resolvconf', "/etc/resolv.conf", + \&read_etc_resolv_conf, undef, \&update_etc_resolv_conf); sub read_etc_timezone { @@ -615,8 +665,8 @@ sub write_etc_timezone { } -register_file('timezone', "/etc/timezone", - \&read_etc_timezone, +register_file('timezone', "/etc/timezone", + \&read_etc_timezone, \&write_etc_timezone); sub read_active_workers { @@ -624,7 +674,7 @@ sub read_active_workers { return [] if !$fh; - my $res = []; + my $res = []; while (defined (my $line = <$fh>)) { if ($line =~ m/^(\S+)\s(0|1)(\s([0-9A-Za-z]{8})(\s(\s*\S.*))?)?$/) { my $upid = $1; @@ -668,25 +718,26 @@ sub write_active_workers { PVE::Tools::safe_print($filename, $fh, $raw) if $raw; } -register_file('active', "/var/log/pve/tasks/active", +register_file('active', "/var/log/pve/tasks/active", \&read_active_workers, \&write_active_workers); -our $bond_modes = { 'balance-rr' => 0, - 'active-backup' => 1, - 'balance-xor' => 2, - 'broadcast' => 3, - '802.3ad' => 4, - 'balance-tlb' => 5, - 'balance-alb' => 6, - }; +our $bond_modes = { + 'balance-rr' => 0, + 'active-backup' => 1, + 'balance-xor' => 2, + 'broadcast' => 3, + '802.3ad' => 4, + 'balance-tlb' => 5, + 'balance-alb' => 6, +}; my $ovs_bond_modes = { 'active-backup' => 1, 'balance-slb' => 1, 'lacp-balance-slb' => 1, - 'lacp-balance-tcp' => 1, + 'lacp-balance-tcp' => 1, }; #sub get_bond_modes { @@ -759,11 +810,16 @@ my $check_mtu = sub { die "check mtu - missing parent interface\n" if !$parent; die "check mtu - missing child interface\n" if !$child; - my $pmtu = $ifaces->{$parent}->{mtu} ? $ifaces->{$parent}->{mtu} : 1500; - my $cmtu = $ifaces->{$child}->{mtu} ? $ifaces->{$child}->{mtu} : 1500; + my $cmtu = $ifaces->{$child}->{mtu}; + return if !$cmtu; + + my $parentdata = $ifaces->{$parent}; + my $pmtu = $parentdata->{mtu}; + $pmtu = $cmtu if $parentdata->{type} eq 'bond' && !$pmtu; + $pmtu = 1500 if !$pmtu; - die "interface '$parent' - mtu $pmtu is bigger than '$child' - mtu $cmtu\n" - if $pmtu > $cmtu; + die "interface '$parent' - mtu $pmtu is lower than '$child' - mtu $cmtu\n" + if $pmtu < $cmtu; }; # config => { @@ -818,6 +874,7 @@ sub __read_etc_network_interfaces { my $options = $config->{options} = []; my $options_alternatives = { + 'ovs_mtu' => 'mtu', 'bond-slaves' => 'slaves', 'bond_slaves' => 'slaves', 'bond-xmit-hash-policy' => 'bond_xmit_hash_policy', @@ -842,18 +899,20 @@ sub __read_etc_network_interfaces { } # we try to keep order inside the file - my $priority = 2; # 1 is reserved for lo + my $priority = 2; # 1 is reserved for lo SECTION: while (defined ($line = <$fh>)) { chomp ($line); next if $line =~ m/^\s*#/; - - if ($line =~ m/^\s*auto\s+(.*)$/) { - my @aa = split (/\s+/, $1); - foreach my $a (@aa) { - $ifaces->{$a}->{autostart} = 1; - } + if ($line =~ m/^\s*(allow-auto|auto|allow-ovs)\s+(.*)$/) { + + $ifaces->{$_}->{autostart} = 1 for split (/\s+/, $2); + + } elsif ($line =~ m/^\s*(allow-hotplug)\s+(.*)$/) { + + # FIXME: handle those differently? auto makes it required on-boot, vs. best-effort + $ifaces->{$_}->{autostart} = 1 for split (/\s+/, $2); } elsif ($line =~ m/^\s*iface\s+(\S+)\s+(inet6?)\s+(\S+)\s*$/) { my $i = $1; @@ -866,19 +925,13 @@ sub __read_etc_network_interfaces { push @{$d->{families}}, $family; while (defined ($line = <$fh>)) { - chomp $line; + $line =~ s/\s+$//; # drop trailing whitespaces + if ($line =~ m/^\s*#(.*?)\s*$/) { - # NOTE: we use 'comments' instead of 'comment' to - # avoid automatic utf8 conversion $f->{comments} = '' if !$f->{comments}; - $f->{comments} .= "$1\n"; - } elsif ($line =~ m/^\s*(?:iface\s - |mapping\s - |auto\s - |allow- - |source\s - |source-directory\s - )/x) { + my $comment = decode('UTF-8', $1); + $f->{comments} .= "$comment\n"; + } elsif ($line =~ m/^\s*(?:(?:iface|mapping|auto|source|source-directory)\s|allow-)/) { last; } elsif ($line =~ m/^\s*((\S+)\s+(.+))$/) { my $option = $1; @@ -902,12 +955,19 @@ sub __read_etc_network_interfaces { 'bridge-multicast-flood' => 1, 'bond_miimon' => 1, 'bond_xmit_hash_policy' => 1, + 'bond-primary' => 1, + 'link-type' => 1, + 'uplink-id' => 1, + 'vlan-protocol' => 1, + 'vlan-raw-device' => 1, + 'vlan-id' => 1, 'vxlan-id' => 1, 'vxlan-svcnodeip' => 1, 'vxlan-physdev' => 1, - 'vxlan-local-tunnelip' => 1 }; + 'vxlan-local-tunnelip' => 1, + }; - if (($id eq 'address') || ($id eq 'netmask') || ($id eq 'broadcast') || ($id eq 'gateway')) { + if ($id eq 'address' || $id eq 'netmask' || $id eq 'broadcast' || $id eq 'gateway') { $f->{$id} = $value; } elsif ($simple_options->{$id}) { $d->{$id} = $value; @@ -934,8 +994,7 @@ sub __read_etc_network_interfaces { } elsif ($id eq 'bond_mode') { # always use names foreach my $bm (keys %$bond_modes) { - my $id = $bond_modes->{$bm}; - if ($id eq $value) { + if ($bond_modes->{$bm} eq $value) { $value = $bm; last; } @@ -950,7 +1009,7 @@ sub __read_etc_network_interfaces { last; } } - $d->{"$_$suffix"} = $f->{$_} foreach (keys %$f); + $d->{"$_$suffix"} = $f->{$_} for keys $f->%*; last SECTION if !defined($line); redo SECTION; } elsif ($line =~ /\w/) { @@ -965,14 +1024,17 @@ sub __read_etc_network_interfaces { } if (!$ifaces->{lo}) { - $ifaces->{lo}->{priority} = 1; - $ifaces->{lo}->{method} = 'loopback'; - $ifaces->{lo}->{type} = 'loopback'; - $ifaces->{lo}->{autostart} = 1; + $ifaces->{lo} = { + priority => 1, + method => 'loopback', + type => 'loopback', + autostart => 1, + }; } foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; + $d->{type} = 'unknown'; if ($iface =~ m/^bond\d+$/) { if (!$d->{ovs_type}) { $d->{type} = 'bond'; @@ -992,23 +1054,18 @@ sub __read_etc_network_interfaces { } my $tag = &$extract_ovs_option($d, 'tag'); $d->{ovs_tag} = $tag if defined($tag); - } else { - $d->{type} = 'unknown'; } } elsif ($iface =~ m/^vmbr\d+$/) { if (!$d->{ovs_type}) { $d->{type} = 'bridge'; - - if (!defined ($d->{bridge_fd})) { - $d->{bridge_fd} = 0; - } if (!defined ($d->{bridge_stp})) { $d->{bridge_stp} = 'off'; } + if (!defined($d->{bridge_fd}) && $d->{bridge_stp} eq 'off') { + $d->{bridge_fd} = 0; + } } elsif ($d->{ovs_type} eq 'OVSBridge') { $d->{type} = $d->{ovs_type}; - } else { - $d->{type} = 'unknown'; } } elsif ($iface =~ m/^(\S+):\d+$/) { $d->{type} = 'alias'; @@ -1018,12 +1075,23 @@ sub __read_etc_network_interfaces { $ifaces->{$1}->{exists} = 0; $d->{exists} = 0; } - } elsif ($iface =~ m/^(\S+)\.\d+$/) { + } elsif ($iface =~ m/^(\S+)\.(\d+)$/ || $d->{'vlan-raw-device'}) { $d->{type} = 'vlan'; - if (defined ($ifaces->{$1})) { - $d->{exists} = $ifaces->{$1}->{exists}; + + my ($dev, $id) = ($1, $2); + $d->{'vlan-raw-device'} = $dev if defined($dev) && !$d->{'vlan-raw-device'}; + + if (!$id && $iface =~ m/^vlan(\d+)$/) { # VLAN id 0 is not valid, so truthy check it is + $id = $1; + } + $d->{'vlan-id'} = $id if $id; + + my $raw_iface = $d->{'vlan-raw-device'}; + + if (defined ($ifaces->{$raw_iface})) { + $d->{exists} = $ifaces->{$raw_iface}->{exists}; } else { - $ifaces->{$1}->{exists} = 0; + $ifaces->{$raw_iface}->{exists} = 0; $d->{exists} = 0; } } elsif ($iface =~ m/^$PVE::Network::PHYSICAL_NIC_RE$/) { @@ -1033,26 +1101,61 @@ sub __read_etc_network_interfaces { $d->{type} = $d->{ovs_type}; my $tag = &$extract_ovs_option($d, 'tag'); $d->{ovs_tag} = $tag if defined($tag); - } else { - $d->{type} = 'unknown'; } } elsif ($iface =~ m/^lo$/) { $d->{type} = 'loopback'; } else { if ($d->{'vxlan-id'}) { $d->{type} = 'vxlan'; - } elsif (!$d->{ovs_type}) { - $d->{type} = 'unknown'; - } elsif ($d->{ovs_type} eq 'OVSIntPort') { - $d->{type} = $d->{ovs_type}; - my $tag = &$extract_ovs_option($d, 'tag'); - $d->{ovs_tag} = $tag if defined($tag); + } elsif (defined($d->{ovs_type})) { + if ($d->{ovs_type} eq 'OVSIntPort') { + $d->{type} = $d->{ovs_type}; + my $tag = &$extract_ovs_option($d, 'tag'); + $d->{ovs_tag} = $tag if defined($tag); + } + } elsif (defined($d->{'link-type'})) { + $d->{type} = $d->{'link-type'} if $d->{'link-type'} eq 'dummy'; + } + } + + # map address and netmask to cidr + if (my $addr = $d->{address}) { + if (_address_is_cidr($addr)) { + $d->{cidr} = $addr; + my ($baseaddr, $mask) = _cidr_split($addr); + $d->{address} = $baseaddr; + $d->{netmask} = $mask; + } elsif (my $cidr = _get_cidr($d->{address}, $d->{netmask})) { + $d->{cidr} = $cidr; + (undef, $d->{netmask}) = _cidr_split($cidr); + } else { + # no mask, else we'd got a cidr above + $d->{cidr} = $addr ."/32"; + } + } + + # map address6 and netmask6 to cidr6 + if (my $addr6 = $d->{address6}) { + if (_address_is_cidr($addr6)) { + $d->{cidr6} = $addr6; + my ($baseaddr, $mask) = _cidr_split($addr6); + $d->{address6} = $baseaddr; + $d->{netmask6} = $mask; + } elsif (my $cidr6 = _get_cidr($d->{address6}, $d->{netmask6})) { + $d->{cidr6} = $cidr6; + } else { + # no mask, else we'd got a cidr above + $d->{cidr6} = $addr6 ."/128"; } } $d->{method} = 'manual' if !$d->{method}; $d->{method6} = 'manual' if !$d->{method6}; + if (my $comments6 = delete $d->{comments6}) { + $d->{comments} = ($d->{comments} // '') . $comments6; + } + $d->{families} ||= ['inet']; } @@ -1060,7 +1163,9 @@ sub __read_etc_network_interfaces { # from the {options} hash for them to be removed correctly. @$options = grep {defined($_)} map { my ($pri, $line) = @$_; - if ($line =~ /^allow-(\S+)\s+(.*)$/) { + if ($line =~ /^allow-ovs\s+(.*)$/) { + undef; + } elsif ($line =~ /^allow-(\S+)\s+(.*)$/) { my $bridge = $1; my @ports = split(/\s+/, $2); if (defined(my $br = $ifaces->{$bridge})) { @@ -1083,6 +1188,31 @@ sub __read_etc_network_interfaces { return $config; } +sub _address_is_cidr { + my ($addr) = @_; + return $addr =~ /\/\d+$/ ? 1 : 0; +} + +sub _cidr_split { + my ($cidr) = @_; + $cidr =~ /^(.+)\/(\d+)$/; + return ($1, $2); # (address, mask) +} + +sub _get_cidr { + my ($addr, $mask) = @_; + + return $addr if _address_is_cidr($addr); + return undef if !$mask; + + if ($mask =~ m/^\d+$/) { # cidr notation + return $addr . "/" . $mask; + } elsif (my $cidrmask = PVE::JSONSchema::get_netmask_bits($mask)) { + return $addr . "/" . $cidrmask; + } + return undef; +} + sub __interface_to_string { my ($iface, $d, $family, $first_block, $ifupdown2) = @_; @@ -1090,45 +1220,67 @@ sub __interface_to_string { return '' if !($d && $d->{"method$suffix"}); - my $raw = ''; + my $raw = "iface $iface $family " . $d->{"method$suffix"} . "\n"; - $raw .= "iface $iface $family " . $d->{"method$suffix"} . "\n"; - $raw .= "\taddress " . $d->{"address$suffix"} . "\n" if $d->{"address$suffix"}; - $raw .= "\tnetmask " . $d->{"netmask$suffix"} . "\n" if $d->{"netmask$suffix"}; - $raw .= "\tgateway " . $d->{"gateway$suffix"} . "\n" if $d->{"gateway$suffix"}; - $raw .= "\tbroadcast " . $d->{"broadcast$suffix"} . "\n" if $d->{"broadcast$suffix"}; + if (my $addr = $d->{"address$suffix"}) { + if ($addr !~ /\/\d+$/ && $d->{"netmask$suffix"}) { + if ($d->{"netmask$suffix"} =~ m/^\d+$/) { + $addr .= "/" . $d->{"netmask$suffix"}; + } elsif (my $mask = PVE::JSONSchema::get_netmask_bits($d->{"netmask$suffix"})) { + $addr .= "/" . $mask; + } + } + $raw .= "\taddress ${addr}\n"; + } + + $raw .= "\tgateway " . $d->{"gateway$suffix"} . "\n" if $d->{"gateway$suffix"}; - my $done = { type => 1, priority => 1, method => 1, active => 1, exists => 1, - comments => 1, autostart => 1, options => 1, - address => 1, netmask => 1, gateway => 1, broadcast => 1, - method6 => 1, families => 1, options6 => 1, - address6 => 1, netmask6 => 1, gateway6 => 1, broadcast6 => 1 }; + my $done = { + type => 1, priority => 1, method => 1, active => 1, exists => 1, comments => 1, + autostart => 1, options => 1, address => 1, netmask => 1, gateway => 1, broadcast => 1, + method6 => 1, families => 1, options6 => 1, comments6 => 1, address6 => 1, + netmask6 => 1, gateway6 => 1, broadcast6 => 1, 'uplink-id' => 1, + }; if (!$first_block) { # not printing out options } elsif ($d->{type} eq 'bridge') { - $d->{bridge_ports} =~ s/[;,\s]+/ /g; my $ports = $d->{bridge_ports} || 'none'; + $ports =~ s/[;,\s]+/ /g; $raw .= "\tbridge-ports $ports\n"; $done->{bridge_ports} = 1; - my $v = defined($d->{bridge_stp}) ? $d->{bridge_stp} : 'off'; - $raw .= "\tbridge-stp $v\n"; + my $br_stp = defined($d->{bridge_stp}) ? $d->{bridge_stp} : 'off'; + my $no_stp = $br_stp eq 'off'; + + $raw .= "\tbridge-stp $br_stp\n"; $done->{bridge_stp} = 1; - $v = defined($d->{bridge_fd}) ? $d->{bridge_fd} : 0; - $raw .= "\tbridge-fd $v\n"; + # NOTE: forwarding delay must be 2 <= FD <= 30 if STP is enabled + if (defined(my $br_fd = $d->{bridge_fd})) { + if ($no_stp || ($br_fd >= 2 && $br_fd <= 30)) { + $raw .= "\tbridge-fd $br_fd\n"; + } else { + # only complain if the user actually set a value, but not for default fallback below + warn "'$iface': ignoring 'bridge_fd' value '$br_fd', outside of allowed range 2-30\n"; + } + } elsif ($no_stp) { + $raw .= "\tbridge-fd 0\n"; + } $done->{bridge_fd} = 1; if( defined($d->{bridge_vlan_aware})) { $raw .= "\tbridge-vlan-aware yes\n"; - $v = defined($d->{bridge_vids}) ? $d->{bridge_vids} : "2-4094"; - $raw .= "\tbridge-vids $v\n"; + my $vlans = defined($d->{bridge_vids}) ? $d->{bridge_vids} : "2-4094"; + $raw .= "\tbridge-vids $vlans\n"; } $done->{bridge_vlan_aware} = 1; $done->{bridge_vids} = 1; - + + $raw .= "\tmtu $d->{mtu}\n" if $d->{mtu}; + $done->{mtu} = 1; + } elsif ($d->{type} eq 'bond') { $d->{slaves} =~ s/[;,\s]+/ /g; @@ -1149,6 +1301,19 @@ sub __interface_to_string { $raw .= "\tbond-xmit-hash-policy $d->{'bond_xmit_hash_policy'}\n"; } $done->{'bond_xmit_hash_policy'} = 1; + + if ($d->{'bond_mode'} && $d->{'bond_mode'} eq 'active-backup' && $d->{'bond-primary'}) { + $raw .= "\tbond-primary $d->{'bond-primary'}\n"; + } + $done->{'bond-primary'} = 1; + + $raw .= "\tmtu $d->{mtu}\n" if $d->{mtu}; + $done->{mtu} = 1; + + } elsif ($d->{type} eq 'vlan') { + die "$iface: wrong vlan-protocol $d->{'vlan-protocol'}\n" + if $d->{'vlan-protocol'} && $d->{'vlan-protocol'} ne '802.1ad' && $d->{'vlan-protocol'} ne '802.1q'; + } elsif ($d->{type} eq 'vxlan') { foreach my $k (qw(vxlan-id vxlan-svcnodeip vxlan-physdev vxlan-local-tunnelip)) { @@ -1162,6 +1327,10 @@ sub __interface_to_string { } $done->{'vxlan-remoteip'} = 1; } + + $raw .= "\tmtu $d->{mtu}\n" if $d->{mtu}; + $done->{mtu} = 1; + } elsif ($d->{type} eq 'OVSBridge') { $raw .= "\tovs_type $d->{type}\n"; @@ -1169,8 +1338,11 @@ sub __interface_to_string { $raw .= "\tovs_ports $d->{ovs_ports}\n" if $d->{ovs_ports}; $done->{ovs_ports} = 1; - } elsif ($d->{type} eq 'OVSPort' || $d->{type} eq 'OVSIntPort' || - $d->{type} eq 'OVSBond') { + + $raw .= "\tovs_mtu $d->{mtu}\n" if $d->{mtu}; + $done->{mtu} = 1; + + } elsif ($d->{type} eq 'OVSPort' || $d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') { $d->{autostart} = 0; # started by the bridge @@ -1205,17 +1377,19 @@ sub __interface_to_string { $raw .= "\tovs_type $d->{type}\n"; $done->{ovs_type} = 1; - if ($d->{ovs_bridge}) { - + if (my $bridge = $d->{ovs_bridge}) { if ($ifupdown2) { $raw = "auto $iface\n$raw"; } else { - $raw = "allow-$d->{ovs_bridge} $iface\n$raw"; + $raw = "allow-$bridge $iface\n$raw"; } - $raw .= "\tovs_bridge $d->{ovs_bridge}\n"; + $raw .= "\tovs_bridge $bridge\n"; $done->{ovs_bridge} = 1; } + + $raw .= "\tovs_mtu $d->{mtu}\n" if $d->{mtu}; + $done->{mtu} = 1; } if ($first_block) { @@ -1245,9 +1419,9 @@ sub __interface_to_string { sub write_etc_network_interfaces { my ($filename, $fh, $config) = @_; - my $ifupdown2 = -e '/usr/share/ifupdown2'; + my $ifupdown2 = -e '/usr/share/ifupdown2/ifupdown2'; my $raw = __write_etc_network_interfaces($config, $ifupdown2); - PVE::Tools::safe_print($filename, $fh, $raw); + PVE::Tools::safe_print($filename, $fh, encode('UTF-8', $raw)); } sub __write_etc_network_interfaces { my ($config, $ifupdown2) = @_; @@ -1260,6 +1434,10 @@ sub __write_etc_network_interfaces { foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; + my ($cidr, $cidr6) = (delete $d->{cidr}, delete $d->{cidr6}); + $d->{address} //= $cidr; + $d->{address6} //= $cidr6; + my $ports = ''; foreach my $k (qw(bridge_ports ovs_ports slaves ovs_bonds)) { $ports .= " $d->{$k}" if $d->{$k}; @@ -1275,15 +1453,16 @@ sub __write_etc_network_interfaces { # delete unused OVS ports foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; - if ($d->{type} eq 'OVSPort' || $d->{type} eq 'OVSIntPort' || - $d->{type} eq 'OVSBond') { + if ($d->{type} eq 'OVSPort' || $d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') { my $brname = $used_ports->{$iface}; if (!$brname || !$ifaces->{$brname}) { if ($iface =~ /^$PVE::Network::PHYSICAL_NIC_RE/) { - $ifaces->{$iface} = { type => 'eth', - exists => 1, - method => 'manual', - families => ['inet'] }; + $ifaces->{$iface} = { + type => 'eth', + exists => 1, + method => 'manual', + families => ['inet'], + }; } else { delete $ifaces->{$iface}; } @@ -1303,8 +1482,7 @@ sub __write_etc_network_interfaces { if ($d->{type} eq 'OVSBridge' && $d->{ovs_ports}) { foreach my $p (split (/\s+/, $d->{ovs_ports})) { my $n = $ifaces->{$p}; - die "OVS bridge '$iface' - unable to find port '$p'\n" - if !$n; + die "OVS bridge '$iface' - unable to find port '$p'\n" if !$n; $n->{autostart} = 0; if ($n->{type} eq 'eth') { $n->{type} = 'OVSPort'; @@ -1327,10 +1505,10 @@ sub __write_etc_network_interfaces { if ($d->{type} eq 'OVSBond' && $d->{ovs_bonds}) { foreach my $p (split (/\s+/, $d->{ovs_bonds})) { my $n = $ifaces->{$p}; - die "OVS bond '$iface' - unable to find slave '$p'\n" - if !$n; - die "OVS bond '$iface' - wrong interface type on slave '$p' " . - "('$n->{type}' != 'eth')\n" if $n->{type} ne 'eth'; + $n->{autostart} = 1; + die "OVS bond '$iface' - unable to find slave '$p'\n" if !$n; + die "OVS bond '$iface' - wrong interface type on slave '$p' ('$n->{type}' != 'eth')\n" + if $n->{type} ne 'eth'; &$check_mtu($ifaces, $iface, $p); } } @@ -1339,17 +1517,21 @@ sub __write_etc_network_interfaces { # check bond foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; - if ($d->{type} eq 'bond' && $d->{slaves}) { - foreach my $p (split (/\s+/, $d->{slaves})) { - my $n = $ifaces->{$p}; + next if !($d->{type} eq 'bond' && $d->{slaves}); - die "bond '$iface' - unable to find slave '$p'\n" - if !$n; - die "bond '$iface' - wrong interface type on slave '$p' " . - "('$n->{type}' != 'eth')\n" if $n->{type} ne 'eth'; - &$check_mtu($ifaces, $iface, $p); - } + my $bond_primary_is_slave = undef; + foreach my $p (split (/\s+/, $d->{slaves})) { + my $n = $ifaces->{$p}; + $n->{autostart} = 1; + + die "bond '$iface' - unable to find slave '$p'\n" if !$n; + die "bond '$iface' - wrong interface type on slave '$p' ('$n->{type}' != 'eth or bond')\n" + if ($n->{type} ne 'eth' && $n->{type} ne 'bond'); + + $check_mtu->($ifaces, $iface, $p); + $bond_primary_is_slave = 1 if $d->{'bond-primary'} && $d->{'bond-primary'} eq $p; } + die "bond '$iface' - bond-primary interface is not a slave" if $d->{'bond-primary'} && !$bond_primary_is_slave; } # check vxlan @@ -1380,34 +1562,84 @@ sub __write_etc_network_interfaces { # check vlan foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; - if ($d->{type} eq 'vlan' && $iface =~ m/^(\S+)\.\d+$/) { - my $p = $1; + if ($d->{type} eq 'vlan') { + + my $p = undef; + my $vlanid = undef; + + if ($iface =~ m/^(\S+)\.(\d+)$/) { + $p = $1; + $vlanid = $2; + delete $d->{'vlan-raw-device'} if $d->{'vlan-raw-device'}; + delete $d->{'vlan-id'} if $d->{'vlan-id'}; + + } else { + die "missing vlan-raw-device option" if !$d->{'vlan-raw-device'}; + $p = $d->{'vlan-raw-device'}; + + if ($iface =~ m/^vlan(\d+)$/) { + $vlanid = $1; + delete $d->{'vlan-id'} if $d->{'vlan-id'}; + } else { + die "custom vlan interface name need ifupdown2" if !$ifupdown2; + die "missing vlan-id option" if !$d->{'vlan-id'}; + $vlanid = $d->{'vlan-id'}; + } + } my $n = $ifaces->{$p}; + die "vlan '$iface' - vlan-id $vlanid should be <= 4094\n" if $vlanid > 4094; die "vlan '$iface' - unable to find parent '$p'\n" - if $n->{exists} eq 0; + if !$n; - if ($n->{type} eq 'bridge' && !$n->{bridge_vlan_aware}) { - die "vlan '$iface' - bridge vlan aware is not enabled on parent '$p'\n"; - } elsif ($n->{type} ne 'eth' && $n->{type} ne 'bridge' && $n->{type} ne 'bond') { + if ($n->{type} ne 'eth' && $n->{type} ne 'bridge' && $n->{type} ne 'bond' && $n->{type} ne 'vlan') { die "vlan '$iface' - wrong interface type on parent '$p' " . - "('$n->{type}' != 'eth|bond|bridge' )\n"; + "('$n->{type}' != 'eth|bond|bridge|vlan' )\n"; } - &$check_mtu($ifaces, $iface, $p); + + &$check_mtu($ifaces, $p, $iface); + + } + } + + # check uplink + my $uplinks = {}; + foreach my $iface (keys %$ifaces) { + my $d = $ifaces->{$iface}; + if (my $uplinkid = $d->{'uplink-id'}) { + die "iface '$iface' - uplink-id $uplinkid is only allowed on physical and linux bond interfaces\n" + if $d->{type} ne 'eth' && $d->{type} ne 'bond'; + + die "iface '$iface' - uplink-id $uplinkid is already assigned on '$uplinks->{$uplinkid}'\n" + if $uplinks->{$uplinkid}; + + $uplinks->{$uplinkid} = $iface; } } # check bridgeport option my $bridgeports = {}; my $bridges = {}; - foreach my $iface (keys %$ifaces) { - my $d = $ifaces->{$iface}; + my $ifaces_copy = { %$ifaces }; + foreach my $iface (keys %$ifaces_copy) { + my $d = $ifaces_copy->{$iface}; if ($d->{type} eq 'bridge') { - foreach my $p (split (/\s+/, $d->{bridge_ports})) { - my $n = $ifaces->{$p}; - die "bridge '$iface' - unable to find bridge port '$p'\n" - if !$n; - &$check_mtu($ifaces, $iface, $p); + foreach my $p (split (/\s+/, $d->{bridge_ports} // '')) { + if($p =~ m/(\S+)\.(\d+)$/) { + my $vlanparent = $1; + if (!defined($ifaces_copy->{$p})) { + $ifaces_copy->{$p}->{type} = 'vlan'; + $ifaces_copy->{$p}->{method} = 'manual'; + $ifaces_copy->{$p}->{method6} = 'manual'; + $ifaces_copy->{$p}->{mtu} = $ifaces_copy->{$vlanparent}->{mtu} if defined($ifaces_copy->{$1}->{mtu}); + } + } + my $n = $ifaces_copy->{$p}; + die "bridge '$iface' - unable to find bridge port '$p'\n" if !$n; + die "iface $p - ip address can't be set on interface if bridged in $iface\n" + if ($n->{method} && $n->{method} eq 'static' && $n->{address} ne '0.0.0.0') || + ($n->{method6} && $n->{method6} eq 'static' && $n->{address6} ne '::'); + &$check_mtu($ifaces_copy, $p, $iface); $bridgeports->{$p} = $iface; } $bridges->{$iface} = $d; @@ -1432,10 +1664,10 @@ sub __write_etc_network_interfaces { # Please do NOT modify this file directly, unless you know what # you're doing. # -# If you want to manage part of the network configuration manually, +# If you want to manage parts of the network configuration manually, # please utilize the 'source' or 'source-directory' directives to do # so. -# PVE will preserve these directives, but will NOT its network +# PVE will preserve these directives, but will NOT read its network # configuration from sourced files, so do not attempt to move any of # the PVE managed interfaces into external files! @@ -1445,40 +1677,34 @@ NETWORKDOC my $if_type_hash = { loopback => 100000, + dummy => 100000, eth => 200000, - bond => 300000, - bridge => 400000, - vxlan => 500000, + OVSPort => 200000, + OVSIntPort => 300000, + OVSBond => 400000, + bond => 400000, + bridge => 500000, + OVSBridge => 500000, + vlan => 600000, + vxlan => 600000, }; my $lookup_type_prio = sub { - my $iface = shift; + my ($iface, $ifaces) = @_; - my $child = 0; - if ($iface =~ m/^(\S+)(\.|:)\d+$/) { - $iface = $1; - $child = 1; - } - - my $pri; - if ($iface eq 'lo') { - $pri = $if_type_hash->{loopback}; - } elsif ($iface =~ m/^$PVE::Network::PHYSICAL_NIC_RE$/) { - $pri = $if_type_hash->{eth} + $child; - } elsif ($iface =~ m/^bond\d+$/) { - $pri = $if_type_hash->{bond} + $child; - } elsif ($iface =~ m/^vmbr\d+$/) { - $pri = $if_type_hash->{bridge} + $child; - } + my ($rootiface, @rest) = split(/[.:]/, $iface); + my $childlevel = scalar(@rest); + my $type = $ifaces->{$rootiface}->{type}; + return if !$type || $type eq 'unknown'; - return $pri; + return $if_type_hash->{$type} + $childlevel }; foreach my $iface (sort { my $ref1 = $ifaces->{$a}; my $ref2 = $ifaces->{$b}; - my $tp1 = &$lookup_type_prio($a); - my $tp2 = &$lookup_type_prio($b); + my $tp1 = &$lookup_type_prio($a, $ifaces); + my $tp2 = &$lookup_type_prio($b, $ifaces); # Only recognized types are in relation to each other. If one type # is unknown then only consider the interfaces' priority attributes. @@ -1492,7 +1718,6 @@ NETWORKDOC return $a cmp $b; } keys %$ifaces) { next if $printed->{$iface}; - my $d = $ifaces->{$iface}; my $pri = $d->{priority} // 0; if (@options && $options[0]->[0] < $pri) { @@ -1503,7 +1728,20 @@ NETWORKDOC } $printed->{$iface} = 1; - $raw .= "auto $iface\n" if $d->{autostart}; + if ($d->{autostart}) { + if ($d->{type} eq 'OVSBridge' && !$ifupdown2) { + # cannot use 'auto' for OVS, would add race with systemd ifup@.service + $raw .= "allow-ovs $iface\n"; + } else { + $raw .= "auto $iface\n"; + } + } + + # if 'inet6' is the only family + if (scalar($d->{families}->@*) == 1 && $d->{families}[0] eq 'inet6') { + $d->{comments6} = delete $d->{comments}; + } + my $i = 0; # some options should be printed only once $raw .= __interface_to_string($iface, $d, $_, !$i++, $ifupdown2) foreach @{$d->{families}}; } @@ -1529,7 +1767,7 @@ sub read_iscsi_initiatorname { return 'undefined'; } -register_file('initiatorname', "/etc/iscsi/initiatorname.iscsi", +register_file('initiatorname', "/etc/iscsi/initiatorname.iscsi", \&read_iscsi_initiatorname); sub read_apt_auth { @@ -1541,7 +1779,7 @@ sub read_apt_auth { $raw =~ s/^\s+//; - + my @tokens = split(/\s+/, $raw); my $data = {}; @@ -1565,8 +1803,11 @@ my $format_apt_auth_data = sub { my $raw = ''; - foreach my $machine (sort keys %$data) { + # sort longer entries first, so machine definitions with higher granularity are preferred + for my $machine (sort { length($b) <=> length($a) || $a cmp $b} keys %$data) { my $d = $data->{$machine}; + next if !defined($d); # allow "deleting" set entries + $raw .= "machine $machine\n"; $raw .= " login $d->{login}\n" if $d->{login}; $raw .= " password $d->{password}\n" if $d->{password}; @@ -1579,10 +1820,10 @@ my $format_apt_auth_data = sub { sub write_apt_auth { my ($filename, $fh, $data) = @_; - my $raw = &$format_apt_auth_data($data); + my $raw = $format_apt_auth_data->($data); die "write failed: $!" unless print $fh "$raw\n"; - + return $data; } @@ -1595,11 +1836,16 @@ sub update_apt_auth { $orig->{$machine} = $data->{$machine}; } - return &$format_apt_auth_data($orig); + return $format_apt_auth_data->($orig); } -register_file('apt-auth', "/etc/apt/auth.conf", - \&read_apt_auth, \&write_apt_auth, - \&update_apt_auth, perm => 0640); +register_file( + 'apt-auth', + "/etc/apt/auth.conf", + \&read_apt_auth, + \&write_apt_auth, + \&update_apt_auth, + perm => 0640, +); 1;