X-Git-Url: https://git.proxmox.com/?p=pve-common.git;a=blobdiff_plain;f=src%2FPVE%2FTools.pm;h=df66047a5032e9f2f4c5fffe47d9b2e46c6009f5;hp=3f3958e0e354f4f5f4cbe6fea92028fdd58f6e19;hb=034a8181c6439567ed1a32d117fe03b7b65d20d7;hpb=cd9bd2526a6c7a74d3f8ce3de32bc149d96324f3 diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm index 3f3958e..df66047 100644 --- a/src/PVE/Tools.pm +++ b/src/PVE/Tools.pm @@ -2,14 +2,17 @@ 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; use File::Basename; use File::Path qw(make_path); +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'; @@ -19,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 @@ -39,6 +45,9 @@ template_replace safe_print trim extract_param +file_copy +O_PATH +O_TMPFILE ); my $pvelogdir = "/var/log/pve"; @@ -65,12 +74,15 @@ our $IPV6RE = "(?:" . our $IPRE = "(?:$IPV4RE|$IPV6RE)"; -use constant (CLONE_NEWNS => 0x00020000, +use constant {CLONE_NEWNS => 0x00020000, CLONE_NEWUTS => 0x04000000, CLONE_NEWIPC => 0x08000000, CLONE_NEWUSER => 0x10000000, CLONE_NEWPID => 0x20000000, - CLONE_NEWNET => 0x40000000); + CLONE_NEWNET => 0x40000000}; + +use constant {O_PATH => 0x00200000, + O_TMPFILE => 0x00410000}; # This includes O_DIRECTORY sub run_with_timeout { my ($timeout, $code, @param) = @_; @@ -123,15 +135,16 @@ sub lock_file_full { my $lock_func = sub { if (!$lock_handles->{$$}->{$filename}) { - $lock_handles->{$$}->{$filename} = new IO::File (">>$filename") || - die "can't open file - $!\n"; + my $fh = new IO::File(">>$filename") || + die "can't open file - $!\n"; + $lock_handles->{$$}->{$filename} = { fh => $fh, refcount => 0}; } - if (!flock ($lock_handles->{$$}->{$filename}, $mode|LOCK_NB)) { - print STDERR "trying to aquire lock..."; + if (!flock($lock_handles->{$$}->{$filename}->{fh}, $mode|LOCK_NB)) { + print STDERR "trying to acquire lock..."; my $success; while(1) { - $success = flock($lock_handles->{$$}->{$filename}, $mode); + $success = flock($lock_handles->{$$}->{$filename}->{fh}, $mode); # try again on EINTR (see bug #273) if ($success || ($! != EINTR)) { last; @@ -139,10 +152,11 @@ sub lock_file_full { } if (!$success) { print STDERR " failed\n"; - die "can't aquire lock - $!\n"; + die "can't acquire lock '$filename' - $!\n"; } print STDERR " OK\n"; } + $lock_handles->{$$}->{$filename}->{refcount}++; }; my $res; @@ -156,9 +170,12 @@ sub lock_file_full { $err = $@; } - if (my $fh = $lock_handles->{$$}->{$filename}) { - $lock_handles->{$$}->{$filename} = undef; - close ($fh); + if (my $fh = $lock_handles->{$$}->{$filename}->{fh}) { + my $refcount = --$lock_handles->{$$}->{$filename}->{refcount}; + if ($refcount <= 0) { + $lock_handles->{$$}->{$filename} = undef; + close ($fh); + } } if ($err) { @@ -186,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; @@ -211,13 +234,19 @@ 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; return $content; } +sub file_copy { + my ($filename, $dst, $max, $perm) = @_; + + file_set_contents ($dst, file_get_contents($filename, $max), $perm); +} + sub file_read_firstline { my ($filename) = @_; @@ -230,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; } @@ -291,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); } @@ -310,6 +341,7 @@ sub run_command { my $timeout; my $oldtimeout; my $pid; + my $exitcode; my $outfunc; my $errfunc; @@ -317,6 +349,7 @@ sub run_command { my $input; my $output; my $afterfork; + my $noerr; eval { @@ -339,6 +372,8 @@ sub run_command { $logfunc = $param{$p}; } elsif ($p eq 'afterfork') { $afterfork = $param{$p}; + } elsif ($p eq 'noerr') { + $noerr = $param{$p}; } else { die "got unknown parameter '$p' for run_command\n"; } @@ -484,14 +519,14 @@ sub run_command { die "failed to execute\n"; } elsif (my $sig = ($? & 127)) { die "got signal $sig\n"; - } elsif (my $ec = ($? >> 8)) { - if (!($ec == 24 && ($cmdstr =~ m|^(\S+/)?rsync\s|))) { + } elsif ($exitcode = ($? >> 8)) { + if (!($exitcode == 24 && ($cmdstr =~ m|^(\S+/)?rsync\s|))) { if ($errmsg && $laststderr) { my $lerr = $laststderr; $laststderr = undef; die "$lerr\n"; } - die "exit code $ec\n"; + die "exit code $exitcode\n"; } } @@ -520,12 +555,12 @@ sub run_command { if ($errmsg) { $err =~ s/^usermod:\s*// if $cmdstr =~ m|^(\S+/)?usermod\s|; die "$errmsg: $err"; - } else { + } elsif(!$noerr) { die "command '$cmdstr' failed: $err"; } } - return undef; + return $exitcode; } sub split_list { @@ -657,7 +692,7 @@ my $keymaphash = { }; my $kvmkeymaparray = []; -foreach my $lc (keys %$keymaphash) { +foreach my $lc (sort keys %$keymaphash) { push @$kvmkeymaparray, $keymaphash->{$lc}->[1]; } @@ -790,33 +825,51 @@ sub next_spice_port { # 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 the spawn external 'df' process instead of using -# Filesys::Df (which uses statfs syscall) +# So fork() before using Filesys::Df sub df { my ($path, $timeout) = @_; - my $cmd = [ 'df', '-P', '-B', '1', $path]; - my $res = { total => 0, used => 0, avail => 0, }; - my $parser = sub { - my $line = shift; - if (my ($fsid, $total, $used, $avail) = $line =~ - m/^(\S+.*)\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s.*$/) { - $res = { - total => $total, - used => $used, - avail => $avail, - }; + my $pipe = IO::Pipe->new(); + my $child = fork(); + if (!defined($child)) { + warn "fork failed: $!\n"; + return $res; + } + + if (!$child) { + $pipe->writer(); + eval { + my $df = Filesys::Df::df($path, 1); + print {$pipe} "$df->{blocks}\n$df->{used}\n$df->{bavail}\n"; + $pipe->close(); + }; + if (my $err = $@) { + warn $err; + POSIX::_exit(1); } + POSIX::_exit(0); + } + + $pipe->reader(); + + my $readvalues = sub { + $res->{total} = int((<$pipe> =~ /^(\d*)$/)[0]); + $res->{used} = int((<$pipe> =~ /^(\d*)$/)[0]); + $res->{avail} = int((<$pipe> =~ /^(\d*)$/)[0]); + }; + eval { + run_with_timeout($timeout, $readvalues); }; - eval { run_command($cmd, timeout => $timeout, outfunc => $parser); }; warn $@ if $@; - + $pipe->close(); + kill('KILL', $child); + waitpid($child, 0); return $res; } @@ -935,28 +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); - my $mac = ''; - for (my $i = 0; $i < 6; $i++) { - my $ss = hex(substr($rand, $i*2, 2)); - if (!$i) { - $ss &= 0xfe; # clear multicast - $ss |= 2; # set local id - } - $ss = sprintf("%02X", $ss); + # clear multicast, set local id + vec($rand, 0, 8) = (vec($rand, 0, 8) & 0xfe) | 2; - if (!$i) { - $mac .= "$ss"; - } else { - $mac .= ":$ss"; - } + my $addr = sprintf("%02X:%02X:%02X:%02X:%02X:%02X", unpack("C6", $rand)); + if (defined($prefix)) { + $addr = uc($prefix) . substr($addr, length($prefix)); } - - return $mac; + return $addr; } sub shellquote { @@ -1036,7 +1081,7 @@ sub dump_logfile { } sub dump_journal { - my ($start, $limit, $filter) = @_; + my ($start, $limit, $since, $until) = @_; my $lines = []; my $count = 0; @@ -1054,6 +1099,9 @@ sub dump_journal { }; my $cmd = ['journalctl', '-o', 'short', '--no-pager']; + + push @$cmd, '--since', $since if $since; + push @$cmd, '--until', $until if $until; run_command($cmd, outfunc => $parser); # HACK: ExtJS store.guaranteeRange() does not like empty array @@ -1162,9 +1210,237 @@ sub parse_host_and_port { return; # nothing } -sub unshare { +sub unshare($) { my ($flags) = @_; - syscall 272, $flags; + return 0 == syscall(272, $flags); +} + +sub setns($$) { + my ($fileno, $nstype) = @_; + return 0 == syscall(308, $fileno, $nstype); +} + +sub syncfs($) { + my ($fileno) = @_; + return 0 == syscall(306, $fileno); +} + +sub sync_mountpoint { + my ($path) = @_; + sysopen my $fd, $path, O_PATH or die "failed to open $path: $!\n"; + my $result = syncfs(fileno($fd)); + close($fd); + return $result; +} + +# support sending multi-part mail messages with a text and or a HTML part +# 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); + + foreach (@$mailto) { + die "illegal character in mailto address\n" + if ($_ =~ $mail_re); + } + + my $rcvrtxt = join (', ', @$mailto); + + $mailfrom = $mailfrom || "root"; + die "illegal character in mailfrom address\n" + if $mailfrom =~ $mail_re; + + $author = $author || 'Proxmox VE'; + + open (MAIL, "|-", "sendmail", "-B", "8BITMIME", "-f", $mailfrom, @$mailto) || + die "unable to open 'sendmail' - $!"; + + # multipart spec see https://www.ietf.org/rfc/rfc1521.txt + my $boundary = "----_=_NextPart_001_".int(time).$$; + + print MAIL "Content-Type: multipart/alternative;\n"; + print MAIL "\tboundary=\"$boundary\"\n"; + print MAIL "MIME-Version: 1.0\n"; + + print MAIL "FROM: $author <$mailfrom>\n"; + print MAIL "TO: $rcvrtxt\n"; + print MAIL "SUBJECT: $subject\n"; + print MAIL "\n"; + print MAIL "This is a multi-part message in MIME format.\n\n"; + print MAIL "--$boundary\n"; + + if (defined($text)) { + print MAIL "Content-Type: text/plain;\n"; + print MAIL "\tcharset=\"UTF8\"\n"; + print MAIL "Content-Transfer-Encoding: 8bit\n"; + print MAIL "\n"; + + # avoid 'remove extra line breaks' issue (MS Outlook) + my $fill = ' '; + $text =~ s/^/$fill/gm; + + print MAIL $text; + + print MAIL "\n--$boundary\n"; + } + + if (defined($html)) { + print MAIL "Content-Type: text/html;\n"; + print MAIL "\tcharset=\"UTF8\"\n"; + print MAIL "Content-Transfer-Encoding: 8bit\n"; + print MAIL "\n"; + + print MAIL $html; + + print MAIL "\n--$boundary--\n"; + } + + close(MAIL); +} + +sub tempfile { + my ($perm, %opts) = @_; + + # default permissions are stricter than with file_set_contents + $perm = 0600 if !defined($perm); + + 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); + 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; +} + +sub tempfile_contents { + my ($data, $perm, %opts) = @_; + + my $fh = tempfile($perm, %opts); + eval { + die "unable to write to tempfile: $!\n" if !print {$fh} $data; + die "unable to flush to tempfile: $!\n" if !defined($fh->flush()); + }; + if (my $err = $@) { + close $fh; + die $err; + } + + 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; } 1;