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