]> git.proxmox.com Git - qemu-server.git/blob - PVE/VZDump/QemuServer.pm
cancel backup before stopping the vm
[qemu-server.git] / PVE / VZDump / QemuServer.pm
1 package PVE::VZDump::QemuServer;
2
3 use strict;
4 use warnings;
5 use File::Path;
6 use File::Basename;
7 use PVE::INotify;
8 use PVE::VZDump;
9 use PVE::IPCC;
10 use PVE::Cluster qw(cfs_read_file);
11 use PVE::Tools;
12 use PVE::Storage::Plugin;
13 use PVE::Storage;
14 use PVE::QemuServer;
15 use IO::File;
16 use IPC::Open3;
17
18 use base qw (PVE::VZDump::Plugin);
19
20 sub new {
21 my ($class, $vzdump) = @_;
22
23 PVE::VZDump::check_bin('qm');
24
25 my $self = bless { vzdump => $vzdump };
26
27 $self->{vmlist} = PVE::QemuServer::vzlist();
28 $self->{storecfg} = PVE::Storage::config();
29
30 return $self;
31 };
32
33
34 sub type {
35 return 'qemu';
36 }
37
38 sub vmlist {
39 my ($self) = @_;
40
41 return [ keys %{$self->{vmlist}} ];
42 }
43
44 sub prepare {
45 my ($self, $task, $vmid, $mode) = @_;
46
47 $task->{disks} = [];
48
49 my $conf = $self->{vmlist}->{$vmid} = PVE::QemuServer::load_config($vmid);
50
51 $self->{vm_was_running} = 1;
52 if (!PVE::QemuServer::check_running($vmid)) {
53 $self->{vm_was_running} = 0;
54 }
55
56 $task->{hostname} = $conf->{name};
57
58 my $hostname = PVE::INotify::nodename();
59
60 my $vollist = [];
61 my $drivehash = {};
62 PVE::QemuServer::foreach_drive($conf, sub {
63 my ($ds, $drive) = @_;
64
65 return if PVE::QemuServer::drive_is_cdrom($drive);
66
67 if (defined($drive->{backup}) && $drive->{backup} eq "no") {
68 $self->loginfo("exclude disk '$ds' (backup=no)");
69 return;
70 }
71
72 my $volid = $drive->{file};
73
74 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
75 push @$vollist, $volid if $storeid;
76 $drivehash->{$ds} = $drive;
77 });
78
79 PVE::Storage::activate_volumes($self->{storecfg}, $vollist);
80
81 foreach my $ds (sort keys %$drivehash) {
82 my $drive = $drivehash->{$ds};
83
84 my $volid = $drive->{file};
85
86 my $path;
87
88 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
89 if ($storeid) {
90 $path = PVE::Storage::path($self->{storecfg}, $volid);
91 } else {
92 $path = $volid;
93 }
94
95 next if !$path;
96
97 die "no such volume '$volid'\n" if ! -e $path;
98
99 my ($size, $format) = PVE::Storage::Plugin::file_size_info($path);
100
101 my $diskinfo = { path => $path , volid => $volid, storeid => $storeid,
102 format => $format, virtdev => $ds, qmdevice => "drive-$ds" };
103
104 if (-b $path) {
105 $diskinfo->{type} = 'block';
106 } else {
107 $diskinfo->{type} = 'file';
108 }
109
110 push @{$task->{disks}}, $diskinfo;
111 }
112 }
113
114 sub vm_status {
115 my ($self, $vmid) = @_;
116
117 my $running = PVE::QemuServer::check_running($vmid) ? 1 : 0;
118
119 return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
120 }
121
122 sub lock_vm {
123 my ($self, $vmid) = @_;
124
125 $self->cmd ("qm set $vmid --lock backup");
126 }
127
128 sub unlock_vm {
129 my ($self, $vmid) = @_;
130
131 $self->cmd ("qm unlock $vmid");
132 }
133
134 sub stop_vm {
135 my ($self, $task, $vmid) = @_;
136
137 my $opts = $self->{vzdump}->{opts};
138
139 my $wait = $opts->{stopwait} * 60;
140 # send shutdown and wait
141 $self->cmd ("qm shutdown $vmid --skiplock --keepActive --timeout $wait");
142 }
143
144 sub start_vm {
145 my ($self, $task, $vmid) = @_;
146
147 $self->cmd ("qm start $vmid --skiplock");
148 }
149
150 sub suspend_vm {
151 my ($self, $task, $vmid) = @_;
152
153 $self->cmd ("qm suspend $vmid --skiplock");
154 }
155
156 sub resume_vm {
157 my ($self, $task, $vmid) = @_;
158
159 $self->cmd ("qm resume $vmid --skiplock");
160 }
161
162 sub assemble {
163 my ($self, $task, $vmid) = @_;
164
165 my $conffile = PVE::QemuServer::config_file ($vmid);
166
167 my $outfile = "$task->{tmpdir}/qemu-server.conf";
168
169 my $outfd;
170 my $conffd;
171
172 eval {
173
174 $outfd = IO::File->new (">$outfile") ||
175 die "unable to open '$outfile'";
176 $conffd = IO::File->new ($conffile, 'r') ||
177 die "unable open '$conffile'";
178
179 my $found_snapshot;
180 while (defined (my $line = <$conffd>)) {
181 next if $line =~ m/^\#vzdump\#/; # just to be sure
182 next if $line =~ m/^\#qmdump\#/; # just to be sure
183 if ($line =~ m/^\[.*\]\s*$/) {
184 $found_snapshot = 1;
185 }
186 next if $found_snapshot; # skip all snapshots data
187 if ($line =~ m/^unused\d+:\s*(\S+)\s*/) {
188 $self->loginfo("skip unused drive '$1' (not included into backup)");
189 next;
190 }
191 next if $line =~ m/^lock:/ || $line =~ m/^parent:/;
192
193 print $outfd $line;
194 }
195
196 foreach my $di (@{$task->{disks}}) {
197 if ($di->{type} eq 'block' || $di->{type} eq 'file') {
198 my $storeid = $di->{storeid} || '';
199 my $format = $di->{format} || '';
200 print $outfd "#qmdump#map:$di->{virtdev}:$di->{qmdevice}:$storeid:$format:\n";
201 } else {
202 die "internal error";
203 }
204 }
205
206 if ($found_snapshot) {
207 $self->loginfo("snapshots found (not included into backup)");
208 }
209 };
210 my $err = $@;
211
212 close ($outfd) if $outfd;
213 close ($conffd) if $conffd;
214
215 die $err if $err;
216 }
217
218 sub archive {
219 my ($self, $task, $vmid, $filename, $comp) = @_;
220
221 my $conffile = "$task->{tmpdir}/qemu-server.conf";
222
223 my $opts = $self->{vzdump}->{opts};
224
225 my $starttime = time ();
226
227 my $speed = 0;
228 if ($opts->{bwlimit}) {
229 $speed = $opts->{bwlimit}*1024;
230 }
231
232 my $devlist = '';
233 foreach my $di (@{$task->{disks}}) {
234 if ($di->{type} eq 'block' || $di->{type} eq 'file') {
235 $devlist .= $devlist ? ",$di->{qmdevice}" : $di->{qmdevice};
236 } else {
237 die "implement me";
238 }
239 }
240
241 my $stop_after_backup;
242 my $resume_on_backup;
243
244 my $skiplock = 1;
245
246 if (!PVE::QemuServer::check_running($vmid)) {
247 eval {
248 $self->loginfo("starting kvm to execute backup task");
249 PVE::QemuServer::vm_start($self->{storecfg}, $vmid, undef,
250 $skiplock, undef, 1);
251 if ($self->{vm_was_running}) {
252 $resume_on_backup = 1;
253 } else {
254 $stop_after_backup = 1;
255 }
256 };
257 if (my $err = $@) {
258 die $err;
259 }
260 }
261
262 my $cpid;
263 my $interrupt_msg = "interrupted by signal\n";
264 eval {
265 $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
266 die $interrupt_msg;
267 };
268
269 my $qmpclient = PVE::QMPClient->new();
270
271 my $uuid;
272
273 my $backup_cb = sub {
274 my ($vmid, $resp) = @_;
275 $uuid = $resp->{return};
276 };
277
278 my $outfh;
279 if ($opts->{stdout}) {
280 $outfh = $opts->{stdout};
281 } else {
282 $outfh = IO::File->new($filename, "w") ||
283 die "unable to open file '$filename' - $!\n";
284 }
285
286 my $outfileno;
287 if ($comp) {
288 my @pipefd = POSIX::pipe();
289 $cpid = fork();
290 die "unable to fork worker - $!" if !defined($cpid);
291 if ($cpid == 0) {
292 eval {
293 POSIX::close($pipefd[1]);
294 # redirect STDIN
295 my $fd = fileno(STDIN);
296 close STDIN;
297 POSIX::close(0) if $fd != 0;
298 die "unable to redirect STDIN - $!"
299 if !open(STDIN, "<&", $pipefd[0]);
300
301 # redirect STDOUT
302 $fd = fileno(STDOUT);
303 close STDOUT;
304 POSIX::close (1) if $fd != 1;
305
306 die "unable to redirect STDOUT - $!"
307 if !open(STDOUT, ">&", fileno($outfh));
308
309 exec($comp);
310 die "fork compressor '$comp' failed\n";
311 };
312 if (my $err = $@) {
313 warn $err;
314 POSIX::_exit(1);
315 }
316 POSIX::_exit(0);
317 kill(-9, $$);
318 } else {
319 POSIX::close($pipefd[0]);
320 $outfileno = $pipefd[1];
321 }
322 } else {
323 $outfileno = fileno($outfh);
324 }
325
326 my $add_fd_cb = sub {
327 my ($vmid, $resp) = @_;
328
329 $qmpclient->queue_cmd($vmid, $backup_cb, 'backup',
330 backupfile => "/dev/fdname/backup",
331 speed => $speed,
332 'config-filename' => $conffile,
333 devlist => $devlist);
334 };
335
336
337 $qmpclient->queue_cmd($vmid, $add_fd_cb, 'getfd',
338 fd => $outfileno, fdname => "backup");
339 $qmpclient->queue_execute();
340
341 die $qmpclient->{errors}->{$vmid} if $qmpclient->{errors}->{$vmid};
342
343 if ($cpid) {
344 POSIX::close($outfileno) == 0 ||
345 die "close output file handle failed\n";
346 }
347
348 die "got no uuid for backup task\n" if !$uuid;
349
350 $self->loginfo("started backup task '$uuid'");
351
352 if ($resume_on_backup) {
353 $self->loginfo("resume VM");
354 PVE::QemuServer::vm_mon_cmd($vmid, 'cont');
355 }
356
357 my $status;
358 my $starttime = time ();
359 my $last_per = -1;
360 my $last_total = 0;
361 my $last_zero = 0;
362 my $last_transferred = 0;
363 my $last_time = time();
364 my $transferred;
365
366 while(1) {
367 $status = PVE::QemuServer::vm_mon_cmd($vmid, 'query-backup');
368 my $total = $status->{total};
369 $transferred = $status->{transferred};
370 my $per = $total ? int(($transferred * 100)/$total) : 0;
371 my $zero = $status->{'zero-bytes'} || 0;
372 my $zero_per = $total ? int(($zero * 100)/$total) : 0;
373
374 die "got unexpected uuid\n" if $status->{uuid} ne $uuid;
375
376 my $ctime = time();
377 my $duration = $ctime - $starttime;
378
379 my $rbytes = $transferred - $last_transferred;
380 my $wbytes = $rbytes - ($zero - $last_zero);
381
382 my $timediff = ($ctime - $last_time) || 1; # fixme
383 my $mbps_read = ($rbytes > 0) ?
384 int(($rbytes/$timediff)/(1000*1000)) : 0;
385 my $mbps_write = ($wbytes > 0) ?
386 int(($wbytes/$timediff)/(1000*1000)) : 0;
387
388 my $statusline = "status: $per% ($transferred/$total), " .
389 "sparse ${zero_per}% ($zero), duration $duration, " .
390 "$mbps_read/$mbps_write MB/s";
391 if ($status->{status} ne 'active') {
392 $self->loginfo($statusline);
393 die(($status->{errmsg} || "unknown error") . "\n")
394 if $status->{status} eq 'error';
395 last;
396 }
397 if ($per != $last_per && ($timediff > 2)) {
398 $self->loginfo($statusline);
399 $last_per = $per;
400 $last_total = $total if $total;
401 $last_zero = $zero if $zero;
402 $last_transferred = $transferred if $transferred;
403 $last_time = $ctime;
404 }
405 sleep(1);
406 }
407
408 my $duration = time() - $starttime;
409 if ($transferred && $duration) {
410 my $mb = int($transferred/(1000*1000));
411 my $mbps = int(($transferred/$duration)/(1000*1000));
412 $self->loginfo("transferred $mb MB in $duration seconds ($mbps MB/s)");
413 }
414 };
415 my $err = $@;
416
417 if ($err) {
418 $self->loginfo("aborting backup job");
419 eval { PVE::QemuServer::vm_mon_cmd($vmid, 'backup_cancel'); };
420 warn $@ if $@;
421 }
422
423 if ($stop_after_backup) {
424 # stop if not running
425 eval {
426 my $resp = PVE::QemuServer::vm_mon_cmd($vmid, 'query-status');
427 my $status = $resp && $resp->{status} ? $resp->{status} : 'unknown';
428 if ($status eq 'prelaunch') {
429 $self->loginfo("stopping kvm after backup task");
430 PVE::QemuServer::vm_stop($self->{storecfg}, $vmid, $skiplock);
431 } else {
432 $self->loginfo("kvm status changed after backup ('$status')" .
433 " - keep VM running");
434 }
435 }
436 }
437
438 if ($err) {
439 if ($cpid) {
440 kill(-9, $cpid);
441 waitpid($cpid, 0);
442 }
443 die $err;
444 }
445
446 if ($cpid && (waitpid($cpid, 0) > 0)) {
447 my $stat = $?;
448 my $ec = $stat >> 8;
449 my $signal = $stat & 127;
450 if ($ec || $signal) {
451 die "$comp failed - wrong exit status $ec" .
452 ($signal ? " (signal $signal)\n" : "\n");
453 }
454 }
455 }
456
457 sub snapshot {
458 my ($self, $task, $vmid) = @_;
459
460 # nothing to do
461 }
462
463 sub cleanup {
464 my ($self, $task, $vmid) = @_;
465
466 # nothing to do ?
467 }
468
469 1;