]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/VZDump.pm
vzdump: make guest include logic testable
[pve-manager.git] / PVE / VZDump.pm
index 3caa7ab88d9fc82bf53c7dce83939a3f21a1ba86..bdbf641e7308a01fd35972f6c97500015d728bd1 100644 (file)
@@ -2,23 +2,27 @@ package PVE::VZDump;
 
 use strict;
 use warnings;
+
 use Fcntl ':flock';
-use PVE::Exception qw(raise_param_exc);
+use File::Path;
 use IO::File;
 use IO::Select;
 use IPC::Open3;
-use File::Path;
-use PVE::RPCEnvironment;
-use PVE::Storage;
-use PVE::Cluster qw(cfs_read_file);
-use PVE::DataCenterConfig;
 use POSIX qw(strftime);
 use Time::Local;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::HA::Env::PVE2;
+
+use PVE::Cluster qw(cfs_read_file);
+use PVE::DataCenterConfig;
+use PVE::Exception qw(raise_param_exc);
 use PVE::HA::Config;
-use PVE::VZDump::Plugin;
+use PVE::HA::Env::PVE2;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Storage;
 use PVE::VZDump::Common;
+use PVE::VZDump::Plugin;
+use PVE::Tools qw(extract_param);
+use PVE::API2Tools;
 
 my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
 
@@ -74,16 +78,25 @@ sub storage_info {
 
     die "can't use storage type '$type' for backup\n"
        if (!($type eq 'dir' || $type eq 'nfs' || $type eq 'glusterfs'
-             || $type eq 'cifs' || $type eq 'cephfs'));
+             || $type eq 'cifs' || $type eq 'cephfs' || $type eq 'pbs'));
     die "can't use storage '$storage' for backups - wrong content type\n"
        if (!$scfg->{content}->{backup});
 
     PVE::Storage::activate_storage($cfg, $storage);
 
-    return {
-       dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
-       maxfiles => $scfg->{maxfiles},
-    };
+    if ($type eq 'pbs') {
+       return {
+           scfg => $scfg,
+           maxfiles => $scfg->{maxfiles},
+           pbs => 1,
+       };
+    } else {
+       return {
+           scfg => $scfg,
+           dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
+           maxfiles => $scfg->{maxfiles},
+       };
+    }
 }
 
 sub format_size {
@@ -96,13 +109,15 @@ sub format_size {
     }
 
     my $mb = $size / (1024*1024);
-
     if ($mb < 1024) {
        return int ($mb) . "MB";
-    } else {
-       my $gb = $mb / 1024;
+    }
+    my $gb = $mb / 1024;
+    if ($gb < 1024) {
        return sprintf ("%.2fGB", $gb);
     }
+    my $tb = $gb / 1024;
+    return sprintf ("%.2fTB", $tb);
 }
 
 sub format_time {
@@ -433,6 +448,10 @@ sub new {
        push @{$self->{plugins}}, $pd;
     }
 
+    if (defined($opts->{storage}) && $opts->{stdout}) {
+       die "unable to use option 'storage' with option 'stdout'\n";
+    }
+
     if (!$opts->{dumpdir} && !$opts->{storage}) {
        $opts->{storage} = 'local';
     }
@@ -444,6 +463,8 @@ sub new {
        $errors .= "could not get storage information for '$opts->{storage}': $@"
            if ($@);
        $opts->{dumpdir} = $info->{dumpdir};
+       $opts->{scfg} = $info->{scfg};
+       $opts->{pbs} = $info->{pbs};
        $maxfiles //= $info->{maxfiles};
     } elsif ($opts->{dumpdir}) {
        $errors .= "dumpdir '$opts->{dumpdir}' does not exist"
@@ -553,15 +574,17 @@ sub run_hook_script {
     my $opts = $self->{opts};
 
     my $script = $opts->{script};
-
     return if !$script;
 
+    if (!-x $script) {
+       die "The hook script '$script' is not executable.\n";
+    }
+
     my $cmd = "$script $phase";
 
     $cmd .= " $task->{mode} $task->{vmid}" if ($task);
 
     local %ENV;
-
     # set immutable opts directly (so they are available in all phases)
     $ENV{STOREID} = $opts->{storage} if $opts->{storage};
     $ENV{DUMPDIR} = $opts->{dumpdir} if $opts->{dumpdir};
@@ -592,6 +615,13 @@ sub compressor_info {
        } else {
            return ('gzip --rsyncable', 'gz');
        }
+    } elsif ($opt_compress eq 'zstd') {
+       my $zstd_threads = $opts->{zstd} // 1;
+       if ($zstd_threads == 0) {
+           my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
+           $zstd_threads = int(($cpuinfo->{cpus} + 1)/2);
+       }
+       return ("zstd --rsyncable --threads=${zstd_threads}", 'zst');
     } else {
        die "internal error - unknown compression option '$opt_compress'";
     }
@@ -603,7 +633,7 @@ sub get_backup_file_list {
     my $bklist = [];
     foreach my $fn (<$dir/${bkname}-*>) {
        next if $exclude_fn && $fn eq $exclude_fn;
-       if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(gz|lzo))?)))$!) {
+       if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(${\PVE::Storage::Plugin::COMPRESSOR_RE}))?)))$!) {
            $fn = "$dir/$1"; # untaint
            my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2);
            push @$bklist, [$fn, $t];
