]> git.proxmox.com Git - pve-manager.git/blame - PVE/VZDump.pm
improve network API - 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;
98e84b16 13use PVE::RPCEnvironment;
4a4051d8
DM
14use PVE::Storage;
15use PVE::Cluster qw(cfs_read_file);
aaeeeebe
DM
16use PVE::VZDump::OpenVZ;
17use Time::localtime;
18use Time::Local;
ac27b58d 19use PVE::JSONSchema qw(get_standard_option);
aaeeeebe
DM
20
21my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
22
23my $lockfile = '/var/run/vzdump.lock';
24
25my $logdir = '/var/log/vzdump';
26
27my @plugins = qw (PVE::VZDump::OpenVZ);
28
29# Load available plugins
30my $pveplug = "/usr/share/perl5/PVE/VZDump/QemuServer.pm";
31if (-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
43my $debugstattxt = {
44 err => 'ERROR:',
45 info => 'INFO:',
46 warn => 'WARN:',
47};
48
49sub 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
68sub run_command {
69 my ($logfd, $cmdstr, %param) = @_;
70
7f910306 71 my $logfunc = sub {
4a4051d8 72 my $line = shift;
4a4051d8 73 debugmsg ('info', $line, $logfd);
aaeeeebe
DM
74 };
75
7f910306 76 PVE::Tools::run_command($cmdstr, %param, logfunc => $logfunc);
aaeeeebe
DM
77}
78
79sub storage_info {
80 my $storage = shift;
81
4a4051d8
DM
82 my $cfg = cfs_read_file('storage.cfg');
83 my $scfg = PVE::Storage::storage_config($cfg, $storage);
aaeeeebe
DM
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
4a4051d8 91 PVE::Storage::activate_storage($cfg, $storage);
aaeeeebe
DM
92
93 return {
30edfad9 94 dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
aaeeeebe
DM
95 };
96}
97
98sub 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
117sub 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
128sub encode8bit {
129 my ($str) = @_;
130
131 $str =~ s/^(.{990})/$1\n/mg; # reduce line length
132
133 return $str;
134}
135
136sub 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
146sub 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
159sub 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
4a4051d8 166 next if !$vmid;
aaeeeebe
DM
167 push @$res, $vmid;
168 }
169
170 return $res;
171}
172
173
174sub 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);
f4a8bab4
DM
216 } elsif ($line =~ m/exclude-path:\s*(.*)\s*$/) {
217 $res->{'exclude-path'} = PVE::Tools::split_args($1);
aaeeeebe
DM
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
231sub 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
aaeeeebe
DM
245my $sendmail = sub {
246 my ($self, $tasklist, $totaltime) = @_;
247
248 my $opts = $self->{opts};
249
250 my $mailto = $opts->{mailto};
251
4a4051d8 252 return if !($mailto && scalar(@$mailto));
aaeeeebe
DM
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
4a4051d8 272 my $hostname = `hostname -f` || PVE::INotify::nodename();
aaeeeebe
DM
273 chomp $hostname;
274
aaeeeebe
DM
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
4a4051d8 416 close(MAIL);
aaeeeebe
DM
417};
418
419sub new {
a7e42354 420 my ($class, $cmdline, $opts, $skiplist) = @_;
aaeeeebe
DM
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
47664cbe 434 if ($opts->{mode} && $opts->{mode} eq 'snapshot') {
aaeeeebe
DM
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
aaeeeebe
DM
451 $opts->{dumpdir} =~ s|/+$|| if ($opts->{dumpdir});
452 $opts->{tmpdir} =~ s|/+$|| if ($opts->{tmpdir});
453
a7e42354
DM
454 $skiplist = [] if !$skiplist;
455 my $self = bless { cmdline => $cmdline, opts => $opts, skiplist => $skiplist };
aaeeeebe
DM
456
457 #always skip '.'
458 push @{$self->{findexcl}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
459
460 $self->find_add_exclude ('-type', 's'); # skip sockets
461
f4a8bab4
DM
462 if ($defaults->{'exclude-path'}) {
463 foreach my $path (@{$defaults->{'exclude-path'}}) {
464 $self->find_add_exclude ('-regex', $path);
465 }
466 }
467
aaeeeebe
DM
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
515sub get_lvm_mapping {
516
517 my $devmapper;
518
519 my $cmd = "lvs --units m --separator ':' --noheadings -o vg_name,lv_name,lv_size";
520 if (my $fd = IO::File->new ("$cmd 2>/dev/null|")) {
521 while (my $line = <$fd>) {
522 if ($line =~ m|^\s*(\S+):(\S+):(\d+(\.\d+))[Mm]$|) {
523 my $vg = $1;
524 my $lv = $2;
525 $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
526 my $qlv = $lv;
527 $qlv =~ s/-/--/g;
528 my $qvg = $vg;
529 $qvg =~ s/-/--/g;
530 $devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
531 }
532 }
533 close ($fd);
534 }
535
536 return $devmapper;
537}
538
539sub get_mount_info {
540 my ($dir) = @_;
541
542 my $out;
543 if (my $fd = IO::File->new ("df -P -T '$dir' 2>/dev/null|")) {
544 <$fd>; #skip first line
545 $out = <$fd>;
546 close ($fd);
547 }
548
549 return undef if !$out;
550
5dc86eb8 551 my @res = $out =~ m/^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/;
aaeeeebe
DM
552
553 return undef if scalar (@res) != 7;
554
555 return {
556 device => $res[0],
557 fstype => $res[1],
558 mountpoint => $res[6]
559 };
560}
561
562sub get_lvm_device {
563 my ($dir, $mapping) = @_;
564
5dc86eb8 565 my $info = get_mount_info($dir);
aaeeeebe
DM
566
567 return undef if !$info;
568
569 my $dev = $info->{device};
570
571 my ($vg, $lv);
572
573 ($vg, $lv) = @{$mapping->{$dev}} if defined $mapping->{$dev};
574
575 return wantarray ? ($dev, $info->{mountpoint}, $vg, $lv, $info->{fstype}) : $dev;
576}
577
578sub getlock {
579 my ($self) = @_;
580
581 my $maxwait = $self->{opts}->{lockwait} || $self->{lockwait};
582
583 if (!open (SERVER_FLCK, ">>$lockfile")) {
584 debugmsg ('err', "can't open lock on file '$lockfile' - $!", undef, 1);
585 exit (-1);
586 }
587
588 if (flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
589 return;
590 }
591
592 if (!$maxwait) {
593 debugmsg ('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
594 exit (-1);
595 }
596
597 debugmsg('info', "trying to get global lock - waiting...", undef, 1);
598
599 eval {
600 alarm ($maxwait * 60);
601
602 local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
603
604 if (!flock (SERVER_FLCK, LOCK_EX)) {
605 my $err = $!;
606 close (SERVER_FLCK);
607 alarm (0);
608 die "$err\n";
609 }
610 alarm (0);
611 };
612 alarm (0);
613
614 my $err = $@;
615
616 if ($err) {
617 debugmsg ('err', "can't aquire lock '$lockfile' - $err", undef, 1);
618 exit (-1);
619 }
620
621 debugmsg('info', "got global lock", undef, 1);
622}
623
624sub run_hook_script {
625 my ($self, $phase, $task, $logfd) = @_;
626
627 my $opts = $self->{opts};
628
629 my $script = $opts->{script};
630
631 return if !$script;
632
633 my $cmd = "$script $phase";
634
635 $cmd .= " $task->{mode} $task->{vmid}" if ($task);
636
637 local %ENV;
638
639 foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile)) {
640 $ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
641 }
642
643 run_command ($logfd, $cmd);
644}
645
d7550e09
DM
646sub compressor_info {
647 my ($opt_compress) = @_;
648
649 if (!$opt_compress || $opt_compress eq '0') {
650 return undef;
651 } elsif ($opt_compress eq '1' || $opt_compress eq 'lzo') {
652 return ('lzop', 'lzo');
653 } elsif ($opt_compress eq 'gzip') {
654 return ('gzip', 'gz');
655 } else {
656 die "internal error - unknown compression option '$opt_compress'";
657 }
658}
659
aaeeeebe
DM
660sub exec_backup_task {
661 my ($self, $task) = @_;
662
663 my $opts = $self->{opts};
664
665 my $vmid = $task->{vmid};
666 my $plugin = $task->{plugin};
667
668 my $vmstarttime = time ();
669
670 my $logfd;
671
672 my $cleanup = {};
673
674 my $vmstoptime = 0;
675
676 eval {
677 die "unable to find VM '$vmid'\n" if !$plugin;
678
679 my $vmtype = $plugin->type();
680
681 my $tmplog = "$logdir/$vmtype-$vmid.log";
682
683 my $lt = localtime();
684
685 my $bkname = "vzdump-$vmtype-$vmid";
686 my $basename = sprintf "${bkname}-%04d_%02d_%02d-%02d_%02d_%02d",
687 $lt->year + 1900, $lt->mon + 1, $lt->mday,
688 $lt->hour, $lt->min, $lt->sec;
689
690 my $logfile = $task->{logfile} = "$opts->{dumpdir}/$basename.log";
691
d7550e09
DM
692 my $ext = '.tar';
693 my ($comp, $comp_ext) = compressor_info($opts->{compress});
694 if ($comp && $comp_ext) {
695 $ext .= ".${comp_ext}";
696 }
aaeeeebe
DM
697
698 if ($opts->{stdout}) {
699 $task->{tarfile} = '-';
700 } else {
701 my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
702 $task->{tmptar} = $task->{tarfile};
703 $task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
704 unlink $task->{tmptar};
705 }
706
707 $task->{vmtype} = $vmtype;
708
709 if ($opts->{tmpdir}) {
710 $task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
711 } else {
712 # dumpdir is posix? then use it as temporary dir
5dc86eb8 713 my $info = get_mount_info($opts->{dumpdir});
aaeeeebe
DM
714 if ($vmtype eq 'qemu' ||
715 grep ($_ eq $info->{fstype}, @posix_filesystems)) {
716 $task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
717 } else {
718 $task->{tmpdir} = "/var/tmp/vzdumptmp$$";
719 debugmsg ('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
720 "using $task->{tmpdir} for temporary files", $logfd);
721 }
722 }
723
724 rmtree $task->{tmpdir};
725 mkdir $task->{tmpdir};
726 -d $task->{tmpdir} ||
727 die "unable to create temporary directory '$task->{tmpdir}'";
728
729 $logfd = IO::File->new (">$tmplog") ||
730 die "unable to create log file '$tmplog'";
731
732 $task->{dumpdir} = $opts->{dumpdir};
733
734 $task->{tmplog} = $tmplog;
735
736 unlink $logfile;
737
738 debugmsg ('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
739
740 $plugin->set_logfd ($logfd);
741
742 # test is VM is running
743 my ($running, $status_text) = $plugin->vm_status ($vmid);
744
745 debugmsg ('info', "status = ${status_text}", $logfd);
746
747 # lock VM (prevent config changes)
748 $plugin->lock_vm ($vmid);
749
750 $cleanup->{unlock} = 1;
751
752 # prepare
753
754 my $mode = $running ? $opts->{mode} : 'stop';
755
756 if ($mode eq 'snapshot') {
757 my %saved_task = %$task;
758 eval { $plugin->prepare ($task, $vmid, $mode); };
759 if (my $err = $@) {
760 die $err if $err !~ m/^mode failure/;
761 debugmsg ('info', $err, $logfd);
762 debugmsg ('info', "trying 'suspend' mode instead", $logfd);
763 $mode = 'suspend'; # so prepare is called again below
764 %$task = %saved_task;
765 }
766 }
767
768 $task->{mode} = $mode;
769
770 debugmsg ('info', "backup mode: $mode", $logfd);
771
772 debugmsg ('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
773 if $opts->{bwlimit};
774
775 debugmsg ('info', "ionice priority: $opts->{ionice}", $logfd);
776
777 if ($mode eq 'stop') {
778
779 $plugin->prepare ($task, $vmid, $mode);
780
781 $self->run_hook_script ('backup-start', $task, $logfd);
782
783 if ($running) {
784 debugmsg ('info', "stopping vm", $logfd);
785 $vmstoptime = time ();
786 $self->run_hook_script ('pre-stop', $task, $logfd);
787 $plugin->stop_vm ($task, $vmid);
788 $cleanup->{restart} = 1;
789 }
790
791
792 } elsif ($mode eq 'suspend') {
793
794 $plugin->prepare ($task, $vmid, $mode);
795
796 $self->run_hook_script ('backup-start', $task, $logfd);
797
798 if ($vmtype eq 'openvz') {
799 # pre-suspend rsync
800 $plugin->copy_data_phase1 ($task, $vmid);
801 }
802
803 debugmsg ('info', "suspend vm", $logfd);
804 $vmstoptime = time ();
805 $self->run_hook_script ('pre-stop', $task, $logfd);
806 $plugin->suspend_vm ($task, $vmid);
807 $cleanup->{resume} = 1;
808
809 if ($vmtype eq 'openvz') {
810 # post-suspend rsync
811 $plugin->copy_data_phase2 ($task, $vmid);
812
813 debugmsg ('info', "resume vm", $logfd);
814 $cleanup->{resume} = 0;
815 $self->run_hook_script ('pre-restart', $task, $logfd);
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 } elsif ($mode eq 'snapshot') {
822
823 my $snapshot_count = $task->{snapshot_count} || 0;
824
825 $self->run_hook_script ('pre-stop', $task, $logfd);
826
827 if ($snapshot_count > 1) {
828 debugmsg ('info', "suspend vm to make snapshot", $logfd);
829 $vmstoptime = time ();
830 $plugin->suspend_vm ($task, $vmid);
831 $cleanup->{resume} = 1;
832 }
833
834 $plugin->snapshot ($task, $vmid);
835
836 $self->run_hook_script ('pre-restart', $task, $logfd);
837
838 if ($snapshot_count > 1) {
839 debugmsg ('info', "resume vm", $logfd);
840 $cleanup->{resume} = 0;
841 $plugin->resume_vm ($task, $vmid);
842 my $delay = time () - $vmstoptime;
843 debugmsg ('info', "vm is online again after $delay seconds", $logfd);
844 }
845
846 } else {
847 die "internal error - unknown mode '$mode'\n";
848 }
849
850 # assemble archive image
851 $plugin->assemble ($task, $vmid);
852
853 # produce archive
854
855 if ($opts->{stdout}) {
856 debugmsg ('info', "sending archive to stdout", $logfd);
d7550e09 857 $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
aaeeeebe
DM
858 $self->run_hook_script ('backup-end', $task, $logfd);
859 return;
860 }
861
862 debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
d7550e09 863 $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
aaeeeebe
DM
864
865 rename ($task->{tmptar}, $task->{tarfile}) ||
866 die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
867
868 # determine size
869 $task->{size} = (-s $task->{tarfile}) || 0;
870 my $cs = format_size ($task->{size});
871 debugmsg ('info', "archive file size: $cs", $logfd);
872
873 # purge older backup
874
875 my $maxfiles = $opts->{maxfiles};
876
877 if ($maxfiles) {
878 my @bklist = ();
879 my $dir = $opts->{dumpdir};
880 foreach my $fn (<$dir/${bkname}-*>) {
881 next if $fn eq $task->{tarfile};
d7550e09 882 if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|(tar(\.(gz|lzo))?)))$!) {
30edfad9
DM
883 $fn = "$dir/$1"; # untaint
884 my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2 - 1900);
aaeeeebe
DM
885 push @bklist, [$fn, $t];
886 }
887 }
888
889 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
890
891 my $ind = scalar (@bklist);
892
893 while (scalar (@bklist) >= $maxfiles) {
894 my $d = pop @bklist;
895 debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
896 unlink $d->[0];
897 my $logfn = $d->[0];
d7550e09 898 $logfn =~ s/\.(tgz|(tar(\.(gz|lzo))?))$/\.log/;
aaeeeebe
DM
899 unlink $logfn;
900 }
901 }
902
903 $self->run_hook_script ('backup-end', $task, $logfd);
904 };
905 my $err = $@;
906
907 if ($plugin) {
908 # clean-up
909
910 if ($cleanup->{unlock}) {
911 eval { $plugin->unlock_vm ($vmid); };
912 warn $@ if $@;
913 }
914
915 eval { $plugin->cleanup ($task, $vmid) };
916 warn $@ if $@;
917
918 eval { $plugin->set_logfd (undef); };
919 warn $@ if $@;
920
921 if ($cleanup->{resume} || $cleanup->{restart}) {
922 eval {
923 $self->run_hook_script ('pre-restart', $task, $logfd);
924 if ($cleanup->{resume}) {
925 debugmsg ('info', "resume vm", $logfd);
926 $plugin->resume_vm ($task, $vmid);
927 } else {
928 debugmsg ('info', "restarting vm", $logfd);
929 $plugin->start_vm ($task, $vmid);
930 }
931 };
932 my $err = $@;
933 if ($err) {
934 warn $err;
935 } else {
936 my $delay = time () - $vmstoptime;
937 debugmsg ('info', "vm is online again after $delay seconds", $logfd);
938 }
939 }
940 }
941
942 eval { unlink $task->{tmptar} if $task->{tmptar} && -f $task->{tmptar}; };
943 warn $@ if $@;
944
945 eval { rmtree $task->{tmpdir} if $task->{tmpdir} && -d $task->{tmpdir}; };
946 warn $@ if $@;
947
948 my $delay = $task->{backuptime} = time () - $vmstarttime;
949
950 if ($err) {
951 $task->{state} = 'err';
952 $task->{msg} = $err;
953 debugmsg ('err', "Backup of VM $vmid failed - $err", $logfd, 1);
954
955 eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
956
957 } else {
958 $task->{state} = 'ok';
959 my $tstr = format_time ($delay);
960 debugmsg ('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
961 }
962
963 close ($logfd) if $logfd;
964
965 if ($task->{tmplog} && $task->{logfile}) {
966 system ("cp '$task->{tmplog}' '$task->{logfile}'");
967 }
968
969 eval { $self->run_hook_script ('log-end', $task); };
970
971 die $err if $err && $err =~ m/^interrupted by signal$/;
972}
973
974sub exec_backup {
d7550e09 975 my ($self, $rpcenv, $authuser) = @_;
aaeeeebe
DM
976
977 my $opts = $self->{opts};
978
979 debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
a7e42354
DM
980 debugmsg ('info', "skip external VMs: " . join(', ', @{$self->{skiplist}}))
981 if scalar(@{$self->{skiplist}});
982
aaeeeebe
DM
983 my $tasklist = [];
984
985 if ($opts->{all}) {
986 foreach my $plugin (@{$self->{plugins}}) {
987 my $vmlist = $plugin->vmlist();
988 foreach my $vmid (sort @$vmlist) {
989 next if grep { $_ eq $vmid } @{$opts->{exclude}};
98e84b16 990 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
aaeeeebe
DM
991 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
992 }
993 }
994 } else {
995 foreach my $vmid (sort @{$opts->{vmids}}) {
996 my $plugin;
997 foreach my $pg (@{$self->{plugins}}) {
998 my $vmlist = $pg->vmlist();
999 if (grep { $_ eq $vmid } @$vmlist) {
1000 $plugin = $pg;
1001 last;
1002 }
1003 }
98e84b16 1004 $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
aaeeeebe
DM
1005 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
1006 }
1007 }
1008
1009 my $starttime = time();
1010 my $errcount = 0;
1011 eval {
1012
1013 $self->run_hook_script ('job-start');
1014
1015 foreach my $task (@$tasklist) {
1016 $self->exec_backup_task ($task);
1017 $errcount += 1 if $task->{state} ne 'ok';
1018 }
1019
1020 $self->run_hook_script ('job-end');
1021 };
1022 my $err = $@;
1023
1024 $self->run_hook_script ('job-abort') if $err;
1025
1026 if ($err) {
1027 debugmsg ('err', "Backup job failed - $err", undef, 1);
1028 } else {
1029 if ($errcount) {
1030 debugmsg ('info', "Backup job finished with errors", undef, 1);
1031 } else {
61ca4432 1032 debugmsg ('info', "Backup job finished successfully", undef, 1);
aaeeeebe
DM
1033 }
1034 }
1035
1036 my $totaltime = time() - $starttime;
1037
1038 eval { $self->$sendmail ($tasklist, $totaltime); };
1039 debugmsg ('err', $@) if $@;
4a4051d8
DM
1040
1041 die $err if $err;
1042
1043 die "job errors\n" if $errcount;
aaeeeebe
DM
1044}
1045
ac27b58d
DM
1046my $confdesc = {
1047 vmid => {
1048 type => 'string', format => 'pve-vmid-list',
1049 description => "The ID of the VM you want to backup.",
1050 optional => 1,
1051 },
1052 node => get_standard_option('pve-node', {
1053 description => "Only run if executed on this node.",
1054 optional => 1,
1055 }),
1056 all => {
1057 type => 'boolean',
1058 description => "Backup all known VMs on this host.",
1059 optional => 1,
1060 default => 0,
1061 },
1062 stdexcludes => {
1063 type => 'boolean',
1064 description => "Exclude temorary files and logs.",
1065 optional => 1,
1066 default => 1,
1067 },
1068 compress => {
d7550e09
DM
1069 type => 'string',
1070 description => "Compress dump file.",
ac27b58d 1071 optional => 1,
d7550e09
DM
1072 enum => ['0', '1', 'gzip', 'lzo'],
1073 default => 'lzo',
ac27b58d
DM
1074 },
1075 quiet => {
1076 type => 'boolean',
1077 description => "Be quiet.",
1078 optional => 1,
1079 default => 0,
1080 },
47664cbe
DM
1081 mode => {
1082 type => 'string',
1083 description => "Backup mode.",
ac27b58d 1084 optional => 1,
47664cbe
DM
1085 default => 'stop',
1086 enum => [ 'snapshot', 'suspend', 'stop' ],
ac27b58d
DM
1087 },
1088 exclude => {
1089 type => 'string', format => 'pve-vmid-list',
1090 description => "exclude specified VMs (assumes --all)",
1091 optional => 1,
1092 },
1093 'exclude-path' => {
1094 type => 'string', format => 'string-alist',
1095 description => "exclude certain files/directories (regex).",
1096 optional => 1,
1097 },
1098 mailto => {
1099 type => 'string', format => 'string-list',
1100 description => "",
1101 optional => 1,
1102 },
1103 tmpdir => {
1104 type => 'string',
1105 description => "Store temporary files to specified directory.",
1106 optional => 1,
1107 },
1108 dumpdir => {
1109 type => 'string',
1110 description => "Store resulting files to specified directory.",
1111 optional => 1,
1112 },
1113 script => {
1114 type => 'string',
1115 description => "Use specified hook script.",
1116 optional => 1,
1117 },
1118 storage => get_standard_option('pve-storage-id', {
1119 description => "Store resulting file to this storage.",
1120 optional => 1,
1121 }),
1122 size => {
1123 type => 'integer',
1124 description => "LVM snapshot size im MB.",
1125 optional => 1,
1126 minimum => 500,
1127 },
1128 bwlimit => {
1129 type => 'integer',
1130 description => "Limit I/O bandwidth (KBytes per second).",
1131 optional => 1,
1132 minimum => 0,
1133 },
1134 ionice => {
1135 type => 'integer',
1136 description => "Set CFQ ionice priority.",
1137 optional => 1,
1138 minimum => 0,
1139 maximum => 8,
1140 },
1141 lockwait => {
1142 type => 'integer',
1143 description => "Maximal time to wait for the global lock (minutes).",
1144 optional => 1,
1145 minimum => 0,
1146 },
1147 stopwait => {
1148 type => 'integer',
1149 description => "Maximal time to wait until a VM is stopped (minutes).",
1150 optional => 1,
1151 minimum => 0,
1152 },
1153 maxfiles => {
1154 type => 'integer',
1155 description => "Maximal number of backup files per VM.",
1156 optional => 1,
1157 minimum => 1,
1158 },
1159};
1160
47664cbe
DM
1161sub option_exists {
1162 my $key = shift;
1163 return defined($confdesc->{$key});
1164}
1165
ac27b58d
DM
1166# add JSON properties for create and set function
1167sub json_config_properties {
1168 my $prop = shift;
1169
1170 foreach my $opt (keys %$confdesc) {
1171 $prop->{$opt} = $confdesc->{$opt};
1172 }
1173
1174 return $prop;
1175}
1176
31aef761
DM
1177sub verify_vzdump_parameters {
1178 my ($param, $check_missing) = @_;
1179
1180 raise_param_exc({ all => "option conflicts with option 'vmid'"})
1181 if $param->{all} && $param->{vmid};
1182
1183 raise_param_exc({ exclude => "option conflicts with option 'vmid'"})
1184 if $param->{exclude} && $param->{vmid};
1185
1186 $param->{all} = 1 if defined($param->{exclude});
1187
1188 return if !$check_missing;
1189
1190 raise_param_exc({ vmid => "property is missing"})
1191 if !$param->{all} && !$param->{vmid};
1192
1193}
1194
1195sub command_line {
1196 my ($param) = @_;
1197
1198 my $cmd = "vzdump";
1199
1200 if ($param->{vmid}) {
1201 $cmd .= " " . join(' ', PVE::Tools::split_list($param->{vmid}));
1202 }
1203
1204 foreach my $p (keys %$param) {
1205 next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
1206 my $v = $param->{$p};
1207 my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
f4a8bab4
DM
1208 if ($p eq 'exclude-path') {
1209 foreach my $path (split(/\0/, $v || '')) {
1210 $cmd .= " --$p " . PVE::Tools::shellquote($path);
1211 }
1212 } else {
1213 $cmd .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
1214 }
31aef761
DM
1215 }
1216
1217 return $cmd;
1218}
1219
aaeeeebe 12201;