]> git.proxmox.com Git - qemu-server.git/blame - PVE/VZDump/QemuServer.pm
backup: is IOThread comaptible: only check running VMs
[qemu-server.git] / PVE / VZDump / QemuServer.pm
CommitLineData
1e3baf05
DM
1package PVE::VZDump::QemuServer;
2
1e3baf05
DM
3use strict;
4use warnings;
d610b145 5
1e3baf05 6use File::Basename;
d610b145
TL
7use File::Path;
8use IO::File;
9use IPC::Open3;
10
11use PVE::Cluster qw(cfs_read_file);
66ab1d91 12use PVE::INotify;
91bd6c90 13use PVE::IPCC;
d610b145 14use PVE::JSONSchema;
f5bdefa4 15use PVE::Storage::Plugin;
1e3baf05 16use PVE::Storage;
d610b145
TL
17use PVE::Tools;
18use PVE::VZDump;
19
1e3baf05 20use PVE::QemuServer;
1e3baf05
DM
21
22use base qw (PVE::VZDump::Plugin);
23
24sub new {
25 my ($class, $vzdump) = @_;
874a096e 26
66ab1d91 27 PVE::VZDump::check_bin('qm');
1e3baf05 28
e2812738 29 my $self = bless { vzdump => $vzdump }, $class;
1e3baf05
DM
30
31 $self->{vmlist} = PVE::QemuServer::vzlist();
32 $self->{storecfg} = PVE::Storage::config();
33
34 return $self;
35};
36
37
38sub type {
39 return 'qemu';
40}
41
42sub vmlist {
43 my ($self) = @_;
44
45 return [ keys %{$self->{vmlist}} ];
46}
47
48sub prepare {
49 my ($self, $task, $vmid, $mode) = @_;
50
51 $task->{disks} = [];
52
ffda963f 53 my $conf = $self->{vmlist}->{$vmid} = PVE::QemuConfig->load_config($vmid);
1e3baf05 54
85b84b7b
WL
55 $self->loginfo("VM Name: $conf->{name}")
56 if defined($conf->{name});
57
91bd6c90
DM
58 $self->{vm_was_running} = 1;
59 if (!PVE::QemuServer::check_running($vmid)) {
60 $self->{vm_was_running} = 0;
585b6e28
DM
61 }
62
1e3baf05
DM
63 $task->{hostname} = $conf->{name};
64
874a096e 65 my $hostname = PVE::INotify::nodename();
1e3baf05 66
b969cc68
DM
67 my $vollist = [];
68 my $drivehash = {};
1e3baf05
DM
69 PVE::QemuServer::foreach_drive($conf, sub {
70 my ($ds, $drive) = @_;
71
f5bdefa4 72 return if PVE::QemuServer::drive_is_cdrom($drive);
1e3baf05 73
b53b958b
EK
74 my $volid = $drive->{file};
75
bea021ac 76 if (defined($drive->{backup}) && !$drive->{backup}) {
b53b958b 77 $self->loginfo("exclude disk '$ds' '$volid' (backup=no)");
1e3baf05 78 return;
317c55c2 79 } elsif ($self->{vm_was_running} && $drive->{iothread}) {
48343b3f
TL
80 if (!PVE::QemuServer::runs_at_least_qemu_version($vmid, 4, 0, 1)) {
81 die "disk '$ds' '$volid' (iothread=on) can't use backup feature with running QEMU " .
82 "version < 4.0.1! Either set backup=no for this drive or upgrade QEMU and restart VM\n";
83 }
b53b958b 84 } else {
0db93c2d
EK
85 my $log = "include disk '$ds' '$volid'";
86 if (defined $drive->{size}) {
87 my $readable_size = PVE::JSONSchema::format_size($drive->{size});
88 $log .= " $readable_size";
89 }
90 $self->loginfo($log);
874a096e 91 }
b969cc68 92
f5bdefa4 93 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
b969cc68
DM
94 push @$vollist, $volid if $storeid;
95 $drivehash->{$ds} = $drive;
96 });
97
98 PVE::Storage::activate_volumes($self->{storecfg}, $vollist);
99
075b417a
DM
100 foreach my $ds (sort keys %$drivehash) {
101 my $drive = $drivehash->{$ds};
874a096e 102
1e3baf05
DM
103 my $volid = $drive->{file};
104
105 my $path;
106
f5bdefa4 107 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1e3baf05 108 if ($storeid) {
b969cc68 109 $path = PVE::Storage::path($self->{storecfg}, $volid);
1e3baf05
DM
110 } else {
111 $path = $volid;
112 }
113
b969cc68 114 next if !$path;
1e3baf05 115
daca220d
AD
116 my $format = undef;
117 my $size = undef;
1e3baf05 118
daca220d
AD
119 eval{
120 ($size, $format) = PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5);
121 };
122 die "no such volume '$volid'\n" if $@;
91bd6c90 123
874a096e 124 my $diskinfo = { path => $path , volid => $volid, storeid => $storeid,
91bd6c90 125 format => $format, virtdev => $ds, qmdevice => "drive-$ds" };
1e3baf05
DM
126
127 if (-b $path) {
1e3baf05 128 $diskinfo->{type} = 'block';
1e3baf05 129 } else {
1e3baf05 130 $diskinfo->{type} = 'file';
1e3baf05
DM
131 }
132
133 push @{$task->{disks}}, $diskinfo;
b969cc68 134 }
1e3baf05
DM
135}
136
137sub vm_status {
138 my ($self, $vmid) = @_;
139
66ab1d91 140 my $running = PVE::QemuServer::check_running($vmid) ? 1 : 0;
874a096e
DM
141
142 return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
1e3baf05
DM
143}
144
145sub lock_vm {
146 my ($self, $vmid) = @_;
147
148 $self->cmd ("qm set $vmid --lock backup");
149}
150
151sub unlock_vm {
152 my ($self, $vmid) = @_;
153
66ab1d91 154 $self->cmd ("qm unlock $vmid");
1e3baf05
DM
155}
156
157sub stop_vm {
158 my ($self, $task, $vmid) = @_;
159
160 my $opts = $self->{vzdump}->{opts};
161
162 my $wait = $opts->{stopwait} * 60;
163 # send shutdown and wait
254575e9 164 $self->cmd ("qm shutdown $vmid --skiplock --keepActive --timeout $wait");
1e3baf05
DM
165}
166
167sub start_vm {
168 my ($self, $task, $vmid) = @_;
169
66ab1d91 170 $self->cmd ("qm start $vmid --skiplock");
1e3baf05
DM
171}
172
173sub suspend_vm {
174 my ($self, $task, $vmid) = @_;
175
66ab1d91 176 $self->cmd ("qm suspend $vmid --skiplock");
1e3baf05
DM
177}
178
179sub resume_vm {
180 my ($self, $task, $vmid) = @_;
181
66ab1d91 182 $self->cmd ("qm resume $vmid --skiplock");
1e3baf05
DM
183}
184
1e3baf05
DM
185sub assemble {
186 my ($self, $task, $vmid) = @_;
187
04096e7b 188 my $conffile = PVE::QemuConfig->config_file($vmid);
1e3baf05
DM
189
190 my $outfile = "$task->{tmpdir}/qemu-server.conf";
c05f7f3f
WL
191 my $firewall_src = "/etc/pve/firewall/$vmid.fw";
192 my $firewall_dest = "$task->{tmpdir}/qemu-server.fw";
193
194 my $outfd = IO::File->new (">$outfile") ||
195 die "unable to open '$outfile'";
196 my $conffd = IO::File->new ($conffile, 'r') ||
197 die "unable open '$conffile'";
198
199 my $found_snapshot;
391c2230 200 my $found_pending;
c05f7f3f
WL
201 while (defined (my $line = <$conffd>)) {
202 next if $line =~ m/^\#vzdump\#/; # just to be sure
203 next if $line =~ m/^\#qmdump\#/; # just to be sure
391c2230
FG
204 if ($line =~ m/^\[(.*)\]\s*$/) {
205 if ($1 =~ m/PENDING/i) {
206 $found_pending = 1;
207 } else {
208 $found_snapshot = 1;
209 }
1e3baf05 210 }
391c2230 211
c05f7f3f 212 next if $found_snapshot; # skip all snapshots data
391c2230
FG
213 next if $found_pending; # skip all pending changes
214
c05f7f3f
WL
215 if ($line =~ m/^unused\d+:\s*(\S+)\s*/) {
216 $self->loginfo("skip unused drive '$1' (not included into backup)");
217 next;
1e3baf05 218 }
c05f7f3f 219 next if $line =~ m/^lock:/ || $line =~ m/^parent:/;
91bd6c90 220
c05f7f3f
WL
221 print $outfd $line;
222 }
223
224 foreach my $di (@{$task->{disks}}) {
225 if ($di->{type} eq 'block' || $di->{type} eq 'file') {
226 my $storeid = $di->{storeid} || '';
227 my $format = $di->{format} || '';
228 print $outfd "#qmdump#map:$di->{virtdev}:$di->{qmdevice}:$storeid:$format:\n";
229 } else {
230 die "internal error";
91bd6c90 231 }
c05f7f3f 232 }
1e3baf05 233
c05f7f3f
WL
234 if ($found_snapshot) {
235 $self->loginfo("snapshots found (not included into backup)");
236 }
874a096e 237
391c2230
FG
238 if ($found_pending) {
239 $self->loginfo("pending configuration changes found (not included into backup)");
240 }
241
c05f7f3f 242 PVE::Tools::file_copy($firewall_src, $firewall_dest) if -f $firewall_src;
1e3baf05
DM
243}
244
245sub archive {
fad02a16 246 my ($self, $task, $vmid, $filename, $comp) = @_;
1e3baf05
DM
247
248 my $conffile = "$task->{tmpdir}/qemu-server.conf";
c05f7f3f 249 my $firewall = "$task->{tmpdir}/qemu-server.fw";
1e3baf05
DM
250
251 my $opts = $self->{vzdump}->{opts};
252
253 my $starttime = time ();
254
91bd6c90
DM
255 my $speed = 0;
256 if ($opts->{bwlimit}) {
874a096e 257 $speed = $opts->{bwlimit}*1024;
91bd6c90 258 }
1e3baf05 259
c82935e9
DM
260 my $diskcount = scalar(@{$task->{disks}});
261
ffda963f 262 if (PVE::QemuConfig->is_template($self->{vmlist}->{$vmid}) || !$diskcount) {
23b4120b
DM
263 my @pathlist;
264 foreach my $di (@{$task->{disks}}) {
265 if ($di->{type} eq 'block' || $di->{type} eq 'file') {
266 push @pathlist, "$di->{qmdevice}=$di->{path}";
267 } else {
268 die "implement me";
269 }
270 }
271
c82935e9
DM
272 if (!$diskcount) {
273 $self->loginfo("backup contains no disks");
274 }
275
23b4120b
DM
276 my $outcmd;
277 if ($comp) {
874a096e 278 $outcmd = "exec:$comp";
23b4120b 279 } else {
874a096e 280 $outcmd = "exec:cat";
23b4120b
DM
281 }
282
a2fab11a 283 $outcmd .= " > $filename" if !$opts->{stdout};
23b4120b 284
c05f7f3f
WL
285 my $cmd = ['/usr/bin/vma', 'create', '-v', '-c', $conffile];
286 push @$cmd, '-c', $firewall if -e $firewall;
287 push @$cmd, $outcmd, @pathlist;
23b4120b
DM
288
289 $self->loginfo("starting template backup");
290 $self->loginfo(join(' ', @$cmd));
291
292 if ($opts->{stdout}) {
293 $self->cmd($cmd, output => ">&=" . fileno($opts->{stdout}));
294 } else {
295 $self->cmd($cmd);
296 }
297
298 return;
299 }
300
301
91bd6c90 302 my $devlist = '';
1e3baf05
DM
303 foreach my $di (@{$task->{disks}}) {
304 if ($di->{type} eq 'block' || $di->{type} eq 'file') {
91bd6c90 305 $devlist .= $devlist ? ",$di->{qmdevice}" : $di->{qmdevice};
1e3baf05
DM
306 } else {
307 die "implement me";
308 }
309 }
310
91bd6c90
DM
311 my $stop_after_backup;
312 my $resume_on_backup;
313
314 my $skiplock = 1;
ab6a9a0c
WL
315 my $vm_is_running = PVE::QemuServer::check_running($vmid);
316 if (!$vm_is_running) {
91bd6c90
DM
317 eval {
318 $self->loginfo("starting kvm to execute backup task");
874a096e 319 PVE::QemuServer::vm_start($self->{storecfg}, $vmid, undef,
91bd6c90
DM
320 $skiplock, undef, 1);
321 if ($self->{vm_was_running}) {
322 $resume_on_backup = 1;
323 } else {
324 $stop_after_backup = 1;
325 }
326 };
327 if (my $err = $@) {
328 die $err;
329 }
9c502e26 330 }
91bd6c90
DM
331
332 my $cpid;
333 my $interrupt_msg = "interrupted by signal\n";
334 eval {
335 $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
336 die $interrupt_msg;
337 };
338
339 my $qmpclient = PVE::QMPClient->new();
340
341 my $uuid;
342
343 my $backup_cb = sub {
344 my ($vmid, $resp) = @_;
663d6641 345 $uuid = $resp->{return}->{UUID};
91bd6c90
DM
346 };
347
348 my $outfh;
349 if ($opts->{stdout}) {
350 $outfh = $opts->{stdout};
351 } else {
352 $outfh = IO::File->new($filename, "w") ||
353 die "unable to open file '$filename' - $!\n";
354 }
355
356 my $outfileno;
357 if ($comp) {
358 my @pipefd = POSIX::pipe();
359 $cpid = fork();
360 die "unable to fork worker - $!" if !defined($cpid);
361 if ($cpid == 0) {
362 eval {
363 POSIX::close($pipefd[1]);
364 # redirect STDIN
365 my $fd = fileno(STDIN);
366 close STDIN;
367 POSIX::close(0) if $fd != 0;
874a096e 368 die "unable to redirect STDIN - $!"
91bd6c90 369 if !open(STDIN, "<&", $pipefd[0]);
874a096e 370
91bd6c90
DM
371 # redirect STDOUT
372 $fd = fileno(STDOUT);
373 close STDOUT;
374 POSIX::close (1) if $fd != 1;
375
874a096e 376 die "unable to redirect STDOUT - $!"
91bd6c90 377 if !open(STDOUT, ">&", fileno($outfh));
874a096e 378
91bd6c90
DM
379 exec($comp);
380 die "fork compressor '$comp' failed\n";
381 };
382 if (my $err = $@) {
60635a57 383 $self->logerr($err);
874a096e 384 POSIX::_exit(1);
91bd6c90 385 }
874a096e
DM
386 POSIX::_exit(0);
387 kill(-9, $$);
91bd6c90
DM
388 } else {
389 POSIX::close($pipefd[0]);
390 $outfileno = $pipefd[1];
874a096e 391 }
91bd6c90
DM
392 } else {
393 $outfileno = fileno($outfh);
394 }
395
396 my $add_fd_cb = sub {
397 my ($vmid, $resp) = @_;
398
c05f7f3f
WL
399 my $params = {
400 'backup-file' => "/dev/fdname/backup",
401 speed => $speed,
402 'config-file' => $conffile,
403 devlist => $devlist
404 };
a022e3fd 405
c05f7f3f
WL
406 $params->{'firewall-file'} = $firewall if -e $firewall;
407 $qmpclient->queue_cmd($vmid, $backup_cb, 'backup', %$params);
91bd6c90
DM
408 };
409
874a096e 410 $qmpclient->queue_cmd($vmid, $add_fd_cb, 'getfd',
91bd6c90 411 fd => $outfileno, fdname => "backup");
ab6a9a0c 412
1a988fd2
DC
413 my $agent_running = 0;
414
415 if ($self->{vmlist}->{$vmid}->{agent} && $vm_is_running) {
416 $agent_running = PVE::QemuServer::qga_check_running($vmid);
417 }
418
419 if ($agent_running){
874a096e 420 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-freeze"); };
ab6a9a0c
WL
421 if (my $err = $@) {
422 $self->logerr($err);
874a096e 423 }
ab6a9a0c 424 }
874a096e 425
f0f30448
WB
426 eval { $qmpclient->queue_execute() };
427 my $qmperr = $@;
91bd6c90 428
1a988fd2 429 if ($agent_running){
874a096e 430 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-thaw"); };
ab6a9a0c
WL
431 if (my $err = $@) {
432 $self->logerr($err);
433 }
434 }
f0f30448 435 die $qmperr if $qmperr;
874a096e 436 die $qmpclient->{errors}->{$vmid} if $qmpclient->{errors}->{$vmid};
91bd6c90
DM
437
438 if ($cpid) {
874a096e 439 POSIX::close($outfileno) == 0 ||
91bd6c90
DM
440 die "close output file handle failed\n";
441 }
442
443 die "got no uuid for backup task\n" if !$uuid;
444
445 $self->loginfo("started backup task '$uuid'");
446
447 if ($resume_on_backup) {
96f00fd8
TL
448 if (my $stoptime = $task->{vmstoptime}) {
449 my $delay = time() - $task->{vmstoptime};
450 $task->{vmstoptime} = undef; # avoid printing 'online after ..' twice
451 $self->loginfo("resuming VM again after $delay seconds");
452 } else {
453 $self->loginfo("resuming VM again");
454 }
91bd6c90
DM
455 PVE::QemuServer::vm_mon_cmd($vmid, 'cont');
456 }
457
458 my $status;
459 my $starttime = time ();
460 my $last_per = -1;
874a096e 461 my $last_total = 0;
91bd6c90
DM
462 my $last_zero = 0;
463 my $last_transferred = 0;
464 my $last_time = time();
465 my $transferred;
466
467 while(1) {
468 $status = PVE::QemuServer::vm_mon_cmd($vmid, 'query-backup');
405b913a
DM
469 my $total = $status->{total} || 0;
470 $transferred = $status->{transferred} || 0;
91bd6c90
DM
471 my $per = $total ? int(($transferred * 100)/$total) : 0;
472 my $zero = $status->{'zero-bytes'} || 0;
473 my $zero_per = $total ? int(($zero * 100)/$total) : 0;
874a096e 474
405b913a 475 die "got unexpected uuid\n" if !$status->{uuid} || ($status->{uuid} ne $uuid);
91bd6c90
DM
476
477 my $ctime = time();
478 my $duration = $ctime - $starttime;
479
480 my $rbytes = $transferred - $last_transferred;
481 my $wbytes = $rbytes - ($zero - $last_zero);
482
483 my $timediff = ($ctime - $last_time) || 1; # fixme
874a096e 484 my $mbps_read = ($rbytes > 0) ?
91bd6c90 485 int(($rbytes/$timediff)/(1000*1000)) : 0;
874a096e 486 my $mbps_write = ($wbytes > 0) ?
91bd6c90
DM
487 int(($wbytes/$timediff)/(1000*1000)) : 0;
488
489 my $statusline = "status: $per% ($transferred/$total), " .
490 "sparse ${zero_per}% ($zero), duration $duration, " .
76312b86 491 "read/write $mbps_read/$mbps_write MB/s";
405b913a
DM
492 my $res = $status->{status} || 'unknown';
493 if ($res ne 'active') {
91bd6c90
DM
494 $self->loginfo($statusline);
495 die(($status->{errmsg} || "unknown error") . "\n")
405b913a
DM
496 if $res eq 'error';
497 die "got unexpected status '$res'\n"
498 if $res ne 'done';
499 die "got wrong number of transfered bytes ($total != $transferred)\n"
500 if ($res eq 'done') && ($total != $transferred);
501
91bd6c90
DM
502 last;
503 }
504 if ($per != $last_per && ($timediff > 2)) {
505 $self->loginfo($statusline);
506 $last_per = $per;
874a096e 507 $last_total = $total if $total;
91bd6c90
DM
508 $last_zero = $zero if $zero;
509 $last_transferred = $transferred if $transferred;
510 $last_time = $ctime;
511 }
512 sleep(1);
513 }
514
515 my $duration = time() - $starttime;
516 if ($transferred && $duration) {
517 my $mb = int($transferred/(1000*1000));
518 my $mbps = int(($transferred/$duration)/(1000*1000));
519 $self->loginfo("transferred $mb MB in $duration seconds ($mbps MB/s)");
520 }
521 };
522 my $err = $@;
523
19599cd9 524 if ($err) {
60635a57 525 $self->logerr($err);
19599cd9 526 $self->loginfo("aborting backup job");
1575bfcc 527 eval { PVE::QemuServer::vm_mon_cmd($vmid, 'backup-cancel'); };
60635a57
DM
528 if (my $err1 = $@) {
529 $self->logerr($err1);
530 }
19599cd9
DM
531 }
532
91bd6c90
DM
533 if ($stop_after_backup) {
534 # stop if not running
535 eval {
536 my $resp = PVE::QemuServer::vm_mon_cmd($vmid, 'query-status');
537 my $status = $resp && $resp->{status} ? $resp->{status} : 'unknown';
538 if ($status eq 'prelaunch') {
19599cd9 539 $self->loginfo("stopping kvm after backup task");
91bd6c90
DM
540 PVE::QemuServer::vm_stop($self->{storecfg}, $vmid, $skiplock);
541 } else {
542 $self->loginfo("kvm status changed after backup ('$status')" .
543 " - keep VM running");
544 }
545 }
874a096e 546 }
91bd6c90
DM
547
548 if ($err) {
874a096e
DM
549 if ($cpid) {
550 kill(9, $cpid);
91bd6c90
DM
551 waitpid($cpid, 0);
552 }
553 die $err;
554 }
555
556 if ($cpid && (waitpid($cpid, 0) > 0)) {
557 my $stat = $?;
558 my $ec = $stat >> 8;
559 my $signal = $stat & 127;
560 if ($ec || $signal) {
874a096e 561 die "$comp failed - wrong exit status $ec" .
91bd6c90
DM
562 ($signal ? " (signal $signal)\n" : "\n");
563 }
564 }
565}
566
567sub snapshot {
568 my ($self, $task, $vmid) = @_;
569
570 # nothing to do
1e3baf05
DM
571}
572
573sub cleanup {
574 my ($self, $task, $vmid) = @_;
575
91bd6c90 576 # nothing to do ?
1e3baf05
DM
577}
578
5791;