6 use PVE
::Exception
qw(raise_param_exc);
11 use POSIX
qw(strftime);
13 use PVE
::RPCEnvironment
;
15 use PVE
::Cluster
qw(cfs_read_file);
18 use PVE
::JSONSchema
qw(get_standard_option);
20 my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
22 my $lockfile = '/var/run/vzdump.lock';
24 my $pidfile = '/var/run/vzdump.pid';
26 my $logdir = '/var/log/vzdump';
30 # Load available plugins
31 my $pveplug = "/usr/share/perl5/PVE/VZDump/QemuServer.pm";
33 eval { require $pveplug; };
35 PVE
::VZDump
::QemuServer-
>import ();
36 push @plugins, "PVE::VZDump::QemuServer";
51 my ($mtype, $msg, $logfd, $syslog) = @_;
57 my $pre = $debugstattxt->{$mtype} || $debugstattxt->{'err'};
59 my $timestr = strftime
("%b %d %H:%M:%S", CORE
::localtime);
61 syslog
($mtype eq 'info' ?
'info' : 'err', "$pre $msg") if $syslog;
63 foreach my $line (split (/\n/, $msg)) {
64 print STDERR
"$pre $line\n";
65 print $logfd "$timestr $pre $line\n" if $logfd;
70 my ($logfd, $cmdstr, %param) = @_;
74 debugmsg
('info', $line, $logfd);
77 PVE
::Tools
::run_command
($cmdstr, %param, logfunc
=> $logfunc);
83 my $cfg = cfs_read_file
('storage.cfg');
84 my $scfg = PVE
::Storage
::storage_config
($cfg, $storage);
85 my $type = $scfg->{type
};
87 die "can't use storage type '$type' for backup\n"
88 if (!($type eq 'dir' || $type eq 'nfs' || $type eq 'glusterfs'));
89 die "can't use storage for backups - wrong content type\n"
90 if (!$scfg->{content
}->{backup
});
92 PVE
::Storage
::activate_storage
($cfg, $storage);
95 dumpdir
=> PVE
::Storage
::get_backup_dir
($cfg, $storage),
96 maxfiles
=> $scfg->{maxfiles
},
103 my $kb = $size / 1024;
106 return int ($kb) . "KB";
109 my $mb = $size / (1024*1024);
112 return int ($mb) . "MB";
115 return sprintf ("%.2fGB", $gb);
122 my $hours = int ($seconds/3600);
123 $seconds = $seconds - $hours*3600;
124 my $min = int ($seconds/60);
125 $seconds = $seconds - $min*60;
127 return sprintf ("%02d:%02d:%02d", $hours, $min, $seconds);
133 $str =~ s/^(.{990})/$1\n/mg; # reduce line length
151 foreach my $p (split (/:/, $ENV{PATH
})) {
158 die "unable to find command '$bin'\n";
165 foreach my $vmid (@vmids) {
166 die "ERROR: strange VM ID '${vmid}'\n" if $vmid !~ m/^\d+$/;
167 $vmid = int ($vmid); # remove leading zeros
176 sub read_vzdump_defaults
{
178 my $fn = "/etc/vzdump.conf";
184 lockwait
=> 3*60, # 3 hours
185 stopwait
=> 10, # 10 minutes
190 my $fh = IO
::File-
>new ("<$fn");
194 while (defined ($line = <$fh>)) {
195 next if $line =~ m/^\s*$/;
196 next if $line =~ m/^\#/;
198 if ($line =~ m/tmpdir:\s*(.*\S)\s*$/) {
200 } elsif ($line =~ m/dumpdir:\s*(.*\S)\s*$/) {
201 $res->{dumpdir
} = $1;
202 } elsif ($line =~ m/storage:\s*(\S+)\s*$/) {
203 $res->{storage
} = $1;
204 } elsif ($line =~ m/script:\s*(.*\S)\s*$/) {
206 } elsif ($line =~ m/bwlimit:\s*(\d+)\s*$/) {
207 $res->{bwlimit
} = int($1);
208 } elsif ($line =~ m/ionice:\s*([0-8])\s*$/) {
209 $res->{ionice
} = int($1);
210 } elsif ($line =~ m/lockwait:\s*(\d+)\s*$/) {
211 $res->{lockwait
} = int($1);
212 } elsif ($line =~ m/stopwait:\s*(\d+)\s*$/) {
213 $res->{stopwait
} = int($1);
214 } elsif ($line =~ m/stop:\s*(\d+)\s*$/) {
215 $res->{stop
} = int($1);
216 } elsif ($line =~ m/size:\s*(\d+)\s*$/) {
217 $res->{size
} = int($1);
218 } elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
219 $res->{maxfiles
} = int($1);
220 } elsif ($line =~ m/exclude-path:\s*(.*)\s*$/) {
221 $res->{'exclude-path'} = PVE
::Tools
::split_args
($1);
222 } elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
225 debugmsg
('warn', "unable to parse configuration file '$fn' - error at line " . $., undef, 1);
235 sub find_add_exclude
{
236 my ($self, $excltype, $value) = @_;
238 if (($excltype eq '-regex') || ($excltype eq '-files')) {
242 if ($excltype eq '-files') {
243 push @{$self->{findexcl
}}, "'('", '-not', '-type', 'd', '-regex' , "'$value'", "')'", '-o';
245 push @{$self->{findexcl
}}, "'('", $excltype , "'$value'", '-prune', "')'", '-o';
250 my ($self, $tasklist, $totaltime, $err) = @_;
252 my $opts = $self->{opts
};
254 my $mailto = $opts->{mailto
};
256 return if !($mailto && scalar(@$mailto));
258 my $cmdline = $self->{cmdline
};
261 foreach my $task (@$tasklist) {
262 $ecount++ if $task->{state} ne 'ok';
263 chomp $task->{msg
} if $task->{msg
};
264 $task->{backuptime
} = 0 if !$task->{backuptime
};
265 $task->{size
} = 0 if !$task->{size
};
266 $task->{tarfile
} = 'unknown' if !$task->{tarfile
};
267 $task->{hostname
} = "VM $task->{vmid}" if !$task->{hostname
};
269 if ($task->{state} eq 'todo') {
270 $task->{msg
} = 'aborted';
274 my $notify = $opts->{mailnotification
} || 'always';
275 return if (!$ecount && !$err && ($notify eq 'failure'));
277 my $stat = ($ecount || $err) ?
'backup failed' : 'backup successful';
278 $stat .= ": $err" if $err;
280 my $hostname = `hostname -f` || PVE
::INotify
::nodename
();
283 my $boundary = "----_=_NextPart_001_".int(time).$$;
286 foreach my $r (@$mailto) {
289 my $dcconf = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
290 my $mailfrom = $dcconf->{email_from
} || "root";
292 open (MAIL
,"|sendmail -B 8BITMIME -f $mailfrom $rcvrarg") ||
293 die "unable to open 'sendmail' - $!";
295 my $rcvrtxt = join (', ', @$mailto);
297 print MAIL
"Content-Type: multipart/alternative;\n";
298 print MAIL
"\tboundary=\"$boundary\"\n";
299 print MAIL "MIME-Version
: 1.0\n";
301 print MAIL "FROM
: vzdump backup tool
<$mailfrom>\n";
302 print MAIL "TO
: $rcvrtxt\n";
303 print MAIL "SUBJECT
: vzdump backup status
($hostname) : $stat\n";
305 print MAIL "This
is a multi-part message
in MIME format
.\n\n";
306 print MAIL "--$boundary\n";
308 print MAIL "Content-Type
: text
/plain
;\n";
309 print MAIL "\tcharset
=\"UTF8
\"\n";
310 print MAIL "Content-Transfer-Encoding
: 8bit
\n";
315 my $fill = ' '; # Avoid The Remove Extra Line Breaks Issue (MS Outlook)
317 print MAIL sprintf ("${fill
}%-10s
%-6s
%10s %10s %s\n", qw(VMID STATUS TIME SIZE FILENAME));
318 foreach my $task (@$tasklist) {
319 my $vmid = $task->{vmid
};
320 if ($task->{state} eq 'ok') {
322 print MAIL
sprintf ("${fill}%-10s %-6s %10s %10s %s\n", $vmid,
324 format_time
($task->{backuptime
}),
325 format_size
($task->{size
}),
328 print MAIL
sprintf ("${fill}%-10s %-6s %10s %8.2fMB %s\n", $vmid,
330 format_time
($task->{backuptime
}),
334 print MAIL
"${fill}\n";
335 print MAIL
"${fill}Detailed backup logs:\n";
336 print MAIL
"${fill}\n";
337 print MAIL
"$fill$cmdline\n";
338 print MAIL
"${fill}\n";
340 foreach my $task (@$tasklist) {
341 my $vmid = $task->{vmid
};
342 my $log = $task->{tmplog
};
344 print MAIL
"${fill}$vmid: no log available\n\n";
348 while (my $line = <TMP
>) { print MAIL encode8bit
("${fill}$vmid: $line"); }
350 print MAIL
"${fill}\n";
354 print MAIL
"\n--$boundary\n";
356 print MAIL
"Content-Type: text/html;\n";
357 print MAIL
"\tcharset=\"UTF8\"\n";
358 print MAIL
"Content-Transfer-Encoding: 8bit\n";
363 print MAIL
"<html><body>\n";
365 print MAIL
"<table border=1 cellpadding=3>\n";
367 print MAIL
"<tr><td>VMID<td>NAME<td>STATUS<td>TIME<td>SIZE<td>FILENAME</tr>\n";
371 foreach my $task (@$tasklist) {
372 my $vmid = $task->{vmid
};
373 my $name = $task->{hostname
};
375 if ($task->{state} eq 'ok') {
377 $ssize += $task->{size
};
379 print MAIL
sprintf ("<tr><td>%s<td>%s<td>OK<td>%s<td align=right>%s<td>%s</tr>\n",
381 format_time
($task->{backuptime
}),
382 format_size
($task->{size
}),
383 escape_html
($task->{tarfile
}));
385 print MAIL
sprintf ("<tr><td>%s<td>%s<td><font color=red>FAILED<td>%s<td colspan=2>%s</tr>\n",
387 $vmid, $name, format_time
($task->{backuptime
}),
388 escape_html
($task->{msg
}));
392 print MAIL
sprintf ("<tr><td align=left colspan=3>TOTAL<td>%s<td>%s<td></tr>",
393 format_time
($totaltime), format_size
($ssize));
395 print MAIL
"</table><br><br>\n";
396 print MAIL
"Detailed backup logs:<br>\n";
398 print MAIL
"<pre>\n";
399 print MAIL escape_html
($cmdline) . "\n";
402 foreach my $task (@$tasklist) {
403 my $vmid = $task->{vmid
};
404 my $log = $task->{tmplog
};
406 print MAIL
"$vmid: no log available\n\n";
410 while (my $line = <TMP
>) {
411 if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
412 print MAIL encode8bit
("$vmid: <font color=red>".
413 escape_html
($line) . "</font>");
415 print MAIL encode8bit
("$vmid: " . escape_html
($line));
421 print MAIL
"</pre>\n";
423 print MAIL
"</body></html>\n";
426 print MAIL
"\n--$boundary--\n";
432 my ($class, $cmdline, $opts, $skiplist) = @_;
438 check_bin
('sendmail');
442 check_bin
('umount');
443 check_bin
('cstream');
444 check_bin
('ionice');
446 if ($opts->{mode
} && $opts->{mode
} eq 'snapshot') {
447 check_bin
('lvcreate');
449 check_bin
('lvremove');
452 my $defaults = read_vzdump_defaults
();
454 my $maxfiles = $opts->{maxfiles
}; # save here, because we overwrite with default
456 $opts->{remove
} = 1 if !defined($opts->{remove
});
458 foreach my $k (keys %$defaults) {
459 if ($k eq 'dumpdir' || $k eq 'storage') {
460 $opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir
}) &&
461 !defined ($opts->{storage
});
463 $opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
467 $opts->{dumpdir
} =~ s
|/+$|| if ($opts->{dumpdir
});
468 $opts->{tmpdir
} =~ s
|/+$|| if ($opts->{tmpdir
});
470 $skiplist = [] if !$skiplist;
471 my $self = bless { cmdline
=> $cmdline, opts
=> $opts, skiplist
=> $skiplist };
474 push @{$self->{findexcl
}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
476 $self->find_add_exclude ('-type', 's'); # skip sockets
478 if ($defaults->{'exclude-path'}) {
479 foreach my $path (@{$defaults->{'exclude-path'}}) {
480 $self->find_add_exclude ('-regex', $path);
484 if ($opts->{'exclude-path'}) {
485 foreach my $path (@{$opts->{'exclude-path'}}) {
486 $self->find_add_exclude ('-regex', $path);
490 if ($opts->{stdexcludes
}) {
491 $self->find_add_exclude ('-files', '/var/log/.+');
492 $self->find_add_exclude ('-regex', '/tmp/.+');
493 $self->find_add_exclude ('-regex', '/var/tmp/.+');
494 $self->find_add_exclude ('-regex', '/var/run/.+pid');
497 foreach my $p (@plugins) {
499 my $pd = $p->new ($self);
501 push @{$self->{plugins
}}, $pd;
504 if (!$opts->{dumpdir
} && !$opts->{storage
}) {
505 $opts->{storage
} = 'local';
508 if ($opts->{storage
}) {
509 my $info = storage_info
($opts->{storage
});
510 $opts->{dumpdir
} = $info->{dumpdir
};
511 $maxfiles = $info->{maxfiles
} if !defined($maxfiles) && defined($info->{maxfiles
});
512 } elsif ($opts->{dumpdir
}) {
513 die "dumpdir '$opts->{dumpdir}' does not exist\n"
514 if ! -d
$opts->{dumpdir
};
516 die "internal error";
519 if ($opts->{tmpdir
} && ! -d
$opts->{tmpdir
}) {
520 die "tmpdir '$opts->{tmpdir}' does not exist\n";
523 $opts->{maxfiles
} = $maxfiles if defined($maxfiles);
529 sub get_lvm_mapping
{
533 my $cmd = ['lvs', '--units', 'm', '--separator', ':', '--noheadings',
534 '-o', 'vg_name,lv_name,lv_size' ];
538 if ($line =~ m
|^\s
*(\S
+):(\S
+):(\d
+(\
.\d
+))[Mm
]$|) {
541 $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
546 $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
550 eval { PVE
::Tools
::run_command
($cmd, errfunc
=> sub {}, outfunc
=> $parser); };
559 # Note: df 'available' can be negative, and percentage set to '-'
561 my $cmd = [ 'df', '-P', '-T', '-B', '1', $dir];
567 if (my ($fsid, $fstype, undef, $mp) = $line =~
568 m!(\S+.*)\s+(\S+)\s+\d+\s+\-?\d+\s+\d+\s+(\d+%|-)\s+(/.*)$!) {
577 eval { PVE
::Tools
::run_command
($cmd, errfunc
=> sub {}, outfunc
=> $parser); };
584 my ($dir, $mapping) = @_;
586 my $info = get_mount_info
($dir);
588 return undef if !$info;
590 my $dev = $info->{device
};
594 ($vg, $lv) = @{$mapping->{$dev}} if defined $mapping->{$dev};
596 return wantarray ?
($dev, $info->{mountpoint
}, $vg, $lv, $info->{fstype
}) : $dev;
600 my ($self, $upid) = @_;
604 my $maxwait = $self->{opts
}->{lockwait
} || $self->{lockwait
};
606 die "missimg UPID" if !$upid; # should not happen
608 if (!open (SERVER_FLCK
, ">>$lockfile")) {
609 debugmsg
('err', "can't open lock on file '$lockfile' - $!", undef, 1);
610 die "can't open lock on file '$lockfile' - $!";
613 if (!flock (SERVER_FLCK
, LOCK_EX
|LOCK_NB
)) {
616 debugmsg
('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
617 die "can't aquire lock '$lockfile' (wait = 0)";
620 debugmsg
('info', "trying to get global lock - waiting...", undef, 1);
623 alarm ($maxwait * 60);
625 local $SIG{ALRM
} = sub { alarm (0); die "got timeout\n"; };
627 if (!flock (SERVER_FLCK
, LOCK_EX
)) {
640 debugmsg
('err', "can't aquire lock '$lockfile' - $err", undef, 1);
641 die "can't aquire lock '$lockfile' - $err";
644 debugmsg
('info', "got global lock", undef, 1);
647 PVE
::Tools
::file_set_contents
($pidfile, $upid);
650 sub run_hook_script
{
651 my ($self, $phase, $task, $logfd) = @_;
653 my $opts = $self->{opts
};
655 my $script = $opts->{script
};
659 my $cmd = "$script $phase";
661 $cmd .= " $task->{mode} $task->{vmid}" if ($task);
665 # set immutable opts directly (so they are available in all phases)
666 $ENV{STOREID
} = $opts->{storage
} if $opts->{storage
};
667 $ENV{DUMPDIR
} = $opts->{dumpdir
} if $opts->{dumpdir
};
669 foreach my $ek (qw(vmtype hostname tarfile logfile)) {
670 $ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
673 run_command
($logfd, $cmd);
676 sub compressor_info
{
677 my ($opt_compress) = @_;
679 if (!$opt_compress || $opt_compress eq '0') {
681 } elsif ($opt_compress eq '1' || $opt_compress eq 'lzo') {
682 return ('lzop', 'lzo');
683 } elsif ($opt_compress eq 'gzip') {
684 return ('gzip', 'gz');
686 die "internal error - unknown compression option '$opt_compress'";
690 sub get_backup_file_list
{
691 my ($dir, $bkname, $exclude_fn) = @_;
694 foreach my $fn (<$dir/${bkname
}-*>) {
695 next if $exclude_fn && $fn eq $exclude_fn;
696 if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(gz|lzo))?)))$!) {
697 $fn = "$dir/$1"; # untaint
698 my $t = timelocal
($7, $6, $5, $4, $3 - 1, $2 - 1900);
699 push @$bklist, [$fn, $t];
706 sub exec_backup_task
{
707 my ($self, $task) = @_;
709 my $opts = $self->{opts
};
711 my $vmid = $task->{vmid
};
712 my $plugin = $task->{plugin
};
714 my $vmstarttime = time ();
723 die "unable to find VM '$vmid'\n" if !$plugin;
725 my $vmtype = $plugin->type();
727 my $tmplog = "$logdir/$vmtype-$vmid.log";
729 my $lt = localtime();
731 my $bkname = "vzdump-$vmtype-$vmid";
732 my $basename = sprintf "${bkname}-%04d_%02d_%02d-%02d_%02d_%02d",
733 $lt->year + 1900, $lt->mon + 1, $lt->mday,
734 $lt->hour, $lt->min, $lt->sec;
736 my $maxfiles = $opts->{maxfiles
};
738 if ($maxfiles && !$opts->{remove
}) {
739 my $bklist = get_backup_file_list
($opts->{dumpdir
}, $bkname);
740 die "only $maxfiles backup(s) allowed - please consider to remove old backup files.\n"
741 if scalar(@$bklist) >= $maxfiles;
744 my $logfile = $task->{logfile
} = "$opts->{dumpdir}/$basename.log";
746 my $ext = $vmtype eq 'qemu' ?
'.vma' : '.tar';
747 my ($comp, $comp_ext) = compressor_info
($opts->{compress
});
748 if ($comp && $comp_ext) {
749 $ext .= ".${comp_ext}";
752 if ($opts->{stdout
}) {
753 $task->{tarfile
} = '-';
755 my $tarfile = $task->{tarfile
} = "$opts->{dumpdir}/$basename$ext";
756 $task->{tmptar
} = $task->{tarfile
};
757 $task->{tmptar
} =~ s/\.[^\.]+$/\.dat/;
758 unlink $task->{tmptar
};
761 $task->{vmtype
} = $vmtype;
763 if ($opts->{tmpdir
}) {
764 $task->{tmpdir
} = "$opts->{tmpdir}/vzdumptmp$$";
766 # dumpdir is posix? then use it as temporary dir
767 my $info = get_mount_info
($opts->{dumpdir
});
768 if ($vmtype eq 'qemu' ||
769 grep ($_ eq $info->{fstype
}, @posix_filesystems)) {
770 $task->{tmpdir
} = "$opts->{dumpdir}/$basename.tmp";
772 $task->{tmpdir
} = "/var/tmp/vzdumptmp$$";
773 debugmsg
('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
774 "using $task->{tmpdir} for temporary files", $logfd);
778 rmtree
$task->{tmpdir
};
779 mkdir $task->{tmpdir
};
780 -d
$task->{tmpdir
} ||
781 die "unable to create temporary directory '$task->{tmpdir}'";
783 $logfd = IO
::File-
>new (">$tmplog") ||
784 die "unable to create log file '$tmplog'";
786 $task->{dumpdir
} = $opts->{dumpdir
};
787 $task->{storeid
} = $opts->{storage
};
788 $task->{tmplog
} = $tmplog;
792 debugmsg
('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
794 $plugin->set_logfd ($logfd);
796 # test is VM is running
797 my ($running, $status_text) = $plugin->vm_status ($vmid);
799 debugmsg
('info', "status = ${status_text}", $logfd);
801 # lock VM (prevent config changes)
802 $plugin->lock_vm ($vmid);
804 $cleanup->{unlock
} = 1;
808 my $mode = $running ?
$opts->{mode
} : 'stop';
810 if ($mode eq 'snapshot') {
811 my %saved_task = %$task;
812 eval { $plugin->prepare ($task, $vmid, $mode); };
814 die $err if $err !~ m/^mode failure/;
815 debugmsg
('info', $err, $logfd);
816 debugmsg
('info', "trying 'suspend' mode instead", $logfd);
817 $mode = 'suspend'; # so prepare is called again below
818 %$task = %saved_task;
822 $task->{mode
} = $mode;
824 debugmsg
('info', "backup mode: $mode", $logfd);
826 debugmsg
('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
829 debugmsg
('info', "ionice priority: $opts->{ionice}", $logfd);
831 if ($mode eq 'stop') {
833 $plugin->prepare ($task, $vmid, $mode);
835 $self->run_hook_script ('backup-start', $task, $logfd);
838 debugmsg
('info', "stopping vm", $logfd);
839 $vmstoptime = time ();
840 $self->run_hook_script ('pre-stop', $task, $logfd);
841 $plugin->stop_vm ($task, $vmid);
842 $cleanup->{restart
} = 1;
846 } elsif ($mode eq 'suspend') {
848 $plugin->prepare ($task, $vmid, $mode);
850 $self->run_hook_script ('backup-start', $task, $logfd);
852 debugmsg
('info', "suspend vm", $logfd);
853 $vmstoptime = time ();
854 $self->run_hook_script ('pre-stop', $task, $logfd);
855 $plugin->suspend_vm ($task, $vmid);
856 $cleanup->{resume
} = 1;
858 } elsif ($mode eq 'snapshot') {
860 $self->run_hook_script ('backup-start', $task, $logfd);
862 my $snapshot_count = $task->{snapshot_count
} || 0;
864 $self->run_hook_script ('pre-stop', $task, $logfd);
866 if ($snapshot_count > 1) {
867 debugmsg
('info', "suspend vm to make snapshot", $logfd);
868 $vmstoptime = time ();
869 $plugin->suspend_vm ($task, $vmid);
870 $cleanup->{resume
} = 1;
873 $plugin->snapshot ($task, $vmid);
875 $self->run_hook_script ('pre-restart', $task, $logfd);
877 if ($snapshot_count > 1) {
878 debugmsg
('info', "resume vm", $logfd);
879 $cleanup->{resume
} = 0;
880 $plugin->resume_vm ($task, $vmid);
881 my $delay = time () - $vmstoptime;
882 debugmsg
('info', "vm is online again after $delay seconds", $logfd);
886 die "internal error - unknown mode '$mode'\n";
889 # assemble archive image
890 $plugin->assemble ($task, $vmid);
894 if ($opts->{stdout
}) {
895 debugmsg
('info', "sending archive to stdout", $logfd);
896 $plugin->archive($task, $vmid, $task->{tmptar
}, $comp);
897 $self->run_hook_script ('backup-end', $task, $logfd);
901 debugmsg
('info', "creating archive '$task->{tarfile}'", $logfd);
902 $plugin->archive($task, $vmid, $task->{tmptar
}, $comp);
904 rename ($task->{tmptar
}, $task->{tarfile
}) ||
905 die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
908 $task->{size
} = (-s
$task->{tarfile
}) || 0;
909 my $cs = format_size
($task->{size
});
910 debugmsg
('info', "archive file size: $cs", $logfd);
914 if ($maxfiles && $opts->{remove
}) {
915 my $bklist = get_backup_file_list
($opts->{dumpdir
}, $bkname, $task->{tarfile
});
916 $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
918 while (scalar (@$bklist) >= $maxfiles) {
919 my $d = pop @$bklist;
920 debugmsg
('info', "delete old backup '$d->[0]'", $logfd);
923 $logfn =~ s/\.(tgz|((tar|vma)(\.(gz|lzo))?))$/\.log/;
928 $self->run_hook_script ('backup-end', $task, $logfd);
935 if ($cleanup->{unlock
}) {
936 eval { $plugin->unlock_vm ($vmid); };
940 eval { $plugin->cleanup ($task, $vmid) };
943 eval { $plugin->set_logfd (undef); };
946 if ($cleanup->{resume
} || $cleanup->{restart
}) {
948 $self->run_hook_script ('pre-restart', $task, $logfd);
949 if ($cleanup->{resume
}) {
950 debugmsg
('info', "resume vm", $logfd);
951 $plugin->resume_vm ($task, $vmid);
953 my $running = $plugin->vm_status($vmid);
955 debugmsg
('info', "restarting vm", $logfd);
956 $plugin->start_vm ($task, $vmid);
964 my $delay = time () - $vmstoptime;
965 debugmsg
('info', "vm is online again after $delay seconds", $logfd);
970 eval { unlink $task->{tmptar
} if $task->{tmptar
} && -f
$task->{tmptar
}; };
973 eval { rmtree
$task->{tmpdir
} if $task->{tmpdir
} && -d
$task->{tmpdir
}; };
976 my $delay = $task->{backuptime
} = time () - $vmstarttime;
979 $task->{state} = 'err';
981 debugmsg
('err', "Backup of VM $vmid failed - $err", $logfd, 1);
983 eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
986 $task->{state} = 'ok';
987 my $tstr = format_time
($delay);
988 debugmsg
('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
991 close ($logfd) if $logfd;
993 if ($task->{tmplog
} && $task->{logfile
}) {
994 system ("cp '$task->{tmplog}' '$task->{logfile}'");
997 eval { $self->run_hook_script ('log-end', $task); };
999 die $err if $err && $err =~ m/^interrupted by signal$/;
1003 my ($self, $rpcenv, $authuser) = @_;
1005 my $opts = $self->{opts
};
1007 debugmsg
('info', "starting new backup job: $self->{cmdline}", undef, 1);
1008 debugmsg
('info', "skip external VMs: " . join(', ', @{$self->{skiplist
}}))
1009 if scalar(@{$self->{skiplist
}});
1014 foreach my $plugin (@{$self->{plugins
}}) {
1015 my $vmlist = $plugin->vmlist();
1016 foreach my $vmid (sort @$vmlist) {
1017 next if grep { $_ eq $vmid } @{$opts->{exclude
}};
1018 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
1019 push @$tasklist, { vmid
=> $vmid, state => 'todo', plugin
=> $plugin };
1023 foreach my $vmid (sort @{$opts->{vmids
}}) {
1025 foreach my $pg (@{$self->{plugins
}}) {
1026 my $vmlist = $pg->vmlist();
1027 if (grep { $_ eq $vmid } @$vmlist) {
1032 $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
1033 push @$tasklist, { vmid
=> $vmid, state => 'todo', plugin
=> $plugin };
1037 my $starttime = time();
1041 $self->run_hook_script ('job-start');
1043 foreach my $task (@$tasklist) {
1044 $self->exec_backup_task ($task);
1045 $errcount += 1 if $task->{state} ne 'ok';
1048 $self->run_hook_script ('job-end');
1052 $self->run_hook_script ('job-abort') if $err;
1055 debugmsg
('err', "Backup job failed - $err", undef, 1);
1058 debugmsg
('info', "Backup job finished with errors", undef, 1);
1060 debugmsg
('info', "Backup job finished successfully", undef, 1);
1064 my $totaltime = time() - $starttime;
1066 eval { $self->sendmail ($tasklist, $totaltime); };
1067 debugmsg
('err', $@) if $@;
1071 die "job errors\n" if $errcount;
1078 type
=> 'string', format
=> 'pve-vmid-list',
1079 description
=> "The ID of the VM you want to backup.",
1082 node
=> get_standard_option
('pve-node', {
1083 description
=> "Only run if executed on this node.",
1088 description
=> "Backup all known VMs on this host.",
1094 description
=> "Exclude temorary files and logs.",
1100 description
=> "Compress dump file.",
1102 enum
=> ['0', '1', 'gzip', 'lzo'],
1107 description
=> "Be quiet.",
1113 description
=> "Backup mode.",
1116 enum
=> [ 'snapshot', 'suspend', 'stop' ],
1119 type
=> 'string', format
=> 'pve-vmid-list',
1120 description
=> "exclude specified VMs (assumes --all)",
1124 type
=> 'string', format
=> 'string-alist',
1125 description
=> "exclude certain files/directories (regex).",
1129 type
=> 'string', format
=> 'string-list',
1133 mailnotification
=> {
1135 description
=> "Specify when to send an email",
1137 enum
=> [ 'always', 'failure' ],
1138 default => 'always',
1142 description
=> "Store temporary files to specified directory.",
1147 description
=> "Store resulting files to specified directory.",
1152 description
=> "Use specified hook script.",
1155 storage
=> get_standard_option
('pve-storage-id', {
1156 description
=> "Store resulting file to this storage.",
1161 description
=> "Stop runnig backup jobs on this host.",
1167 description
=> "LVM snapshot size in MB.",
1173 description
=> "Limit I/O bandwidth (KBytes per second).",
1179 description
=> "Set CFQ ionice priority.",
1186 description
=> "Maximal time to wait for the global lock (minutes).",
1192 description
=> "Maximal time to wait until a VM is stopped (minutes).",
1198 description
=> "Maximal number of backup files per VM.",
1204 description
=> "Remove old backup files if there are more than 'maxfiles' backup files.",
1212 return defined($confdesc->{$key});
1215 # add JSON properties for create and set function
1216 sub json_config_properties
{
1219 foreach my $opt (keys %$confdesc) {
1220 $prop->{$opt} = $confdesc->{$opt};
1226 sub verify_vzdump_parameters
{
1227 my ($param, $check_missing) = @_;
1229 raise_param_exc
({ all
=> "option conflicts with option 'vmid'"})
1230 if $param->{all
} && $param->{vmid
};
1232 raise_param_exc
({ exclude
=> "option conflicts with option 'vmid'"})
1233 if $param->{exclude
} && $param->{vmid
};
1235 $param->{all
} = 1 if defined($param->{exclude
});
1237 return if !$check_missing;
1239 raise_param_exc
({ vmid
=> "property is missing"})
1240 if !($param->{all
} || $param->{stop
}) && !$param->{vmid
};
1244 sub stop_running_backups
{
1247 my $upid = PVE
::Tools
::file_read_firstline
($pidfile);
1250 my $task = PVE
::Tools
::upid_decode
($upid);
1252 if (PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
}) &&
1253 PVE
::ProcFSTools
::read_proc_starttime
($task->{pid
}) == $task->{pstart
}) {
1254 kill(15, $task->{pid
});
1255 # wait max 15 seconds to shut down (else, do nothing for now)
1257 for ($i = 15; $i > 0; $i--) {
1258 last if !PVE
::ProcFSTools
::check_process_running
(($task->{pid
}, $task->{pstart
}));
1261 die "stoping backup process $task->{pid} failed\n" if $i == 0;
1270 if ($param->{vmid
}) {
1271 $cmd .= " " . join(' ', PVE
::Tools
::split_list
($param->{vmid
}));
1274 foreach my $p (keys %$param) {
1275 next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow' || $p eq 'stdout';
1276 my $v = $param->{$p};
1277 my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
1278 if ($p eq 'exclude-path') {
1279 foreach my $path (split(/\0/, $v || '')) {
1280 $cmd .= " --$p " . PVE
::Tools
::shellquote
($path);
1283 $cmd .= " --$p " . PVE
::Tools
::shellquote
($v) if defined($v) && $v ne '';