@@ -620,6 +650,24 @@ sub exec_backup_task {
 
     my $vmid = $task->{vmid};
     my $plugin = $task->{plugin};
+    my $vmtype = $plugin->type();
+
+    $task->{backup_time} = time();
+
+    my $pbs_group_name;
+    my $pbs_snapshot_name;
+
+    if ($self->{opts}->{pbs}) {
+       if ($vmtype eq 'lxc') {
+           $pbs_group_name = "ct/$vmid";
+       } elsif  ($vmtype eq 'qemu') {
+           $pbs_group_name = "vm/$vmid";
+       } else {
+           die "pbs backup not implemented for plugin type '$vmtype'\n";
+       }
+       my $btime = strftime("%FT%TZ", gmtime($task->{backup_time}));
+       $pbs_snapshot_name = "$pbs_group_name/$btime";
+    }
 
     my $vmstarttime = time ();
 
@@ -646,24 +694,31 @@ sub exec_backup_task {
                " enabled Service. Use snapshot mode or disable the Service.\n";
        }
 
-       my $vmtype = $plugin->type();
-
        my $tmplog = "$logdir/$vmtype-$vmid.log";
 
        my $bkname = "vzdump-$vmtype-$vmid";
-       my $basename = $bkname . strftime("-%Y_%m_%d-%H_%M_%S", localtime());
+       my $basename = $bkname . strftime("-%Y_%m_%d-%H_%M_%S", localtime($task->{backup_time}));
 
        my $maxfiles = $opts->{maxfiles};
 
        if ($maxfiles && !$opts->{remove}) {
-           my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname);
+           my $count;
+           if ($self->{opts}->{pbs}) {
+               my $res = PVE::Storage::PBSPlugin::run_client_cmd($opts->{scfg}, $opts->{storage}, 'snapshots', $pbs_group_name);
+               $count = scalar(@$res);
+           } else {
+               my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname);
+               $count = scalar(@$bklist);
+           }
            die "There is a max backup limit of ($maxfiles) enforced by the".
-           " target storage or the vzdump parameters.".
-           " Either increase the limit or delete old backup(s).\n"
-               if scalar(@$bklist) >= $maxfiles;
+               " target storage or the vzdump parameters.".
+               " Either increase the limit or delete old backup(s).\n"
+               if $count >= $maxfiles;
        }
 
-       my $logfile = $task->{logfile} = "$opts->{dumpdir}/$basename.log";
+       if (!$self->{opts}->{pbs}) {
+           $task->{logfile} = "$opts->{dumpdir}/$basename.log";
+       }
 
        my $ext = $vmtype eq 'qemu' ? '.vma' : '.tar';
        my ($comp, $comp_ext) = compressor_info($opts);
@@ -671,18 +726,24 @@ sub exec_backup_task {
            $ext .= ".${comp_ext}";
        }
 
