]> git.proxmox.com Git - pve-manager.git/blame - PVE/VZDump.pm
check if user is enabled after verifying a ticket
[pve-manager.git] / PVE / VZDump.pm
CommitLineData
aaeeeebe
DM
1package PVE::VZDump;
2
aaeeeebe
DM
3use strict;
4use warnings;
df2d3636 5
aaeeeebe 6use Fcntl ':flock';
df2d3636 7use File::Path;
aaeeeebe
DM
8use IO::File;
9use IO::Select;
10use IPC::Open3;
e2b5eb29 11use POSIX qw(strftime);
aaeeeebe 12use Time::Local;
df2d3636
TL
13
14use PVE::Cluster qw(cfs_read_file);
15use PVE::DataCenterConfig;
16use PVE::Exception qw(raise_param_exc);
2de3ee58 17use PVE::HA::Config;
df2d3636
TL
18use PVE::HA::Env::PVE2;
19use PVE::JSONSchema qw(get_standard_option);
20use PVE::RPCEnvironment;
21use PVE::Storage;
2424074e 22use PVE::VZDump::Common;
df2d3636 23use PVE::VZDump::Plugin;
aaeeeebe
DM
24
25my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
26
27my $lockfile = '/var/run/vzdump.lock';
8682f6fc 28my $pidfile = '/var/run/vzdump.pid';
aaeeeebe
DM
29my $logdir = '/var/log/vzdump';
30
dafb6246 31my @plugins = qw();
aaeeeebe 32
2424074e 33my $confdesc = PVE::VZDump::Common::get_confdesc();
cc61ea36 34
aaeeeebe 35# Load available plugins
8187f6b0
DM
36my @pve_vzdump_classes = qw(PVE::VZDump::QemuServer PVE::VZDump::LXC);
37foreach my $plug (@pve_vzdump_classes) {
38 my $filename = "/usr/share/perl5/$plug.pm";
39 $filename =~ s!::!/!g;
40 if (-f $filename) {
41 eval { require $filename; };
42 if (!$@) {
43 $plug->import ();
44 push @plugins, $plug;
45 } else {
c76d0106 46 die $@;
8187f6b0
DM
47 }
48 }
aaeeeebe
DM
49}
50
51# helper functions
52
aaeeeebe
DM
53sub debugmsg {
54 my ($mtype, $msg, $logfd, $syslog) = @_;
55
a5c94797 56 PVE::VZDump::Plugin::debugmsg(@_);
aaeeeebe
DM
57}
58
59sub run_command {
60 my ($logfd, $cmdstr, %param) = @_;
61
7f910306 62 my $logfunc = sub {
4a4051d8 63 my $line = shift;
4a4051d8 64 debugmsg ('info', $line, $logfd);
aaeeeebe
DM
65 };
66
7f910306 67 PVE::Tools::run_command($cmdstr, %param, logfunc => $logfunc);
aaeeeebe
DM
68}
69
70sub storage_info {
71 my $storage = shift;
72
bbcfdc08 73 my $cfg = PVE::Storage::config();
4a4051d8 74 my $scfg = PVE::Storage::storage_config($cfg, $storage);
aaeeeebe 75 my $type = $scfg->{type};
60e049c2
TM
76
77 die "can't use storage type '$type' for backup\n"
6d09915c 78 if (!($type eq 'dir' || $type eq 'nfs' || $type eq 'glusterfs'
1a87db9e 79 || $type eq 'cifs' || $type eq 'cephfs' || $type eq 'pbs'));
60e049c2 80 die "can't use storage '$storage' for backups - wrong content type\n"
aaeeeebe
DM
81 if (!$scfg->{content}->{backup});
82
4a4051d8 83 PVE::Storage::activate_storage($cfg, $storage);
aaeeeebe 84
1a87db9e
DM
85 if ($type eq 'pbs') {
86 return {
87 scfg => $scfg,
88 maxfiles => $scfg->{maxfiles},
89 };
90 } else {
91 return {
92 scfg => $scfg,
93 dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
94 maxfiles => $scfg->{maxfiles},
95 };
96 }
aaeeeebe
DM
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);
60e049c2 115 }
aaeeeebe
DM
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
cc61ea36 179 my $defaults = {
1ab98f05
WB
180 map {
181 my $default = $confdesc->{$_}->{default};
182 defined($default) ? ($_ => $default) : ()
183 } keys %$confdesc
aaeeeebe
DM
184 };
185
cc61ea36
TL
186 my $raw;
187 eval { $raw = PVE::Tools::file_get_contents($fn); };
188 return $defaults if $@;
aaeeeebe 189
cc61ea36
TL
190 my $conf_schema = { type => 'object', properties => $confdesc, };
191 my $res = PVE::JSONSchema::parse_config($conf_schema, $fn, $raw);
6368bbd4
WB
192 if (my $excludes = $res->{'exclude-path'}) {
193 $res->{'exclude-path'} = PVE::Tools::split_args($excludes);
194 }
74b47fd8
FG
195 if (defined($res->{mailto})) {
196 my @mailto = PVE::Tools::split_list($res->{mailto});
197 $res->{mailto} = [ @mailto ];
198 }
cc61ea36
TL
199
200 foreach my $key (keys %$defaults) {
e94fa5cb 201 $res->{$key} = $defaults->{$key} if !defined($res->{$key});
aaeeeebe 202 }
aaeeeebe
DM
203
204 return $res;
205}
206
ce84a950 207use constant MAX_MAIL_SIZE => 1024*1024;
6ec9de44 208sub sendmail {
44b21574 209 my ($self, $tasklist, $totaltime, $err, $detail_pre, $detail_post) = @_;
aaeeeebe
DM
210
211 my $opts = $self->{opts};
212
213 my $mailto = $opts->{mailto};
214
4a4051d8 215 return if !($mailto && scalar(@$mailto));
aaeeeebe
DM
216
217 my $cmdline = $self->{cmdline};
218
219 my $ecount = 0;
220 foreach my $task (@$tasklist) {
221 $ecount++ if $task->{state} ne 'ok';
222 chomp $task->{msg} if $task->{msg};
223 $task->{backuptime} = 0 if !$task->{backuptime};
224 $task->{size} = 0 if !$task->{size};
225 $task->{tarfile} = 'unknown' if !$task->{tarfile};
226 $task->{hostname} = "VM $task->{vmid}" if !$task->{hostname};
227
228 if ($task->{state} eq 'todo') {
229 $task->{msg} = 'aborted';
230 }
231 }
232
02e59759
DM
233 my $notify = $opts->{mailnotification} || 'always';
234 return if (!$ecount && !$err && ($notify eq 'failure'));
8b4b4860 235
6ec9de44 236 my $stat = ($ecount || $err) ? 'backup failed' : 'backup successful';
403761c4
WB
237 if ($err) {
238 if ($err =~ /\n/) {
239 $stat .= ": multiple problems";
240 } else {
241 $stat .= ": $err";
242 $err = undef;
243 }
244 }
aaeeeebe 245
4a4051d8 246 my $hostname = `hostname -f` || PVE::INotify::nodename();
aaeeeebe
DM
247 chomp $hostname;
248
aaeeeebe 249 # text part
403761c4
WB
250 my $text = $err ? "$err\n\n" : '';
251 $text .= sprintf ("%-10s %-6s %10s %10s %s\n", qw(VMID STATUS TIME SIZE FILENAME));
aaeeeebe
DM
252 foreach my $task (@$tasklist) {
253 my $vmid = $task->{vmid};
254 if ($task->{state} eq 'ok') {
255
7a0a8e67
TL
256 $text .= sprintf ("%-10s %-6s %10s %10s %s\n", $vmid,
257 $task->{state},
aaeeeebe
DM
258 format_time($task->{backuptime}),
259 format_size ($task->{size}),
260 $task->{tarfile});
261 } else {
7a0a8e67
TL
262 $text .= sprintf ("%-10s %-6s %10s %8.2fMB %s\n", $vmid,
263 $task->{state},
aaeeeebe
DM
264 format_time($task->{backuptime}),
265 0, '-');
266 }
267 }
7a0a8e67 268
ce84a950
DJ
269 my $text_log_part;
270 $text_log_part .= "\nDetailed backup logs:\n\n";
271 $text_log_part .= "$cmdline\n\n";
aaeeeebe 272
ce84a950 273 $text_log_part .= $detail_pre . "\n" if defined($detail_pre);
aaeeeebe
DM
274 foreach my $task (@$tasklist) {
275 my $vmid = $task->{vmid};
276 my $log = $task->{tmplog};
277 if (!$log) {
ce84a950 278 $text_log_part .= "$vmid: no log available\n\n";
aaeeeebe
DM
279 next;
280 }
ce84a950
DJ
281 if (open (TMP, "$log")) {
282 while (my $line = <TMP>) {
283 next if $line =~ /^status: \d+/; # not useful in mails
284 $text_log_part .= encode8bit ("$vmid: $line");
285 }
286 } else {
287 $text_log_part .= "$vmid: Could not open log file\n\n";
288 }
aaeeeebe 289 close (TMP);
ce84a950 290 $text_log_part .= "\n";
aaeeeebe 291 }
ce84a950 292 $text_log_part .= $detail_post if defined($detail_post);
aaeeeebe 293
aaeeeebe 294 # html part
7a0a8e67 295 my $html = "<html><body>\n";
403761c4 296 $html .= "<p>" . (escape_html($err) =~ s/\n/<br>/gr) . "</p>\n" if $err;
7a0a8e67
TL
297 $html .= "<table border=1 cellpadding=3>\n";
298 $html .= "<tr><td>VMID<td>NAME<td>STATUS<td>TIME<td>SIZE<td>FILENAME</tr>\n";
aaeeeebe
DM
299
300 my $ssize = 0;
301
302 foreach my $task (@$tasklist) {
303 my $vmid = $task->{vmid};
304 my $name = $task->{hostname};
305
306 if ($task->{state} eq 'ok') {
307
308 $ssize += $task->{size};
309
7a0a8e67 310 $html .= sprintf ("<tr><td>%s<td>%s<td>OK<td>%s<td align=right>%s<td>%s</tr>\n",
aaeeeebe
DM
311 $vmid, $name,
312 format_time($task->{backuptime}),
313 format_size ($task->{size}),
314 escape_html ($task->{tarfile}));
315 } else {
7a0a8e67
TL
316 $html .= sprintf ("<tr><td>%s<td>%s<td><font color=red>FAILED<td>%s<td colspan=2>%s</tr>\n",
317 $vmid, $name, format_time($task->{backuptime}),
aaeeeebe
DM
318 escape_html ($task->{msg}));
319 }
320 }
321
7a0a8e67 322 $html .= sprintf ("<tr><td align=left colspan=3>TOTAL<td>%s<td>%s<td></tr>",
aaeeeebe
DM
323 format_time ($totaltime), format_size ($ssize));
324
ce84a950
DJ
325 $html .= "\n</table><br><br>\n";
326 my $html_log_part;
327 $html_log_part .= "Detailed backup logs:<br /><br />\n";
328 $html_log_part .= "<pre>\n";
329 $html_log_part .= escape_html($cmdline) . "\n\n";
aaeeeebe 330
ce84a950 331 $html_log_part .= escape_html($detail_pre) . "\n" if defined($detail_pre);
aaeeeebe
DM
332 foreach my $task (@$tasklist) {
333 my $vmid = $task->{vmid};
334 my $log = $task->{tmplog};
335 if (!$log) {
ce84a950 336 $html_log_part .= "$vmid: no log available\n\n";
aaeeeebe
DM
337 next;
338 }
ce84a950
DJ
339 if (open (TMP, "$log")) {
340 while (my $line = <TMP>) {
341 next if $line =~ /^status: \d+/; # not useful in mails
342 if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
343 $html_log_part .= encode8bit ("$vmid: <font color=red>".
344 escape_html ($line) . "</font>");
345 } else {
346 $html_log_part .= encode8bit ("$vmid: " . escape_html ($line));
347 }
aaeeeebe 348 }
ce84a950
DJ
349 } else {
350 $html_log_part .= "$vmid: Could not open log file\n\n";
aaeeeebe
DM
351 }
352 close (TMP);
ce84a950 353 $html_log_part .= "\n";
aaeeeebe 354 }
ce84a950
DJ
355 $html_log_part .= escape_html($detail_post) if defined($detail_post);
356 $html_log_part .= "</pre>";
357 my $html_end .= "\n</body></html>\n";
7a0a8e67 358 # end html part
aaeeeebe 359
ce84a950
DJ
360 if (length($text) + length($text_log_part) +
361 length($html) + length($html_log_part) < MAX_MAIL_SIZE)
362 {
363 $html .= $html_log_part;
364 $text .= $text_log_part;
365 } else {
366 my $msg = "Log output was too long to be sent by mail. ".
367 "See Task History for details!\n";
368 $text .= $msg;
369 $html .= "<p>$msg</p>";
370 $html .= $html_end;
371 }
372
7a0a8e67 373 my $subject = "vzdump backup status ($hostname) : $stat";
aaeeeebe 374
7a0a8e67
TL
375 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
376 my $mailfrom = $dcconf->{email_from} || "root";
aaeeeebe 377
7a0a8e67 378 PVE::Tools::sendmail($mailto, $subject, $text, $html, $mailfrom, "vzdump backup tool");
aaeeeebe
DM
379};
380
381sub new {
a7e42354 382 my ($class, $cmdline, $opts, $skiplist) = @_;
aaeeeebe
DM
383
384 mkpath $logdir;
385
386 check_bin ('cp');
387 check_bin ('df');
388 check_bin ('sendmail');
389 check_bin ('rsync');
390 check_bin ('tar');
391 check_bin ('mount');
392 check_bin ('umount');
393 check_bin ('cstream');
394 check_bin ('ionice');
395
47664cbe 396 if ($opts->{mode} && $opts->{mode} eq 'snapshot') {
aaeeeebe
DM
397 check_bin ('lvcreate');
398 check_bin ('lvs');
399 check_bin ('lvremove');
400 }
401
402 my $defaults = read_vzdump_defaults();
403
84ad4385
DM
404 my $maxfiles = $opts->{maxfiles}; # save here, because we overwrite with default
405
899b8373
DM
406 $opts->{remove} = 1 if !defined($opts->{remove});
407
aaeeeebe 408 foreach my $k (keys %$defaults) {
b1f09117 409 next if $k eq 'exclude-path'; # dealt with separately
aaeeeebe
DM
410 if ($k eq 'dumpdir' || $k eq 'storage') {
411 $opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir}) &&
412 !defined ($opts->{storage});
413 } else {
414 $opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
415 }
416 }
417
aaeeeebe
DM
418 $opts->{dumpdir} =~ s|/+$|| if ($opts->{dumpdir});
419 $opts->{tmpdir} =~ s|/+$|| if ($opts->{tmpdir});
420
a7e42354
DM
421 $skiplist = [] if !$skiplist;
422 my $self = bless { cmdline => $cmdline, opts => $opts, skiplist => $skiplist };
aaeeeebe 423
45f16a06 424 my $findexcl = $self->{findexcl} = [];
f4a8bab4 425 if ($defaults->{'exclude-path'}) {
45f16a06 426 push @$findexcl, @{$defaults->{'exclude-path'}};
f4a8bab4
DM
427 }
428
aaeeeebe 429 if ($opts->{'exclude-path'}) {
45f16a06 430 push @$findexcl, @{$opts->{'exclude-path'}};
aaeeeebe
DM
431 }
432
433 if ($opts->{stdexcludes}) {
e42ae622 434 push @$findexcl, '/tmp/?*',
45f16a06 435 '/var/tmp/?*',
0568e342 436 '/var/run/?*.pid';
aaeeeebe
DM
437 }
438
439 foreach my $p (@plugins) {
440
441 my $pd = $p->new ($self);
442
443 push @{$self->{plugins}}, $pd;
aaeeeebe
DM
444 }
445
1a87db9e
DM
446 if (defined($opts->{storage}) && $opts->{stdout}) {
447 die "unable to use option 'storage' with option 'stdout'\n";
448 }
449
aaeeeebe 450 if (!$opts->{dumpdir} && !$opts->{storage}) {
19d5c0f2 451 $opts->{storage} = 'local';
aaeeeebe
DM
452 }
453
403761c4
WB
454 my $errors = '';
455
aaeeeebe 456 if ($opts->{storage}) {
1e4583d5 457 my $info = eval { storage_info ($opts->{storage}) };
c3b58274
FG
458 $errors .= "could not get storage information for '$opts->{storage}': $@"
459 if ($@);
aaeeeebe 460 $opts->{dumpdir} = $info->{dumpdir};
1a87db9e 461 $opts->{scfg} = $info->{scfg};
1e4583d5 462 $maxfiles //= $info->{maxfiles};
aaeeeebe 463 } elsif ($opts->{dumpdir}) {
403761c4 464 $errors .= "dumpdir '$opts->{dumpdir}' does not exist"
aaeeeebe
DM
465 if ! -d $opts->{dumpdir};
466 } else {
60e049c2 467 die "internal error";
aaeeeebe
DM
468 }
469
470 if ($opts->{tmpdir} && ! -d $opts->{tmpdir}) {
403761c4
WB
471 $errors .= "\n" if $errors;
472 $errors .= "tmpdir '$opts->{tmpdir}' does not exist";
473 }
474
475 if ($errors) {
476 eval { $self->sendmail([], 0, $errors); };
477 debugmsg ('err', $@) if $@;
478 die "$errors\n";
aaeeeebe
DM
479 }
480
84ad4385 481 $opts->{maxfiles} = $maxfiles if defined($maxfiles);
899b8373 482
aaeeeebe
DM
483 return $self;
484
485}
486
aaeeeebe
DM
487sub get_mount_info {
488 my ($dir) = @_;
489
8572d646
DM
490 # Note: df 'available' can be negative, and percentage set to '-'
491
d93d0459 492 my $cmd = [ 'df', '-P', '-T', '-B', '1', $dir];
aaeeeebe 493
d93d0459 494 my $res;
aaeeeebe 495
d93d0459
DM
496 my $parser = sub {
497 my $line = shift;
8572d646
DM
498 if (my ($fsid, $fstype, undef, $mp) = $line =~
499 m!(\S+.*)\s+(\S+)\s+\d+\s+\-?\d+\s+\d+\s+(\d+%|-)\s+(/.*)$!) {
d93d0459
DM
500 $res = {
501 device => $fsid,
502 fstype => $fstype,
503 mountpoint => $mp,
504 };
505 }
aaeeeebe 506 };
d93d0459
DM
507
508 eval { PVE::Tools::run_command($cmd, errfunc => sub {}, outfunc => $parser); };
509 warn $@ if $@;
510
511 return $res;
aaeeeebe
DM
512}
513
aaeeeebe 514sub getlock {
eab837c4 515 my ($self, $upid) = @_;
aaeeeebe 516
8682f6fc 517 my $fh;
60e049c2 518
aaeeeebe 519 my $maxwait = $self->{opts}->{lockwait} || $self->{lockwait};
60e049c2 520
eab837c4
DM
521 die "missimg UPID" if !$upid; # should not happen
522
aaeeeebe
DM
523 if (!open (SERVER_FLCK, ">>$lockfile")) {
524 debugmsg ('err', "can't open lock on file '$lockfile' - $!", undef, 1);
6ec9de44 525 die "can't open lock on file '$lockfile' - $!";
aaeeeebe
DM
526 }
527
eab837c4 528 if (!flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
aaeeeebe 529
eab837c4 530 if (!$maxwait) {
76189130
TL
531 debugmsg ('err', "can't acquire lock '$lockfile' (wait = 0)", undef, 1);
532 die "can't acquire lock '$lockfile' (wait = 0)";
eab837c4 533 }
aaeeeebe 534
eab837c4 535 debugmsg('info', "trying to get global lock - waiting...", undef, 1);
aaeeeebe 536
eab837c4
DM
537 eval {
538 alarm ($maxwait * 60);
60e049c2 539
eab837c4 540 local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
aaeeeebe 541
eab837c4
DM
542 if (!flock (SERVER_FLCK, LOCK_EX)) {
543 my $err = $!;
544 close (SERVER_FLCK);
545 alarm (0);
546 die "$err\n";
547 }
aaeeeebe 548 alarm (0);
eab837c4 549 };
aaeeeebe 550 alarm (0);
60e049c2 551
eab837c4 552 my $err = $@;
60e049c2 553
eab837c4 554 if ($err) {
76189130
TL
555 debugmsg ('err', "can't acquire lock '$lockfile' - $err", undef, 1);
556 die "can't acquire lock '$lockfile' - $err";
eab837c4 557 }
aaeeeebe 558
eab837c4 559 debugmsg('info', "got global lock", undef, 1);
aaeeeebe
DM
560 }
561
eab837c4 562 PVE::Tools::file_set_contents($pidfile, $upid);
aaeeeebe
DM
563}
564
565sub run_hook_script {
566 my ($self, $phase, $task, $logfd) = @_;
567
568 my $opts = $self->{opts};
569
570 my $script = $opts->{script};
571
572 return if !$script;
573
574 my $cmd = "$script $phase";
575
576 $cmd .= " $task->{mode} $task->{vmid}" if ($task);
577
578 local %ENV;
579
28272ca2
DM
580 # set immutable opts directly (so they are available in all phases)
581 $ENV{STOREID} = $opts->{storage} if $opts->{storage};
582 $ENV{DUMPDIR} = $opts->{dumpdir} if $opts->{dumpdir};
583
584 foreach my $ek (qw(vmtype hostname tarfile logfile)) {
aaeeeebe
DM
585 $ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
586 }
587
588 run_command ($logfd, $cmd);
589}
590
d7550e09 591sub compressor_info {
778d5b6d
TL
592 my ($opts) = @_;
593 my $opt_compress = $opts->{compress};
d7550e09
DM
594
595 if (!$opt_compress || $opt_compress eq '0') {
596 return undef;
597 } elsif ($opt_compress eq '1' || $opt_compress eq 'lzo') {
598 return ('lzop', 'lzo');
599 } elsif ($opt_compress eq 'gzip') {
778d5b6d 600 if ($opts->{pigz} > 0) {
8555b100
TL
601 my $pigz_threads = $opts->{pigz};
602 if ($pigz_threads == 1) {
603 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
604 $pigz_threads = int(($cpuinfo->{cpus} + 1)/2);
605 }
819e4ff5 606 return ("pigz -p ${pigz_threads} --rsyncable", 'gz');
778d5b6d 607 } else {
e953f92a 608 return ('gzip --rsyncable', 'gz');
778d5b6d 609 }
d7550e09
DM
610 } else {
611 die "internal error - unknown compression option '$opt_compress'";
612 }
613}
899b8373
DM
614
615sub get_backup_file_list {
616 my ($dir, $bkname, $exclude_fn) = @_;
617
618 my $bklist = [];
619 foreach my $fn (<$dir/${bkname}-*>) {
620 next if $exclude_fn && $fn eq $exclude_fn;
757fd3d5 621 if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(gz|lzo))?)))$!) {
899b8373 622 $fn = "$dir/$1"; # untaint
998e4eeb 623 my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2);
899b8373
DM
624 push @$bklist, [$fn, $t];
625 }
626 }
627
628 return $bklist;
629}
60e049c2 630
aaeeeebe
DM
631sub exec_backup_task {
632 my ($self, $task) = @_;
60e049c2 633
aaeeeebe
DM
634 my $opts = $self->{opts};
635
636 my $vmid = $task->{vmid};
637 my $plugin = $task->{plugin};
1a87db9e
DM
638 my $vmtype = $plugin->type();
639
640 $task->{backup_time} = time();
641
642 my $pbs_group_name;
643 my $pbs_snapshot_name;
644
645 if ($opts->{scfg}->{type} eq 'pbs') {
646 if ($vmtype eq 'lxc') {
647 $pbs_group_name = "ct/$vmid";
648 } elsif ($vmtype eq 'qemu') {
649 $pbs_group_name = "vm/$vmid";
650 } else {
651 die "pbs backup not implemented for plugin type '$vmtype'\n";
652 }
653 my $btime = strftime("%FT%TZ", gmtime($task->{backup_time}));
654 $pbs_snapshot_name = "$pbs_group_name/$btime";
655 }
aaeeeebe
DM
656
657 my $vmstarttime = time ();
60e049c2 658
aaeeeebe
DM
659 my $logfd;
660
661 my $cleanup = {};
662
4e0947c8
TL
663 my $log_vm_online_again = sub {
664 return if !defined($task->{vmstoptime});
665 $task->{vmconttime} //= time();
666 my $delay = $task->{vmconttime} - $task->{vmstoptime};
667 debugmsg ('info', "guest is online again after $delay seconds", $logfd);
668 };
aaeeeebe
DM
669
670 eval {
671 die "unable to find VM '$vmid'\n" if !$plugin;
672
2de3ee58 673 # for now we deny backups of a running ha managed service in *stop* mode
83ec8f81 674 # as it interferes with the HA stack (started services should not stop).
2de3ee58 675 if ($opts->{mode} eq 'stop' &&
83ec8f81 676 PVE::HA::Config::vm_is_ha_managed($vmid, 'started'))
2de3ee58
TL
677 {
678 die "Cannot execute a backup with stop mode on a HA managed and".
679 " enabled Service. Use snapshot mode or disable the Service.\n";
680 }
681
aaeeeebe
DM
682 my $tmplog = "$logdir/$vmtype-$vmid.log";
683
aaeeeebe 684 my $bkname = "vzdump-$vmtype-$vmid";
1a87db9e 685 my $basename = $bkname . strftime("-%Y_%m_%d-%H_%M_%S", localtime($task->{backup_time}));
aaeeeebe 686
899b8373
DM
687 my $maxfiles = $opts->{maxfiles};
688
689 if ($maxfiles && !$opts->{remove}) {
1a87db9e
DM
690 my $count;
691 if ($opts->{scfg}->{type} eq 'pbs') {
692 my $res = PVE::Storage::PBSPlugin::run_client_cmd($opts->{scfg}, $opts->{storage}, 'snapshots', $pbs_group_name);
693 $count = scalar(@$res);
694 } else {
695 my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname);
696 $count = scalar(@$bklist);
697 }
6c09101a 698 die "There is a max backup limit of ($maxfiles) enforced by the".
1a87db9e
DM
699 " target storage or the vzdump parameters.".
700 " Either increase the limit or delete old backup(s).\n"
701 if $count >= $maxfiles;
899b8373
DM
702 }
703
1a87db9e
DM
704 if ($opts->{scfg}->{type} ne 'pbs') {
705 $task->{logfile} = "$opts->{dumpdir}/$basename.log";
706 }
aaeeeebe 707
757fd3d5 708 my $ext = $vmtype eq 'qemu' ? '.vma' : '.tar';
778d5b6d 709 my ($comp, $comp_ext) = compressor_info($opts);
d7550e09
DM
710 if ($comp && $comp_ext) {
711 $ext .= ".${comp_ext}";
712 }
aaeeeebe 713
1a87db9e
DM
714 if ($opts->{scfg}->{type} eq 'pbs') {
715 die "unable to pipe backup to stdout\n" if $opts->{stdout};
aaeeeebe 716 } else {
1a87db9e
DM
717 if ($opts->{stdout}) {
718 $task->{tarfile} = '-';
719 } else {
720 my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
721 $task->{tmptar} = $task->{tarfile};
722 $task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
723 unlink $task->{tmptar};
724 }
aaeeeebe
DM
725 }
726
727 $task->{vmtype} = $vmtype;
728
1a87db9e
DM
729 if ($opts->{scfg}->{type} eq 'pbs') {
730 $task->{tmpdir} = "/var/tmp/vzdumptmp$$"; #fixme
731 } elsif ($opts->{tmpdir}) {
60e049c2 732 $task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
aaeeeebe
DM
733 } else {
734 # dumpdir is posix? then use it as temporary dir
5dc86eb8 735 my $info = get_mount_info($opts->{dumpdir});
60e049c2 736 if ($vmtype eq 'qemu' ||
aaeeeebe
DM
737 grep ($_ eq $info->{fstype}, @posix_filesystems)) {
738 $task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
739 } else {
740 $task->{tmpdir} = "/var/tmp/vzdumptmp$$";
741 debugmsg ('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
742 "using $task->{tmpdir} for temporary files", $logfd);
743 }
744 }
745
746 rmtree $task->{tmpdir};
747 mkdir $task->{tmpdir};
748 -d $task->{tmpdir} ||
749 die "unable to create temporary directory '$task->{tmpdir}'";
750
751 $logfd = IO::File->new (">$tmplog") ||
752 die "unable to create log file '$tmplog'";
753
754 $task->{dumpdir} = $opts->{dumpdir};
ff00abe6 755 $task->{storeid} = $opts->{storage};
1a87db9e 756 $task->{scfg} = $opts->{scfg};
aaeeeebe
DM
757 $task->{tmplog} = $tmplog;
758
1a87db9e 759 unlink $task->{logfile} if defined($task->{logfile});
aaeeeebe 760
29f26f6d
DJ
761 debugmsg ('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
762 debugmsg ('info', "Backup started at " . strftime("%F %H:%M:%S", localtime()));
aaeeeebe
DM
763
764 $plugin->set_logfd ($logfd);
765
766 # test is VM is running
767 my ($running, $status_text) = $plugin->vm_status ($vmid);
768
769 debugmsg ('info', "status = ${status_text}", $logfd);
770
771 # lock VM (prevent config changes)
772 $plugin->lock_vm ($vmid);
773
774 $cleanup->{unlock} = 1;
775
776 # prepare
777
6acb632a 778 my $mode = $running ? $task->{mode} : 'stop';
aaeeeebe
DM
779
780 if ($mode eq 'snapshot') {
781 my %saved_task = %$task;
782 eval { $plugin->prepare ($task, $vmid, $mode); };
783 if (my $err = $@) {
784 die $err if $err !~ m/^mode failure/;
785 debugmsg ('info', $err, $logfd);
786 debugmsg ('info', "trying 'suspend' mode instead", $logfd);
787 $mode = 'suspend'; # so prepare is called again below
60e049c2 788 %$task = %saved_task;
aaeeeebe
DM
789 }
790 }
791
6acb632a
WB
792 $cleanup->{prepared} = 1;
793
aaeeeebe
DM
794 $task->{mode} = $mode;
795
796 debugmsg ('info', "backup mode: $mode", $logfd);
797
798 debugmsg ('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
799 if $opts->{bwlimit};
800
801 debugmsg ('info', "ionice priority: $opts->{ionice}", $logfd);
802
803 if ($mode eq 'stop') {
804
805 $plugin->prepare ($task, $vmid, $mode);
806
807 $self->run_hook_script ('backup-start', $task, $logfd);
808
809 if ($running) {
810 debugmsg ('info', "stopping vm", $logfd);
4e0947c8 811 $task->{vmstoptime} = time();
aaeeeebe
DM
812 $self->run_hook_script ('pre-stop', $task, $logfd);
813 $plugin->stop_vm ($task, $vmid);
814 $cleanup->{restart} = 1;
815 }
60e049c2 816
aaeeeebe
DM
817
818 } elsif ($mode eq 'suspend') {
819
820 $plugin->prepare ($task, $vmid, $mode);
821
822 $self->run_hook_script ('backup-start', $task, $logfd);
823
8187f6b0
DM
824 if ($vmtype eq 'lxc') {
825 # pre-suspend rsync
826 $plugin->copy_data_phase1($task, $vmid);
827 }
828
aaeeeebe 829 debugmsg ('info', "suspend vm", $logfd);
4e0947c8 830 $task->{vmstoptime} = time ();
aaeeeebe
DM
831 $self->run_hook_script ('pre-stop', $task, $logfd);
832 $plugin->suspend_vm ($task, $vmid);
833 $cleanup->{resume} = 1;
834
8187f6b0
DM
835 if ($vmtype eq 'lxc') {
836 # post-suspend rsync
837 $plugin->copy_data_phase2($task, $vmid);
838
839 debugmsg ('info', "resume vm", $logfd);
840 $cleanup->{resume} = 0;
841 $self->run_hook_script('pre-restart', $task, $logfd);
842 $plugin->resume_vm($task, $vmid);
d5047243 843 $self->run_hook_script('post-restart', $task, $logfd);
4e0947c8 844 $log_vm_online_again->();
8187f6b0 845 }
60e049c2 846
aaeeeebe
DM
847 } elsif ($mode eq 'snapshot') {
848
c5be0f8c
DM
849 $self->run_hook_script ('backup-start', $task, $logfd);
850
aaeeeebe
DM
851 my $snapshot_count = $task->{snapshot_count} || 0;
852
853 $self->run_hook_script ('pre-stop', $task, $logfd);
854
855 if ($snapshot_count > 1) {
856 debugmsg ('info', "suspend vm to make snapshot", $logfd);
4e0947c8 857 $task->{vmstoptime} = time ();
aaeeeebe
DM
858 $plugin->suspend_vm ($task, $vmid);
859 $cleanup->{resume} = 1;
860 }
861
862 $plugin->snapshot ($task, $vmid);
863
864 $self->run_hook_script ('pre-restart', $task, $logfd);
865
866 if ($snapshot_count > 1) {
867 debugmsg ('info', "resume vm", $logfd);
868 $cleanup->{resume} = 0;
869 $plugin->resume_vm ($task, $vmid);
4e0947c8 870 $log_vm_online_again->();
aaeeeebe
DM
871 }
872
d5047243
FG
873 $self->run_hook_script ('post-restart', $task, $logfd);
874
aaeeeebe
DM
875 } else {
876 die "internal error - unknown mode '$mode'\n";
877 }
878
879 # assemble archive image
880 $plugin->assemble ($task, $vmid);
60e049c2
TM
881
882 # produce archive
aaeeeebe
DM
883
884 if ($opts->{stdout}) {
885 debugmsg ('info', "sending archive to stdout", $logfd);
d7550e09 886 $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
aaeeeebe
DM
887 $self->run_hook_script ('backup-end', $task, $logfd);
888 return;
889 }
890
1a87db9e
DM
891 # fixme: ??
892 if ($opts->{scfg}->{type} eq 'pbs') {
893 debugmsg ('info', "creating pbs archive on storage '$opts->{storage}'", $logfd);
894 } else {
895 debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
896 }
d7550e09 897 $plugin->archive($task, $vmid, $task->{tmptar}, $comp);
aaeeeebe 898
1a87db9e
DM
899 if ($opts->{scfg}->{type} eq 'pbs') {
900 # fixme: log size ?
901 debugmsg ('info', "pbs upload finished", $logfd);
902 } else {
903 rename ($task->{tmptar}, $task->{tarfile}) ||
904 die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
aaeeeebe 905
1a87db9e
DM
906 # determine size
907 $task->{size} = (-s $task->{tarfile}) || 0;
908 my $cs = format_size ($task->{size});
909 debugmsg ('info', "archive file size: $cs", $logfd);
910 }
aaeeeebe
DM
911
912 # purge older backup
899b8373 913 if ($maxfiles && $opts->{remove}) {
1a87db9e
DM
914
915 if ($opts->{scfg}->{type} eq 'pbs') {
916 my $args = [$pbs_group_name, '--keep-last', $maxfiles];
917 my $logfunc = sub { my $line = shift; debugmsg ('info', $line, $logfd); };
918 PVE::Storage::PBSPlugin::run_raw_client_cmd(
919 $opts->{scfg}, $opts->{storage}, 'prune', $args, logfunc => $logfunc);
920 } else {
921 my $bklist = get_backup_file_list($opts->{dumpdir}, $bkname, $task->{tarfile});
922 $bklist = [ sort { $b->[1] <=> $a->[1] } @$bklist ];
923
924 while (scalar (@$bklist) >= $maxfiles) {
925 my $d = pop @$bklist;
926 debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
927 unlink $d->[0];
928 my $logfn = $d->[0];
929 $logfn =~ s/\.(tgz|((tar|vma)(\.(gz|lzo))?))$/\.log/;
930 unlink $logfn;
931 }
aaeeeebe
DM
932 }
933 }
934
935 $self->run_hook_script ('backup-end', $task, $logfd);
936 };
937 my $err = $@;
938
939 if ($plugin) {
940 # clean-up
941
942 if ($cleanup->{unlock}) {
943 eval { $plugin->unlock_vm ($vmid); };
944 warn $@ if $@;
945 }
946
6acb632a 947 if ($cleanup->{prepared}) {
ca2605e7
DM
948 # only call cleanup when necessary (when prepare was executed)
949 eval { $plugin->cleanup ($task, $vmid) };
950 warn $@ if $@;
951 }
aaeeeebe
DM
952
953 eval { $plugin->set_logfd (undef); };
954 warn $@ if $@;
955
60e049c2
TM
956 if ($cleanup->{resume} || $cleanup->{restart}) {
957 eval {
aaeeeebe
DM
958 $self->run_hook_script ('pre-restart', $task, $logfd);
959 if ($cleanup->{resume}) {
960 debugmsg ('info', "resume vm", $logfd);
961 $plugin->resume_vm ($task, $vmid);
962 } else {
757fd3d5
DM
963 my $running = $plugin->vm_status($vmid);
964 if (!$running) {
965 debugmsg ('info', "restarting vm", $logfd);
966 $plugin->start_vm ($task, $vmid);
967 }
d5047243
FG
968 }
969 $self->run_hook_script ('post-restart', $task, $logfd);
aaeeeebe
DM
970 };
971 my $err = $@;
972 if ($err) {
973 warn $err;
974 } else {
4e0947c8 975 $log_vm_online_again->();
aaeeeebe
DM
976 }
977 }
978 }
979
980 eval { unlink $task->{tmptar} if $task->{tmptar} && -f $task->{tmptar}; };
981 warn $@ if $@;
982
fe6643b6 983 eval { rmtree $task->{tmpdir} if $task->{tmpdir} && -d $task->{tmpdir}; };
aaeeeebe
DM
984 warn $@ if $@;
985
986 my $delay = $task->{backuptime} = time () - $vmstarttime;
987
988 if ($err) {
989 $task->{state} = 'err';
990 $task->{msg} = $err;
991 debugmsg ('err', "Backup of VM $vmid failed - $err", $logfd, 1);
29f26f6d 992 debugmsg ('info', "Failed at " . strftime("%F %H:%M:%S", localtime()));
aaeeeebe
DM
993
994 eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
995
996 } else {
997 $task->{state} = 'ok';
998 my $tstr = format_time ($delay);
999 debugmsg ('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
29f26f6d 1000 debugmsg ('info', "Backup finished at " . strftime("%F %H:%M:%S", localtime()));
aaeeeebe
DM
1001 }
1002
1003 close ($logfd) if $logfd;
60e049c2 1004
1a87db9e
DM
1005 if ($task->{tmplog}) {
1006 if ($opts->{scfg}->{type} eq 'pbs') {
1007 if ($task->{state} eq 'ok') {
1008 my $param = [$pbs_snapshot_name, $task->{tmplog}];
1009 PVE::Storage::PBSPlugin::run_raw_client_cmd(
1010 $opts->{scfg}, $opts->{storage}, 'upload-log', $param, errmsg => "upload log failed");
1011 }
1012 } elsif ($task->{logfile}) {
1013 system {'cp'} 'cp', $task->{tmplog}, $task->{logfile};
1014 }
aaeeeebe
DM
1015 }
1016
1017 eval { $self->run_hook_script ('log-end', $task); };
1018
1019 die $err if $err && $err =~ m/^interrupted by signal$/;
1020}
1021
1022sub exec_backup {
d7550e09 1023 my ($self, $rpcenv, $authuser) = @_;
aaeeeebe
DM
1024
1025 my $opts = $self->{opts};
1026
1027 debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
a7e42354
DM
1028 debugmsg ('info', "skip external VMs: " . join(', ', @{$self->{skiplist}}))
1029 if scalar(@{$self->{skiplist}});
60e049c2 1030
aaeeeebe
DM
1031 my $tasklist = [];
1032
1033 if ($opts->{all}) {
1034 foreach my $plugin (@{$self->{plugins}}) {
1035 my $vmlist = $plugin->vmlist();
1036 foreach my $vmid (sort @$vmlist) {
1037 next if grep { $_ eq $vmid } @{$opts->{exclude}};
98e84b16 1038 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
6acb632a 1039 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin, mode => $opts->{mode} };
aaeeeebe
DM
1040 }
1041 }
1042 } else {
1043 foreach my $vmid (sort @{$opts->{vmids}}) {
1044 my $plugin;
1045 foreach my $pg (@{$self->{plugins}}) {
1046 my $vmlist = $pg->vmlist();
1047 if (grep { $_ eq $vmid } @$vmlist) {
1048 $plugin = $pg;
1049 last;
1050 }
1051 }
98e84b16 1052 $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
6acb632a 1053 push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin, mode => $opts->{mode} };
aaeeeebe
DM
1054 }
1055 }
1056
44b21574
WB
1057 # Use in-memory files for the outer hook logs to pass them to sendmail.
1058 my $job_start_log = '';
1059 my $job_end_log = '';
1060 open my $job_start_fd, '>', \$job_start_log;
1061 open my $job_end_fd, '>', \$job_end_log;
1062
aaeeeebe
DM
1063 my $starttime = time();
1064 my $errcount = 0;
1065 eval {
1066
44b21574 1067 $self->run_hook_script ('job-start', undef, $job_start_fd);
aaeeeebe
DM
1068
1069 foreach my $task (@$tasklist) {
1070 $self->exec_backup_task ($task);
1071 $errcount += 1 if $task->{state} ne 'ok';
1072 }
1073
44b21574 1074 $self->run_hook_script ('job-end', undef, $job_end_fd);
aaeeeebe
DM
1075 };
1076 my $err = $@;
1077
44b21574 1078 $self->run_hook_script ('job-abort', undef, $job_end_fd) if $err;
aaeeeebe
DM
1079
1080 if ($err) {
1081 debugmsg ('err', "Backup job failed - $err", undef, 1);
1082 } else {
1083 if ($errcount) {
1084 debugmsg ('info', "Backup job finished with errors", undef, 1);
1085 } else {
61ca4432 1086 debugmsg ('info', "Backup job finished successfully", undef, 1);
aaeeeebe
DM
1087 }
1088 }
1089
44b21574
WB
1090 close $job_start_fd;
1091 close $job_end_fd;
1092
aaeeeebe
DM
1093 my $totaltime = time() - $starttime;
1094
44b21574 1095 eval { $self->sendmail ($tasklist, $totaltime, undef, $job_start_log, $job_end_log); };
aaeeeebe 1096 debugmsg ('err', $@) if $@;
4a4051d8
DM
1097
1098 die $err if $err;
1099
60e049c2 1100 die "job errors\n" if $errcount;
8682f6fc
WL
1101
1102 unlink $pidfile;
aaeeeebe
DM
1103}
1104
ac27b58d 1105
47664cbe
DM
1106sub option_exists {
1107 my $key = shift;
1108 return defined($confdesc->{$key});
1109}
1110
31aef761
DM
1111sub verify_vzdump_parameters {
1112 my ($param, $check_missing) = @_;
1113
1114 raise_param_exc({ all => "option conflicts with option 'vmid'"})
eab837c4 1115 if $param->{all} && $param->{vmid};
31aef761
DM
1116
1117 raise_param_exc({ exclude => "option conflicts with option 'vmid'"})
1118 if $param->{exclude} && $param->{vmid};
1119
f3376261
TM
1120 raise_param_exc({ pool => "option conflicts with option 'vmid'"})
1121 if $param->{pool} && $param->{vmid};
1122
1123 $param->{all} = 1 if (defined($param->{exclude}) && !$param->{pool});
31aef761 1124
e2a2525e
FG
1125 warn "option 'size' is deprecated and will be removed in a future " .
1126 "release, please update your script/configuration!\n"
1127 if defined($param->{size});
1128
31aef761
DM
1129 return if !$check_missing;
1130
1131 raise_param_exc({ vmid => "property is missing"})
f3376261 1132 if !($param->{all} || $param->{stop} || $param->{pool}) && !$param->{vmid};
8682f6fc
WL
1133
1134}
1135
eab837c4 1136sub stop_running_backups {
8682f6fc
WL
1137 my($self) = @_;
1138
eab837c4
DM
1139 my $upid = PVE::Tools::file_read_firstline($pidfile);
1140 return if !$upid;
8682f6fc 1141
eab837c4 1142 my $task = PVE::Tools::upid_decode($upid);
8682f6fc 1143
60e049c2 1144 if (PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart}) &&
eab837c4
DM
1145 PVE::ProcFSTools::read_proc_starttime($task->{pid}) == $task->{pstart}) {
1146 kill(15, $task->{pid});
1147 # wait max 15 seconds to shut down (else, do nothing for now)
1148 my $i;
1149 for ($i = 15; $i > 0; $i--) {
1150 last if !PVE::ProcFSTools::check_process_running(($task->{pid}, $task->{pstart}));
1151 sleep (1);
1152 }
76189130 1153 die "stopping backup process $task->{pid} failed\n" if $i == 0;
8682f6fc 1154 }
31aef761
DM
1155}
1156
aaeeeebe 11571;