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