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;
return {
dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
+ maxfiles => $scfg->{maxfiles},
};
}
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}) &&
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 = $out =~ m/^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/;
+ 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 {
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} = '-';
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))$!) {
- $fn = "$dir/$1"; # untaint
- my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2 - 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};
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 };
}
}
default => 1,
},
compress => {
- type => 'boolean',
- description => "Compress dump file (gzip).",
+ type => 'string',
+ description => "Compress dump file.",
optional => 1,
- default => 0,
+ enum => ['0', '1', 'gzip', 'lzo'],
+ default => 'lzo',
},
quiet => {
type => 'boolean',
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 {
}
foreach my $p (keys %$param) {
- next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
+ 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') {