]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Tools.pm
Add AbstractConfig base class
[pve-common.git] / src / PVE / Tools.pm
index 8e18087ab788634307ae24f69b5b836e75b3a2c5..1d1f4b8429e951503fe71175159796f0dbacfee7 100644 (file)
@@ -8,6 +8,8 @@ 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 IPC::Open3;
@@ -18,7 +20,7 @@ use Encode;
 use Digest::SHA;
 use Text::ParseWords;
 use String::ShellQuote;
-use Time::HiRes qw(usleep gettimeofday tv_interval);
+use Time::HiRes qw(usleep gettimeofday tv_interval alarm);
 
 # avoid warning when parsing long hex values with hex()
 no warnings 'portable'; # Support for 64-bit ints required
@@ -39,6 +41,7 @@ template_replace
 safe_print
 trim
 extract_param
+file_copy
 );
 
 my $pvelogdir = "/var/log/pve";
@@ -47,7 +50,7 @@ my $pvetaskdir = "$pvelogdir/tasks";
 mkdir $pvelogdir;
 mkdir $pvetaskdir;
 
-my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
+my $IPV4OCTET = "(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
 our $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
@@ -63,35 +66,47 @@ our $IPV6RE = "(?:" .
     "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" .            ")$IPV6H16)|" .
     "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" .                    ")))";
 
+our $IPRE = "(?:$IPV4RE|$IPV6RE)";
+
+use constant {CLONE_NEWNS   => 0x00020000,
+              CLONE_NEWUTS  => 0x04000000,
+              CLONE_NEWIPC  => 0x08000000,
+              CLONE_NEWUSER => 0x10000000,
+              CLONE_NEWPID  => 0x20000000,
+              CLONE_NEWNET  => 0x40000000};
+
+use constant O_PATH => 0x10000000;
+
 sub run_with_timeout {
     my ($timeout, $code, @param) = @_;
 
     die "got timeout\n" if $timeout <= 0;
 
-    my $prev_alarm;
+    my $prev_alarm = alarm 0; # suspend outer alarm early
 
     my $sigcount = 0;
 
     my $res;
 
-    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);
+       alarm($timeout);
 
-       $res = &$code(@param);
+       eval { $res = &$code(@param); };
 
        alarm(0); # avoid race conditions
+
+       die $@ if $@;
     };
 
     my $err = $@;
 
-    alarm($prev_alarm) if defined($prev_alarm);
+    alarm $prev_alarm;
 
+    # this shouldn't happen anymore?
     die "unknown error" if $sigcount && !$err; # seems to happen sometimes
 
     die $err if $err;
