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