]> git.proxmox.com Git - pve-manager.git/blame - PVE/VZDump.pm
add more gettext markers
[pve-manager.git] / PVE / VZDump.pm
CommitLineData
aaeeeebe
DM
1package PVE::VZDump;
2
aaeeeebe
DM
3use strict;
4use warnings;
5use Fcntl ':flock';
31aef761 6use PVE::Exception qw(raise_param_exc);
4a4051d8 7use PVE::SafeSyslog;
aaeeeebe
DM
8use IO::File;
9use IO::Select;
10use IPC::Open3;
11use POSIX qw(strftime);
12use File::Path;
4a4051d8
DM
13use PVE::Storage;
14use PVE::Cluster qw(cfs_read_file);
aaeeeebe
DM
15use PVE::VZDump::OpenVZ;
16use Time::localtime;
17use Time::Local;
ac27b58d 18use PVE::JSONSchema qw(get_standard_option);
aaeeeebe
DM
19
20my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
21
22my $lockfile = '/var/run/vzdump.lock';
23
24my $logdir = '/var/log/vzdump';
25
26my @plugins = qw (PVE::VZDump::OpenVZ);
27
28# Load available plugins
29my $pveplug = "/usr/share/perl5/PVE/VZDump/QemuServer.pm";
30if (-f $pveplug) {
31 eval { require $pveplug; };
32 if (!$@) {
33 PVE::VZDump::QemuServer->import ();
34 push @plugins, "PVE::VZDump::QemuServer";
35 } else {
36 warn $@;
37 }
38}
39
40# helper functions
41
42my $debugstattxt = {
43 err => 'ERROR:',
44 info => 'INFO:',
45 warn => 'WARN:',
46};
47
48sub debugmsg {
49 my ($mtype, $msg, $logfd, $syslog) = @_;
50
51 chomp $msg;
52
53 return if !$msg;
54
55 my $pre = $debugstattxt->{$mtype} || $debugstattxt->{'err'};
56
57 my $timestr = strftime ("%b %d %H:%M:%S", CORE::localtime);
58
59 syslog ($mtype eq 'info' ? 'info' : 'err', "$pre $msg") if $syslog;
60
61 foreach my $line (split (/\n/, $msg)) {
62 print STDERR "$pre $line\n";
63 print $logfd "$timestr $pre $line\n" if $logfd;
64 }
65}
66
67sub run_command {
68 my ($logfd, $cmdstr, %param) = @_;
69
7f910306 70 my $logfunc = sub {
4a4051d8 71 my $line = shift;
4a4051d8 72 debugmsg ('info', $line, $logfd);
aaeeeebe
DM
73 };
74
7f910306 75 PVE::Tools::run_command($cmdstr, %param, logfunc => $logfunc);
aaeeeebe
DM
76}
77
78sub storage_info {
79 my $storage = shift;
80
4a4051d8
DM
81 my $cfg = cfs_read_file('storage.cfg');
82 my $scfg = PVE::Storage::storage_config($cfg, $storage);
aaeeeebe
DM
83 my $type = $scfg->{type};
84
85 die "can't use storage type '$type' for backup\n"
86 if (!($type eq 'dir' || $type eq 'nfs'));
87 die "can't use storage for backups - wrong content type\n"
88 if (!$scfg->{content}->{backup});
89
4a4051d8 90 PVE::Storage::activate_storage($cfg, $storage);
aaeeeebe
DM
91
92 return {
30edfad9 93 dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
aaeeeebe
DM
94 };
95}
96
97sub format_size {
98 my $size = shift;
99
100 my $kb = $size / 1024;
101
102 if ($kb < 1024) {
103 return int ($kb) . "KB";
104 }
105
106 my $mb = $size / (1024*1024);
107
108 if ($mb < 1024) {
109 return int ($mb) . "MB";
110 } else {
111 my $gb = $mb / 1024;
112 return sprintf ("%.2fGB", $gb);
113 }
114}
115
116sub format_time {
117 my $seconds = shift;
118
119 my $hours = int ($seconds/3600);
120 $seconds = $seconds - $hours*3600;
121 my $min = int ($seconds/60);
122 $seconds = $seconds - $min*60;
123
124 return sprintf ("%02d:%02d:%02d", $hours, $min, $seconds);
125}
126
127sub encode8bit {
128 my ($str) = @_;
129
130 $str =~ s/^(.{990})/$1\n/mg; # reduce line length
131
132 return $str;
133}
134
135sub escape_html {
136 my ($str) = @_;
137
138 $str =~ s/&/&amp;/g;
139 $str =~ s/</&lt;/g;
140 $str =~ s/>/&gt;/g;
141
142 return $str;
143}
144
145sub check_bin {
146 my ($bin) = @_;
147
148 foreach my $p (split (/:/, $ENV{PATH})) {
149 my $fn = "$p/$bin";
150 if (-x $fn) {
151 return $fn;
152 }
153 }
154
155 die "unable to find command '$bin'\n";
156}
157
158sub check_vmids {
159 my (@vmids) = @_;
160
161 my $res = [];
162 foreach my $vmid (@vmids) {
163 die "ERROR: strange VM ID '${vmid}'\n" if $vmid !~ m/^\d+$/;
164 $vmid = int ($vmid); # remove leading zeros
4a4051d8 165 next if !$vmid;
aaeeeebe
DM
166 push @$res, $vmid;
167 }
168
169 return $res;
170}
171
172
173sub read_vzdump_defaults {
174
175 my $fn = "/etc/vzdump.conf";
176
177 my $res = {
178 bwlimit => 0,
179 ionice => 7,
180 size => 1024,
181 lockwait => 3*60, # 3 hours
182 stopwait => 10, # 10 minutes
183 mode => 'snapshot',
184 maxfiles => 1,
185 };
186
187 my $fh = IO::File->new ("<$fn");
188 return $res if !$fh;
189
190 my $line;
191 while (defined ($line = <$fh>)) {
192 next if $line =~ m/^\s*$/;
193 next if $line =~ m/^\#/;
194
195 if ($line =~ m/tmpdir:\s*(.*\S)\s*$/) {
196 $res->{tmpdir} = $1;
197 } elsif ($line =~ m/dumpdir:\s*(.*\S)\s*$/) {
198 $res->{dumpdir} = $1;
199 } elsif ($line =~ m/storage:\s*(\S+)\s*$/) {
200 $res->{storage} = $1;
201 } elsif ($line =~ m/script:\s*(.*\S)\s*$/) {
202 $res->{script} = $1;
203 } elsif ($line =~ m/bwlimit:\s*(\d+)\s*$/) {
204 $res->{bwlimit} = int($1);
205 } elsif ($line =~ m/ionice:\s*([0-8])\s*$/) {
206 $res->{ionice} = int($1);
207 } elsif ($line =~ m/lockwait:\s*(\d+)\s*$/) {
208 $res->{lockwait} = int($1);
209 } elsif ($line =~ m/stopwait:\s*(\d+)\s*$/) {
210 $res->{stopwait} = int($1);
211 } elsif ($line =~ m/size:\s*(\d+)\s*$/) {
212 $res->{size} = int($1);
213 } elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
214 $res->{maxfiles} = int($1);
215 } elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
216 $res->{mode} = $1;
217 } else {
218 debugmsg ('warn', "unable to parse configuration file '$fn' - error at line " . $., undef, 1);
219 }
220
221 }
222 close ($fh);
223
224 return $res;
225}
226
227
228sub find_add_exclude {
229 my ($self, $excltype, $value) = @_;
230
231 if (($excltype eq '-regex') || ($excltype eq '-files')) {
232 $value = "\.$value";
233 }
234
235 if ($excltype eq '-files') {
236 push @{$self->{findexcl}}, "'('", '-not', '-type', 'd', '-regex' , "'$value'", "')'", '-o';
237 } else {
238 push @{$self->{findexcl}}, "'('", $excltype , "'$value'", '-prune', "')'", '-o';
239 }
240}
241
aaeeeebe
DM
242my $sendmail = sub {
243 my ($self, $tasklist, $totaltime) = @_;
244
245 my $opts = $self->{opts};
246
247 my $mailto = $opts->{mailto};
248
4a4051d8 249 return if !($mailto && scalar(@$mailto));
aaeeeebe
DM
250
251 my $cmdline = $self->{cmdline};
252
253 my $ecount = 0;
254 foreach my $task (@$tasklist) {
255 $ecount++ if $task->{state} ne 'ok';
256 chomp $task->{msg} if $task->{msg};
257 $task->{backuptime} = 0 if !$task->{backuptime};
258 $task->{size} = 0 if !$task->{size};
259 $task->{tarfile} = 'unknown' if !$task->{tarfile};
260 $task->{hostname} = "VM $task->{vmid}" if !$task->{hostname};
261
262 if ($task->{state} eq 'todo') {
263 $task->{msg} = 'aborted';
264 }
265 }
266
267 my $stat = $ecount ? 'backup failed' : 'backup successful';
268
4a4051d8 269 my $hostname = `hostname -f` || PVE::INotify::nodename();
aaeeeebe
DM
270 chomp $hostname;
271
aaeeeebe
DM
272 my $boundary = "----_=_NextPart_001_".int(time).$$;
273
274 my $rcvrarg = '';
275 foreach my $r (@$mailto) {
276 $rcvrarg .= " '$r'";
277 }
278
279 open (MAIL,"|sendmail -B 8BITMIME $rcvrarg") ||
280 die "unable to open 'sendmail' - $!";
281
282 my $rcvrtxt = join (', ', @$mailto);
283
284 print MAIL "Content-Type: multipart/alternative;\n";
285 print MAIL "\tboundary=\"$boundary\"\n";
286 print MAIL "FROM: vzdump backup tool <root>\n";
287 print MAIL "TO: $rcvrtxt\n";
288 print MAIL "SUBJECT: vzdump backup status ($hostname) : $stat\n";
289 print MAIL "\n";
290 print MAIL "This is a multi-part message in MIME format.\n\n";
291 print MAIL "--$boundary\n";
292
293 print MAIL "Content-Type: text/plain;\n";
294 print MAIL "\tcharset=\"UTF8\"\n";
295 print MAIL "Content-Transfer-Encoding: 8bit\n";
296 print MAIL "\n";
297
298 # text part
299
300 my $fill = ' '; # Avoid The Remove Extra Line Breaks Issue (MS Outlook)
301
302 print MAIL sprintf ("${fill}%-10s %-6s %10s %10s %s\n", qw(VMID STATUS TIME SIZE FILENAME));
303 foreach my $task (@$tasklist) {
304 my $vmid = $task->{vmid};
305 if ($task->{state} eq 'ok') {
306
307 print MAIL sprintf ("${fill}%-10s %-6s %10s %10s %s\n", $vmid,
308 $task->{state},
309 format_time($task->{backuptime}),
310 format_size ($task->{size}),
311 $task->{tarfile});
312 } else {
313 print MAIL sprintf ("${fill}%-10s %-6s %10s %8.2fMB %s\n", $vmid,
314 $task->{state},
315 format_time($task->{backuptime}),
316 0, '-');
317 }
318 }
319 print MAIL "${fill}\n";
320 print MAIL "${fill}Detailed backup logs:\n";
321 print MAIL "${fill}\n";
322 print MAIL "$fill$cmdline\n";
323 print MAIL "${fill}\n";
324
325 foreach my $task (@$tasklist) {
326 my $vmid = $task->{vmid};
327 my $log = $task->{tmplog};
328 if (!$log) {
329 print MAIL "${fill}$vmid: no log available\n\n";
330 next;
331 }
332 open (TMP, "$log");
333 while (my $line = <TMP>) { print MAIL encode8bit ("${fill}$vmid: $line"); }
334 close (TMP);
335 print MAIL "${fill}\n";
336 }
337
338 # end text part
339 print MAIL "\n--$boundary\n";
340
341 print MAIL "Content-Type: text/html;\n";
342 print MAIL "\tcharset=\"UTF8\"\n";
343 print MAIL "Content-Transfer-Encoding: 8bit\n";
344 print MAIL "\n";
345
346 # html part
347
348 print MAIL "<html><body>\n";
349
350 print MAIL "<table border=1 cellpadding=3>\n";
351
352 print MAIL "<tr><td>VMID<td>NAME<td>STATUS<td>TIME<td>SIZE<td>FILENAME</tr>\n";
353
354 my $ssize = 0;
355
356 foreach my $task (@$tasklist) {
357 my $vmid = $task->{vmid};
358 my $name = $task->{hostname};
359
360 if ($task->{state} eq 'ok') {
361
362 $ssize += $task->{size};
363
364 print MAIL sprintf ("<tr><td>%s<td>%s<td>OK<td>%s<td align=right>%s<td>%s</tr>\n",
365 $vmid, $name,
366 format_time($task->{backuptime}),
367 format_size ($task->{size}),
368 escape_html ($task->{tarfile}));
369 } else {
370 print MAIL sprintf ("<tr><td>%s<td>%s<td><font color=red>FAILED<td>%s<td colspan=2>%s</tr>\n",
371
372 $vmid, $name, format_time($task->{backuptime}),
373 escape_html ($task->{msg}));
374 }
375 }
376
377 print MAIL sprintf ("<tr><td align=left colspan=3>TOTAL<td>%s<td>%s<td></tr>",
378 format_time ($totaltime), format_size ($ssize));
379
380 print MAIL "</table><br><br>\n";
381 print MAIL "Detailed backup logs:<br>\n";
382 print MAIL "<br>\n";
383 print MAIL "<pre>\n";
384 print MAIL escape_html($cmdline) . "\n";
385 print MAIL "\n";
386
387 foreach my $task (@$tasklist) {
388 my $vmid = $task->{vmid};
389 my $log = $task->{tmplog};
390 if (!$log) {
391 print MAIL "$vmid: no log available\n\n";
392 next;
393 }
394 open (TMP, "$log");
395 while (my $line = <TMP>) {
396 if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
397 print MAIL encode8bit ("$vmid: <font color=red>".
398 escape_html ($line) . "</font>");
399 } else {
400 print MAIL encode8bit ("$vmid: " . escape_html ($line));
401 }
402 }
403 close (TMP);
404 print MAIL "\n";
405 }
406 print MAIL "</pre>\n";
407
408 print MAIL "</body></html>\n";
409
410 # end html part
411 print MAIL "\n--$boundary--\n";
412
4a4051d8 413 close(MAIL);
aaeeeebe
DM
414};
415
416sub new {
417 my ($class, $cmdline, $opts) = @_;
418
419 mkpath $logdir;
420
421 check_bin ('cp');
422 check_bin ('df');
423 check_bin ('sendmail');
424 check_bin ('rsync');
425 check_bin ('tar');
426 check_bin ('mount');
427 check_bin ('umount');
428 check_bin ('cstream');
429 check_bin ('ionice');
430
47664cbe 431 if ($opts->{mode} && $opts->{mode} eq 'snapshot') {
aaeeeebe
DM
432 check_bin ('lvcreate');
433 check_bin ('lvs');
434 check_bin ('lvremove');
435 }
436
437 my $defaults = read_vzdump_defaults();
438
439 foreach my $k (keys %$defaults) {
440 if ($k eq 'dumpdir' || $k eq 'storage') {
441 $opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir}) &&
442 !defined ($opts->{storage});
443 } else {
444 $opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
445 }
446 }
447
aaeeeebe
DM
448 $opts->{dumpdir} =~ s|/+$|| if ($opts->{dumpdir});
449 $opts->{tmpdir} =~ s|/+$|| if ($opts->{tmpdir});
450
451 my $self = bless { cmdline => $cmdline, opts => $opts };
452
453 #always skip '.'
454 push @{$self->{findexcl}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
455
456 $self->find_add_exclude ('-type', 's'); # skip sockets
457
458 if ($opts->{'exclude-path'}) {
459 foreach my $path (@{$opts->{'exclude-path'}}) {
460 $self->find_add_exclude ('-regex', $path);
461 }
462 }
463
464 if ($opts->{stdexcludes}) {
465 $self->find_add_exclude ('-files', '/var/log/.+');
466 $self->find_add_exclude ('-regex', '/tmp/.+');
467 $self->find_add_exclude ('-regex', '/var/tmp/.+');
468 $self->find_add_exclude ('-regex', '/var/run/.+pid');
469 }
470
471 foreach my $p (@plugins) {
472
473 my $pd = $p->new ($self);
474
475 push @{$self->{plugins}}, $pd;
476
477 if (!$opts->{dumpdir} && !$opts->{storage} &&
478 ($p eq 'PVE::VZDump::OpenVZ')) {
479 $opts->{dumpdir} = $pd->{dumpdir};
480 }
481 }
482
483 if (!$opts->{dumpdir} && !$opts->{storage}) {
484 die "no dumpdir/storage specified - use option '--dumpdir' or option '--storage'\n";
485 }
486
487 if ($opts->{storage}) {
488 my $info = storage_info ($opts->{storage});
489 $opts->{dumpdir} = $info->{dumpdir};
490 } elsif ($opts->{dumpdir}) {
491 die "dumpdir '$opts->{dumpdir}' does not exist\n"
492 if ! -d $opts->{dumpdir};
493 } else {
494 die "internal error";
495 }
496
497 if ($opts->{tmpdir} && ! -d $opts->{tmpdir}) {
498 die "tmpdir '$opts->{tmpdir}' does not exist\n";
499 }
500
501 return $self;
502
503}
504
505sub get_lvm_mapping {
506
507 my $devmapper;
508
509 my $cmd = "lvs --units m --separator ':' --noheadings -o vg_name,lv_name,lv_size";
510 if (my $fd = IO::File->new ("$cmd 2>/dev/null|")) {
511 while (my $line = <$fd>) {
512 if ($line =~ m|^\s*(\S+):(\S+):(\d+(\.\d+))[Mm]$|) {
513 my $vg = $1;
514 my $lv = $2;
515 $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
516 my $qlv = $lv;
517 $qlv =~ s/-/--/g;
518 my $qvg = $vg;
519 $qvg =~ s/-/--/g;
520 $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
521 }
522 }
523 close ($fd);
524 }
525
526 return $devmapper;
527}
528
529sub get_mount_info {
530 my ($dir) = @_;
531
532 my $out;
533 if (my $fd = IO::File->new ("df -P -T '$dir' 2>/dev/null|")) {
534 <$fd>; #skip first line
535 $out = <$fd>;
536 close ($fd);
537 }
538
539 return undef if !$out;
540
5dc86eb8 541 my @res = $out =~ m/^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/;
aaeeeebe
DM
542
543 return undef if scalar (@res) != 7;
544
545 return {
546 device => $res[0],
547 fstype => $res[1],
548 mountpoint => $res[6]
549 };
550}
551
552sub get_lvm_device {
553 my ($dir, $mapping) = @_;
554
5dc86eb8 555 my $info = get_mount_info($dir);
aaeeeebe
DM
556
557 return undef if !$info;
558
559 my $dev = $info->{device};
560
561 my ($vg, $lv);
562
563 ($vg, $lv) = @{$mapping->{$dev}} if defined $mapping->{$dev};
564
565 return wantarray ? ($dev, $info->{mountpoint}, $vg, $lv, $info->{fstype}) : $dev;
566}
567
568sub getlock {
569 my ($self) = @_;
570
571 my $maxwait = $self->{opts}->{lockwait} || $self->{lockwait};
572
573 if (!open (SERVER_FLCK, ">>$lockfile")) {
574 debugmsg ('err', "can't open lock on file '$lockfile' - $!", undef, 1);
575 exit (-1);
576 }
577
578 if (flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
579 return;
580 }
581
582 if (!$maxwait) {
583 debugmsg ('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
584 exit (-1);
585 }
586
587 debugmsg('info', "trying to get global lock - waiting...", undef, 1);
588
589 eval {
590 alarm ($maxwait * 60);
591
592 local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
593
594 if (!flock (SERVER_FLCK, LOCK_EX)) {
595 my $err = $!;
596 close (SERVER_FLCK);
597 alarm (0);
598 die "$err\n";
599 }
600 alarm (0);
601 };
602 alarm (0);
603
604 my $err = $@;
605
606 if ($err) {
607 debugmsg ('err', "can't aquire lock '$lockfile' - $err", undef, 1);
608 exit (-1);
609 }
610
611 debugmsg('info', "got global lock", undef, 1);
612}
613
614sub run_hook_script {
615 my ($self, $phase, $task, $logfd) = @_;
616
617 my $opts = $self->{opts};
618
619 my $script = $opts->{script};
620
621 return if !$script;
622
623 my $cmd = "$script $phase";
624
625 $cmd .= " $task->{mode} $task->{vmid}" if ($task);
626
627 local %ENV;
628
629 foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile)) {
630 $ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
631 }
632
633 run_command ($logfd, $cmd);
634}
635
636sub exec_backup_task {
637 my ($self, $task) = @_;
638
639 my $opts = $self->{opts};
640
641 my $vmid = $task->{vmid};
642 my $plugin = $task->{plugin};
643
644 my $vmstarttime = time ();
645
646 my $logfd;
647
648 my $cleanup = {};
649
650 my $vmstoptime = 0;
651
652 eval {
653 die "unable to find VM '$vmid'\n" if !$plugin;
654
655 my $vmtype = $plugin->type();
656
657 my $tmplog = "$logdir/$vmtype-$vmid.log";
658
659 my $lt = localtime();
660
661 my $bkname = "vzdump-$vmtype-$vmid";
662 my $basename = sprintf "${bkname}-%04d_%02d_%02d-%02d_%02d_%02d",
663 $lt->year + 1900, $lt->mon + 1, $lt->mday,
664 $lt->hour, $lt->min, $lt->sec;
665
666 my $logfile = $task->{logfile} = "$opts->{dumpdir}/$basename.log";
667
668 my $ext = $opts->{compress} ? '.tgz' : '.tar';
669
670 if ($opts->{stdout}) {
671 $task->{tarfile} = '-';
672 } else {
673 my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
674 $task->{tmptar} = $task->{tarfile};
675 $task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
676 unlink $task->{tmptar};
677 }
678
679 $task->{vmtype} = $vmtype;
680
681 if ($opts->{tmpdir}) {
682 $task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
683 } else {
684 # dumpdir is posix? then use it as temporary dir
5dc86eb8 685 my $info = get_mount_info($opts->{dumpdir});
aaeeeebe
DM
686 if ($vmtype eq 'qemu' ||
687 grep ($_ eq $info->{fstype}, @posix_filesystems)) {
688 $task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
689 } else {
690 $task->{tmpdir} = "/var/tmp/vzdumptmp$$";
691 debugmsg ('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
692 "using $task->{tmpdir} for temporary files", $logfd);
693 }
694 }
695
696 rmtree $task->{tmpdir};
697 mkdir $task->{tmpdir};
698 -d $task->{tmpdir} ||
699 die "unable to create temporary directory '$task->{tmpdir}'";
700
701 $logfd = IO::File->new (">$tmplog") ||
702 die "unable to create log file '$tmplog'";
703
704 $task->{dumpdir} = $opts->{dumpdir};
705
706 $task->{tmplog} = $tmplog;
707
708 unlink $logfile;
709
710 debugmsg ('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
711
712 $plugin->set_logfd ($logfd);
713
714 # test is VM is running
715 my ($running, $status_text) = $plugin->vm_status ($vmid);
716
717 debugmsg ('info', "status = ${status_text}", $logfd);
718
719 # lock VM (prevent config changes)
720 $plugin->lock_vm ($vmid);
721
722 $cleanup->{unlock} = 1;
723
724 # prepare
725
726 my $mode = $running ? $opts->{mode} : 'stop';
727
728 if ($mode eq 'snapshot') {
729 my %saved_task = %$task;
730 eval { $plugin->prepare ($task, $vmid, $mode); };
731 if (my $err = $@) {
732 die $err if $err !~ m/^mode failure/;
733 debugmsg ('info', $err, $logfd);
734 debugmsg ('info', "trying 'suspend' mode instead", $logfd);
735 $mode = 'suspend'; # so prepare is called again below
736 %$task = %saved_task;
737 }
738 }
739
740 $task->{mode} = $mode;
741
742 debugmsg ('info', "backup mode: $mode", $logfd);
743
744 debugmsg ('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
745 if $opts->{bwlimit};
746
747 debugmsg ('info', "ionice priority: $opts->{ionice}", $logfd);
748
749 if ($mode eq 'stop') {
750
751 $plugin->prepare ($task, $vmid, $mode);
752
753 $self->run_hook_script ('backup-start', $task, $logfd);
754
755 if ($running) {
756 debugmsg ('info', "stopping vm", $logfd);
757 $vmstoptime = time ();
758 $self->run_hook_script ('pre-stop', $task, $logfd);
759 $plugin->stop_vm ($task, $vmid);
760 $cleanup->{restart} = 1;
761 }
762
763
764 } elsif ($mode eq 'suspend') {
765
766 $plugin->prepare ($task, $vmid, $mode);
767
768 $self->run_hook_script ('backup-start', $task, $logfd);
769
770 if ($vmtype eq 'openvz') {
771 # pre-suspend rsync
772 $plugin->copy_data_phase1 ($task, $vmid);
773 }
774
775 debugmsg ('info', "suspend vm", $logfd);
776 $vmstoptime = time ();
777 $self->run_hook_script ('pre-stop', $task, $logfd);
778 $plugin->suspend_vm ($task, $vmid);
779 $cleanup->{resume} = 1;
780
781 if ($vmtype eq 'openvz') {
782 # post-suspend rsync
783 $plugin->copy_data_phase2 ($task, $vmid);
784
785 debugmsg ('info', "resume vm", $logfd);
786 $cleanup->{resume} = 0;
787 $self->run_hook_script ('pre-restart', $task, $logfd);
788 $plugin->resume_vm ($task, $vmid);
789 my $delay = time () - $vmstoptime;
790 debugmsg ('info', "vm is online again after $delay seconds", $logfd);
791 }
792
793 } elsif ($mode eq 'snapshot') {
794
795 my $snapshot_count = $task->{snapshot_count} || 0;
796
797 $self->run_hook_script ('pre-stop', $task, $logfd);
798
799 if ($snapshot_count > 1) {
800 debugmsg ('info', "suspend vm to make snapshot", $logfd);
801 $vmstoptime = time ();
802 $plugin->suspend_vm ($task, $vmid);
803 $cleanup->{resume} = 1;
804 }
805
806 $plugin->snapshot ($task, $vmid);
807
808 $self->run_hook_script ('pre-restart', $task, $logfd);
809
810 if ($snapshot_count > 1) {
811 debugmsg ('info', "resume vm", $logfd);
812 $cleanup->{resume} = 0;
813 $plugin->resume_vm ($task, $vmid);
814 my $delay = time () - $vmstoptime;
815 debugmsg ('info', "vm is online again after $delay seconds", $logfd);
816 }
817
818 } else {
819 die "internal error - unknown mode '$mode'\n";
820 }
821
822 # assemble archive image
823 $plugin->assemble ($task, $vmid);
824
825 # produce archive
826
827 if ($opts->{stdout}) {
828 debugmsg ('info', "sending archive to stdout", $logfd);
30edfad9 829 $plugin->archive($task, $vmid, $task->{tmptar});
aaeeeebe
DM
830 $self->run_hook_script ('backup-end', $task, $logfd);
831 return;
832 }
833
834 debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
835 $plugin->archive ($task, $vmid, $task->{tmptar});
836
837 rename ($task->{tmptar}, $task->{tarfile}) ||
838 die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
839
840 # determine size
841 $task->{size} = (-s $task->{tarfile}) || 0;
842 my $cs = format_size ($task->{size});
843 debugmsg ('info', "archive file size: $cs", $logfd);
844
845 # purge older backup
846
847 my $maxfiles = $opts->{maxfiles};
848
849 if ($maxfiles) {
850 my @bklist = ();
851 my $dir = $opts->{dumpdir};
852 foreach my $fn (<$dir/${bkname}-*>) {
853 next if $fn eq $task->{tarfile};
30edfad9
DM
854 if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|tar))$!) {
855 $fn = "$dir/$1"; # untaint
856 my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2 - 1900);
aaeeeebe
DM
857 push @bklist, [$fn, $t];
858 }
859 }
860
861 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
862
863 my $ind = scalar (@bklist);
864
865 while (scalar (@bklist) >= $maxfiles) {
866 my $d = pop @bklist;
867 debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
868 unlink $d->[0];
869 my $logfn = $d->[0];
870 $logfn =~ s/\.(tgz|tar)$/\.log/;
871 unlink $logfn;
872 }
873 }
874
875 $self->run_hook_script ('backup-end', $task, $logfd);
876 };
877 my $err = $@;
878
879 if ($plugin) {
880 # clean-up
881
882 if ($cleanup->{unlock}) {
883 eval { $plugin->unlock_vm ($vmid); };
884 warn $@ if $@;
885 }
886
887 eval { $plugin->cleanup ($task, $vmid) };
888 warn $@ if $@;
889
890 eval { $plugin->set_logfd (undef); };
891 warn $@ if $@;
892
893 if ($cleanup->{resume} || $cleanup->{restart}) {
894 eval {
895 $self->run_hook_script ('pre-restart', $task, $logfd);
896 if ($cleanup->{resume}) {
897 debugmsg ('info', "resume vm", $logfd);
898 $plugin->resume_vm ($task, $vmid);
899 } else {
900 debugmsg ('info', "restarting vm", $logfd);
901 $plugin->start_vm ($task, $vmid);
902 }
903 };
904 my $err = $@;
905 if ($err) {
906 warn $err;
907 } else {
908 my $delay = time () - $vmstoptime;
909 debugmsg ('info', "vm is online again after $delay seconds", $logfd);
910 }
911 }
912 }
913
914 eval { unlink $task->{tmptar} if $task->{tmptar} && -f $task->{tmptar}; };
915 warn $@ if $@;
916
917 eval { rmtree $task->{tmpdir} if $task->{tmpdir} && -d $task->{tmpdir}; };
918 warn $@ if $@;
919
920 my $delay = $task->{backuptime} = time () - $vmstarttime;
921
922 if ($err) {
923 $task->{state} = 'err';
924 $task->{msg} = $err;
925 debugmsg ('err', "Backup of VM $vmid failed - $err", $logfd, 1);
926
927 eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
928
929 } else {
930 $task->{state} = 'ok';
931 my $tstr = format_time ($delay);
932 debugmsg ('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
933 }
934
935 close ($logfd) if $logfd;
936
937 if ($task->{tmplog} && $task->{logfile}) {
938 system ("cp '$task->{tmplog}' '$task->{logfile}'");
939 }
940
941 eval { $self->run_hook_script ('log-end', $task); };
942
943 die $err if $err && $err =~ m/^interrupted by signal$/;
944}
945
946sub exec_backup {
947 my ($self) = @_;
948
949 my $opts = $self->{opts};
950
951 debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
952
953 my $tasklist = [];
954
955 if ($opts->{all}) {
956 foreach my $plugin (@{$self->{plugins}}) {
957 my $vmlist = $plugin->vmlist();
958 foreach my $vmid (sort @$vmlist) {
959 next if grep { $_ eq $vmid } @{$opts->{exclude}};
960 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
961 }
962 }
963 } else {
964 foreach my $vmid (sort @{$opts->{vmids}}) {
965 my $plugin;
966 foreach my $pg (@{$self->{plugins}}) {
967 my $vmlist = $pg->vmlist();
968 if (grep { $_ eq $vmid } @$vmlist) {
969 $plugin = $pg;
970 last;
971 }
972 }
973 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
974 }
975 }
976
977 my $starttime = time();
978 my $errcount = 0;
979 eval {
980
981 $self->run_hook_script ('job-start');
982
983 foreach my $task (@$tasklist) {
984 $self->exec_backup_task ($task);
985 $errcount += 1 if $task->{state} ne 'ok';
986 }
987
988 $self->run_hook_script ('job-end');
989 };
990 my $err = $@;
991
992 $self->run_hook_script ('job-abort') if $err;
993
994 if ($err) {
995 debugmsg ('err', "Backup job failed - $err", undef, 1);
996 } else {
997 if ($errcount) {
998 debugmsg ('info', "Backup job finished with errors", undef, 1);
999 } else {
61ca4432 1000 debugmsg ('info', "Backup job finished successfully", undef, 1);
aaeeeebe
DM
1001 }
1002 }
1003
1004 my $totaltime = time() - $starttime;
1005
1006 eval { $self->$sendmail ($tasklist, $totaltime); };
1007 debugmsg ('err', $@) if $@;
4a4051d8
DM
1008
1009 die $err if $err;
1010
1011 die "job errors\n" if $errcount;
aaeeeebe
DM
1012}
1013
ac27b58d
DM
1014my $confdesc = {
1015 vmid => {
1016 type => 'string', format => 'pve-vmid-list',
1017 description => "The ID of the VM you want to backup.",
1018 optional => 1,
1019 },
1020 node => get_standard_option('pve-node', {
1021 description => "Only run if executed on this node.",
1022 optional => 1,
1023 }),
1024 all => {
1025 type => 'boolean',
1026 description => "Backup all known VMs on this host.",
1027 optional => 1,
1028 default => 0,
1029 },
1030 stdexcludes => {
1031 type => 'boolean',
1032 description => "Exclude temorary files and logs.",
1033 optional => 1,
1034 default => 1,
1035 },
1036 compress => {
1037 type => 'boolean',
1038 description => "Compress dump file (gzip).",
1039 optional => 1,
1040 default => 0,
1041 },
1042 quiet => {
1043 type => 'boolean',
1044 description => "Be quiet.",
1045 optional => 1,
1046 default => 0,
1047 },
47664cbe
DM
1048 mode => {
1049 type => 'string',
1050 description => "Backup mode.",
ac27b58d 1051 optional => 1,
47664cbe
DM
1052 default => 'stop',
1053 enum => [ 'snapshot', 'suspend', 'stop' ],
ac27b58d
DM
1054 },
1055 exclude => {
1056 type => 'string', format => 'pve-vmid-list',
1057 description => "exclude specified VMs (assumes --all)",
1058 optional => 1,
1059 },
1060 'exclude-path' => {
1061 type => 'string', format => 'string-alist',
1062 description => "exclude certain files/directories (regex).",
1063 optional => 1,
1064 },
1065 mailto => {
1066 type => 'string', format => 'string-list',
1067 description => "",
1068 optional => 1,
1069 },
1070 tmpdir => {
1071 type => 'string',
1072 description => "Store temporary files to specified directory.",
1073 optional => 1,
1074 },
1075 dumpdir => {
1076 type => 'string',
1077 description => "Store resulting files to specified directory.",
1078 optional => 1,
1079 },
1080 script => {
1081 type => 'string',
1082 description => "Use specified hook script.",
1083 optional => 1,
1084 },
1085 storage => get_standard_option('pve-storage-id', {
1086 description => "Store resulting file to this storage.",
1087 optional => 1,
1088 }),
1089 size => {
1090 type => 'integer',
1091 description => "LVM snapshot size im MB.",
1092 optional => 1,
1093 minimum => 500,
1094 },
1095 bwlimit => {
1096 type => 'integer',
1097 description => "Limit I/O bandwidth (KBytes per second).",
1098 optional => 1,
1099 minimum => 0,
1100 },
1101 ionice => {
1102 type => 'integer',
1103 description => "Set CFQ ionice priority.",
1104 optional => 1,
1105 minimum => 0,
1106 maximum => 8,
1107 },
1108 lockwait => {
1109 type => 'integer',
1110 description => "Maximal time to wait for the global lock (minutes).",
1111 optional => 1,
1112 minimum => 0,
1113 },
1114 stopwait => {
1115 type => 'integer',
1116 description => "Maximal time to wait until a VM is stopped (minutes).",
1117 optional => 1,
1118 minimum => 0,
1119 },
1120 maxfiles => {
1121 type => 'integer',
1122 description => "Maximal number of backup files per VM.",
1123 optional => 1,
1124 minimum => 1,
1125 },
1126};
1127
47664cbe
DM
1128sub option_exists {
1129 my $key = shift;
1130 return defined($confdesc->{$key});
1131}
1132
ac27b58d
DM
1133# add JSON properties for create and set function
1134sub json_config_properties {
1135 my $prop = shift;
1136
1137 foreach my $opt (keys %$confdesc) {
1138 $prop->{$opt} = $confdesc->{$opt};
1139 }
1140
1141 return $prop;
1142}
1143
31aef761
DM
1144sub verify_vzdump_parameters {
1145 my ($param, $check_missing) = @_;
1146
1147 raise_param_exc({ all => "option conflicts with option 'vmid'"})
1148 if $param->{all} && $param->{vmid};
1149
1150 raise_param_exc({ exclude => "option conflicts with option 'vmid'"})
1151 if $param->{exclude} && $param->{vmid};
1152
1153 $param->{all} = 1 if defined($param->{exclude});
1154
1155 return if !$check_missing;
1156
1157 raise_param_exc({ vmid => "property is missing"})
1158 if !$param->{all} && !$param->{vmid};
1159
1160}
1161
1162sub command_line {
1163 my ($param) = @_;
1164
1165 my $cmd = "vzdump";
1166
1167 if ($param->{vmid}) {
1168 $cmd .= " " . join(' ', PVE::Tools::split_list($param->{vmid}));
1169 }
1170
1171 foreach my $p (keys %$param) {
1172 next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
1173 my $v = $param->{$p};
1174 my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
1175 $cmd .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
1176 }
1177
1178 return $cmd;
1179}
1180
aaeeeebe 11811;