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