]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Tools.pm
fix #1819: fork_worker: ensure sync'ed workers control terminal
[pve-common.git] / src / PVE / Tools.pm
index dd9cd0f8e5f0e4d0aed19ba3bb52a2cce4c2c723..0a7efeb8dce920d1904d651b66d1b20234ff1e46 100644 (file)
@@ -4,7 +4,8 @@ use strict;
 use warnings;
 use POSIX qw(EINTR EEXIST EOPNOTSUPP);
 use IO::Socket::IP;
-use Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED AI_CANONNAME SOCK_DGRAM);
+use Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED AI_CANONNAME SOCK_DGRAM
+             IPPROTO_TCP);
 use IO::Select;
 use File::Basename;
 use File::Path qw(make_path);
@@ -19,12 +20,10 @@ use base 'Exporter';
 use URI::Escape;
 use Encode;
 use Digest::SHA;
+use JSON;
 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;
 use Scalar::Util 'weaken';
 use PVE::Syscall;
 
@@ -148,7 +147,7 @@ sub lock_file_full {
            or die "can't open file - $!\n";
 
        if (!flock($fh, $mode|LOCK_NB)) {
-           print STDERR "trying to acquire lock...";
+           print STDERR "trying to acquire lock...\n";
            my $success;
            while(1) {
                $success = flock($fh, $mode);
@@ -367,6 +366,7 @@ sub run_command {
     my $afterfork;
     my $noerr;
     my $keeplocale;
+    my $quiet;
 
     eval {
 
@@ -393,6 +393,8 @@ sub run_command {
                $noerr = $param{$p};
            } elsif ($p eq 'keeplocale') {
                $keeplocale = $param{$p};
+           } elsif ($p eq 'quiet') {
+               $quiet = $param{$p};
            } else {
                die "got unknown parameter '$p' for run_command\n";
            }
@@ -495,7 +497,7 @@ sub run_command {
                            waitpid ($pid, 0);
                            die $err;
                        }
-                   } else {
+                   } elsif (!$quiet) {
                        print $buf;
                        *STDOUT->flush();
                    }
@@ -515,7 +517,7 @@ sub run_command {
                            waitpid ($pid, 0);
                            die $err;
                        }
-                   } else {
+                   } elsif (!$quiet) {
                        print STDERR $buf;
                        *STDERR->flush();
                    }
@@ -579,6 +581,64 @@ sub run_command {
     return $exitcode;
 }
 
+# Run a command with a tcp socket as standard input.
+sub pipe_socket_to_command  {
+    my ($cmd, $ip, $port) = @_;
+
+    my $params = {
+       Listen => 1,
+       ReuseAddr => 1,
+       Proto => &Socket::IPPROTO_TCP,
+       GetAddrInfoFlags => 0,
+       LocalAddr => $ip,
+       LocalPort => $port,
+    };
+    my $socket = IO::Socket::IP->new(%$params) or die "failed to open socket: $!\n";
+
+    print "$ip\n$port\n"; # tell remote where to connect
+    *STDOUT->flush();
+
+    alarm 0;
+    local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
+    alarm 30;
+    my $client = $socket->accept; # Wait for a client
+    alarm 0;
+    close($socket);
+
+    # We want that the command talks over the TCP socket and takes
+    # ownership of it, so that when it closes it the connection is
+    # terminated, so we need to be able to close the socket. So we
+    # can't really use PVE::Tools::run_command().
+    my $pid = fork() // die "fork failed: $!\n";
+    if (!$pid) {
+       POSIX::dup2(fileno($client), 0);
+       POSIX::dup2(fileno($client), 1);
+       close($client);
+       exec {$cmd->[0]} @$cmd or do {
+           warn "exec failed: $!\n";
+           POSIX::_exit(1);
+       };
+    }
+
+    close($client);
+    if (waitpid($pid, 0) != $pid) {
+       kill(15 => $pid); # if we got interrupted terminate the child
+       my $count = 0;
+       while (waitpid($pid, POSIX::WNOHANG) != $pid) {
+           usleep(100000);
+           $count++;
+           kill(9 => $pid), last if $count > 300; # 30 second timeout
+       }
+    }
+    if (my $sig = ($? & 127)) {
+       die "got signal $sig\n";
+    } elsif (my $exitcode = ($? >> 8)) {
+       die "exit code $exitcode\n";
+    }
+
+    return undef;
+}
+
 sub split_list {
     my $listtxt = shift || '';
 
@@ -731,34 +791,35 @@ sub extract_param {
 
 # Note: we use this to wait until vncterm/spiceterm is ready
 sub wait_for_vnc_port {
-    my ($port, $timeout) = @_;
+    my ($port, $family, $timeout) = @_;
 
     $timeout = 5 if !$timeout;
     my $sleeptime = 0;
     my $starttime = [gettimeofday];
     my $elapsed;
 
+    my $cmd = ['/bin/ss', '-Htln', "sport = :$port"];
+    push @$cmd, $family == AF_INET6 ? '-6' : '-4' if defined($family);
+
+    my $found;
     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;
-                   }
-               }
+       # -Htln = don't print header, tcp, listening sockets only, numeric ports
+       run_command($cmd, outfunc => sub {
+           my $line = shift;
+           if ($line =~ m/^LISTEN\s+\d+\s+\d+\s+\S+:(\d+)\s/) {
+               $found = 1 if ($port == $1);
            }
-           close($fh);
-       }
+       });
+       return 1 if $found;
        $sleeptime += 100000 if  $sleeptime < 1000000;
        usleep($sleeptime);
     }
 
-    return undef;
+    die "Timeout while waiting for port '$port' to get ready!\n";
 }
 
 sub next_unused_port {
-    my ($range_start, $range_end, $family) = @_;
+    my ($range_start, $range_end, $family, $address) = @_;
 
     # We use a file to register allocated ports.
     # Those registrations expires after $expiretime.
@@ -786,16 +847,18 @@ sub next_unused_port {
        }
 
        my $newport;
+       my %sockargs = (Listen => 5,
+                       ReuseAddr => 1,
+                       Family    => $family,
+                       Proto     => IPPROTO_TCP,
+                       GetAddrInfoFlags => 0);
+       $sockargs{LocalAddr} = $address if defined($address);
 
        for (my $p = $range_start; $p < $range_end; $p++) {
            next if $ports->{$p}; # reserved
 
-           my $sock = IO::Socket::IP->new(Listen => 5,
-                                          LocalPort => $p,
-                                          ReuseAddr => 1,
-                                          Family    => $family,
-                                          Proto     => 0,
-                                          GetAddrInfoFlags => 0);
+           $sockargs{LocalPort} = $p;
+           my $sock = IO::Socket::IP->new(%sockargs);
 
            if ($sock) {
                close($sock);
@@ -824,71 +887,140 @@ sub next_unused_port {
 }
 
 sub next_migrate_port {
-    my ($family) = @_;
-    return next_unused_port(60000, 60050, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(60000, 60050, $family, $address);
 }
 
 sub next_vnc_port {
-    my ($family) = @_;
-    return next_unused_port(5900, 6000, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(5900, 6000, $family, $address);
 }
 
 sub next_spice_port {
-    my ($family) = @_;
-    return next_unused_port(61000, 61099, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(61000, 61099, $family, $address);
 }
 
-# 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
-sub df {
-    my ($path, $timeout) = @_;
+sub must_stringify {
+       my ($value) = @_;
+       eval { $value = "$value" };
+       return "error turning value into a string: $@" if $@;
+       return $value;
+}
 
-    my $res = {
-       total => 0,
-       used => 0,
-       avail => 0,
-    };
+# sigkill after $timeout  a $sub running in a fork if it can't write a pipe
+# the $sub has to return a single scalar
+sub run_fork_with_timeout {
+    my ($timeout, $sub) = @_;
+
+    my $res;
+    my $error;
+    my $pipe_out = IO::Pipe->new();
+
+    # disable pending alarms, save their remaining time
+    my $prev_alarm = alarm 0;
+
+    # avoid leaving a zombie if the parent gets interrupted
+    my $sig_received;
+    local $SIG{INT} = sub { $sig_received++; };
 
-    my $pipe = IO::Pipe->new();
     my $child = fork();
     if (!defined($child)) {
-       warn "fork failed: $!\n";
+       die "fork failed: $!\n";
        return $res;
     }
 
     if (!$child) {
-       $pipe->writer();
+       $pipe_out->writer();
+
        eval {
-           my $df = Filesys::Df::df($path, 1);
-           print {$pipe} "$df->{blocks}\n$df->{used}\n$df->{bavail}\n";
-           $pipe->close();
+           $res = $sub->();
+           print {$pipe_out} encode_json({ result => $res });
+           $pipe_out->flush();
        };
        if (my $err = $@) {
-           warn $err;
+           print {$pipe_out} encode_json({ error => must_stringify($err) });
+           $pipe_out->flush();
            POSIX::_exit(1);
        }
        POSIX::_exit(0);
     }
 
-    $pipe->reader();
+    $pipe_out->reader();
 
     my $readvalues = sub {
-       $res->{total} = int((<$pipe> =~ /^(\d*)$/)[0]);
-       $res->{used}  = int((<$pipe> =~ /^(\d*)$/)[0]);
-       $res->{avail} = int((<$pipe> =~ /^(\d*)$/)[0]);
+       local $/ = undef;
+       my $child_res = decode_json(scalar<$pipe_out>);
+       $res = $child_res->{result};
+       $error = $child_res->{error};
     };
     eval {
-       run_with_timeout($timeout, $readvalues);
+       if (defined($timeout)) {
+           run_with_timeout($timeout, $readvalues);
+       } else {
+           $readvalues->();
+       }
     };
     warn $@ if $@;
-    $pipe->close();
+    $pipe_out->close();
     kill('KILL', $child);
     waitpid($child, 0);
+
+    alarm $prev_alarm;
+    die "interrupted by unexpected signal\n" if $sig_received;
+
+    die $error if $error;
     return $res;
 }
 
+sub run_fork {
+    my ($code) = @_;
+    return run_fork_with_timeout(undef, $code);
+}
+
+# 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
+sub df {
+    my ($path, $timeout) = @_;
+
+    my $df = sub { return Filesys::Df::df($path, 1) };
+
+    my $res = eval { run_fork_with_timeout($timeout, $df) } // {};
+    warn $@ if $@;
+
+    # untaint the values
+    my ($blocks, $used, $bavail) = map { defined($_) ? (/^(\d+)$/) : 0 }
+       $res->@{qw(blocks used bavail)};
+
+    return {
+       total => $blocks,
+       used => $used,
+       avail => $bavail,
+    };
+}
+
+sub du {
+    my ($path, $timeout) = @_;
+
+    my $size;
+
+    $timeout //= 10;
+
+    my $parser = sub {
+       my $line = shift;
+
+       if ($line =~ m/^(\d+)\s+total$/) {
+           $size = $1;
+       }
+    };
+
+    run_command(['du', '-scb', $path], outfunc => $parser, timeout => $timeout);
+
+    return $size;
+}
+
 # UPID helper
 # We use this to uniquely identify a process.
 # An 'Unique Process ID' has the following format:
@@ -1409,77 +1541,6 @@ sub mkdirat($$$) {
     return syscall(PVE::Syscall::mkdirat, $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 {
@@ -1494,4 +1555,45 @@ sub encrypt_pw {
     return crypt(encode("utf8", $pw), "\$5\$$salt\$");
 }
 
+# intended usage: convert_size($val, "kb" => "gb")
+# we round up to the next integer by default
+# E.g. `convert_size(1023, "b" => "kb")` returns 1
+# use $no_round_up to switch this off, above example would then return 0
+# this is also true for converting down e.g. 0.0005 gb to mb returns 1
+# (0 if $no_round_up is true)
+# allowed formats for value:
+# 1234
+# 1234.
+# 1234.1234
+# .1234
+sub convert_size {
+    my ($value, $from, $to, $no_round_up) = @_;
+
+    my $units = {
+       b  => 0,
+       kb => 1,
+       mb => 2,
+       gb => 3,
+       tb => 4,
+       pb => 5,
+    };
+
+    die "no value given"
+       if !defined($value) || $value eq "";
+
+    $from = lc($from // ''); $to = lc($to // '');
+    die "unknown 'from' and/or 'to' units ($from => $to)"
+       if !defined($units->{$from}) || !defined($units->{$to});
+
+    die "value '$value' is not a valid, positive number"
+       if $value !~ m/^(?:[0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)$/;
+
+    my $shift_amount = ($units->{$from} - $units->{$to}) * 10;
+
+    $value *= 2**$shift_amount;
+    $value++ if !$no_round_up && ($value - int($value)) > 0.0;
+
+    return int($value);
+}
+
 1;