-       if ($opts->{stdout}) {
-           $task->{tarfile} = '-';
+       if ($self->{opts}->{pbs}) {
+           die "unable to pipe backup to stdout\n" if $opts->{stdout};
        } else {
-           my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
-           $task->{tmptar} = $task->{tarfile};
-           $task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
-           unlink $task->{tmptar};
+           if ($opts->{stdout}) {
+               $task->{tarfile} = '-';
+           } else {
+               my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
+               $task->{tmptar} = $task->{tarfile};
+               $task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
+               unlink $task->{tmptar};
+           }
        }
 
        $task->{vmtype} = $vmtype;
 
-       if ($opts->{tmpdir}) {
+       if ($self->{opts}->{pbs}) {
+           $task->{tmpdir} = "/var/tmp/vzdumptmp$$"; #fixme
+       } elsif ($opts->{tmpdir}) {
            $task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
        } else {
            # dumpdir is posix? then use it as temporary dir
@@ -707,9 +768,10 @@ sub exec_backup_task {
 
        $task->{dumpdir} = $opts->{dumpdir};
        $task->{storeid} = $opts->{storage};
+       $task->{scfg} = $opts->{scfg};
        $task->{tmplog} = $tmplog;
 
-       unlink $logfile;
+       unlink $task->{logfile} if defined($task->{logfile});
 
        debugmsg ('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
        debugmsg ('info', "Backup started at " . strftime("%F %H:%M:%S", localtime()));
@@ -841,30 +903,47 @@ sub exec_backup_task {
            return;
        }
 
-       debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
+       # fixme: ??
+       if ($self->{opts}->{pbs}) {
+           debugmsg ('info', "creating pbs archive on storage '$opts->{storage}'", $logfd);
+       } else {
+           debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
+       }
        $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
 
-       rename ($task->{tmptar}, $task->{tarfile}) ||
-           die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
+       if ($self->{opts}->{pbs}) {
+           # fixme: log size ?
+           debugmsg ('info', "pbs upload finished", $logfd);
+       } else {
+           rename ($task->{tmptar}, $task->{tarfile}) ||
+               die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
 
-       # determine size
-       $task->{size} = (-s $task->{tarfile}) || 0;
-       my $cs = format_size ($task->{size});
-       debugmsg ('info', "archive file size: $cs", $logfd);
+           # determine size
+           $task->{size} = (-s $task->{tarfile}) || 0;
+           my $cs = format_size ($task->{size});
+           debugmsg ('info', "archive file size: $cs", $logfd);
+       }
 
        # purge older backup
-
        if ($maxfiles && $opts->{remove}) {
-           my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname, $task->{tarfile});
-           $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
-
-           while (scalar (@$bklist) >= $maxfiles) {
-               my $d = pop @$bklist;
-               debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
-               unlink $d->[0];
-               my $logfn = $d->[0];
-               $logfn =~ s/\.(tgz|((tar|vma)(\.(gz|lzo))?))$/\.log/;
-               unlink $logfn;
+
+           if ($self->{opts}->{pbs}) {
+               my $args = [$pbs_group_name, '--quiet', '1', '--keep-last', $maxfiles];
+               my $logfunc = sub { my $line = shift; debugmsg ('info', $line, $logfd); };
+               PVE::Storage::PBSPlugin::run_raw_client_cmd(
+                   $opts->{scfg}, $opts->{storage}, 'prune', $args, logfunc => $logfunc);
+           } else {
+               my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname, $task->{tarfile});
+               $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
+
+               while (scalar (@$bklist) >= $maxfiles) {
+                   my $d = pop @$bklist;
+                   debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
+                   unlink $d->[0];
+                   my $logfn = $d->[0];
+                   $logfn =~ s/\.(tgz|((tar|vma)(\.(${\PVE::Storage::Plugin::COMPRESSOR_RE}))?))$/\.log/;
+                   unlink $logfn;
+               }
            }
        }
 
@@ -938,8 +1017,16 @@ sub exec_backup_task {
 
     close ($logfd) if $logfd;
 
-    if ($task->{tmplog} && $task->{logfile}) {
-       system {'cp'} 'cp', $task->{tmplog}, $task->{logfile};
+    if ($task->{tmplog}) {
+       if ($self->{opts}->{pbs}) {
+           if ($task->{state} eq 'ok') {
+               my $param = [$pbs_snapshot_name, $task->{tmplog}];
+               PVE::Storage::PBSPlugin::run_raw_client_cmd(
+                   $opts->{scfg}, $opts->{storage}, 'upload-log', $param, errmsg => "upload log failed");
+           }
+       } elsif ($task->{logfile}) {
+           system {'cp'} 'cp', $task->{tmplog}, $task->{logfile};
+       }
     }
 
     eval { $self->run_hook_script ('log-end', $task); };
@@ -1082,4 +1169,39 @@ sub stop_running_backups {
     }
 }
 
+sub get_included_guests {
+    my ($job) = @_;
+
+    my $nodename = PVE::INotify::nodename();
+    my $vmids = [];
+
+    # convert string lists to arrays
+    if ($job->{pool}) {
+       $vmids = PVE::API2Tools::get_resource_pool_guest_members($job->{pool});
+    } else {
+       $vmids = [ PVE::Tools::split_list(extract_param($job, 'vmid')) ];
+    }
+
+    my $skiplist = [];
+    if (!$job->{all}) {
+       if (!$job->{node} || $job->{node} eq $nodename) {
+           my $vmlist = PVE::Cluster::get_vmlist();
+           my $localvmids = [];
+           foreach my $vmid (@{$vmids}) {
+               my $d = $vmlist->{ids}->{$vmid};
+               if ($d && ($d->{node} ne $nodename)) {
+                   push @{$skiplist}, $vmid;
+               } else {
+                   push @{$localvmids}, $vmid;
+               }
+           }
+           $vmids = $localvmids;
+       }
+
+       $job->{vmids} = PVE::VZDump::check_vmids(@{$vmids})
+    }
+
+    return ($vmids, $skiplist);
+}
+
 1;