package PVE::VZDump;
-# Copyright (C) 2007-2009 Proxmox Server Solutions GmbH
-#
-# Copyright: vzdump is under GNU GPL, the GNU General Public License.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; version 2 dated June, 1991.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the
-# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
-# MA 02110-1301, USA.
-#
-# Author: Dietmar Maurer <dietmar@proxmox.com>
-
use strict;
use warnings;
use Fcntl ':flock';
-use Sys::Hostname;
-use Sys::Syslog;
+use PVE::Exception qw(raise_param_exc);
+use PVE::SafeSyslog;
use IO::File;
use IO::Select;
use IPC::Open3;
use POSIX qw(strftime);
use File::Path;
+use PVE::RPCEnvironment;
+use PVE::Storage;
+use PVE::Cluster qw(cfs_read_file);
use PVE::VZDump::OpenVZ;
use Time::localtime;
use Time::Local;
+use PVE::JSONSchema qw(get_standard_option);
my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
sub run_command {
my ($logfd, $cmdstr, %param) = @_;
- my $timeout;
- my $input;
- my $output;
-
- foreach my $p (keys %param) {
- if ($p eq 'timeout') {
- $timeout = $param{$p};
- } elsif ($p eq 'input') {
- $input = $param{$p};
- } elsif ($p eq 'output') {
- $output = $param{$p};
- } else {
- die "got unknown parameter '$p' for run_command\n";
- }
- }
-
- my $reader = $output && $output =~ m/^>&/ ? $output : IO::File->new();
- my $writer = $input && $input =~ m/^<&/ ? $input : IO::File->new();
- my $error = IO::File->new();
-
- my $orig_pid = $$;
-
- my $pid;
- eval {
- # suppress LVM warnings like: "File descriptor 3 left open";
- local $ENV{LVM_SUPPRESS_FD_WARNINGS} = "1";
-
- $pid = open3 ($writer, $reader, $error, ($cmdstr)) || die $!;
+ my $logfunc = sub {
+ my $line = shift;
+ debugmsg ('info', $line, $logfd);
};
- my $err = $@;
-
- # catch exec errors
- if ($orig_pid != $$) {
- debugmsg ('err', "command '$cmdstr' failed - fork failed: $!", $logfd);
- POSIX::_exit (1);
- kill ('KILL', $$);
- }
-
- die $err if $err;
-
- if (ref($writer)) {
- print $writer $input if defined $input;
- close $writer;
- }
-
- my $select = new IO::Select;
- $select->add ($reader) if ref($reader);
- $select->add ($error);
-
- my ($ostream, $estream, $logout, $logerr) = ('', '', '', '');
-
- while ($select->count) {
- my @handles = $select->can_read ($timeout);
-
- if (defined ($timeout) && (scalar (@handles) == 0)) {
- die "command '$cmdstr' failed: timeout\n";
- }
-
- foreach my $h (@handles) {
- my $buf = '';
- my $count = sysread ($h, $buf, 4096);
- if (!defined ($count)) {
- waitpid ($pid, 0);
- die "command '$cmdstr' failed: $!\n";
- }
- $select->remove ($h) if !$count;
-
- if ($h eq $reader) {
- $ostream .= $buf;
- $logout .= $buf;
- while ($logout =~ s/^([^\n]*\n)//s) {
- my $line = $1;
- debugmsg ('info', $line, $logfd);
- }
- } elsif ($h eq $error) {
- $estream .= $buf;
- $logerr .= $buf;
- while ($logerr =~ s/^([^\n]*\n)//s) {
- my $line = $1;
- debugmsg ('info', $line, $logfd);
- }
- }
- }
- }
-
- debugmsg ('info', $logout, $logfd);
- debugmsg ('info', $logerr, $logfd);
-
- waitpid ($pid, 0);
- my $ec = ($? >> 8);
-
- return $ostream if $ec == 24 && ($cmdstr =~ m|^(\S+/)?rsync\s|);
-
- die "command '$cmdstr' failed with exit code $ec\n" if $ec;
-
- return $ostream;
+ PVE::Tools::run_command($cmdstr, %param, logfunc => $logfunc);
}
sub storage_info {
my $storage = shift;
- eval { require PVE::Storage; };
- die "unable to query storage info for '$storage' - $@\n" if $@;
- my $cfg = PVE::Storage::load_config();
- my $scfg = PVE::Storage::storage_config ($cfg, $storage);
+ my $cfg = cfs_read_file('storage.cfg');
+ my $scfg = PVE::Storage::storage_config($cfg, $storage);
my $type = $scfg->{type};
die "can't use storage type '$type' for backup\n"
die "can't use storage for backups - wrong content type\n"
if (!$scfg->{content}->{backup});
- PVE::Storage::activate_storage ($cfg, $storage);
+ PVE::Storage::activate_storage($cfg, $storage);
return {
- dumpdir => $scfg->{path},
+ dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
+ maxfiles => $scfg->{maxfiles},
};
}
foreach my $vmid (@vmids) {
die "ERROR: strange VM ID '${vmid}'\n" if $vmid !~ m/^\d+$/;
$vmid = int ($vmid); # remove leading zeros
- die "ERROR: got reserved VM ID '${vmid}'\n" if $vmid < 100;
+ next if !$vmid;
push @$res, $vmid;
}
$res->{size} = int($1);
} elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
$res->{maxfiles} = int($1);
+ } elsif ($line =~ m/exclude-path:\s*(.*)\s*$/) {
+ $res->{'exclude-path'} = PVE::Tools::split_args($1);
} elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
$res->{mode} = $1;
} else {
}
}
-sub read_firstfile {
- my $archive = shift;
-
- die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
-
- # try to detect archive type first
- my $pid = open (TMP, "tar tf '$archive'|") ||
- die "unable to open file '$archive'\n";
- my $firstfile = <TMP>;
- kill 15, $pid;
- close TMP;
-
- die "ERROR: archive contaions no data\n" if !$firstfile;
- chomp $firstfile;
-
- return $firstfile;
-}
-
my $sendmail = sub {
my ($self, $tasklist, $totaltime) = @_;
my $mailto = $opts->{mailto};
- return if !$mailto;
+ return if !($mailto && scalar(@$mailto));
my $cmdline = $self->{cmdline};
my $stat = $ecount ? 'backup failed' : 'backup successful';
- my $hostname = `hostname -f` || hostname();
+ my $hostname = `hostname -f` || PVE::INotify::nodename();
chomp $hostname;
-
my $boundary = "----_=_NextPart_001_".int(time).$$;
my $rcvrarg = '';
# end html part
print MAIL "\n--$boundary--\n";
+ close(MAIL);
};
sub new {
- my ($class, $cmdline, $opts) = @_;
+ my ($class, $cmdline, $opts, $skiplist) = @_;
mkpath $logdir;
check_bin ('cstream');
check_bin ('ionice');
- if ($opts->{snapshot}) {
+ if ($opts->{mode} && $opts->{mode} eq 'snapshot') {
check_bin ('lvcreate');
check_bin ('lvs');
check_bin ('lvremove');
my $defaults = read_vzdump_defaults();
+ my $maxfiles = $opts->{maxfiles}; # save here, because we overwrite with default
+
+ $opts->{remove} = 1 if !defined($opts->{remove});
+
foreach my $k (keys %$defaults) {
if ($k eq 'dumpdir' || $k eq 'storage') {
$opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir}) &&
}
}
- $opts->{mode} = 'stop' if $opts->{stop};
- $opts->{mode} = 'suspend' if $opts->{suspend};
- $opts->{mode} = 'snapshot' if $opts->{snapshot};
-
$opts->{dumpdir} =~ s|/+$|| if ($opts->{dumpdir});
$opts->{tmpdir} =~ s|/+$|| if ($opts->{tmpdir});
- my $self = bless { cmdline => $cmdline, opts => $opts };
+ $skiplist = [] if !$skiplist;
+ my $self = bless { cmdline => $cmdline, opts => $opts, skiplist => $skiplist };
#always skip '.'
push @{$self->{findexcl}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
$self->find_add_exclude ('-type', 's'); # skip sockets
+ if ($defaults->{'exclude-path'}) {
+ foreach my $path (@{$defaults->{'exclude-path'}}) {
+ $self->find_add_exclude ('-regex', $path);
+ }
+ }
+
if ($opts->{'exclude-path'}) {
foreach my $path (@{$opts->{'exclude-path'}}) {
$self->find_add_exclude ('-regex', $path);
my $pd = $p->new ($self);
push @{$self->{plugins}}, $pd;
-
- if (!$opts->{dumpdir} && !$opts->{storage} &&
- ($p eq 'PVE::VZDump::OpenVZ')) {
- $opts->{dumpdir} = $pd->{dumpdir};
- }
}
if (!$opts->{dumpdir} && !$opts->{storage}) {
- die "no dumpdir/storage specified - use option '--dumpdir' or option '--storage'\n";
+ $opts->{storage} = 'local';
}
if ($opts->{storage}) {
my $info = storage_info ($opts->{storage});
$opts->{dumpdir} = $info->{dumpdir};
+ $maxfiles = $info->{maxfiles} if !defined($maxfiles) && defined($info->{maxfiles});
} elsif ($opts->{dumpdir}) {
die "dumpdir '$opts->{dumpdir}' does not exist\n"
if ! -d $opts->{dumpdir};
die "tmpdir '$opts->{tmpdir}' does not exist\n";
}
+ $opts->{maxfiles} = $maxfiles if defined($maxfiles);
+
return $self;
}
my $devmapper;
- my $cmd = "lvs --units m --separator ':' --noheadings -o vg_name,lv_name,lv_size";
- if (my $fd = IO::File->new ("$cmd 2>/dev/null|")) {
- while (my $line = <$fd>) {
- if ($line =~ m|^\s*(\S+):(\S+):(\d+(\.\d+))[Mm]$|) {
- my $vg = $1;
- my $lv = $2;
- $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
- my $qlv = $lv;
- $qlv =~ s/-/--/g;
- my $qvg = $vg;
- $qvg =~ s/-/--/g;
- $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
- }
- }
- close ($fd);
- }
+ my $cmd = ['lvs', '--units', 'm', '--separator', ':', '--noheadings',
+ '-o', 'vg_name,lv_name,lv_size' ];
+
+ my $parser = sub {
+ my $line = shift;
+ if ($line =~ m|^\s*(\S+):(\S+):(\d+(\.\d+))[Mm]$|) {
+ my $vg = $1;
+ my $lv = $2;
+ $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
+ my $qlv = $lv;
+ $qlv =~ s/-/--/g;
+ my $qvg = $vg;
+ $qvg =~ s/-/--/g;
+ $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
+ }
+ };
+
+ eval { PVE::Tools::run_command($cmd, errfunc => sub {}, outfunc => $parser); };
+ warn $@ if $@;
return $devmapper;
}
sub get_mount_info {
my ($dir) = @_;
- my $out;
- if (my $fd = IO::File->new ("df -P -T '$dir' 2>/dev/null|")) {
- <$fd>; #skip first line
- $out = <$fd>;
- close ($fd);
- }
+ # Note: df 'available' can be negative, and percentage set to '-'
- return undef if !$out;
-
- my @res = split (/\s+/, $out);
+ my $cmd = [ 'df', '-P', '-T', '-B', '1', $dir];
- return undef if scalar (@res) != 7;
+ my $res;
- return {
- device => $res[0],
- fstype => $res[1],
- mountpoint => $res[6]
+ my $parser = sub {
+ my $line = shift;
+ if (my ($fsid, $fstype, undef, $mp) = $line =~
+ m!(\S+.*)\s+(\S+)\s+\d+\s+\-?\d+\s+\d+\s+(\d+%|-)\s+(/.*)$!) {
+ $res = {
+ device => $fsid,
+ fstype => $fstype,
+ mountpoint => $mp,
+ };
+ }
};
+
+ eval { PVE::Tools::run_command($cmd, errfunc => sub {}, outfunc => $parser); };
+ warn $@ if $@;
+
+ return $res;
}
sub get_lvm_device {
my ($dir, $mapping) = @_;
- my $info = get_mount_info ($dir);
+ my $info = get_mount_info($dir);
return undef if !$info;
local %ENV;
- foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile)) {
+ foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile storeid)) {
$ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
}
run_command ($logfd, $cmd);
}
+sub compressor_info {
+ my ($opt_compress) = @_;
+
+ if (!$opt_compress || $opt_compress eq '0') {
+ return undef;
+ } elsif ($opt_compress eq '1' || $opt_compress eq 'lzo') {
+ return ('lzop', 'lzo');
+ } elsif ($opt_compress eq 'gzip') {
+ return ('gzip', 'gz');
+ } else {
+ die "internal error - unknown compression option '$opt_compress'";
+ }
+}
+
+sub get_backup_file_list {
+ my ($dir, $bkname, $exclude_fn) = @_;
+
+ 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))?)))$!) {
+ $fn = "$dir/$1"; # untaint
+ my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2 - 1900);
+ push @$bklist, [$fn, $t];
+ }
+ }
+
+ return $bklist;
+}
+
sub exec_backup_task {
my ($self, $task) = @_;
$lt->year + 1900, $lt->mon + 1, $lt->mday,
$lt->hour, $lt->min, $lt->sec;
+ my $maxfiles = $opts->{maxfiles};
+
+ if ($maxfiles && !$opts->{remove}) {
+ my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname);
+ die "only $maxfiles backup(s) allowed - please consider to remove old backup files.\n"
+ if scalar(@$bklist) >= $maxfiles;
+ }
+
my $logfile = $task->{logfile} = "$opts->{dumpdir}/$basename.log";
- my $ext = $opts->{compress} ? '.tgz' : '.tar';
+ my $ext = $vmtype eq 'qemu' ? '.vma' : '.tar';
+ my ($comp, $comp_ext) = compressor_info($opts->{compress});
+ if ($comp && $comp_ext) {
+ $ext .= ".${comp_ext}";
+ }
if ($opts->{stdout}) {
$task->{tarfile} = '-';
$task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
} else {
# dumpdir is posix? then use it as temporary dir
- my $info = get_mount_info ($opts->{dumpdir});
+ my $info = get_mount_info($opts->{dumpdir});
if ($vmtype eq 'qemu' ||
grep ($_ eq $info->{fstype}, @posix_filesystems)) {
$task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
die "unable to create log file '$tmplog'";
$task->{dumpdir} = $opts->{dumpdir};
-
+ $task->{storeid} = $opts->{storage};
$task->{tmplog} = $tmplog;
unlink $logfile;
} elsif ($mode eq 'snapshot') {
+ $self->run_hook_script ('backup-start', $task, $logfd);
+
my $snapshot_count = $task->{snapshot_count} || 0;
$self->run_hook_script ('pre-stop', $task, $logfd);
if ($opts->{stdout}) {
debugmsg ('info', "sending archive to stdout", $logfd);
- $plugin->archive ($task, $vmid, $task->{tmptar});
+ $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
$self->run_hook_script ('backup-end', $task, $logfd);
return;
}
debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
- $plugin->archive ($task, $vmid, $task->{tmptar});
+ $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
rename ($task->{tmptar}, $task->{tarfile}) ||
die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
# purge older backup
- my $maxfiles = $opts->{maxfiles};
+ if ($maxfiles && $opts->{remove}) {
+ my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname, $task->{tarfile});
+ $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
- if ($maxfiles) {
- my @bklist = ();
- my $dir = $opts->{dumpdir};
- foreach my $fn (<$dir/${bkname}-*>) {
- next if $fn eq $task->{tarfile};
- if ($fn =~ m!/${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|tar)$!) {
- my $t = timelocal ($6, $5, $4, $3, $2 - 1, $1 - 1900);
- push @bklist, [$fn, $t];
- }
- }
-
- @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
-
- my $ind = scalar (@bklist);
-
- while (scalar (@bklist) >= $maxfiles) {
- my $d = pop @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)$/\.log/;
+ $logfn =~ s/\.(tgz|((tar|vma)(\.(gz|lzo))?))$/\.log/;
unlink $logfn;
}
}
debugmsg ('info', "resume vm", $logfd);
$plugin->resume_vm ($task, $vmid);
} else {
- debugmsg ('info', "restarting vm", $logfd);
- $plugin->start_vm ($task, $vmid);
+ my $running = $plugin->vm_status($vmid);
+ if (!$running) {
+ debugmsg ('info', "restarting vm", $logfd);
+ $plugin->start_vm ($task, $vmid);
+ }
}
};
my $err = $@;
}
sub exec_backup {
- my ($self) = @_;
+ my ($self, $rpcenv, $authuser) = @_;
my $opts = $self->{opts};
debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
-
+ debugmsg ('info', "skip external VMs: " . join(', ', @{$self->{skiplist}}))
+ if scalar(@{$self->{skiplist}});
+
my $tasklist = [];
if ($opts->{all}) {
my $vmlist = $plugin->vmlist();
foreach my $vmid (sort @$vmlist) {
next if grep { $_ eq $vmid } @{$opts->{exclude}};
+ next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
}
}
last;
}
}
+ $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
}
}
if ($errcount) {
debugmsg ('info', "Backup job finished with errors", undef, 1);
} else {
- debugmsg ('info', "Backup job finished successfuly", undef, 1);
+ debugmsg ('info', "Backup job finished successfully", undef, 1);
}
}
eval { $self->$sendmail ($tasklist, $totaltime); };
debugmsg ('err', $@) if $@;
+
+ die $err if $err;
+
+ die "job errors\n" if $errcount;
+}
+
+my $confdesc = {
+ vmid => {
+ type => 'string', format => 'pve-vmid-list',
+ description => "The ID of the VM you want to backup.",
+ optional => 1,
+ },
+ node => get_standard_option('pve-node', {
+ description => "Only run if executed on this node.",
+ optional => 1,
+ }),
+ all => {
+ type => 'boolean',
+ description => "Backup all known VMs on this host.",
+ optional => 1,
+ default => 0,
+ },
+ stdexcludes => {
+ type => 'boolean',
+ description => "Exclude temorary files and logs.",
+ optional => 1,
+ default => 1,
+ },
+ compress => {
+ type => 'string',
+ description => "Compress dump file.",
+ optional => 1,
+ enum => ['0', '1', 'gzip', 'lzo'],
+ default => 'lzo',
+ },
+ quiet => {
+ type => 'boolean',
+ description => "Be quiet.",
+ optional => 1,
+ default => 0,
+ },
+ mode => {
+ type => 'string',
+ description => "Backup mode.",
+ optional => 1,
+ default => 'stop',
+ enum => [ 'snapshot', 'suspend', 'stop' ],
+ },
+ exclude => {
+ type => 'string', format => 'pve-vmid-list',
+ description => "exclude specified VMs (assumes --all)",
+ optional => 1,
+ },
+ 'exclude-path' => {
+ type => 'string', format => 'string-alist',
+ description => "exclude certain files/directories (regex).",
+ optional => 1,
+ },
+ mailto => {
+ type => 'string', format => 'string-list',
+ description => "",
+ optional => 1,
+ },
+ tmpdir => {
+ type => 'string',
+ description => "Store temporary files to specified directory.",
+ optional => 1,
+ },
+ dumpdir => {
+ type => 'string',
+ description => "Store resulting files to specified directory.",
+ optional => 1,
+ },
+ script => {
+ type => 'string',
+ description => "Use specified hook script.",
+ optional => 1,
+ },
+ storage => get_standard_option('pve-storage-id', {
+ description => "Store resulting file to this storage.",
+ optional => 1,
+ }),
+ size => {
+ type => 'integer',
+ description => "LVM snapshot size im MB.",
+ optional => 1,
+ minimum => 500,
+ },
+ bwlimit => {
+ type => 'integer',
+ description => "Limit I/O bandwidth (KBytes per second).",
+ optional => 1,
+ minimum => 0,
+ },
+ ionice => {
+ type => 'integer',
+ description => "Set CFQ ionice priority.",
+ optional => 1,
+ minimum => 0,
+ maximum => 8,
+ },
+ lockwait => {
+ type => 'integer',
+ description => "Maximal time to wait for the global lock (minutes).",
+ optional => 1,
+ minimum => 0,
+ },
+ stopwait => {
+ type => 'integer',
+ description => "Maximal time to wait until a VM is stopped (minutes).",
+ optional => 1,
+ minimum => 0,
+ },
+ maxfiles => {
+ type => 'integer',
+ description => "Maximal number of backup files per VM.",
+ optional => 1,
+ minimum => 1,
+ },
+ remove => {
+ type => 'boolean',
+ description => "Remove old backup files if there are more than 'maxfiles' backup files.",
+ optional => 1,
+ default => 1,
+ },
+};
+
+sub option_exists {
+ my $key = shift;
+ return defined($confdesc->{$key});
+}
+
+# add JSON properties for create and set function
+sub json_config_properties {
+ my $prop = shift;
+
+ foreach my $opt (keys %$confdesc) {
+ $prop->{$opt} = $confdesc->{$opt};
+ }
+
+ return $prop;
+}
+
+sub verify_vzdump_parameters {
+ my ($param, $check_missing) = @_;
+
+ raise_param_exc({ all => "option conflicts with option 'vmid'"})
+ if $param->{all} && $param->{vmid};
+
+ raise_param_exc({ exclude => "option conflicts with option 'vmid'"})
+ if $param->{exclude} && $param->{vmid};
+
+ $param->{all} = 1 if defined($param->{exclude});
+
+ return if !$check_missing;
+
+ raise_param_exc({ vmid => "property is missing"})
+ if !$param->{all} && !$param->{vmid};
+
+}
+
+sub command_line {
+ my ($param) = @_;
+
+ my $cmd = "vzdump";
+
+ if ($param->{vmid}) {
+ $cmd .= " " . join(' ', PVE::Tools::split_list($param->{vmid}));
+ }
+
+ foreach my $p (keys %$param) {
+ next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow' || $p eq 'stdout';
+ my $v = $param->{$p};
+ my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
+ if ($p eq 'exclude-path') {
+ foreach my $path (split(/\0/, $v || '')) {
+ $cmd .= " --$p " . PVE::Tools::shellquote($path);
+ }
+ } else {
+ $cmd .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
+ }
+ }
+
+ return $cmd;
}
1;