]> git.proxmox.com Git - qemu-server.git/blob - PVE/QemuMigrate.pm
use new AbstractMigrate.pm
[qemu-server.git] / PVE / QemuMigrate.pm
1 package PVE::QemuMigrate;
2
3 use strict;
4 use warnings;
5 use PVE::AbstractMigrate;
6 use IO::File;
7 use IPC::Open2;
8 use PVE::INotify;
9 use PVE::Cluster;
10 use PVE::Storage;
11 use PVE::QemuServer;
12
13 use base qw(PVE::AbstractMigrate);
14
15 sub fork_command_pipe {
16 my ($cmd) = @_;
17
18 my $reader = IO::File->new();
19 my $writer = IO::File->new();
20
21 my $orig_pid = $$;
22
23 my $cpid;
24
25 eval { $cpid = open2($reader, $writer, @$cmd); };
26
27 my $err = $@;
28
29 # catch exec errors
30 if ($orig_pid != $$) {
31 logmsg('err', "can't fork command pipe\n");
32 POSIX::_exit(1);
33 kill('KILL', $$);
34 }
35
36 die $err if $err;
37
38 return { writer => $writer, reader => $reader, pid => $cpid };
39 }
40
41 sub finish_command_pipe {
42 my $cmdpipe = shift;
43
44 my $writer = $cmdpipe->{writer};
45 my $reader = $cmdpipe->{reader};
46
47 $writer->close();
48 $reader->close();
49
50 my $cpid = $cmdpipe->{pid};
51
52 kill(15, $cpid) if kill(0, $cpid);
53
54 waitpid($cpid, 0);
55 }
56
57 sub run_with_timeout {
58 my ($timeout, $code, @param) = @_;
59
60 die "got timeout\n" if $timeout <= 0;
61
62 my $prev_alarm;
63
64 my $sigcount = 0;
65
66 my $res;
67
68 eval {
69 local $SIG{ALRM} = sub { $sigcount++; die "got timeout\n"; };
70 local $SIG{PIPE} = sub { $sigcount++; die "broken pipe\n" };
71 local $SIG{__DIE__}; # see SA bug 4631
72
73 $prev_alarm = alarm($timeout);
74
75 $res = &$code(@param);
76
77 alarm(0); # avoid race conditions
78 };
79
80 my $err = $@;
81
82 alarm($prev_alarm) if defined($prev_alarm);
83
84 die "unknown error" if $sigcount && !$err; # seems to happen sometimes
85
86 die $err if $err;
87
88 return $res;
89 }
90
91 sub fork_tunnel {
92 my ($self, $nodeip, $lport, $rport) = @_;
93
94 my $cmd = [@{$self->{rem_ssh}}, '-L', "$lport:localhost:$rport",
95 'qm', 'mtunnel' ];
96
97 my $tunnel = fork_command_pipe($cmd);
98
99 my $reader = $tunnel->{reader};
100
101 my $helo;
102 eval {
103 run_with_timeout(60, sub { $helo = <$reader>; });
104 die "no reply\n" if !$helo;
105 die "no quorum on target node\n" if $helo =~ m/^no quorum$/;
106 die "got strange reply from mtunnel ('$helo')\n"
107 if $helo !~ m/^tunnel online$/;
108 };
109 my $err = $@;
110
111 if ($err) {
112 finish_command_pipe($tunnel);
113 die "can't open migration tunnel - $err";
114 }
115 return $tunnel;
116 }
117
118 sub finish_tunnel {
119 my ($self, $tunnel) = @_;
120
121 my $writer = $tunnel->{writer};
122
123 eval {
124 run_with_timeout(30, sub {
125 print $writer "quit\n";
126 $writer->flush();
127 });
128 };
129 my $err = $@;
130
131 finish_command_pipe($tunnel);
132
133 die $err if $err;
134 }
135
136 sub lock_vm {
137 my ($self, $vmid, $code, @param) = @_;
138
139 return PVE::QemuServer::lock_config($vmid, $code, @param);
140 }
141
142 sub prepare {
143 my ($self, $vmid) = @_;
144
145 my $online = $self->{opts}->{online};
146
147 $self->{storecfg} = PVE::Storage::config();
148
149 # test is VM exist
150 my $conf = $self->{vmconf} = PVE::QemuServer::load_config($vmid);
151
152 PVE::QemuServer::check_lock($conf);
153
154 my $running = 0;
155 if (my $pid = PVE::QemuServer::check_running($vmid)) {
156 die "cant migrate running VM without --online\n" if !$online;
157 $running = $pid;
158 }
159
160 if (my $loc_res = PVE::QemuServer::check_local_resources($conf, 1)) {
161 if ($self->{running} || !$self->{opts}->{force}) {
162 die "can't migrate VM which uses local devices\n";
163 } else {
164 $self->log('info', "migrating VM which uses local devices");
165 }
166 }
167
168 # activate volumes
169 my $vollist = PVE::QemuServer::get_vm_volumes($conf);
170 PVE::Storage::activate_volumes($self->{storecfg}, $vollist);
171
172 # fixme: check if storage is available on both nodes
173
174 # test ssh connection
175 my $cmd = [ @{$self->{rem_ssh}}, '/bin/true' ];
176 eval { $self->cmd_quiet($cmd); };
177 die "Can't connect to destination address using public key\n" if $@;
178
179 return $running;
180 }
181
182 sub sync_disks {
183 my ($self, $vmid) = @_;
184
185 $self->log('info', "copying disk images");
186
187 my $conf = $self->{vmconf};
188
189 $self->{volumes} = [];
190
191 my $res = [];
192
193 eval {
194
195 my $volhash = {};
196 my $cdromhash = {};
197
198 # get list from PVE::Storage (for unused volumes)
199 my $dl = PVE::Storage::vdisk_list($self->{storecfg}, undef, $vmid);
200 PVE::Storage::foreach_volid($dl, sub {
201 my ($volid, $sid, $volname) = @_;
202
203 # check if storage is available on both nodes
204 my $scfg = PVE::Storage::storage_check_node($self->{storecfg}, $sid);
205 PVE::Storage::storage_check_node($self->{storecfg}, $sid, $self->{node});
206
207 return if $scfg->{shared};
208
209 $volhash->{$volid} = 1;
210 });
211
212 # and add used,owned/non-shared disks (just to be sure we have all)
213
214 my $sharedvm = 1;
215 PVE::QemuServer::foreach_drive($conf, sub {
216 my ($ds, $drive) = @_;
217
218 my $volid = $drive->{file};
219 return if !$volid;
220
221 die "cant migrate local file/device '$volid'\n" if $volid =~ m|^/|;
222
223 if (PVE::QemuServer::drive_is_cdrom($drive)) {
224 die "cant migrate local cdrom drive\n" if $volid eq 'cdrom';
225 return if $volid eq 'none';
226 $cdromhash->{$volid} = 1;
227 }
228
229 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
230
231 # check if storage is available on both nodes
232 my $scfg = PVE::Storage::storage_check_node($self->{storecfg}, $sid);
233 PVE::Storage::storage_check_node($self->{storecfg}, $sid, $self->{node});
234
235 return if $scfg->{shared};
236
237 die "can't migrate local cdrom '$volid'\n" if $cdromhash->{$volid};
238
239 $sharedvm = 0;
240
241 my ($path, $owner) = PVE::Storage::path($self->{storecfg}, $volid);
242
243 die "can't migrate volume '$volid' - owned by other VM (owner = VM $owner)\n"
244 if !$owner || ($owner != $self->{vmid});
245
246 $volhash->{$volid} = 1;
247 });
248
249 if ($self->{running} && !$sharedvm) {
250 die "can't do online migration - VM uses local disks\n";
251 }
252
253 # do some checks first
254 foreach my $volid (keys %$volhash) {
255 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
256 my $scfg = PVE::Storage::storage_config($self->{storecfg}, $sid);
257
258 die "can't migrate '$volid' - storagy type '$scfg->{type}' not supported\n"
259 if $scfg->{type} ne 'dir';
260 }
261
262 foreach my $volid (keys %$volhash) {
263 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
264 push @{$self->{volumes}}, $volid;
265 PVE::Storage::storage_migrate($self->{storecfg}, $volid, $self->{nodeip}, $sid);
266 }
267 };
268 die "Failed to sync data - $@" if $@;
269 }
270
271 sub phase1 {
272 my ($self, $vmid) = @_;
273
274 $self->log('info', "starting migration of VM $vmid to node '$self->{node}' ($self->{nodeip})");
275
276 my $conf = $self->{vmconf};
277
278 # set migrate lock in config file
279 PVE::QemuServer::change_config_nolock($vmid, { lock => 'migrate' }, {}, 1);
280
281 sync_disks($self, $vmid);
282
283 # move config to remote node
284 my $conffile = PVE::QemuServer::config_file($vmid);
285 my $newconffile = PVE::QemuServer::config_file($vmid, $self->{node});
286
287 die "Failed to move config to node '$self->{node}' - rename failed: $!\n"
288 if !rename($conffile, $newconffile);
289 };
290
291 sub phase1_cleanup {
292 my ($self, $vmid, $err) = @_;
293
294 $self->log('info', "aborting phase 1 - cleanup resources");
295
296 my $unset = { lock => 1 };
297 eval { PVE::QemuServer::change_config_nolock($vmid, {}, $unset, 1) };
298 if (my $err = $@) {
299 $self->log('err', $err);
300 }
301
302 if ($self->{volumes}) {
303 foreach my $volid (@{$self->{volumes}}) {
304 $self->log('err', "found stale volume copy '$volid' on node '$self->{node}'");
305 # fixme: try to remove ?
306 }
307 }
308 }
309
310 sub phase2 {
311 my ($self, $vmid) = @_;
312
313 my $conf = $self->{vmconf};
314
315 logmsg('info', "starting VM $vmid on remote node '$self->{node}'");
316
317 my $rport;
318
319 ## start on remote node
320 my $cmd = [@{$self->{rem_ssh}}, 'qm', 'start',
321 $vmid, '--stateuri', 'tcp', '--skiplock'];
322
323 $self->cmd($cmd, outfunc => sub {
324 my $line = shift;
325
326 if ($line =~ m/^migration listens on port (\d+)$/) {
327 $rport = $1;
328 }
329 });
330
331 die "unable to detect remote migration port\n" if !$rport;
332
333 $self->log('info', "starting migration tunnel");
334
335 ## create tunnel to remote port
336 my $lport = PVE::QemuServer::next_migrate_port();
337 $self->{tunnel} = $self->fork_tunnel($self->{nodeip}, $lport, $rport);
338
339 $self->log('info', "starting online/live migration");
340 # start migration
341
342 my $start = time();
343
344 PVE::QemuServer::vm_monitor_command($vmid, "migrate -d \"tcp:localhost:$lport\"", 1);
345
346 my $lstat = '';
347 while (1) {
348 sleep (2);
349 my $stat = PVE::QemuServer::vm_monitor_command($vmid, "info migrate", 1);
350 if ($stat =~ m/^Migration status: (active|completed|failed|cancelled)$/im) {
351 my $ms = $1;
352
353 if ($stat ne $lstat) {
354 if ($ms eq 'active') {
355 my ($trans, $rem, $total) = (0, 0, 0);
356 $trans = $1 if $stat =~ m/^transferred ram: (\d+) kbytes$/im;
357 $rem = $1 if $stat =~ m/^remaining ram: (\d+) kbytes$/im;
358 $total = $1 if $stat =~ m/^total ram: (\d+) kbytes$/im;
359
360 $self->log('info', "migration status: $ms (transferred ${trans}KB, " .
361 "remaining ${rem}KB), total ${total}KB)");
362 } else {
363 $self->log('info', "migration status: $ms");
364 }
365 }
366
367 if ($ms eq 'completed') {
368 my $delay = time() - $start;
369 if ($delay > 0) {
370 my $mbps = sprintf "%.2f", $conf->{memory}/$delay;
371 $self->log('info', "migration speed: $mbps MB/s");
372 }
373 }
374
375 if ($ms eq 'failed' || $ms eq 'cancelled') {
376 die "aborting\n"
377 }
378
379 last if $ms ne 'active';
380 } else {
381 die "unable to parse migration status '$stat' - aborting\n";
382 }
383 $lstat = $stat;
384 };
385 }
386
387 sub phase3 {
388 my ($self, $vmid) = @_;
389
390 my $volids = $self->{volumes};
391
392 # destroy local copies
393 foreach my $volid (@$volids) {
394 eval { PVE::Storage::vdisk_free($self->{storecfg}, $volid); };
395 if (my $err = $@) {
396 $self->log('err', "removing local copy of '$volid' failed - $err");
397 $self->{errors} = 1;
398 last if $err =~ /^interrupted by signal$/;
399 }
400 }
401
402 if ($self->{tunnel}) {
403 eval { finish_tunnel($self, $self->{tunnel}); };
404 if (my $err = $@) {
405 $self->log('err', $err);
406 $self->{errors} = 1;
407 }
408 }
409 }
410
411 sub phase3_cleanup {
412 my ($self, $vmid, $err) = @_;
413
414 my $conf = $self->{vmconf};
415
416 # always stop local VM
417 eval { PVE::QemuServer::vm_stop($self->{storecfg}, $vmid, 1, 1); };
418 if (my $err = $@) {
419 $self->log('err', "stopping vm failed - $err");
420 $self->{errors} = 1;
421 }
422
423 # always deactivate volumes - avoid lvm LVs to be active on several nodes
424 eval {
425 my $vollist = PVE::QemuServer::get_vm_volumes($conf);
426 PVE::Storage::deactivate_volumes($self->{storecfg}, $vollist);
427 };
428 if (my $err = $@) {
429 $self->log('err', $err);
430 $self->{errors} = 1;
431 }
432
433 # clear migrate lock
434 my $cmd = [ @{$self->{rem_ssh}}, 'qm', 'unlock', $vmid ];
435 $self->cmd_logerr($cmd, errmsg => "failed to clear migrate lock");
436 }
437
438 sub final_cleanup {
439 my ($self, $vmid) = @_;
440
441 # nothing to do
442 }
443
444 1;