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