X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=src%2FPVE%2FINotify.pm;h=1a528fa356bc0f7207dfeb4f816487ca596b7d4e;hp=e773cf4d93649b56c5fc93775abf01a151d4ac84;hb=c8ff0bdf3dd8606400cf2999c770408f1ef02d84;hpb=48ab17b3eec31fdb89b8523f7ca2577fa87298c5 diff --git a/src/PVE/INotify.pm b/src/PVE/INotify.pm index e773cf4..1a528fa 100644 --- a/src/PVE/INotify.pm +++ b/src/PVE/INotify.pm @@ -13,11 +13,16 @@ use File::Basename; use Fcntl qw(:DEFAULT :flock); use PVE::SafeSyslog; use PVE::Exception qw(raise_param_exc); +use PVE::Network; use PVE::Tools; -use Storable qw(dclone); +use PVE::ProcFSTools; +use PVE::JSONSchema; +use Clone qw(clone); use Linux::Inotify2; use base 'Exporter'; -use JSON; +use JSON; +use Digest::SHA; +use Encode qw(encode decode); our @EXPORT_OK = qw(read_file write_file register_file); @@ -54,13 +59,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; @@ -81,15 +84,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); } @@ -132,7 +135,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; @@ -165,7 +168,7 @@ sub update_file { my $code = sub { $fd = IO::File->new ($filename, "r"); - + my $new = &$update($filename, $fd, $data, @args); if (defined($new)) { @@ -197,23 +200,29 @@ sub discard_changes { return read_file ($filename, $full); } +sub poll_changes { + my ($filename) = @_; + + poll() if $inotify; # read new inotify events + + $versions->{$filename} = 0 if !defined ($versions->{$filename}); + + return $versions->{$filename}; +} + sub read_file { my ($fileid, $full) = @_; my $parser; my ($ccinfo, $filename) = ccache_info($fileid); - + $parser = $ccinfo->{parser}; - + my $fd; my $shadow; - poll() if $inotify; # read new inotify events - - $versions->{$filename} = 0 if !defined ($versions->{$filename}); - - my $cver = $versions->{$filename}; + my $cver = poll_changes($filename); if (my $copy = $shadowfiles->{$filename}) { if ($fd = IO::File->new ($copy, "r")) { @@ -229,7 +238,7 @@ sub read_file { if (!$fd) { $ccinfo->{version} = undef; - $ccinfo->{data} = undef; + $ccinfo->{data} = undef; $ccinfo->{diff} = undef; return undef if !$acp; } @@ -238,20 +247,20 @@ sub read_file { # file unchanged? if (!$ccinfo->{nocache} && - $inotify && $versions->{$filename} && + $inotify && $cver && defined ($ccinfo->{data}) && defined ($ccinfo->{version}) && ($ccinfo->{readonce} || - ($ccinfo->{version} == $versions->{$filename}))) { + ($ccinfo->{version} == $cver))) { my $ret; if (!$noclone && ref ($ccinfo->{data})) { - $ret->{data} = dclone ($ccinfo->{data}); + $ret->{data} = clone ($ccinfo->{data}); } else { $ret->{data} = $ccinfo->{data}; } $ret->{changes} = $ccinfo->{diff}; - + return $full ? $ret : $ret->{data}; } @@ -268,7 +277,7 @@ sub read_file { } # we cache data with references, so we always need to - # dclone this data. Else the original data may get + # clone this data. Else the original data may get # modified. $ccinfo->{data} = $res; @@ -277,14 +286,14 @@ sub read_file { my $ret; if (!$noclone && ref ($ccinfo->{data})) { - $ret->{data} = dclone ($ccinfo->{data}); + $ret->{data} = clone ($ccinfo->{data}); } else { $ret->{data} = $ccinfo->{data}; } $ret->{changes} = $ccinfo->{diff}; return $full ? $ret : $ret->{data}; -} +} sub parse_ccache_options { my ($ccinfo, %options) = @_; @@ -303,7 +312,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 { @@ -328,7 +337,7 @@ sub register_file { $ccinfo->{update} = $update; parse_ccache_options($ccinfo, %options); - + if ($options{shadow}) { $shadowfiles->{$filename} = $options{shadow}; } @@ -340,11 +349,11 @@ sub register_file { sub register_regex { my ($dir, $regex, $parser, $writer, $update, %options) = @_; - die "can't register regex after initify_init" if $inotify; + die "can't register regex after inotify_init" if $inotify; my $uid = "$dir/$regex"; die "regular expression '$uid' already added :ERROR" if defined ($ccacheregex->{$uid}); - + my $ccinfo = {}; $ccinfo->{dir} = $dir; @@ -408,7 +417,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 = $$; @@ -438,7 +447,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; } @@ -471,7 +480,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 @@ -526,10 +535,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) = @_; @@ -540,7 +597,7 @@ sub read_etc_resolv_conf { chomp $line; if ($line =~ m/^(search|domain)\s+(\S+)\s*/) { $res->{search} = $2; - } elsif ($line =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*/) { + } elsif ($line =~ m/^\s*nameserver\s+($PVE::Tools::IPRE)\s*/) { $nscount++; if ($nscount <= 3) { $res->{"dns$nscount"} = $1; @@ -572,12 +629,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 { @@ -607,8 +664,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 { @@ -616,7 +673,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; @@ -648,9 +705,9 @@ sub write_active_workers { my $saved = $task->{saved} ? 1 : 0; if ($task->{endtime}) { if ($task->{status}) { - $raw .= sprintf("$upid $saved %08X $task->{status}\n", $task->{endtime}); + $raw .= sprintf("%s %s %08X %s\n", $upid, $saved, $task->{endtime}, $task->{status}); } else { - $raw .= sprintf("$upid $saved %08X\n", $task->{endtime}); + $raw .= sprintf("%s %s %08X\n", $upid, $saved, $task->{endtime}); } } else { $raw .= "$upid $saved\n"; @@ -660,12 +717,12 @@ 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); -my $bond_modes = { 'balance-rr' => 0, +our $bond_modes = { 'balance-rr' => 0, 'active-backup' => 1, 'balance-xor' => 2, 'broadcast' => 3, @@ -678,7 +735,7 @@ 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 { @@ -745,6 +802,24 @@ my $extract_ovs_option = sub { return $v; }; +my $check_mtu = sub { + my ($ifaces, $parent, $child) = @_; + + die "check mtu - missing parent interface\n" if !$parent; + die "check mtu - missing child interface\n" if !$child; + + 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 lower than '$child' - mtu $cmtu\n" + if $pmtu < $cmtu; +}; + # config => { # ifaces => { # $ifname => { @@ -784,31 +859,52 @@ my $extract_ovs_option = sub { # } sub read_etc_network_interfaces { my ($filename, $fh) = @_; + my $proc_net_dev = IO::File->new('/proc/net/dev', 'r'); + my $active = PVE::ProcFSTools::get_active_network_interfaces(); + return __read_etc_network_interfaces($fh, $proc_net_dev, $active); +} + +sub __read_etc_network_interfaces { + my ($fh, $proc_net_dev, $active_ifaces) = @_; my $config = {}; my $ifaces = $config->{ifaces} = {}; my $options = $config->{options} = []; + my $options_alternatives = { + 'bond-slaves' => 'slaves', + 'bond_slaves' => 'slaves', + 'bond-xmit-hash-policy' => 'bond_xmit_hash_policy', + 'bond-mode' => 'bond_mode', + 'bond-miimon' =>'bond_miimon', + 'bridge-vlan-aware' => 'bridge_vlan_aware', + 'bridge-fd' => 'bridge_fd', + 'bridge-stp' => 'bridge_stp', + 'bridge-ports' => 'bridge_ports', + 'bridge-vids' => 'bridge_vids' + }; + my $line; - if (my $fd2 = IO::File->new("/proc/net/dev", "r")) { - while (defined ($line = <$fd2>)) { - if ($line =~ m/^\s*(eth\d+):.*/) { + if ($proc_net_dev) { + while (defined ($line = <$proc_net_dev>)) { + if ($line =~ m/^\s*($PVE::Network::PHYSICAL_NIC_RE):.*/) { $ifaces->{$1}->{exists} = 1; } } - close($fd2); + close($proc_net_dev); } # 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); + next if $line =~ m/^\s*(allow-hotplug)\s+(.*)$/; + + if ($line =~ m/^\s*(auto|allow-ovs)\s+(.*)$/) { + my @aa = split (/\s+/, $2); foreach my $a (@aa) { $ifaces->{$a}->{autostart} = 1; @@ -820,17 +916,16 @@ sub read_etc_network_interfaces { my $f = { method => $3 }; # by family, merged to $d with a $suffix (my $suffix = $family) =~ s/^inet//; - my $d = $ifaces->{$i}; + my $d = $ifaces->{$i} ||= {}; $d->{priority} = $priority++ if !$d->{priority}; push @{$d->{families}}, $family; while (defined ($line = <$fh>)) { chomp $line; 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"; + my $comment = decode('UTF-8', $1); + $f->{comments} .= "$comment\n"; } elsif ($line =~ m/^\s*(?:iface\s |mapping\s |auto\s @@ -842,10 +937,35 @@ sub read_etc_network_interfaces { } elsif ($line =~ m/^\s*((\S+)\s+(.+))$/) { my $option = $1; my ($id, $value) = ($2, $3); + + $id = $options_alternatives->{$id} if $options_alternatives->{$id}; + + my $simple_options = { + 'mtu' => 1, + 'ovs_type' => 1, + 'ovs_options' => 1, + 'ovs_bridge' => 1, + 'ovs_bonds' => 1, + 'ovs_ports' => 1, + 'bridge_fd' => 1, + 'bridge_vids' => 1, + 'bridge-access' => 1, + 'bridge-learning' => 1, + 'bridge-arp-nd-suppress' => 1, + 'bridge-unicast-flood' => 1, + 'bridge-multicast-flood' => 1, + 'bond_miimon' => 1, + 'bond_xmit_hash_policy' => 1, + 'uplink-id' => 1, + 'vlan-protocol' => 1, + 'vxlan-id' => 1, + 'vxlan-svcnodeip' => 1, + 'vxlan-physdev' => 1, + 'vxlan-local-tunnelip' => 1 }; + if (($id eq 'address') || ($id eq 'netmask') || ($id eq 'broadcast') || ($id eq 'gateway')) { $f->{$id} = $value; - } elsif ($id eq 'ovs_type' || $id eq 'ovs_options'|| $id eq 'ovs_bridge' || - $id eq 'ovs_bonds' || $id eq 'ovs_ports') { + } elsif ($simple_options->{$id}) { $d->{$id} = $value; } elsif ($id eq 'slaves' || $id eq 'bridge_ports') { my $devs = {}; @@ -865,12 +985,8 @@ sub read_etc_network_interfaces { } else { $d->{$id} = 'off'; } - } elsif ($id eq 'bridge_fd') { - $d->{$id} = $value; - } elsif ($id eq 'bond_miimon') { - $d->{$id} = $value; - } elsif ($id eq 'bond_xmit_hash_policy') { - $d->{$id} = $value; + } elsif ($id eq 'bridge_vlan_aware') { + $d->{$id} = 1; } elsif ($id eq 'bond_mode') { # always use names foreach my $bm (keys %$bond_modes) { @@ -881,6 +997,8 @@ sub read_etc_network_interfaces { } } $d->{$id} = $value; + } elsif ($id eq 'vxlan-remoteip') { + push @{$d->{$id}}, $value; } else { push @{$f->{options}}, $option; } @@ -896,7 +1014,11 @@ sub read_etc_network_interfaces { } } - + foreach my $ifname (@$active_ifaces) { + if (my $iface = $ifaces->{$ifname}) { + $iface->{active} = 1; + } + } if (!$ifaces->{lo}) { $ifaces->{lo}->{priority} = 1; @@ -952,7 +1074,15 @@ sub read_etc_network_interfaces { $ifaces->{$1}->{exists} = 0; $d->{exists} = 0; } - } elsif ($iface =~ m/^eth\d+$/) { + } elsif ($iface =~ m/^(\S+)\.\d+$/) { + $d->{type} = 'vlan'; + if (defined ($ifaces->{$1})) { + $d->{exists} = $ifaces->{$1}->{exists}; + } else { + $ifaces->{$1}->{exists} = 0; + $d->{exists} = 0; + } + } elsif ($iface =~ m/^$PVE::Network::PHYSICAL_NIC_RE$/) { if (!$d->{ovs_type}) { $d->{type} = 'eth'; } elsif ($d->{ovs_type} eq 'OVSPort') { @@ -965,7 +1095,9 @@ sub read_etc_network_interfaces { } elsif ($iface =~ m/^lo$/) { $d->{type} = 'loopback'; } else { - if (!$d->{ovs_type}) { + 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}; @@ -974,24 +1106,70 @@ sub read_etc_network_interfaces { } } + # map address and netmask to cidr + if ($d->{address}) { + if ($d->{netmask} && $d->{netmask} =~ m/^\d+$/) { # e.g. netmask 20 + $d->{cidr} = $d->{address} . "/" . $d->{netmask}; + } elsif ($d->{netmask} && + (my $cidr = PVE::JSONSchema::get_netmask_bits($d->{netmask}))) { # e.g. netmask 255.255.255.0 + $d->{cidr} = $d->{address} . "/" . $cidr; + } elsif ($d->{address} =~ m!^(.*)/(\d+)$!) { + $d->{cidr} = $d->{address}; + $d->{address} = $1; + $d->{netmask} = $2; + } else { + $d->{cidr} = $d->{address}; + } + } + + # map address6 and netmask6 to cidr6 + if ($d->{address6}) { + $d->{cidr6} = $d->{address6}; + if ($d->{netmask6}) { + $d->{cidr6} .= "/" . $d->{netmask6}; + } elsif ($d->{address6} =~ m!^(.*)/(\d+)$!) { + $d->{address6} = $1; + $d->{netmask6} = $2; + } + } + $d->{method} = 'manual' if !$d->{method}; $d->{method6} = 'manual' if !$d->{method6}; + + $d->{families} ||= ['inet']; } - if (my $fd2 = IO::File->new("/proc/net/if_inet6", "r")) { - while (defined ($line = <$fd2>)) { - if ($line =~ m/^[a-f0-9]{32}\s+[a-f0-9]{2}\s+[a-f0-9]{2}\s+[a-f0-9]{2}\s+[a-f0-9]{2}\s+(\S+)$/) { - $ifaces->{$1}->{active} = 1 if defined($ifaces->{$1}); + # OVS bridges create "allow-$BRIDGE $IFACE" lines which we need to remove + # from the {options} hash for them to be removed correctly. + @$options = grep {defined($_)} map { + my ($pri, $line) = @$_; + 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})) { + # if this port is part of a bridge, remove it + my %in_ovs_ports = map {$_=>1} split(/\s+/, $br->{ovs_ports}); + @ports = grep { not $in_ovs_ports{$_} } @ports; } + # create the allow line for the remaining ports, or delete if empty + if (@ports) { + [$pri, "allow-$bridge " . join(' ', @ports)]; + } else { + undef; + } + } else { + # don't modify other lines + $_; } - close ($fd2); - } + } @$options; return $config; } sub __interface_to_string { - my ($iface, $d, $family, $first_block) = @_; + my ($iface, $d, $family, $first_block, $ifupdown2) = @_; (my $suffix = $family) =~ s/^inet//; @@ -1009,50 +1187,77 @@ sub __interface_to_string { 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 }; + 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'; - $raw .= "\tbridge_ports $ports\n"; + $raw .= "\tbridge-ports $ports\n"; $done->{bridge_ports} = 1; my $v = defined($d->{bridge_stp}) ? $d->{bridge_stp} : 'off'; - $raw .= "\tbridge_stp $v\n"; + $raw .= "\tbridge-stp $v\n"; $done->{bridge_stp} = 1; $v = defined($d->{bridge_fd}) ? $d->{bridge_fd} : 0; - $raw .= "\tbridge_fd $v\n"; + $raw .= "\tbridge-fd $v\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"; + } + $done->{bridge_vlan_aware} = 1; + $done->{bridge_vids} = 1; + } elsif ($d->{type} eq 'bond') { + $d->{slaves} =~ s/[;,\s]+/ /g; my $slaves = $d->{slaves} || 'none'; - $raw .= "\tslaves $slaves\n"; + $raw .= "\tbond-slaves $slaves\n"; $done->{slaves} = 1; my $v = defined ($d->{'bond_miimon'}) ? $d->{'bond_miimon'} : 100; - $raw .= "\tbond_miimon $v\n"; + $raw .= "\tbond-miimon $v\n"; $done->{'bond_miimon'} = 1; $v = defined ($d->{'bond_mode'}) ? $d->{'bond_mode'} : 'balance-rr'; - $raw .= "\tbond_mode $v\n"; + $raw .= "\tbond-mode $v\n"; $done->{'bond_mode'} = 1; if ($d->{'bond_mode'} && $d->{'bond_xmit_hash_policy'} && ($d->{'bond_mode'} eq 'balance-xor' || $d->{'bond_mode'} eq '802.3ad')) { - $raw .= "\tbond_xmit_hash_policy $d->{'bond_xmit_hash_policy'}\n"; + $raw .= "\tbond-xmit-hash-policy $d->{'bond_xmit_hash_policy'}\n"; } $done->{'bond_xmit_hash_policy'} = 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)) { + $raw .= "\t$k $d->{$k}\n" if defined $d->{$k}; + $done->{$k} = 1; + } + + if ($d->{'vxlan-remoteip'}) { + foreach my $remoteip (@{$d->{'vxlan-remoteip'}}) { + $raw .= "\tvxlan-remoteip $remoteip\n"; + } + $done->{'vxlan-remoteip'} = 1; + } } elsif ($d->{type} eq 'OVSBridge') { $raw .= "\tovs_type $d->{type}\n"; $done->{ovs_type} = 1; $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') { @@ -1090,16 +1295,16 @@ sub __interface_to_string { $raw .= "\tovs_type $d->{type}\n"; $done->{ovs_type} = 1; - if ($d->{ovs_bridge}) { - $raw = "allow-$d->{ovs_bridge} $iface\n$raw"; - $raw .= "\tovs_bridge $d->{ovs_bridge}\n"; + if (my $bridge = $d->{ovs_bridge}) { + $raw = "allow-$bridge $iface\n$raw"; + $raw .= "\tovs_bridge $bridge\n"; $done->{ovs_bridge} = 1; } } if ($first_block) { # print other settings - foreach my $k (keys %$d) { + foreach my $k (sort keys %$d) { next if $done->{$k}; next if !$d->{$k}; $raw .= "\t$k $d->{$k}\n"; @@ -1121,8 +1326,15 @@ sub __interface_to_string { return $raw; } + sub write_etc_network_interfaces { my ($filename, $fh, $config) = @_; + my $ifupdown2 = -e '/usr/share/ifupdown2'; + my $raw = __write_etc_network_interfaces($config, $ifupdown2); + PVE::Tools::safe_print($filename, $fh, encode('UTF-8', $raw)); +} +sub __write_etc_network_interfaces { + my ($config, $ifupdown2) = @_; my $ifaces = $config->{ifaces}; my @options = @{$config->{options}}; @@ -1132,6 +1344,9 @@ sub write_etc_network_interfaces { foreach my $iface (keys %$ifaces) { my $d = $ifaces->{$iface}; + delete $d->{cidr}; + delete $d->{cidr6}; + my $ports = ''; foreach my $k (qw(bridge_ports ovs_ports slaves ovs_bonds)) { $ports .= " $d->{$k}" if $d->{$k}; @@ -1147,11 +1362,18 @@ 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' || + if ($d->{type} eq 'OVSPort' || $d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') { my $brname = $used_ports->{$iface}; - if (!$brname || !$ifaces->{$brname}) { - delete $ifaces->{$iface}; + if (!$brname || !$ifaces->{$brname}) { + if ($iface =~ /^$PVE::Network::PHYSICAL_NIC_RE/) { + $ifaces->{$iface} = { type => 'eth', + exists => 1, + method => 'manual', + families => ['inet'] }; + } else { + delete $ifaces->{$iface}; + } next; } my $bd = $ifaces->{$brname}; @@ -1170,15 +1392,18 @@ sub write_etc_network_interfaces { my $n = $ifaces->{$p}; die "OVS bridge '$iface' - unable to find port '$p'\n" if !$n; + $n->{autostart} = 0; if ($n->{type} eq 'eth') { $n->{type} = 'OVSPort'; - $n->{ovs_bridge} = $iface; + $n->{ovs_bridge} = $iface; } elsif ($n->{type} eq 'OVSBond' || $n->{type} eq 'OVSPort' || $n->{type} eq 'OVSIntPort') { $n->{ovs_bridge} = $iface; } else { die "interface '$p' is not defined as OVS port/bond\n"; } + + &$check_mtu($ifaces, $iface, $p); } } } @@ -1193,81 +1418,205 @@ sub write_etc_network_interfaces { 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); + } + } + } + + # 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}; + + 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 $raw = "# network interface settings\n"; + # check vxlan + my $vxlans = {}; + foreach my $iface (keys %$ifaces) { + my $d = $ifaces->{$iface}; + + if ($d->{type} eq 'vxlan' && $d->{'vxlan-id'}) { + my $vxlanid = $d->{'vxlan-id'}; + die "iface $iface - duplicate vxlan-id $vxlanid already used in $vxlans->{$vxlanid}\n" if $vxlans->{$vxlanid}; + $vxlans->{$vxlanid} = $iface; + } + + my $ips = 0; + ++$ips if defined $d->{'vxlan-svcnodeip'}; + ++$ips if defined $d->{'vxlan-remoteip'}; + ++$ips if defined $d->{'vxlan-local-tunnelip'}; + if ($ips > 1) { + die "iface $iface - vxlan-svcnodeip, vxlan-remoteip and vxlan-localtunnelip are mutually exclusive\n"; + } + + if (defined($d->{'vxlan-svcnodeip'}) != defined($d->{'vxlan-physdev'})) { + die "iface $iface - vxlan-svcnodeip and vxlan-physdev must be define together\n"; + } + #fixme : check if vxlan mtu is lower than 50bytes than physical interface where tunnel is going out + } + + # check vlan + foreach my $iface (keys %$ifaces) { + my $d = $ifaces->{$iface}; + if ($d->{type} eq 'vlan' && $iface =~ m/^(\S+)\.\d+$/) { + my $p = $1; + my $n = $ifaces->{$p}; + + die "vlan '$iface' - unable to find parent '$p'\n" + if !$n; + + 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|vlan' )\n"; + } + + &$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}; + if ($d->{type} eq 'bridge') { + foreach my $p (split (/\s+/, $d->{bridge_ports})) { + $p =~ s/\.\d+$//; + my $n = $ifaces->{$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} eq 'static' && $n->{address} ne '0.0.0.0') || + ($n->{method6} eq 'static' && $n->{address} ne "\:\:"); + + &$check_mtu($ifaces, $iface, $p); + $bridgeports->{$p} = $iface; + } + $bridges->{$iface} = $d; + } + } + + foreach my $iface (keys %$ifaces) { + my $d = $ifaces->{$iface}; + + foreach my $k (qw(bridge-learning bridge-arp-nd-suppress bridge-unicast-flood bridge-multicast-flood bridge-access)) { + die "iface $iface - $k: bridge port specific options can be used only on interfaces attached to a bridge\n" + if $d->{$k} && !$bridgeports->{$iface}; + } + + if ($d->{'bridge-access'} && !$bridges->{$bridgeports->{$iface}}->{bridge_vlan_aware}) { + die "iface $iface - bridge-access option can be only used if interface is in a vlan aware bridge\n"; + } + } + + my $raw = <<'NETWORKDOC'; +# network interface settings; autogenerated +# Please do NOT modify this file directly, unless you know what +# you're doing. +# +# 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 read its network +# configuration from sourced files, so do not attempt to move any of +# the PVE managed interfaces into external files! + +NETWORKDOC my $printed = {}; my $if_type_hash = { - unknown => 0, - loopback => 10, - eth => 20, - bond => 30, - bridge => 40, + loopback => 100000, + eth => 200000, + OVSPort => 200000, + OVSIntPort => 200000, + bond => 300000, + bridge => 400000, + OVSBridge => 400000, + vxlan => 500000, }; my $lookup_type_prio = sub { - my $iface = shift; + my ($iface, $ifaces) = @_; - my $alias = 0; - if ($iface =~ m/^(\S+):\d+$/) { - $iface = $1; - $alias = 1; - } + my ($rootiface, @rest) = split(/[.:]/, $iface); + my $childlevel = scalar(@rest); + my $n = $ifaces->{$rootiface}; - my $pri; - if ($iface eq 'lo') { - $pri = $if_type_hash->{loopback}; - } elsif ($iface =~ m/^eth\d+$/) { - $pri = $if_type_hash->{eth} + $alias; - } elsif ($iface =~ m/^bond\d+$/) { - $pri = $if_type_hash->{bond} + $alias; - } elsif ($iface =~ m/^vmbr\d+$/) { - $pri = $if_type_hash->{bridge} + $alias; - } + my $pri = $if_type_hash->{$n->{type}} + $childlevel + if $n->{type} && $n->{type} ne 'unknown'; - return $pri || ($if_type_hash->{unknown} + $alias); + return $pri; }; foreach my $iface (sort { my $ref1 = $ifaces->{$a}; my $ref2 = $ifaces->{$b}; - my $p1 = &$lookup_type_prio($a); - my $p2 = &$lookup_type_prio($b); + my $tp1 = &$lookup_type_prio($a, $ifaces); + my $tp2 = &$lookup_type_prio($b, $ifaces); - return $p1 <=> $p2 if $p1 != $p2; + # Only recognized types are in relation to each other. If one type + # is unknown then only consider the interfaces' priority attributes. + $tp1 = $tp2 = 0 if !defined($tp1) || !defined($tp2); - $p1 = $ref1->{priority} || 100000; - $p2 = $ref2->{priority} || 100000; + my $p1 = $tp1 + ($ref1->{priority} // 50000); + my $p2 = $tp2 + ($ref2->{priority} // 50000); return $p1 <=> $p2 if $p1 != $p2; return $a cmp $b; - } keys %$ifaces) { - - my $d = $ifaces->{$iface}; - + } keys %$ifaces) { next if $printed->{$iface}; - - if (@options && $options[0]->[0] < $d->{priority}) { + my $d = $ifaces->{$iface}; + my $pri = $d->{priority} // 0; + if (@options && $options[0]->[0] < $pri) { do { $raw .= (shift @options)->[1] . "\n"; - } while (@options && $options[0]->[0] < $d->{priority}); + } while (@options && $options[0]->[0] < $pri); $raw .= "\n"; } $printed->{$iface} = 1; - $raw .= "auto $iface\n" if $d->{autostart}; + if ($d->{autostart}) { + if ($d->{type} eq 'OVSBridge') { + # cannot use 'auto' for OVS, would add race with systemd ifup@.service + $raw .= "allow-ovs $iface\n"; + } else { + $raw .= "auto $iface\n"; + } + } + my $i = 0; # some options should be printed only once - $raw .= __interface_to_string($iface, $d, $_, !$i++) foreach @{$d->{families}}; + $raw .= __interface_to_string($iface, $d, $_, !$i++, $ifupdown2) foreach @{$d->{families}}; } $raw .= $_->[1] . "\n" foreach @options; - - PVE::Tools::safe_print($filename, $fh, $raw); + return $raw; } register_file('interfaces', "/etc/network/interfaces", @@ -1287,7 +1636,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 { @@ -1299,7 +1648,7 @@ sub read_apt_auth { $raw =~ s/^\s+//; - + my @tokens = split(/\s+/, $raw); my $data = {}; @@ -1340,7 +1689,7 @@ sub write_apt_auth { my $raw = &$format_apt_auth_data($data); die "write failed: $!" unless print $fh "$raw\n"; - + return $data; } @@ -1356,7 +1705,7 @@ sub update_apt_auth { return &$format_apt_auth_data($orig); } -register_file('apt-auth', "/etc/apt/auth.conf", +register_file('apt-auth', "/etc/apt/auth.conf", \&read_apt_auth, \&write_apt_auth, \&update_apt_auth, perm => 0640);