]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Tools.pm
mark decode_utf8_parameters() as depreciated
[pve-common.git] / src / PVE / Tools.pm
index 68c4e68ba934370c4ec05aca5c6cfeee5ca0a516..1fe7f4c8ae12e49515d3d27072980f9587cc55d8 100644 (file)
@@ -2,9 +2,9 @@ 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 Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED AI_CANONNAME SOCK_DGRAM);
 use IO::Select;
 use File::Basename;
 use File::Path qw(make_path);
@@ -32,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
@@ -203,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;
@@ -218,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;
     }
 }
 
@@ -228,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;
 
@@ -253,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;
 }
@@ -333,7 +341,7 @@ sub run_command {
     my $timeout;
     my $oldtimeout;
     my $pid;
-    my $exitcode;
+    my $exitcode = -1;
 
     my $outfunc;
     my $errfunc;
@@ -342,6 +350,7 @@ sub run_command {
     my $output;
     my $afterfork;
     my $noerr;
+    my $keeplocale;
 
     eval {
 
@@ -366,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";
            }
@@ -381,7 +392,7 @@ sub run_command {
                        print STDERR "$laststderr\n" if $laststderr;
                    }
                }
-               $laststderr = shift; 
+               $laststderr = shift;
            };
        }
 
@@ -389,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";
@@ -417,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;
@@ -506,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)) {
@@ -559,7 +567,7 @@ sub split_list {
     my $listtxt = shift || '';
 
     return split (/\0/, $listtxt) if $listtxt =~ m/\0/;
-    
+
     $listtxt =~ s/[,;]/ /g;
     $listtxt =~ s/^\s+//;
 
@@ -575,7 +583,7 @@ sub trim {
 
     $txt =~ s/^\s+//;
     $txt =~ s/\s+$//;
-    
+
     return $txt;
 }
 
@@ -584,7 +592,7 @@ sub template_replace {
     my ($tmpl, $data) = @_;
 
     return $tmpl if !$tmpl;
+
     my $res = '';
     while ($tmpl =~ m/([^{]+)?({([^}]+)})?/g) {
        $res .= $1 if $1;
@@ -651,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'],
@@ -672,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'],
@@ -756,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++) {
@@ -780,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;
@@ -814,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
@@ -867,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 {
@@ -875,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});
 }
 
@@ -911,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);
@@ -920,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;
@@ -954,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) = @_;
 
@@ -969,6 +977,8 @@ sub decode_text {
     return Encode::decode("utf8", uri_unescape($data));
 }
 
+# depreciated - do not use!
+# we now decode all parameters by default
 sub decode_utf8_parameters {
     my ($param) = @_;
 
@@ -980,15 +990,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 {
@@ -1024,7 +1039,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);
@@ -1068,11 +1083,11 @@ sub dump_logfile {
 }
 
 sub dump_journal {
-    my ($start, $limit, $since, $until) = @_;
+    my ($start, $limit, $since, $until, $service) = @_;
 
     my $lines = [];
     my $count = 0;
-    
+
     $start = 0 if !$start;
     $limit = 50 if !$limit;
 
@@ -1087,6 +1102,7 @@ sub dump_journal {
 
     my $cmd = ['journalctl', '-o', 'short', '--no-pager'];
 
+    push @$cmd, '--unit', $service if $service;
     push @$cmd, '--since', $since if $since;
     push @$cmd, '--until', $until if $until;
     run_command($cmd, outfunc => $parser);
@@ -1106,8 +1122,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;
@@ -1128,7 +1144,7 @@ sub dir_glob_foreach {
                &$func (@res);
            }
        }
-    } 
+    }
 }
 
 sub assert_if_modified {
@@ -1184,6 +1200,24 @@ sub get_host_address_family {
     return $res[0]->{family};
 }
 
+# get the fully qualified domain name of a host
+# same logic as hostname(1): The FQDN is the name getaddrinfo(3) returns,
+# given a nodename as a parameter
+sub get_fqdn {
+    my ($nodename) = @_;
+
+    my $hints = {
+       flags => AI_CANONNAME,
+       socktype => SOCK_DGRAM
+    };
+
+    my ($err, @addrs) = Socket::getaddrinfo($nodename, undef, $hints);
+
+    die "getaddrinfo: $err" if $err;
+
+    return $addrs[0]->{canonname};
+}
+
 # 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 {
@@ -1293,12 +1327,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;
 }
 
@@ -1424,4 +1464,18 @@ sub enter_systemd_scope {
     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;