my $lockfile = '/var/run/vzdump.lock';
+my $pidfile = '/var/run/vzdump.pid';
+
my $logdir = '/var/log/vzdump';
my @plugins = qw (PVE::VZDump::OpenVZ);
my $type = $scfg->{type};
die "can't use storage type '$type' for backup\n"
- if (!($type eq 'dir' || $type eq 'nfs'));
+ if (!($type eq 'dir' || $type eq 'nfs' || $type eq 'glusterfs'));
die "can't use storage for backups - wrong content type\n"
if (!$scfg->{content}->{backup});
return {
dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
+ maxfiles => $scfg->{maxfiles},
};
}
$res->{lockwait} = int($1);
} elsif ($line =~ m/stopwait:\s*(\d+)\s*$/) {
$res->{stopwait} = int($1);
+ } elsif ($line =~ m/stop:\s*(\d+)\s*$/) {
+ $res->{stop} = int($1);
} elsif ($line =~ m/size:\s*(\d+)\s*$/) {
$res->{size} = int($1);
} elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
}
}
-my $sendmail = sub {
- my ($self, $tasklist, $totaltime) = @_;
+sub sendmail {
+ my ($self, $tasklist, $totaltime, $err) = @_;
my $opts = $self->{opts};
}
}
- my $stat = $ecount ? 'backup failed' : 'backup successful';
+ my $notify = $opts->{mailnotification} || 'always';
+ return if (!$ecount && !$err && ($notify eq 'failure'));
+
+ my $stat = ($ecount || $err) ? 'backup failed' : 'backup successful';
+ $stat .= ": $err" if $err;
my $hostname = `hostname -f` || PVE::INotify::nodename();
chomp $hostname;
foreach my $r (@$mailto) {
$rcvrarg .= " '$r'";
}
+ my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ my $mailfrom = $dcconf->{email_from} || "root";
- open (MAIL,"|sendmail -B 8BITMIME $rcvrarg") ||
+ open (MAIL,"|sendmail -B 8BITMIME -f $mailfrom $rcvrarg") ||
die "unable to open 'sendmail' - $!";
my $rcvrtxt = join (', ', @$mailto);
print MAIL "Content-Type: multipart/alternative;\n";
print MAIL "\tboundary=\"$boundary\"\n";
- print MAIL "FROM: vzdump backup tool <root>\n";
+ print MAIL "MIME-Version: 1.0\n";
+
+ print MAIL "FROM: vzdump backup tool <$mailfrom>\n";
print MAIL "TO: $rcvrtxt\n";
print MAIL "SUBJECT: vzdump backup status ($hostname) : $stat\n";
print MAIL "\n";
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 {
}
sub getlock {
- my ($self) = @_;
+ my ($self, $upid) = @_;
+ my $fh;
+
my $maxwait = $self->{opts}->{lockwait} || $self->{lockwait};
+ die "missimg UPID" if !$upid; # should not happen
+
if (!open (SERVER_FLCK, ">>$lockfile")) {
debugmsg ('err', "can't open lock on file '$lockfile' - $!", undef, 1);
- exit (-1);
+ die "can't open lock on file '$lockfile' - $!";
}
- if (flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
- return;
- }
+ if (!flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
- if (!$maxwait) {
- debugmsg ('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
- exit (-1);
- }
+ if (!$maxwait) {
+ debugmsg ('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
+ die "can't aquire lock '$lockfile' (wait = 0)";
+ }
- debugmsg('info', "trying to get global lock - waiting...", undef, 1);
+ debugmsg('info', "trying to get global lock - waiting...", undef, 1);
- eval {
- alarm ($maxwait * 60);
+ eval {
+ alarm ($maxwait * 60);
- local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
+ local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
- if (!flock (SERVER_FLCK, LOCK_EX)) {
- my $err = $!;
- close (SERVER_FLCK);
+ if (!flock (SERVER_FLCK, LOCK_EX)) {
+ my $err = $!;
+ close (SERVER_FLCK);
+ alarm (0);
+ die "$err\n";
+ }
alarm (0);
- die "$err\n";
- }
+ };
alarm (0);
- };
- alarm (0);
- my $err = $@;
+ my $err = $@;
+
+ if ($err) {
+ debugmsg ('err', "can't aquire lock '$lockfile' - $err", undef, 1);
+ die "can't aquire lock '$lockfile' - $err";
+ }
- if ($err) {
- debugmsg ('err', "can't aquire lock '$lockfile' - $err", undef, 1);
- exit (-1);
+ debugmsg('info', "got global lock", undef, 1);
}
- debugmsg('info', "got global lock", undef, 1);
+ PVE::Tools::file_set_contents($pidfile, $upid);
}
sub run_hook_script {
local %ENV;
- foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile)) {
+ # 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};
+
+ foreach my $ek (qw(vmtype hostname tarfile logfile)) {
$ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
}
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 = '.tar';
+ my $ext = $vmtype eq 'qemu' ? '.vma' : '.tar';
my ($comp, $comp_ext) = compressor_info($opts->{compress});
if ($comp && $comp_ext) {
$ext .= ".${comp_ext}";
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);
# purge older backup
- my $maxfiles = $opts->{maxfiles};
-
- 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(\.(gz|lzo))?)))$!) {
- $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;
+ if ($maxfiles && $opts->{remove}) {
+ my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname, $task->{tarfile});
+ $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(\.(gz|lzo))?))$/\.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 = $@;
my $totaltime = time() - $starttime;
- eval { $self->$sendmail ($tasklist, $totaltime); };
+ eval { $self->sendmail ($tasklist, $totaltime); };
debugmsg ('err', $@) if $@;
die $err if $err;
die "job errors\n" if $errcount;
+
+ unlink $pidfile;
}
my $confdesc = {
description => "",
optional => 1,
},
+ mailnotification => {
+ type => 'string',
+ description => "Specify when to send an email",
+ optional => 1,
+ enum => [ 'always', 'failure' ],
+ default => 'always',
+ },
tmpdir => {
type => 'string',
description => "Store temporary files to specified directory.",
description => "Store resulting file to this storage.",
optional => 1,
}),
+ stop => {
+ type => 'boolean',
+ description => "Stop runnig backup jobs on this host.",
+ optional => 1,
+ default => 0,
+ },
size => {
type => 'integer',
- description => "LVM snapshot size im MB.",
+ description => "LVM snapshot size in MB.",
optional => 1,
minimum => 500,
},
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 {
return if !$check_missing;
raise_param_exc({ vmid => "property is missing"})
- if !$param->{all} && !$param->{vmid};
+ if !($param->{all} || $param->{stop}) && !$param->{vmid};
+
+}
+
+sub stop_running_backups {
+ my($self) = @_;
+ my $upid = PVE::Tools::file_read_firstline($pidfile);
+ return if !$upid;
+
+ my $task = PVE::Tools::upid_decode($upid);
+
+ if (PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart}) &&
+ PVE::ProcFSTools::read_proc_starttime($task->{pid}) == $task->{pstart}) {
+ kill(15, $task->{pid});
+ # wait max 15 seconds to shut down (else, do nothing for now)
+ my $i;
+ for ($i = 15; $i > 0; $i--) {
+ last if !PVE::ProcFSTools::check_process_running(($task->{pid}, $task->{pstart}));
+ sleep (1);
+ }
+ die "stoping backup process $task->{pid} failed\n" if $i == 0;
+ }
}
sub command_line {
}
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') {