X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=data%2FPVE%2FTools.pm;h=f8e9c4773dbe94ed7e57d1c9de5102d96410b85e;hp=b42a5bb5f603b4ff5b3d13a9503aabb9067f2a64;hb=2f13cbb5a6557da77c644e112c09d797066496ea;hpb=804b104122b78b07f81fdc259db4dd5ab82f016a diff --git a/data/PVE/Tools.pm b/data/PVE/Tools.pm index b42a5bb..f8e9c47 100644 --- a/data/PVE/Tools.pm +++ b/data/PVE/Tools.pm @@ -1,27 +1,35 @@ package PVE::Tools; use strict; -use POSIX; +use warnings; +use POSIX qw(EINTR); use IO::Socket::INET; use IO::Select; use File::Basename; use File::Path qw(make_path); use IO::File; +use IO::Dir; use IPC::Open3; use Fcntl qw(:DEFAULT :flock); use base 'Exporter'; use URI::Escape; use Encode; -use Digest::SHA1; +use Digest::SHA; use Text::ParseWords; use String::ShellQuote; +use Time::HiRes qw(usleep gettimeofday tv_interval); our @EXPORT_OK = qw( +$IPV6RE +$IPV4RE lock_file +lock_file_full run_command file_set_contents file_get_contents file_read_firstline +dir_glob_regex +dir_glob_foreach split_list template_replace safe_print @@ -35,45 +43,104 @@ my $pvetaskdir = "$pvelogdir/tasks"; mkdir $pvelogdir; mkdir $pvetaskdir; -# flock: we use one file handle per process, so lock file -# can be called multiple times and succeeds for the same process. +my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])"; +our $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)"; +my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})"; +my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))"; -my $lock_handles = {}; +our $IPV6RE = "(?:" . + "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" . + "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" . + "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" . + "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))"; -sub lock_file { - my ($filename, $timeout, $code, @param) = @_; +sub run_with_timeout { + my ($timeout, $code, @param) = @_; + + die "got timeout\n" if $timeout <= 0; + + my $prev_alarm; + + my $sigcount = 0; my $res; - $timeout = 10 if !$timeout; + local $SIG{ALRM} = sub { $sigcount++; }; # catch alarm outside eval eval { + local $SIG{ALRM} = sub { $sigcount++; die "got timeout\n"; }; + local $SIG{PIPE} = sub { $sigcount++; die "broken pipe\n" }; + local $SIG{__DIE__}; # see SA bug 4631 + + $prev_alarm = alarm($timeout); + + $res = &$code(@param); + + alarm(0); # avoid race conditions + }; + + my $err = $@; + + alarm($prev_alarm) if defined($prev_alarm); - local $SIG{ALRM} = sub { die "got timeout (can't lock '$filename')\n"; }; + die "unknown error" if $sigcount && !$err; # seems to happen sometimes - alarm ($timeout); + die $err if $err; + return $res; +} + +# flock: we use one file handle per process, so lock file +# can be called multiple times and succeeds for the same process. + +my $lock_handles = {}; + +sub lock_file_full { + my ($filename, $timeout, $shared, $code, @param) = @_; + + $timeout = 10 if !$timeout; + + my $mode = $shared ? LOCK_SH : LOCK_EX; + + my $lock_func = sub { if (!$lock_handles->{$$}->{$filename}) { $lock_handles->{$$}->{$filename} = new IO::File (">>$filename") || - die "can't open lock file '$filename' - $!\n"; + die "can't open file - $!\n"; } - if (!flock ($lock_handles->{$$}->{$filename}, LOCK_EX|LOCK_NB)) { + if (!flock ($lock_handles->{$$}->{$filename}, $mode|LOCK_NB)) { print STDERR "trying to aquire lock..."; - if (!flock ($lock_handles->{$$}->{$filename}, LOCK_EX)) { + my $success; + while(1) { + $success = flock($lock_handles->{$$}->{$filename}, $mode); + # try again on EINTR (see bug #273) + if ($success || ($! != EINTR)) { + last; + } + } + if (!$success) { print STDERR " failed\n"; - die "can't aquire lock for '$filename' - $!\n"; + die "can't aquire lock - $!\n"; } print STDERR " OK\n"; } - alarm (0); - - $res = &$code(@param); }; - my $err = $@; + my $res; - alarm (0); + eval { run_with_timeout($timeout, $lock_func); }; + my $err = $@; + if ($err) { + $err = "can't lock file '$filename' - $err"; + } else { + eval { $res = &$code(@param) }; + $err = $@; + } if ($lock_handles->{$$}->{$filename}) { my $fh = $lock_handles->{$$}->{$filename}; @@ -91,6 +158,13 @@ sub lock_file { return $res; } + +sub lock_file { + my ($filename, $timeout, $code, @param) = @_; + + return lock_file_full($filename, $timeout, 0, $code, @param); +} + sub file_set_contents { my ($filename, $data, $perm) = @_; @@ -137,7 +211,7 @@ sub file_read_firstline { my $fh = IO::File->new ($filename, "r"); return undef if !$fh; my $res = <$fh>; - chomp $res; + chomp $res if $res; $fh->close; return $res; } @@ -167,10 +241,19 @@ sub run_command { my ($cmd, %param) = @_; my $old_umask; + my $cmdstr; - $cmd = [ $cmd ] if !ref($cmd); - - my $cmdstr = join (' ', @$cmd); + if (!ref($cmd)) { + $cmdstr = $cmd; + if ($cmd =~ m/|/) { + # see 'man bash' for option pipefail + $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ]; + } else { + $cmd = [ $cmd ]; + } + } else { + $cmdstr = cmd2string($cmd); + } my $errmsg; my $laststderr; @@ -178,24 +261,22 @@ sub run_command { my $oldtimeout; my $pid; + my $outfunc; + my $errfunc; + my $logfunc; + my $input; + my $output; + my $afterfork; + eval { - my $input; - my $output; - my $outfunc; - my $errfunc; - my $logfunc; foreach my $p (keys %param) { if ($p eq 'timeout') { $timeout = $param{$p}; } elsif ($p eq 'umask') { - umask($param{$p}); + $old_umask = umask($param{$p}); } elsif ($p eq 'errmsg') { $errmsg = $param{$p}; - $errfunc = sub { - print STDERR "$laststderr\n" if $laststderr; - $laststderr = shift; - }; } elsif ($p eq 'input') { $input = $param{$p}; } elsif ($p eq 'output') { @@ -206,11 +287,27 @@ sub run_command { $errfunc = $param{$p}; } elsif ($p eq 'logfunc') { $logfunc = $param{$p}; + } elsif ($p eq 'afterfork') { + $afterfork = $param{$p}; } else { die "got unknown parameter '$p' for run_command\n"; } } + if ($errmsg) { + my $origerrfunc = $errfunc; + $errfunc = sub { + if ($laststderr) { + if ($origerrfunc) { + &$origerrfunc("$laststderr\n"); + } else { + print STDERR "$laststderr\n" if $laststderr; + } + } + $laststderr = shift; + }; + } + my $reader = $output && $output =~ m/^>&/ ? $output : IO::File->new(); my $writer = $input && $input =~ m/^<&/ ? $input : IO::File->new(); my $error = IO::File->new(); @@ -252,6 +349,8 @@ sub run_command { local $SIG{ALRM} = sub { die "got timeout\n"; } if $timeout; $oldtimeout = alarm($timeout) if $timeout; + &$afterfork() if $afterfork; + if (ref($writer)) { print $writer $input if defined $input; close $writer; @@ -353,7 +452,9 @@ sub run_command { alarm(0); - print STDERR "$laststderr\n" if $laststderr; + if ($errmsg && $laststderr) { + &$errfunc(undef); # flush laststderr + } umask ($old_umask) if defined($old_umask); @@ -367,6 +468,7 @@ sub run_command { } if ($errmsg) { + $err =~ s/^usermod:\s*// if $cmdstr =~ m|^(\S+/)?usermod\s|; die "$errmsg: $err"; } else { die "command '$cmdstr' failed: $err"; @@ -404,6 +506,8 @@ sub trim { sub template_replace { my ($tmpl, $data) = @_; + return $tmpl if !$tmpl; + my $res = ''; while ($tmpl =~ m/([^{]+)?({([^}]+)})?/g) { $res .= $1 if $1; @@ -467,41 +571,52 @@ sub debmirrors { }; } +my $keymaphash = { + 'dk' => ['Danish', 'da', 'qwerty/dk-latin1.kmap.gz', 'dk', 'nodeadkeys'], + 'de' => ['German', 'de', 'qwertz/de-latin1-nodeadkeys.kmap.gz', 'de', 'nodeadkeys' ], + 'de-ch' => ['Swiss-German', 'de-ch', 'qwertz/sg-latin1.kmap.gz', 'ch', 'de_nodeadkeys' ], + 'en-gb' => ['United Kingdom', 'en-gb', 'qwerty/uk.kmap.gz' , 'gb', undef], + 'en-us' => ['U.S. English', 'en-us', 'qwerty/us-latin1.kmap.gz', 'us', undef ], + 'es' => ['Spanish', 'es', 'qwerty/es.kmap.gz', 'es', 'nodeadkeys'], + #'et' => [], # Ethopia or Estonia ?? + 'fi' => ['Finnish', 'fi', 'qwerty/fi-latin1.kmap.gz', 'fi', 'nodeadkeys'], + #'fo' => ['Faroe Islands', 'fo', ???, 'fo', 'nodeadkeys'], + 'fr' => ['French', 'fr', 'azerty/fr-latin1.kmap.gz', 'fr', 'nodeadkeys'], + 'fr-be' => ['Belgium-French', 'fr-be', 'azerty/be2-latin1.kmap.gz', 'be', 'nodeadkeys'], + 'fr-ca' => ['Canada-French', 'fr-ca', 'qwerty/cf.kmap.gz', 'ca', 'fr-legacy'], + 'fr-ch' => ['Swiss-French', 'fr-ch', 'qwertz/fr_CH-latin1.kmap.gz', 'ch', 'fr_nodeadkeys'], + #'hr' => ['Croatia', 'hr', 'qwertz/croat.kmap.gz', 'hr', ??], # latin2? + 'hu' => ['Hungarian', 'hu', 'qwertz/hu.kmap.gz', 'hu', undef], + 'is' => ['Icelandic', 'is', 'qwerty/is-latin1.kmap.gz', 'is', 'nodeadkeys'], + 'it' => ['Italian', 'it', 'qwerty/it2.kmap.gz', 'it', 'nodeadkeys'], + 'jp' => ['Japanese', 'ja', 'qwerty/jp106.kmap.gz', 'jp', undef], + 'lt' => ['Lithuanian', 'lt', 'qwerty/lt.kmap.gz', 'lt', 'std'], + #'lv' => ['Latvian', 'lv', 'qwerty/lv-latin4.kmap.gz', 'lv', ??], # latin4 or latin7? + 'mk' => ['Macedonian', 'mk', 'qwerty/mk.kmap.gz', 'mk', 'nodeadkeys'], + 'nl' => ['Dutch', 'nl', 'qwerty/nl.kmap.gz', 'nl', undef], + #'nl-be' => ['Belgium-Dutch', 'nl-be', ?, ?, ?], + 'no' => ['Norwegian', 'no', 'qwerty/no-latin1.kmap.gz', 'no', 'nodeadkeys'], + 'pl' => ['Polish', 'pl', 'qwerty/pl.kmap.gz', 'pl', undef], + 'pt' => ['Portuguese', 'pt', 'qwerty/pt-latin1.kmap.gz', 'pt', 'nodeadkeys'], + 'pt-br' => ['Brazil-Portuguese', 'pt-br', 'qwerty/br-latin1.kmap.gz', 'br', 'nodeadkeys'], + #'ru' => ['Russian', 'ru', 'qwerty/ru.kmap.gz', 'ru', undef], # dont know? + 'si' => ['Slovenian', 'sl', 'qwertz/slovene.kmap.gz', 'si', undef], + 'se' => ['Swedish', 'sv', 'qwerty/se-latin1.kmap.gz', 'se', 'nodeadkeys'], + #'th' => [], + 'tr' => ['Turkish', 'tr', 'qwerty/trq.kmap.gz', 'tr', undef], +}; + +my $kvmkeymaparray = []; +foreach my $lc (keys %$keymaphash) { + push @$kvmkeymaparray, $keymaphash->{$lc}->[1]; +} + sub kvmkeymaps { - return { - 'dk' => ['Danish', 'da', 'qwerty/dk-latin1.kmap.gz', 'dk', 'nodeadkeys'], - 'de' => ['German', 'de', 'qwertz/de-latin1-nodeadkeys.kmap.gz', 'de', 'nodeadkeys' ], - 'de-ch' => ['Swiss-German', 'de-ch', 'qwertz/sg-latin1.kmap.gz', 'ch', 'de_nodeadkeys' ], - 'en-gb' => ['United Kingdom', 'en-gb', 'qwerty/uk.kmap.gz' , 'gb', 'intl' ], - 'en-us' => ['U.S. English', 'en-us', 'qwerty/us-latin1.kmap.gz', 'us', 'intl' ], - 'es' => ['Spanish', 'es', 'qwerty/es.kmap.gz', 'es', 'nodeadkeys'], - #'et' => [], # Ethopia or Estonia ?? - 'fi' => ['Finnish', 'fi', 'qwerty/fi-latin1.kmap.gz', 'fi', 'nodeadkeys'], - #'fo' => ['Faroe Islands', 'fo', ???, 'fo', 'nodeadkeys'], - 'fr' => ['French', 'fr', 'azerty/fr-latin1.kmap.gz', 'fr', 'nodeadkeys'], - 'fr-be' => ['Belgium-French', 'fr-be', 'azerty/be2-latin1.kmap.gz', 'be', 'nodeadkeys'], - 'fr-ca' => ['Canada-French', 'fr-ca', 'qwerty/cf.kmap.gz', 'ca', 'fr-legacy'], - 'fr-ch' => ['Swiss-French', 'fr-ch', 'qwertz/fr_CH-latin1.kmap.gz', 'ch', 'fr_nodeadkeys'], - #'hr' => ['Croatia', 'hr', 'qwertz/croat.kmap.gz', 'hr', ??], # latin2? - 'hu' => ['Hungarian', 'hu', 'qwertz/hu.kmap.gz', 'hu', undef], - 'is' => ['Icelandic', 'is', 'qwerty/is-latin1.kmap.gz', 'is', 'nodeadkeys'], - 'it' => ['Italian', 'it', 'qwerty/it2.kmap.gz', 'it', 'nodeadkeys'], - 'jp' => ['Japanese', 'ja', 'qwerty/jp106.kmap.gz', 'jp', undef], - 'lt' => ['Lithuanian', 'lt', 'qwerty/lt.kmap.gz', 'lt', 'std'], - #'lv' => ['Latvian', 'lv', 'qwerty/lv-latin4.kmap.gz', 'lv', ??], # latin4 or latin7? - 'mk' => ['Macedonian', 'mk', 'qwerty/mk.kmap.gz', 'mk', 'nodeadkeys'], - 'nl' => ['Dutch', 'nl', 'qwerty/nl.kmap.gz', 'nl', undef], - #'nl-be' => ['Belgium-Dutch', 'nl-be', ?, ?, ?], - 'no' => ['Norwegian', 'no', 'qwerty/no-latin1.kmap.gz', 'no', 'nodeadkeys'], - 'pl' => ['Polish', 'pl', 'qwerty/pl.kmap.gz', 'pl', undef], - 'pt' => ['Portuguese', 'pt', 'qwerty/pt-latin1.kmap.gz', 'pt', 'nodeadkeys'], - 'pt-br' => ['Brazil-Portuguese', 'pt-br', 'qwerty/br-latin1.kmap.gz', 'br', 'nodeadkeys'], - #'ru' => ['Russian', 'ru', 'qwerty/ru.kmap.gz', 'ru', undef], # dont know? - 'si' => ['Slovenian', 'sl', 'qwertz/slovene.kmap.gz', 'si', undef], - #'sv' => [], Swedish ? - #'th' => [], - #'tr' => [], - }; + return $keymaphash; +} + +sub kvmkeymaplist { + return $kvmkeymaparray; } sub extract_param { @@ -513,24 +628,110 @@ sub extract_param { return $res; } -sub next_vnc_port { +# Note: we use this to wait until vncterm/spiceterm is ready +sub wait_for_vnc_port { + my ($port, $timeout) = @_; + + $timeout = 5 if !$timeout; + my $sleeptime = 0; + my $starttime = [gettimeofday]; + my $elapsed; + + while (($elapsed = tv_interval($starttime)) < $timeout) { + if (my $fh = IO::File->new ("/proc/net/tcp", "r")) { + while (defined (my $line = <$fh>)) { + if ($line =~ m/^\s*\d+:\s+([0-9A-Fa-f]{8}):([0-9A-Fa-f]{4})\s/) { + if ($port == hex($2)) { + close($fh); + return 1; + } + } + } + close($fh); + } + $sleeptime += 100000 if $sleeptime < 1000000; + usleep($sleeptime); + } + + return undef; +} + +sub next_unused_port { + my ($range_start, $range_end) = @_; + + # We use a file to register allocated ports. + # Those registrations expires after $expiretime. + # We use this to avoid race conditions between + # allocation and use of ports. + + my $filename = "/var/tmp/pve-reserved-ports"; - for (my $p = 5900; $p < 6000; $p++) { + my $code = sub { - my $sock = IO::Socket::INET->new (Listen => 5, - LocalAddr => 'localhost', - LocalPort => $p, - ReuseAddr => 1, - Proto => 0); + my $expiretime = 5; + my $ctime = time(); - if ($sock) { - close ($sock); - return $p; + my $ports = {}; + + if (my $fh = IO::File->new ($filename, "r")) { + while (my $line = <$fh>) { + if ($line =~ m/^(\d+)\s(\d+)$/) { + my ($port, $timestamp) = ($1, $2); + if (($timestamp + $expiretime) > $ctime) { + $ports->{$port} = $timestamp; # not expired + } + } + } } - } + + my $newport; + + for (my $p = $range_start; $p < $range_end; $p++) { + next if $ports->{$p}; # reserved + + my $sock = IO::Socket::INET->new(Listen => 5, + LocalAddr => 'localhost', + LocalPort => $p, + ReuseAddr => 1, + Proto => 0); + + if ($sock) { + close($sock); + $newport = $p; + $ports->{$p} = $ctime; + last; + } + } + + my $data = ""; + foreach my $p (keys %$ports) { + $data .= "$p $ports->{$p}\n"; + } + + file_set_contents($filename, $data); - die "unable to find free vnc port"; -}; + return $newport; + }; + + my $p = lock_file($filename, 10, $code); + die $@ if $@; + + die "unable to find free port (${range_start}-${range_end})\n" if !$p; + + return $p; +} + +sub next_migrate_port { + return next_unused_port(60000, 60010); +} + +sub next_vnc_port { + return next_unused_port(5900, 6000); +} + +sub next_spice_port { + return next_unused_port(61000, 61099); +} # NOTE: NFS syscall can't be interrupted, so alarm does # not work to provide timeouts. @@ -573,6 +774,8 @@ sub df { sub upid_encode { my $d = shift; + # Note: pstart can be > 32bit if uptime > 497 days, so this can result in + # more that 8 characters for pstart return sprintf("UPID:%s:%08X:%08X:%08X:%s:%s:%s:", $d->{node}, $d->{pid}, $d->{pstart}, $d->{starttime}, $d->{type}, $d->{id}, $d->{user}); @@ -585,16 +788,17 @@ sub upid_decode { my $filename; # "UPID:$node:$pid:$pstart:$startime:$dtype:$id:$user" - if ($upid =~ m/^UPID:([A-Za-z][[:alnum:]\-]*[[:alnum:]]+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/) { + # Note: allow up to 9 characters for pstart (work until 20 years uptime) + if ($upid =~ m/^UPID:([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/) { $res->{node} = $1; - $res->{pid} = hex($2); - $res->{pstart} = hex($3); - $res->{starttime} = hex($4); - $res->{type} = $5; - $res->{id} = $6; - $res->{user} = $7; - - my $subdir = substr($4, 7, 8); + $res->{pid} = hex($3); + $res->{pstart} = hex($4); + $res->{starttime} = hex($5); + $res->{type} = $6; + $res->{id} = $7; + $res->{user} = $8; + + my $subdir = substr($5, 7, 8); $filename = "$pvetaskdir/$subdir/$upid"; } else { @@ -631,7 +835,7 @@ sub upid_read_status { my ($task, $filename) = upid_decode($upid); my $fh = IO::File->new($filename, "r"); return "unable to open file - $!" if !$fh; - my $maxlen = 1024; + my $maxlen = 4096; sysseek($fh, -$maxlen, 2); my $readbuf = ''; my $br = sysread($fh, $readbuf, $maxlen); @@ -666,9 +870,19 @@ sub decode_text { return Encode::decode("utf8", uri_unescape($data)); } +sub decode_utf8_parameters { + my ($param) = @_; + + foreach my $p (qw(comment description firstname lastname)) { + $param->{$p} = decode('utf8', $param->{$p}) if $param->{$p}; + } + + return $param; +} + sub random_ether_addr { - my $rand = Digest::SHA1::sha1_hex(rand(), time()); + my $rand = Digest::SHA::sha1_hex(rand(), time()); my $mac = ''; for (my $i = 0; $i < 6; $i++) { @@ -695,6 +909,19 @@ sub shellquote { return String::ShellQuote::shell_quote($str); } +sub cmd2string { + my ($cmd) = @_; + + die "no arguments" if !$cmd; + + return $cmd if !ref($cmd); + + my @qa = (); + foreach my $arg (@$cmd) { push @qa, shellquote($arg); } + + return join (' ', @qa); +} + # split an shell argument string into an array, sub split_args { my ($str) = @_; @@ -739,4 +966,34 @@ sub dump_logfile { return ($count, $lines); } +sub dir_glob_regex { + my ($dir, $regex) = @_; + + my $dh = IO::Dir->new ($dir); + return wantarray ? () : undef if !$dh; + + while (defined(my $tmp = $dh->read)) { + if (my @res = $tmp =~ m/^($regex)$/) { + $dh->close; + return wantarray ? @res : $tmp; + } + } + $dh->close; + + return wantarray ? () : undef; +} + +sub dir_glob_foreach { + my ($dir, $regex, $func) = @_; + + my $dh = IO::Dir->new ($dir); + if (defined $dh) { + while (defined(my $tmp = $dh->read)) { + if (my @res = $tmp =~ m/^($regex)$/) { + &$func (@res); + } + } + } +} + 1;