@@ -113,15 +128,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;
@@ -129,10 +145,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;
@@ -146,9 +163,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) {
@@ -208,6 +228,12 @@ sub file_get_contents {
     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) = @_;
 
@@ -240,13 +266,52 @@ sub safe_read_from {
     return $input;
 }
 
+# The $cmd parameter can be:
+#  -) a string
+#    This is generally executed by passing it to the shell with the -c option.
+#    However, it can be executed in one of two ways, depending on whether
+#    there's a pipe involved:
+#      *) with pipe: passed explicitly to bash -c, prefixed with:
+#          set -o pipefail &&
+#      *) without a pipe: passed to perl's open3 which uses 'sh -c'
+#      (Note that this may result in two different syntax requirements!)
+#      FIXME?
+#  -) an array of arguments (strings)
+#    Will be executed without interference from a shell. (Parameters are passed
+#    as is, no escape sequences of strings will be touched.)
+#  -) an array of arrays
+#    Each array represents a command, and each command's output is piped into
+#    the following command's standard input.
+#    For this a shell command string is created with pipe symbols between each
+#    command.
+#    Each command is a list of strings meant to end up in the final command
+#    unchanged. In order to achieve this, every argument is shell-quoted.
+#    Quoting can be disabled for a particular argument by turning it into a
+#    reference, this allows inserting arbitrary shell options.
+#    For instance: the $cmd [ [ 'echo', 'hello', \'>/dev/null' ] ] will not
+#    produce any output, while the $cmd [ [ 'echo', 'hello', '>/dev/null' ] ]
+#    will literally print: hello >/dev/null
 sub run_command {
     my ($cmd, %param) = @_;
 
     my $old_umask;
     my $cmdstr;
 
-    if (!ref($cmd)) {
+    if (my $ref = ref($cmd)) {
+       if (ref($cmd->[0])) {
+           $cmdstr = 'set -o pipefail && ';
+           my $pipe = '';
+           foreach my $command (@$cmd) {
+               # concatenate quoted parameters
+               # strings which are passed by reference are NOT shell quoted
+               $cmdstr .= $pipe .  join(' ', map { ref($_) ? $$_ : shellquote($_) } @$command);
+               $pipe = ' | ';
+           }
+           $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmdstr" ];
+       } else {
+           $cmdstr = cmd2string($cmd);
+       }
+    } else {
        $cmdstr = $cmd;
        if ($cmd =~ m/\|/) {
            # see 'man bash' for option pipefail
@@ -254,8 +319,6 @@ sub run_command {
        } else {
            $cmd = [ $cmd ];
        }
-    } else {
-       $cmdstr = cmd2string($cmd);
     }
 
     my $errmsg;
@@ -263,6 +326,7 @@ sub run_command {
     my $timeout;
     my $oldtimeout;
     my $pid;
+    my $exitcode;
 
     my $outfunc;
     my $errfunc;
@@ -270,6 +334,7 @@ sub run_command {
     my $input;
     my $output;
     my $afterfork;
+    my $noerr;
 
     eval {
 
@@ -292,6 +357,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";
            }
@@ -437,14 +504,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";
            }
        }
 
@@ -473,12 +540,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 {
@@ -743,33 +810,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>);
+       $res->{used}  = int(<$pipe>);
+       $res->{avail} = int(<$pipe>);
+    };
+    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;
 }
 
@@ -893,23 +978,10 @@ sub random_ether_addr {
 
     my $rand = Digest::SHA::sha1_hex($$, 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);
-
-       if (!$i) {
-           $mac .= "$ss";
-       } else {
-           $mac .= ":$ss";
-       }
-    }
+    # clear multicast, set local id
+    vec($rand, 0, 8) = (vec($rand, 0, 8) & 0xfe) | 2;
 
-    return $mac;
+    return sprintf("%02X:%02X:%02X:%02X:%02X:%02X", unpack("C6", $rand));
 }
 
 sub shellquote {
@@ -988,6 +1060,40 @@ sub dump_logfile {
     return ($count, $lines);
 }
 
+sub dump_journal {
+    my ($start, $limit, $since, $until) = @_;
+
+    my $lines = [];
+    my $count = 0;
+    
+    $start = 0 if !$start;
+    $limit = 50 if !$limit;
+
+    my $parser = sub {
+       my $line = shift;
+
+        return if $count++ < $start;
+       return if $limit <= 0;
+       push @$lines, { n => int($count), t => $line};
+       $limit--;
+    };
+
+    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
+    # 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) = @_;
 
@@ -1071,4 +1177,40 @@ sub get_host_address_family {
     return $res[0]->{family};
 }
 
+# Parses any sane kind of host, or host+port pair:
+# The port is always optional and thus may be undef.
+sub parse_host_and_port {
+    my ($address) = @_;
+    if ($address =~ /^($IPV4RE|[[:alnum:]\-.]+)(?::(\d+))?$/ ||             # ipv4 or host with optional ':port'
+        $address =~ /^\[($IPV6RE|$IPV4RE|[[:alnum:]\-.]+)\](?::(\d+))?$/ || # anything in brackets with optional ':port'
+        $address =~ /^($IPV6RE)(?:\.(\d+))?$/)                              # ipv6 with optional port separated by dot
+    {
+       return ($1, $2, 1); # end with 1 to support simple if(parse...) tests
+    }
+    return; # nothing
+}
+
+sub unshare($) {
+    my ($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;
+}
+
 1;