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