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);
16 use PVE
::VZDump
::OpenVZ
;
19 use PVE
::JSONSchema
qw(get_standard_option);
21 my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
23 my $lockfile = '/var/run/vzdump.lock';
25 my $logdir = '/var/log/vzdump';
27 my @plugins = qw
(PVE
::VZDump
::OpenVZ
);
29 # Load available plugins
30 my $pveplug = "/usr/share/perl5/PVE/VZDump/QemuServer.pm";
32 eval { require $pveplug; };
34 PVE
::VZDump
::QemuServer-
>import ();
35 push @plugins, "PVE::VZDump::QemuServer";
50 my ($mtype, $msg, $logfd, $syslog) = @_;
56 my $pre = $debugstattxt->{$mtype} || $debugstattxt->{'err'};
58 my $timestr = strftime
("%b %d %H:%M:%S", CORE
::localtime);
60 syslog
($mtype eq 'info' ?
'info' : 'err', "$pre $msg") if $syslog;
62 foreach my $line (split (/\n/, $msg)) {
63 print STDERR
"$pre $line\n";
64 print $logfd "$timestr $pre $line\n" if $logfd;
69 my ($logfd, $cmdstr, %param) = @_;
73 debugmsg
('info', $line, $logfd);
76 PVE
::Tools
::run_command
($cmdstr, %param, logfunc
=> $logfunc);
82 my $cfg = cfs_read_file
('storage.cfg');
83 my $scfg = PVE
::Storage
::storage_config
($cfg, $storage);
84 my $type = $scfg->{type
};
86 die "can't use storage type '$type' for backup\n"
87 if (!($type eq 'dir' || $type eq 'nfs' || $type eq 'glusterfs'));
88 die "can't use storage for backups - wrong content type\n"
89 if (!$scfg->{content
}->{backup
});
91 PVE
::Storage
::activate_storage
($cfg, $storage);
94 dumpdir
=> PVE
::Storage
::get_backup_dir
($cfg, $storage),
95 maxfiles
=> $scfg->{maxfiles
},
102 my $kb = $size / 1024;
105 return int ($kb) . "KB";
108 my $mb = $size / (1024*1024);
111 return int ($mb) . "MB";
114 return sprintf ("%.2fGB", $gb);
121 my $hours = int ($seconds/3600);
122 $seconds = $seconds - $hours*3600;
123 my $min = int ($seconds/60);
124 $seconds = $seconds - $min*60;
126 return sprintf ("%02d:%02d:%02d", $hours, $min, $seconds);
132 $str =~ s/^(.{990})/$1\n/mg; # reduce line length
150 foreach my $p (split (/:/, $ENV{PATH
})) {
157 die "unable to find command '$bin'\n";
164 foreach my $vmid (@vmids) {
165 die "ERROR: strange VM ID '${vmid}'\n" if $vmid !~ m/^\d+$/;
166 $vmid = int ($vmid); # remove leading zeros
175 sub read_vzdump_defaults
{
177 my $fn = "/etc/vzdump.conf";
183 lockwait
=> 3*60, # 3 hours
184 stopwait
=> 10, # 10 minutes
189 my $fh = IO
::File-
>new ("<$fn");
193 while (defined ($line = <$fh>)) {
194 next if $line =~ m/^\s*$/;
195 next if $line =~ m/^\#/;
197 if ($line =~ m/tmpdir:\s*(.*\S)\s*$/) {
199 } elsif ($line =~ m/dumpdir:\s*(.*\S)\s*$/) {
200 $res->{dumpdir
} = $1;
201 } elsif ($line =~ m/storage:\s*(\S+)\s*$/) {
202 $res->{storage
} = $1;
203 } elsif ($line =~ m/script:\s*(.*\S)\s*$/) {
205 } elsif ($line =~ m/bwlimit:\s*(\d+)\s*$/) {
206 $res->{bwlimit
} = int($1);
207 } elsif ($line =~ m/ionice:\s*([0-8])\s*$/) {
208 $res->{ionice
} = int($1);
209 } elsif ($line =~ m/lockwait:\s*(\d+)\s*$/) {
210 $res->{lockwait
} = int($1);
211 } elsif ($line =~ m/stopwait:\s*(\d+)\s*$/) {
212 $res->{stopwait
} = int($1);
213 } elsif ($line =~ m/size:\s*(\d+)\s*$/) {
214 $res->{size
} = int($1);
215 } elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
216 $res->{maxfiles
} = int($1);
217 } elsif ($line =~ m/exclude-path:\s*(.*)\s*$/) {
218 $res->{'exclude-path'} = PVE
::Tools
::split_args
($1);
219 } elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
222 debugmsg
('warn', "unable to parse configuration file '$fn' - error at line " . $., undef, 1);
232 sub find_add_exclude
{
233 my ($self, $excltype, $value) = @_;
235 if (($excltype eq '-regex') || ($excltype eq '-files')) {
239 if ($excltype eq '-files') {
240 push @{$self->{findexcl
}}, "'('", '-not', '-type', 'd', '-regex' , "'$value'", "')'", '-o';
242 push @{$self->{findexcl
}}, "'('", $excltype , "'$value'", '-prune', "')'", '-o';
247 my ($self, $tasklist, $totaltime) = @_;
249 my $opts = $self->{opts
};
251 my $mailto = $opts->{mailto
};
253 return if !($mailto && scalar(@$mailto));
255 my $cmdline = $self->{cmdline
};
258 foreach my $task (@$tasklist) {
259 $ecount++ if $task->{state} ne 'ok';
260 chomp $task->{msg
} if $task->{msg
};
261 $task->{backuptime
} = 0 if !$task->{backuptime
};
262 $task->{size
} = 0 if !$task->{size
};
263 $task->{tarfile
} = 'unknown' if !$task->{tarfile
};
264 $task->{hostname
} = "VM $task->{vmid}" if !$task->{hostname
};
266 if ($task->{state} eq 'todo') {
267 $task->{msg
} = 'aborted';
271 my $stat = $ecount ?
'backup failed' : 'backup successful';
273 my $hostname = `hostname -f` || PVE
::INotify
::nodename
();
276 my $boundary = "----_=_NextPart_001_".int(time).$$;
279 foreach my $r (@$mailto) {
283 open (MAIL
,"|sendmail -B 8BITMIME $rcvrarg") ||
284 die "unable to open 'sendmail' - $!";
286 my $rcvrtxt = join (', ', @$mailto);
288 print MAIL
"Content-Type: multipart/alternative;\n";
289 print MAIL
"\tboundary=\"$boundary\"\n";
290 print MAIL "MIME-Version
: 1.0\n";
292 print MAIL "FROM
: vzdump backup tool
<root
>\n";
293 print MAIL "TO
: $rcvrtxt\n";
294 print MAIL "SUBJECT
: vzdump backup status
($hostname) : $stat\n";
296 print MAIL "This
is a multi-part message
in MIME format
.\n\n";
297 print MAIL "--$boundary\n";
299 print MAIL "Content-Type
: text
/plain
;\n";
300 print MAIL "\tcharset
=\"UTF8
\"\n";
301 print MAIL "Content-Transfer-Encoding
: 8bit
\n";
306 my $fill = ' '; # Avoid The Remove Extra Line Breaks Issue (MS Outlook)
308 print MAIL sprintf ("${fill
}%-10s
%-6s
%10s %10s %s\n", qw(VMID STATUS TIME SIZE FILENAME));
309 foreach my $task (@$tasklist) {
310 my $vmid = $task->{vmid
};
311 if ($task->{state} eq 'ok') {
313 print MAIL
sprintf ("${fill}%-10s %-6s %10s %10s %s\n", $vmid,
315 format_time
($task->{backuptime
}),
316 format_size
($task->{size
}),
319 print MAIL
sprintf ("${fill}%-10s %-6s %10s %8.2fMB %s\n", $vmid,
321 format_time
($task->{backuptime
}),
325 print MAIL
"${fill}\n";
326 print MAIL
"${fill}Detailed backup logs:\n";
327 print MAIL
"${fill}\n";
328 print MAIL
"$fill$cmdline\n";
329 print MAIL
"${fill}\n";
331 foreach my $task (@$tasklist) {
332 my $vmid = $task->{vmid
};
333 my $log = $task->{tmplog
};
335 print MAIL
"${fill}$vmid: no log available\n\n";
339 while (my $line = <TMP
>) { print MAIL encode8bit
("${fill}$vmid: $line"); }
341 print MAIL
"${fill}\n";
345 print MAIL
"\n--$boundary\n";
347 print MAIL
"Content-Type: text/html;\n";
348 print MAIL
"\tcharset=\"UTF8\"\n";
349 print MAIL
"Content-Transfer-Encoding: 8bit\n";
354 print MAIL
"<html><body>\n";
356 print MAIL
"<table border=1 cellpadding=3>\n";
358 print MAIL
"<tr><td>VMID<td>NAME<td>STATUS<td>TIME<td>SIZE<td>FILENAME</tr>\n";
362 foreach my $task (@$tasklist) {
363 my $vmid = $task->{vmid
};
364 my $name = $task->{hostname
};
366 if ($task->{state} eq 'ok') {
368 $ssize += $task->{size
};
370 print MAIL
sprintf ("<tr><td>%s<td>%s<td>OK<td>%s<td align=right>%s<td>%s</tr>\n",
372 format_time
($task->{backuptime
}),
373 format_size
($task->{size
}),
374 escape_html
($task->{tarfile
}));
376 print MAIL
sprintf ("<tr><td>%s<td>%s<td><font color=red>FAILED<td>%s<td colspan=2>%s</tr>\n",
378 $vmid, $name, format_time
($task->{backuptime
}),
379 escape_html
($task->{msg
}));
383 print MAIL
sprintf ("<tr><td align=left colspan=3>TOTAL<td>%s<td>%s<td></tr>",
384 format_time
($totaltime), format_size
($ssize));
386 print MAIL
"</table><br><br>\n";
387 print MAIL
"Detailed backup logs:<br>\n";
389 print MAIL
"<pre>\n";
390 print MAIL escape_html
($cmdline) . "\n";
393 foreach my $task (@$tasklist) {
394 my $vmid = $task->{vmid
};
395 my $log = $task->{tmplog
};
397 print MAIL
"$vmid: no log available\n\n";
401 while (my $line = <TMP
>) {
402 if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
403 print MAIL encode8bit
("$vmid: <font color=red>".
404 escape_html
($line) . "</font>");
406 print MAIL encode8bit
("$vmid: " . escape_html
($line));
412 print MAIL
"</pre>\n";
414 print MAIL
"</body></html>\n";
417 print MAIL
"\n--$boundary--\n";
423 my ($class, $cmdline, $opts, $skiplist) = @_;
429 check_bin
('sendmail');
433 check_bin
('umount');
434 check_bin
('cstream');
435 check_bin
('ionice');
437 if ($opts->{mode
} && $opts->{mode
} eq 'snapshot') {
438 check_bin
('lvcreate');
440 check_bin
('lvremove');
443 my $defaults = read_vzdump_defaults
();
445 my $maxfiles = $opts->{maxfiles
}; # save here, because we overwrite with default
447 $opts->{remove
} = 1 if !defined($opts->{remove
});
449 foreach my $k (keys %$defaults) {
450 if ($k eq 'dumpdir' || $k eq 'storage') {
451 $opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir
}) &&
452 !defined ($opts->{storage
});
454 $opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
458 $opts->{dumpdir
} =~ s
|/+$|| if ($opts->{dumpdir
});
459 $opts->{tmpdir
} =~ s
|/+$|| if ($opts->{tmpdir
});
461 $skiplist = [] if !$skiplist;
462 my $self = bless { cmdline
=> $cmdline, opts
=> $opts, skiplist
=> $skiplist };
465 push @{$self->{findexcl
}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
467 $self->find_add_exclude ('-type', 's'); # skip sockets
469 if ($defaults->{'exclude-path'}) {
470 foreach my $path (@{$defaults->{'exclude-path'}}) {
471 $self->find_add_exclude ('-regex', $path);
475 if ($opts->{'exclude-path'}) {
476 foreach my $path (@{$opts->{'exclude-path'}}) {
477 $self->find_add_exclude ('-regex', $path);
481 if ($opts->{stdexcludes
}) {
482 $self->find_add_exclude ('-files', '/var/log/.+');
483 $self->find_add_exclude ('-regex', '/tmp/.+');
484 $self->find_add_exclude ('-regex', '/var/tmp/.+');
485 $self->find_add_exclude ('-regex', '/var/run/.+pid');
488 foreach my $p (@plugins) {
490 my $pd = $p->new ($self);
492 push @{$self->{plugins
}}, $pd;
495 if (!$opts->{dumpdir
} && !$opts->{storage
}) {
496 $opts->{storage
} = 'local';
499 if ($opts->{storage
}) {
500 my $info = storage_info
($opts->{storage
});
501 $opts->{dumpdir
} = $info->{dumpdir
};
502 $maxfiles = $info->{maxfiles
} if !defined($maxfiles) && defined($info->{maxfiles
});
503 } elsif ($opts->{dumpdir
}) {
504 die "dumpdir '$opts->{dumpdir}' does not exist\n"
505 if ! -d
$opts->{dumpdir
};
507 die "internal error";
510 if ($opts->{tmpdir
} && ! -d
$opts->{tmpdir
}) {
511 die "tmpdir '$opts->{tmpdir}' does not exist\n";
514 $opts->{maxfiles
} = $maxfiles if defined($maxfiles);
520 sub get_lvm_mapping
{
524 my $cmd = ['lvs', '--units', 'm', '--separator', ':', '--noheadings',
525 '-o', 'vg_name,lv_name,lv_size' ];
529 if ($line =~ m
|^\s
*(\S
+):(\S
+):(\d
+(\
.\d
+))[Mm
]$|) {
532 $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
537 $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
541 eval { PVE
::Tools
::run_command
($cmd, errfunc
=> sub {}, outfunc
=> $parser); };
550 # Note: df 'available' can be negative, and percentage set to '-'
552 my $cmd = [ 'df', '-P', '-T', '-B', '1', $dir];
558 if (my ($fsid, $fstype, undef, $mp) = $line =~
559 m!(\S+.*)\s+(\S+)\s+\d+\s+\-?\d+\s+\d+\s+(\d+%|-)\s+(/.*)$!) {
568 eval { PVE
::Tools
::run_command
($cmd, errfunc
=> sub {}, outfunc
=> $parser); };
575 my ($dir, $mapping) = @_;
577 my $info = get_mount_info
($dir);
579 return undef if !$info;
581 my $dev = $info->{device
};
585 ($vg, $lv) = @{$mapping->{$dev}} if defined $mapping->{$dev};
587 return wantarray ?
($dev, $info->{mountpoint
}, $vg, $lv, $info->{fstype
}) : $dev;
593 my $maxwait = $self->{opts
}->{lockwait
} || $self->{lockwait
};
595 if (!open (SERVER_FLCK
, ">>$lockfile")) {
596 debugmsg
('err', "can't open lock on file '$lockfile' - $!", undef, 1);
600 if (flock (SERVER_FLCK
, LOCK_EX
|LOCK_NB
)) {
605 debugmsg
('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
609 debugmsg
('info', "trying to get global lock - waiting...", undef, 1);
612 alarm ($maxwait * 60);
614 local $SIG{ALRM
} = sub { alarm (0); die "got timeout\n"; };
616 if (!flock (SERVER_FLCK
, LOCK_EX
)) {
629 debugmsg
('err', "can't aquire lock '$lockfile' - $err", undef, 1);
633 debugmsg
('info', "got global lock", undef, 1);
636 sub run_hook_script
{
637 my ($self, $phase, $task, $logfd) = @_;
639 my $opts = $self->{opts
};
641 my $script = $opts->{script
};
645 my $cmd = "$script $phase";
647 $cmd .= " $task->{mode} $task->{vmid}" if ($task);
651 # set immutable opts directly (so they are available in all phases)
652 $ENV{STOREID
} = $opts->{storage
} if $opts->{storage
};
653 $ENV{DUMPDIR
} = $opts->{dumpdir
} if $opts->{dumpdir
};
655 foreach my $ek (qw(vmtype hostname tarfile logfile)) {
656 $ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
659 run_command
($logfd, $cmd);
662 sub compressor_info
{
663 my ($opt_compress) = @_;
665 if (!$opt_compress || $opt_compress eq '0') {
667 } elsif ($opt_compress eq '1' || $opt_compress eq 'lzo') {
668 return ('lzop', 'lzo');
669 } elsif ($opt_compress eq 'gzip') {
670 return ('gzip', 'gz');
672 die "internal error - unknown compression option '$opt_compress'";
676 sub get_backup_file_list
{
677 my ($dir, $bkname, $exclude_fn) = @_;
680 foreach my $fn (<$dir/${bkname
}-*>) {
681 next if $exclude_fn && $fn eq $exclude_fn;
682 if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(gz|lzo))?)))$!) {
683 $fn = "$dir/$1"; # untaint
684 my $t = timelocal
($7, $6, $5, $4, $3 - 1, $2 - 1900);
685 push @$bklist, [$fn, $t];
692 sub exec_backup_task
{
693 my ($self, $task) = @_;
695 my $opts = $self->{opts
};
697 my $vmid = $task->{vmid
};
698 my $plugin = $task->{plugin
};
700 my $vmstarttime = time ();
709 die "unable to find VM '$vmid'\n" if !$plugin;
711 my $vmtype = $plugin->type();
713 my $tmplog = "$logdir/$vmtype-$vmid.log";
715 my $lt = localtime();
717 my $bkname = "vzdump-$vmtype-$vmid";
718 my $basename = sprintf "${bkname}-%04d_%02d_%02d-%02d_%02d_%02d",
719 $lt->year + 1900, $lt->mon + 1, $lt->mday,
720 $lt->hour, $lt->min, $lt->sec;
722 my $maxfiles = $opts->{maxfiles
};
724 if ($maxfiles && !$opts->{remove
}) {
725 my $bklist = get_backup_file_list
($opts->{dumpdir
}, $bkname);
726 die "only $maxfiles backup(s) allowed - please consider to remove old backup files.\n"
727 if scalar(@$bklist) >= $maxfiles;
730 my $logfile = $task->{logfile
} = "$opts->{dumpdir}/$basename.log";
732 my $ext = $vmtype eq 'qemu' ?
'.vma' : '.tar';
733 my ($comp, $comp_ext) = compressor_info
($opts->{compress
});
734 if ($comp && $comp_ext) {
735 $ext .= ".${comp_ext}";
738 if ($opts->{stdout
}) {
739 $task->{tarfile
} = '-';
741 my $tarfile = $task->{tarfile
} = "$opts->{dumpdir}/$basename$ext";
742 $task->{tmptar
} = $task->{tarfile
};
743 $task->{tmptar
} =~ s/\.[^\.]+$/\.dat/;
744 unlink $task->{tmptar
};
747 $task->{vmtype
} = $vmtype;
749 if ($opts->{tmpdir
}) {
750 $task->{tmpdir
} = "$opts->{tmpdir}/vzdumptmp$$";
752 # dumpdir is posix? then use it as temporary dir
753 my $info = get_mount_info
($opts->{dumpdir
});
754 if ($vmtype eq 'qemu' ||
755 grep ($_ eq $info->{fstype
}, @posix_filesystems)) {
756 $task->{tmpdir
} = "$opts->{dumpdir}/$basename.tmp";
758 $task->{tmpdir
} = "/var/tmp/vzdumptmp$$";
759 debugmsg
('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
760 "using $task->{tmpdir} for temporary files", $logfd);
764 rmtree
$task->{tmpdir
};
765 mkdir $task->{tmpdir
};
766 -d
$task->{tmpdir
} ||
767 die "unable to create temporary directory '$task->{tmpdir}'";
769 $logfd = IO
::File-
>new (">$tmplog") ||
770 die "unable to create log file '$tmplog'";
772 $task->{dumpdir
} = $opts->{dumpdir
};
773 $task->{storeid
} = $opts->{storage
};
774 $task->{tmplog
} = $tmplog;
778 debugmsg
('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
780 $plugin->set_logfd ($logfd);
782 # test is VM is running
783 my ($running, $status_text) = $plugin->vm_status ($vmid);
785 debugmsg
('info', "status = ${status_text}", $logfd);
787 # lock VM (prevent config changes)
788 $plugin->lock_vm ($vmid);
790 $cleanup->{unlock
} = 1;
794 my $mode = $running ?
$opts->{mode
} : 'stop';
796 if ($mode eq 'snapshot') {
797 my %saved_task = %$task;
798 eval { $plugin->prepare ($task, $vmid, $mode); };
800 die $err if $err !~ m/^mode failure/;
801 debugmsg
('info', $err, $logfd);
802 debugmsg
('info', "trying 'suspend' mode instead", $logfd);
803 $mode = 'suspend'; # so prepare is called again below
804 %$task = %saved_task;
808 $task->{mode
} = $mode;
810 debugmsg
('info', "backup mode: $mode", $logfd);
812 debugmsg
('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
815 debugmsg
('info', "ionice priority: $opts->{ionice}", $logfd);
817 if ($mode eq 'stop') {
819 $plugin->prepare ($task, $vmid, $mode);
821 $self->run_hook_script ('backup-start', $task, $logfd);
824 debugmsg
('info', "stopping vm", $logfd);
825 $vmstoptime = time ();
826 $self->run_hook_script ('pre-stop', $task, $logfd);
827 $plugin->stop_vm ($task, $vmid);
828 $cleanup->{restart
} = 1;
832 } elsif ($mode eq 'suspend') {
834 $plugin->prepare ($task, $vmid, $mode);
836 $self->run_hook_script ('backup-start', $task, $logfd);
838 if ($vmtype eq 'openvz') {
840 $plugin->copy_data_phase1 ($task, $vmid);
843 debugmsg
('info', "suspend vm", $logfd);
844 $vmstoptime = time ();
845 $self->run_hook_script ('pre-stop', $task, $logfd);
846 $plugin->suspend_vm ($task, $vmid);
847 $cleanup->{resume
} = 1;
849 if ($vmtype eq 'openvz') {
851 $plugin->copy_data_phase2 ($task, $vmid);
853 debugmsg
('info', "resume vm", $logfd);
854 $cleanup->{resume
} = 0;
855 $self->run_hook_script ('pre-restart', $task, $logfd);
856 $plugin->resume_vm ($task, $vmid);
857 my $delay = time () - $vmstoptime;
858 debugmsg
('info', "vm is online again after $delay seconds", $logfd);
861 } elsif ($mode eq 'snapshot') {
863 $self->run_hook_script ('backup-start', $task, $logfd);
865 my $snapshot_count = $task->{snapshot_count
} || 0;
867 $self->run_hook_script ('pre-stop', $task, $logfd);
869 if ($snapshot_count > 1) {
870 debugmsg
('info', "suspend vm to make snapshot", $logfd);
871 $vmstoptime = time ();
872 $plugin->suspend_vm ($task, $vmid);
873 $cleanup->{resume
} = 1;
876 $plugin->snapshot ($task, $vmid);
878 $self->run_hook_script ('pre-restart', $task, $logfd);
880 if ($snapshot_count > 1) {
881 debugmsg
('info', "resume vm", $logfd);
882 $cleanup->{resume
} = 0;
883 $plugin->resume_vm ($task, $vmid);
884 my $delay = time () - $vmstoptime;
885 debugmsg
('info', "vm is online again after $delay seconds", $logfd);
889 die "internal error - unknown mode '$mode'\n";
892 # assemble archive image
893 $plugin->assemble ($task, $vmid);
897 if ($opts->{stdout
}) {
898 debugmsg
('info', "sending archive to stdout", $logfd);
899 $plugin->archive($task, $vmid, $task->{tmptar
}, $comp);
900 $self->run_hook_script ('backup-end', $task, $logfd);
904 debugmsg
('info', "creating archive '$task->{tarfile}'", $logfd);
905 $plugin->archive($task, $vmid, $task->{tmptar
}, $comp);
907 rename ($task->{tmptar
}, $task->{tarfile
}) ||
908 die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
911 $task->{size
} = (-s
$task->{tarfile
}) || 0;
912 my $cs = format_size
($task->{size
});
913 debugmsg
('info', "archive file size: $cs", $logfd);
917 if ($maxfiles && $opts->{remove
}) {
918 my $bklist = get_backup_file_list
($opts->{dumpdir
}, $bkname, $task->{tarfile
});
919 $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
921 while (scalar (@$bklist) >= $maxfiles) {
922 my $d = pop @$bklist;
923 debugmsg
('info', "delete old backup '$d->[0]'", $logfd);
926 $logfn =~ s/\.(tgz|((tar|vma)(\.(gz|lzo))?))$/\.log/;
931 $self->run_hook_script ('backup-end', $task, $logfd);
938 if ($cleanup->{unlock
}) {
939 eval { $plugin->unlock_vm ($vmid); };
943 eval { $plugin->cleanup ($task, $vmid) };
946 eval { $plugin->set_logfd (undef); };
949 if ($cleanup->{resume
} || $cleanup->{restart
}) {
951 $self->run_hook_script ('pre-restart', $task, $logfd);
952 if ($cleanup->{resume
}) {
953 debugmsg
('info', "resume vm", $logfd);
954 $plugin->resume_vm ($task, $vmid);
956 my $running = $plugin->vm_status($vmid);
958 debugmsg
('info', "restarting vm", $logfd);
959 $plugin->start_vm ($task, $vmid);
967 my $delay = time () - $vmstoptime;
968 debugmsg
('info', "vm is online again after $delay seconds", $logfd);
973 eval { unlink $task->{tmptar
} if $task->{tmptar
} && -f
$task->{tmptar
}; };
976 eval { rmtree
$task->{tmpdir
} if $task->{tmpdir
} && -d
$task->{tmpdir
}; };
979 my $delay = $task->{backuptime
} = time () - $vmstarttime;
982 $task->{state} = 'err';
984 debugmsg
('err', "Backup of VM $vmid failed - $err", $logfd, 1);
986 eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
989 $task->{state} = 'ok';
990 my $tstr = format_time
($delay);
991 debugmsg
('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
994 close ($logfd) if $logfd;
996 if ($task->{tmplog
} && $task->{logfile
}) {
997 system ("cp '$task->{tmplog}' '$task->{logfile}'");
1000 eval { $self->run_hook_script ('log-end', $task); };
1002 die $err if $err && $err =~ m/^interrupted by signal$/;
1006 my ($self, $rpcenv, $authuser) = @_;
1008 my $opts = $self->{opts
};
1010 debugmsg
('info', "starting new backup job: $self->{cmdline}", undef, 1);
1011 debugmsg
('info', "skip external VMs: " . join(', ', @{$self->{skiplist
}}))
1012 if scalar(@{$self->{skiplist
}});
1017 foreach my $plugin (@{$self->{plugins
}}) {
1018 my $vmlist = $plugin->vmlist();
1019 foreach my $vmid (sort @$vmlist) {
1020 next if grep { $_ eq $vmid } @{$opts->{exclude
}};
1021 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
1022 push @$tasklist, { vmid
=> $vmid, state => 'todo', plugin
=> $plugin };
1026 foreach my $vmid (sort @{$opts->{vmids
}}) {
1028 foreach my $pg (@{$self->{plugins
}}) {
1029 my $vmlist = $pg->vmlist();
1030 if (grep { $_ eq $vmid } @$vmlist) {
1035 $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
1036 push @$tasklist, { vmid
=> $vmid, state => 'todo', plugin
=> $plugin };
1040 my $starttime = time();
1044 $self->run_hook_script ('job-start');
1046 foreach my $task (@$tasklist) {
1047 $self->exec_backup_task ($task);
1048 $errcount += 1 if $task->{state} ne 'ok';
1051 $self->run_hook_script ('job-end');
1055 $self->run_hook_script ('job-abort') if $err;
1058 debugmsg
('err', "Backup job failed - $err", undef, 1);
1061 debugmsg
('info', "Backup job finished with errors", undef, 1);
1063 debugmsg
('info', "Backup job finished successfully", undef, 1);
1067 my $totaltime = time() - $starttime;
1069 eval { $self->$sendmail ($tasklist, $totaltime); };
1070 debugmsg
('err', $@) if $@;
1074 die "job errors\n" if $errcount;
1079 type
=> 'string', format
=> 'pve-vmid-list',
1080 description
=> "The ID of the VM you want to backup.",
1083 node
=> get_standard_option
('pve-node', {
1084 description
=> "Only run if executed on this node.",
1089 description
=> "Backup all known VMs on this host.",
1095 description
=> "Exclude temorary files and logs.",
1101 description
=> "Compress dump file.",
1103 enum
=> ['0', '1', 'gzip', 'lzo'],
1108 description
=> "Be quiet.",
1114 description
=> "Backup mode.",
1117 enum
=> [ 'snapshot', 'suspend', 'stop' ],
1120 type
=> 'string', format
=> 'pve-vmid-list',
1121 description
=> "exclude specified VMs (assumes --all)",
1125 type
=> 'string', format
=> 'string-alist',
1126 description
=> "exclude certain files/directories (regex).",
1130 type
=> 'string', format
=> 'string-list',
1136 description
=> "Store temporary files to specified directory.",
1141 description
=> "Store resulting files to specified directory.",
1146 description
=> "Use specified hook script.",
1149 storage
=> get_standard_option
('pve-storage-id', {
1150 description
=> "Store resulting file to this storage.",
1155 description
=> "LVM snapshot size im MB.",
1161 description
=> "Limit I/O bandwidth (KBytes per second).",
1167 description
=> "Set CFQ ionice priority.",
1174 description
=> "Maximal time to wait for the global lock (minutes).",
1180 description
=> "Maximal time to wait until a VM is stopped (minutes).",
1186 description
=> "Maximal number of backup files per VM.",
1192 description
=> "Remove old backup files if there are more than 'maxfiles' backup files.",
1200 return defined($confdesc->{$key});
1203 # add JSON properties for create and set function
1204 sub json_config_properties
{
1207 foreach my $opt (keys %$confdesc) {
1208 $prop->{$opt} = $confdesc->{$opt};
1214 sub verify_vzdump_parameters
{
1215 my ($param, $check_missing) = @_;
1217 raise_param_exc
({ all
=> "option conflicts with option 'vmid'"})
1218 if $param->{all
} && $param->{vmid
};
1220 raise_param_exc
({ exclude
=> "option conflicts with option 'vmid'"})
1221 if $param->{exclude
} && $param->{vmid
};
1223 $param->{all
} = 1 if defined($param->{exclude
});
1225 return if !$check_missing;
1227 raise_param_exc
({ vmid
=> "property is missing"})
1228 if !$param->{all
} && !$param->{vmid
};
1237 if ($param->{vmid
}) {
1238 $cmd .= " " . join(' ', PVE
::Tools
::split_list
($param->{vmid
}));
1241 foreach my $p (keys %$param) {
1242 next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow' || $p eq 'stdout';
1243 my $v = $param->{$p};
1244 my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
1245 if ($p eq 'exclude-path') {
1246 foreach my $path (split(/\0/, $v || '')) {
1247 $cmd .= " --$p " . PVE
::Tools
::shellquote
($path);
1250 $cmd .= " --$p " . PVE
::Tools
::shellquote
($v) if defined($v) && $v ne '';