X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=src%2FPVE%2FTools.pm;h=406aa2aa7834bba3af1bd6796b196ab90408c385;hp=cb8d9b27b78558c1d46a28f8dbb320e2eb90c521;hb=9867ff7a8327fc2847de299caa05e94211859b12;hpb=b61a47dbebdf1c8fb4c1cfbd7f1b3755277d583d diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm index cb8d9b2..406aa2a 100644 --- a/src/PVE/Tools.pm +++ b/src/PVE/Tools.pm @@ -2,7 +2,7 @@ package PVE::Tools; use strict; use warnings; -use POSIX qw(EINTR); +use POSIX qw(EINTR EEXIST EOPNOTSUPP); use IO::Socket::IP; use Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED); use IO::Select; @@ -12,6 +12,7 @@ use Filesys::Df (); # don't overwrite our df() use IO::Pipe; use IO::File; use IO::Dir; +use IO::Handle; use IPC::Open3; use Fcntl qw(:DEFAULT :flock); use base 'Exporter'; @@ -21,6 +22,9 @@ use Digest::SHA; use Text::ParseWords; use String::ShellQuote; use Time::HiRes qw(usleep gettimeofday tv_interval alarm); +use Net::DBus qw(dbus_uint32 dbus_uint64); +use Net::DBus::Callback; +use Net::DBus::Reactor; # avoid warning when parsing long hex values with hex() no warnings 'portable'; # Support for 64-bit ints required @@ -28,10 +32,10 @@ no warnings 'portable'; # Support for 64-bit ints required our @EXPORT_OK = qw( $IPV6RE $IPV4RE -lock_file +lock_file lock_file_full -run_command -file_set_contents +run_command +file_set_contents file_get_contents file_read_firstline dir_glob_regex @@ -42,6 +46,8 @@ safe_print trim extract_param file_copy +O_PATH +O_TMPFILE ); my $pvelogdir = "/var/log/pve"; @@ -197,7 +203,13 @@ sub file_set_contents { my $tmpname = "$filename.tmp.$$"; eval { - my $fh = IO::File->new($tmpname, O_WRONLY|O_CREAT, $perm); + my ($fh, $tries) = (undef, 0); + while (!$fh && $tries++ < 3) { + $fh = IO::File->new($tmpname, O_WRONLY|O_CREAT|O_EXCL, $perm); + if (!$fh && $! == EEXIST) { + unlink($tmpname) or die "unable to delete old temp file: $!\n"; + } + } die "unable to open file '$tmpname' - $!\n" if !$fh; die "unable to write '$tmpname' - $!\n" unless print $fh $data; die "closing file '$tmpname' failed - $!\n" unless close $fh; @@ -212,7 +224,7 @@ sub file_set_contents { if (!rename($tmpname, $filename)) { my $msg = "close (rename) atomic file '$filename' failed: $!\n"; unlink $tmpname; - die $msg; + die $msg; } } @@ -222,7 +234,7 @@ sub file_get_contents { my $fh = IO::File->new($filename, "r") || die "can't open '$filename' - $!\n"; - my $content = safe_read_from($fh, $max); + my $content = safe_read_from($fh, $max, 0, $filename); close $fh; @@ -247,22 +259,24 @@ sub file_read_firstline { } sub safe_read_from { - my ($fh, $max, $oneline) = @_; + my ($fh, $max, $oneline, $filename) = @_; $max = 32768 if !$max; + my $subject = defined($filename) ? "file '$filename'" : 'input'; + my $br = 0; my $input = ''; my $count; while ($count = sysread($fh, $input, 8192, $br)) { $br += $count; - die "input too long - aborting\n" if $br > $max; + die "$subject too long - aborting\n" if $br > $max; if ($oneline && $input =~ m/^(.*)\n/) { $input = $1; last; } - } - die "unable to read input - $!\n" if !defined($count); + } + die "unable to read $subject - $!\n" if !defined($count); return $input; } @@ -308,7 +322,7 @@ sub run_command { $cmdstr .= $pipe . join(' ', map { ref($_) ? $$_ : shellquote($_) } @$command); $pipe = ' | '; } - $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmdstr" ]; + $cmd = [ '/bin/bash', '-c', "$cmdstr" ]; } else { $cmdstr = cmd2string($cmd); } @@ -336,6 +350,7 @@ sub run_command { my $output; my $afterfork; my $noerr; + my $keeplocale; eval { @@ -360,6 +375,8 @@ sub run_command { $afterfork = $param{$p}; } elsif ($p eq 'noerr') { $noerr = $param{$p}; + } elsif ($p eq 'keeplocale') { + $keeplocale = $param{$p}; } else { die "got unknown parameter '$p' for run_command\n"; } @@ -375,7 +392,7 @@ sub run_command { print STDERR "$laststderr\n" if $laststderr; } } - $laststderr = shift; + $laststderr = shift; }; } @@ -383,13 +400,10 @@ sub run_command { my $writer = $input && $input =~ m/^<&/ ? $input : IO::File->new(); my $error = IO::File->new(); - # try to avoid locale related issues/warnings - my $lang = $param{lang} || 'C'; - my $orig_pid = $$; eval { - local $ENV{LC_ALL} = $lang; + local $ENV{LC_ALL} = 'C' if !$keeplocale; # suppress LVM warnings like: "File descriptor 3 left open"; local $ENV{LVM_SUPPRESS_FD_WARNINGS} = "1"; @@ -411,8 +425,8 @@ sub run_command { # catch exec errors if ($orig_pid != $$) { warn "ERROR: $err"; - POSIX::_exit (1); - kill ('KILL', $$); + POSIX::_exit (1); + kill ('KILL', $$); } die $err if $err; @@ -500,7 +514,7 @@ sub run_command { &$logfunc($errlog) if $logfunc && $errlog; waitpid ($pid, 0); - + if ($? == -1) { die "failed to execute\n"; } elsif (my $sig = ($? & 127)) { @@ -553,7 +567,7 @@ sub split_list { my $listtxt = shift || ''; return split (/\0/, $listtxt) if $listtxt =~ m/\0/; - + $listtxt =~ s/[,;]/ /g; $listtxt =~ s/^\s+//; @@ -569,7 +583,7 @@ sub trim { $txt =~ s/^\s+//; $txt =~ s/\s+$//; - + return $txt; } @@ -578,7 +592,7 @@ sub template_replace { my ($tmpl, $data) = @_; return $tmpl if !$tmpl; - + my $res = ''; while ($tmpl =~ m/([^{]+)?({([^}]+)})?/g) { $res .= $1 if $1; @@ -645,7 +659,7 @@ 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' ], + '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'], @@ -666,7 +680,7 @@ my $keymaphash = { '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'], + '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'], @@ -678,7 +692,7 @@ my $keymaphash = { }; my $kvmkeymaparray = []; -foreach my $lc (keys %$keymaphash) { +foreach my $lc (sort keys %$keymaphash) { push @$kvmkeymaparray, $keymaphash->{$lc}->[1]; } @@ -750,11 +764,11 @@ sub next_unused_port { 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++) { @@ -774,20 +788,20 @@ sub next_unused_port { 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); + my $p = lock_file('/var/lock/pve-ports.lck', 10, $code); die $@ if $@; - + die "unable to find free port (${range_start}-${range_end})\n" if !$p; return $p; @@ -808,7 +822,7 @@ sub next_spice_port { return next_unused_port(61000, 61099, $family); } -# NOTE: NFS syscall can't be interrupted, so alarm does +# NOTE: NFS syscall can't be interrupted, so alarm does # not work to provide timeouts. # from 'man nfs': "Only SIGKILL can interrupt a pending NFS operation" # So fork() before using Filesys::Df @@ -845,9 +859,9 @@ sub df { $pipe->reader(); my $readvalues = sub { - $res->{total} = int(<$pipe>); - $res->{used} = int(<$pipe>); - $res->{avail} = int(<$pipe>); + $res->{total} = int((<$pipe> =~ /^(\d*)$/)[0]); + $res->{used} = int((<$pipe> =~ /^(\d*)$/)[0]); + $res->{avail} = int((<$pipe> =~ /^(\d*)$/)[0]); }; eval { run_with_timeout($timeout, $readvalues); @@ -861,7 +875,7 @@ sub df { # UPID helper # We use this to uniquely identify a process. -# An 'Unique Process ID' has the following format: +# An 'Unique Process ID' has the following format: # "UPID:$node:$pid:$pstart:$startime:$dtype:$id:$user" sub upid_encode { @@ -869,8 +883,8 @@ sub upid_encode { # 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}, + return sprintf("UPID:%s:%08X:%08X:%08X:%s:%s:%s:", $d->{node}, $d->{pid}, + $d->{pstart}, $d->{starttime}, $d->{type}, $d->{id}, $d->{user}); } @@ -905,7 +919,7 @@ sub upid_decode { sub upid_open { my ($upid) = @_; - my ($task, $filename) = upid_decode($upid); + my ($task, $filename) = upid_decode($upid); my $dirname = dirname($filename); make_path($dirname); @@ -914,7 +928,7 @@ sub upid_open { die "getpwnam failed"; my $perm = 0640; - + my $outfh = IO::File->new ($filename, O_WRONLY|O_CREAT|O_EXCL, $perm) || die "unable to create output file '$filename' - $!\n"; chown $wwwid, -1, $outfh; @@ -948,7 +962,7 @@ sub upid_read_status { return "unable to read tail (got $br bytes)"; } -# useful functions to store comments in config files +# useful functions to store comments in config files sub encode_text { my ($text) = @_; @@ -974,15 +988,20 @@ sub decode_utf8_parameters { } sub random_ether_addr { + my ($prefix) = @_; my ($seconds, $microseconds) = gettimeofday; - my $rand = Digest::SHA::sha1_hex($$, rand(), $seconds, $microseconds); + my $rand = Digest::SHA::sha1($$, rand(), $seconds, $microseconds); # clear multicast, set local id vec($rand, 0, 8) = (vec($rand, 0, 8) & 0xfe) | 2; - return sprintf("%02X:%02X:%02X:%02X:%02X:%02X", unpack("C6", $rand)); + my $addr = sprintf("%02X:%02X:%02X:%02X:%02X:%02X", unpack("C6", $rand)); + if (defined($prefix)) { + $addr = uc($prefix) . substr($addr, length($prefix)); + } + return $addr; } sub shellquote { @@ -1018,7 +1037,7 @@ sub dump_logfile { my $count = 0; my $fh = IO::File->new($filename, "r"); - if (!$fh) { + if (!$fh) { $count++; push @$lines, { n => $count, t => "unable to open file - $!"}; return ($count, $lines); @@ -1066,7 +1085,7 @@ sub dump_journal { my $lines = []; my $count = 0; - + $start = 0 if !$start; $limit = 50 if !$limit; @@ -1100,8 +1119,8 @@ sub dir_glob_regex { my $dh = IO::Dir->new ($dir); return wantarray ? () : undef if !$dh; - - while (defined(my $tmp = $dh->read)) { + + while (defined(my $tmp = $dh->read)) { if (my @res = $tmp =~ m/^($regex)$/) { $dh->close; return wantarray ? @res : $tmp; @@ -1122,7 +1141,7 @@ sub dir_glob_foreach { &$func (@res); } } - } + } } sub assert_if_modified { @@ -1218,19 +1237,24 @@ sub sync_mountpoint { # mailto may be a single email string or an array of receivers sub sendmail { my ($mailto, $subject, $text, $html, $mailfrom, $author) = @_; + my $mail_re = qr/[^-a-zA-Z0-9+._@]/; - $mailto = [ $mailto ] if ref($mailto) ne 'ARRAY'; + $mailto = [ $mailto ] if !ref($mailto); - my $rcvrarg = ''; - foreach my $r (@$mailto) { - $rcvrarg .= " '$r'"; + foreach (@$mailto) { + die "illegal character in mailto address\n" + if ($_ =~ $mail_re); } + my $rcvrtxt = join (', ', @$mailto); $mailfrom = $mailfrom || "root"; - $author = $author || 'Proxmox VE mail notifier'; + die "illegal character in mailfrom address\n" + if $mailfrom =~ $mail_re; + + $author = $author || 'Proxmox VE'; - open (MAIL,"|sendmail -B 8BITMIME -f $mailfrom $rcvrarg") || + open (MAIL, "|-", "sendmail", "-B", "8BITMIME", "-f", $mailfrom, @$mailto) || die "unable to open 'sendmail' - $!"; # multipart spec see https://www.ietf.org/rfc/rfc1521.txt @@ -1247,7 +1271,7 @@ sub sendmail { print MAIL "This is a multi-part message in MIME format.\n\n"; print MAIL "--$boundary\n"; - if ($text) { + if (defined($text)) { print MAIL "Content-Type: text/plain;\n"; print MAIL "\tcharset=\"UTF8\"\n"; print MAIL "Content-Transfer-Encoding: 8bit\n"; @@ -1262,7 +1286,7 @@ sub sendmail { print MAIL "\n--$boundary\n"; } - if($html) { + if (defined($html)) { print MAIL "Content-Type: text/html;\n"; print MAIL "\tcharset=\"UTF8\"\n"; print MAIL "Content-Transfer-Encoding: 8bit\n"; @@ -1274,7 +1298,6 @@ sub sendmail { } close(MAIL); - } sub tempfile { @@ -1283,12 +1306,18 @@ sub tempfile { # default permissions are stricter than with file_set_contents $perm = 0600 if !defined($perm); - my $dir = $opts{dir} // '/tmp'; + my $dir = $opts{dir} // '/run'; my $mode = $opts{mode} // O_RDWR; $mode |= O_EXCL if !$opts{allow_links}; - my $fh = IO::File->new($dir, $mode | O_TMPFILE, $perm) - or die "failed to create tempfile: $!\n"; + my $fh = IO::File->new($dir, $mode | O_TMPFILE, $perm); + if (!$fh && $! == EOPNOTSUPP) { + $dir = '/tmp' if !defined($opts{dir}); + $dir .= "/.tmpfile.$$"; + $fh = IO::File->new($dir, $mode | O_CREAT | O_EXCL, $perm); + unlink($dir) if $fh; + } + die "failed to create tempfile: $!\n" if !$fh; return $fh; } @@ -1308,4 +1337,124 @@ sub tempfile_contents { return ("/proc/$$/fd/".$fh->fileno, $fh); } +sub validate_ssh_public_keys { + my ($raw) = @_; + my @lines = split(/\n/, $raw); + + foreach my $line (@lines) { + next if $line =~ m/^\s*$/; + eval { + my ($filename, $handle) = tempfile_contents($line); + run_command(["ssh-keygen", "-l", "-f", $filename], + outfunc => sub {}, errfunc => sub {}); + }; + die "SSH public key validation error\n" if $@; + } +} + +sub openat($$$;$) { + my ($dirfd, $pathname, $flags, $mode) = @_; + my $fd = syscall(257, $dirfd, $pathname, $flags, $mode//0); + return undef if $fd < 0; + # sysopen() doesn't deal with numeric file descriptors apparently + # so we need to convert to a mode string for IO::Handle->new_from_fd + my $flagstr = ($flags & O_RDWR) ? 'rw' : ($flags & O_WRONLY) ? 'w' : 'r'; + my $handle = IO::Handle->new_from_fd($fd, $flagstr); + return $handle if $handle; + my $err = $!; # save error before closing the raw fd + syscall(3, $fd); # close + $! = $err; + return undef; +} + +sub mkdirat($$$) { + my ($dirfd, $name, $mode) = @_; + return syscall(258, $dirfd, $name, $mode) == 0; +} + +# NOTE: This calls the dbus main loop and must not be used when another dbus +# main loop is being used as we need to wait for the JobRemoved signal. +# Polling the job status instead doesn't work because this doesn't give us the +# distinction between success and failure. +# +# Note that the description is mandatory for security reasons. +sub enter_systemd_scope { + my ($unit, $description, %extra) = @_; + die "missing description\n" if !defined($description); + + my $timeout = delete $extra{timeout}; + + $unit .= '.scope'; + my $properties = [ [PIDs => [dbus_uint32($$)]] ]; + + foreach my $key (keys %extra) { + if ($key eq 'Slice' || $key eq 'KillMode') { + push @$properties, [$key, $extra{$key}]; + } elsif ($key eq 'CPUShares') { + push @$properties, [$key, dbus_uint64($extra{$key})]; + } elsif ($key eq 'CPUQuota') { + push @$properties, ['CPUQuotaPerSecUSec', + dbus_uint64($extra{$key} * 10000)]; + } else { + die "Don't know how to encode $key for systemd scope\n"; + } + } + + my $job; + my $done = 0; + + my $bus = Net::DBus->system(); + my $reactor = Net::DBus::Reactor->main(); + + my $service = $bus->get_service('org.freedesktop.systemd1'); + my $if = $service->get_object('/org/freedesktop/systemd1', 'org.freedesktop.systemd1.Manager'); + # Connect to the JobRemoved signal since we want to wait for it to finish + my $sigid; + my $timer; + my $cleanup = sub { + my ($no_shutdown) = @_; + $if->disconnect_from_signal('JobRemoved', $sigid) if defined($if); + $if = undef; + $sigid = undef; + $reactor->remove_timeout($timer) if defined($timer); + $timer = undef; + return if $no_shutdown; + $reactor->shutdown(); + }; + + $sigid = $if->connect_to_signal('JobRemoved', sub { + my ($id, $removed_job, $signaled_unit, $result) = @_; + return if $signaled_unit ne $unit || $removed_job ne $job; + $cleanup->(0); + die "systemd job failed\n" if $result ne 'done'; + $done = 1; + }); + + my $on_timeout = sub { + $cleanup->(0); + die "systemd job timed out\n"; + }; + + $timer = $reactor->add_timeout($timeout * 1000, Net::DBus::Callback->new(method => $on_timeout)) + if defined($timeout); + $job = $if->StartTransientUnit($unit, 'fail', $properties, []); + $reactor->run(); + $cleanup->(1); + die "systemd job never completed\n" if !$done; +} + +my $salt_starter = time(); + +sub encrypt_pw { + my ($pw) = @_; + + $salt_starter++; + my $salt = substr(Digest::SHA::sha1_base64(time() + $salt_starter + $$), 0, 8); + + # crypt does not want '+' in salt (see 'man crypt') + $salt =~ s/\+/X/g; + + return crypt(encode("utf8", $pw), "\$5\$$salt\$"); +} + 1;