]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/VZDump.pm
cleanup vzdump -stop implementation
[pve-manager.git] / PVE / VZDump.pm
index d19bba628c7f8fd2c9c203997f2a2085db4fc426..f5aac84b88cc426ba18114b9d91131b2782590aa 100644 (file)
@@ -22,6 +22,8 @@ my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
 
 my $lockfile = '/var/run/vzdump.lock';
 
+my $pidfile = '/var/run/vzdump.pid';
+
 my $logdir = '/var/log/vzdump';
 
 my @plugins = qw (PVE::VZDump::OpenVZ);
@@ -84,7 +86,7 @@ sub storage_info {
     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});
 
@@ -92,6 +94,7 @@ sub storage_info {
 
     return {
        dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
+       maxfiles => $scfg->{maxfiles},
     };
 }
 
@@ -209,6 +212,8 @@ sub read_vzdump_defaults {
            $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*$/) {
@@ -242,8 +247,8 @@ sub find_add_exclude {
     }
 }
 
-my $sendmail = sub {
-    my ($self, $tasklist, $totaltime) = @_;
+sub sendmail {
+    my ($self, $tasklist, $totaltime, $err) = @_;
 
     my $opts = $self->{opts};
 
@@ -267,7 +272,11 @@ my $sendmail = sub {
        }
     }
 
-    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;
@@ -278,15 +287,19 @@ my $sendmail = sub {
     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";
@@ -439,6 +452,10 @@ sub new {
 
     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}) &&
@@ -483,20 +500,16 @@ sub new {
        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};
@@ -508,6 +521,8 @@ sub new {
        die "tmpdir '$opts->{tmpdir}' does not exist\n";
     }
 
+    $opts->{maxfiles} = $maxfiles if defined($maxfiles);
+
     return $self;
 
 }
@@ -516,22 +531,25 @@ sub get_lvm_mapping {
 
     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;
 }
@@ -539,24 +557,28 @@ sub get_lvm_mapping {
 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 {
@@ -576,49 +598,54 @@ 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 {
@@ -636,7 +663,11 @@ 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};
     }
 
@@ -656,6 +687,22 @@ sub compressor_info {
        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) = @_;
@@ -687,9 +734,17 @@ sub exec_backup_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}";
@@ -730,7 +785,7 @@ sub exec_backup_task {
            die "unable to create log file '$tmplog'";
 
        $task->{dumpdir} = $opts->{dumpdir};
-
+       $task->{storeid} = $opts->{storage};
        $task->{tmplog} = $tmplog;
 
        unlink $logfile;
@@ -820,6 +875,8 @@ sub exec_backup_task {
 
        } 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);
@@ -872,30 +929,16 @@ sub exec_backup_task {
 
        # 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;
            }
        }
@@ -925,8 +968,11 @@ sub exec_backup_task {
                    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 = $@;
@@ -1035,12 +1081,14 @@ sub exec_backup {
 
     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 = {
@@ -1100,6 +1148,13 @@ 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.",
@@ -1119,9 +1174,15 @@ my $confdesc = {
        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,
     },
@@ -1156,6 +1217,12 @@ my $confdesc = {
        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 {
@@ -1188,8 +1255,29 @@ sub verify_vzdump_parameters {
     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 {
@@ -1202,7 +1290,7 @@ 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') {