X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=data%2FPVE%2FTools.pm;h=0961e9289a690caa4f4e65a6bd54309ccca6118d;hp=4c4e25958ad38336ae44b05cce672d9bbbc76f22;hb=5a5ca434b2a25edcf9fe89b8dcadf34e1dc1d967;hpb=f38995ab1443d3ab0ce4b89acc3bf5cb9704ee1c diff --git a/data/PVE/Tools.pm b/data/PVE/Tools.pm index 4c4e259..0961e92 100644 --- a/data/PVE/Tools.pm +++ b/data/PVE/Tools.pm @@ -1,25 +1,32 @@ 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; our @EXPORT_OK = qw( 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 @@ -33,45 +40,88 @@ 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. +sub run_with_timeout { + my ($timeout, $code, @param) = @_; -my $lock_handles = {}; + die "got timeout\n" if $timeout <= 0; -sub lock_file { - my ($filename, $timeout, $code, @param) = @_; + 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); + + die "unknown error" if $sigcount && !$err; # seems to happen sometimes + + die $err if $err; + + return $res; +} - local $SIG{ALRM} = sub { die "got timeout (can't lock '$filename')\n"; }; +# 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 = {}; - alarm ($timeout); +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}; @@ -89,6 +139,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) = @_; @@ -135,7 +192,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; } @@ -165,10 +222,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; @@ -176,24 +242,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') { @@ -204,11 +268,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(); @@ -250,6 +330,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; @@ -351,7 +433,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); @@ -365,6 +449,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"; @@ -402,6 +487,8 @@ sub trim { sub template_replace { my ($tmpl, $data) = @_; + return $tmpl if !$tmpl; + my $res = ''; while ($tmpl =~ m/([^{]+)?({([^}]+)})?/g) { $res .= $1 if $1; @@ -465,41 +552,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 { @@ -511,24 +609,102 @@ sub extract_param { return $res; } -sub next_vnc_port { +# Note: we use this to wait until vncterm is ready +sub wait_for_vnc_port { + my ($port, $timeout) = @_; - for (my $p = 5900; $p < 6000; $p++) { + $timeout = 5 if !$timeout; - my $sock = IO::Socket::INET->new (Listen => 5, - LocalAddr => 'localhost', - LocalPort => $p, - ReuseAddr => 1, - Proto => 0); - - if ($sock) { - close ($sock); - return $p; + for (my $i = 0; $i < $timeout; $i++) { + 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); } + sleep(1); } - die "unable to find free vnc port"; -}; + 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"; + + my $code = sub { + + my $expiretime = 5; + my $ctime = time(); + + 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); + + 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); +} # NOTE: NFS syscall can't be interrupted, so alarm does # not work to provide timeouts. @@ -583,16 +759,16 @@ 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]+):$/) { + 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}):([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 { @@ -618,7 +794,7 @@ sub upid_open { my $outfh = IO::File->new ($filename, O_WRONLY|O_CREAT|O_EXCL, $perm) || die "unable to create output file '$filename' - $!\n"; - chown $wwwid, $outfh; + chown $wwwid, -1, $outfh; return $outfh; }; @@ -629,7 +805,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); @@ -664,9 +840,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++) { @@ -687,4 +873,97 @@ sub random_ether_addr { return $mac; } +sub shellquote { + my $str = shift; + + 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) = @_; + + return $str ? [ Text::ParseWords::shellwords($str) ] : []; +} + +sub dump_logfile { + my ($filename, $start, $limit) = @_; + + my $lines = []; + my $count = 0; + + my $fh = IO::File->new($filename, "r"); + if (!$fh) { + $count++; + push @$lines, { n => $count, t => "unable to open file - $!"}; + return ($count, $lines); + } + + $start = 0 if !$start; + $limit = 50 if !$limit; + + my $line; + while (defined($line = <$fh>)) { + next if $count++ < $start; + next if $limit <= 0; + chomp $line; + push @$lines, { n => $count, t => $line}; + $limit--; + } + + close($fh); + + # HACK: ExtJS store.guaranteeRange() does not like empty array + # so we add a line + if (!$count) { + $count++; + push @$lines, { n => $count, t => "no content"}; + } + + 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;