12 use POSIX
qw(strftime);
14 # fimxe: adopt for new cluster filestem
16 die "not implemented - fixme!";
18 # fixme: kvm > 88 has more migration options and verbose status
21 PVE
::Cluster
::cfs_update
();
26 print STDERR
"ERROR: $msg\n" if $msg;
27 print STDERR
"USAGE: qmigrate [--online] [--verbose]\n";
28 print STDERR
" destination_address VMID\n";
39 my ($level, $msg) = @_;
45 my $tstr = strftime
("%b %d %H:%M:%S", localtime);
47 syslog
($level, $msg);
49 foreach my $line (split (/\n/, $msg)) {
50 print STDOUT
"$tstr $line\n";
55 if (!GetOptions
('online' => \
$opt_online,
56 'verbose' => \
$opt_verbose)) {
60 if (scalar (@ARGV) != 2) {
67 # blowfish is a fast block cipher, much faster then 3des
68 my @ssh_opts = ('-c', 'blowfish', '-o', 'BatchMode=yes');
69 my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
70 my @rem_ssh = (@ssh_cmd, "root\@$host");
71 my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
72 my $qm_cmd = '/usr/sbin/qm';
74 $ENV{RSYNC_RSH
} = join (' ', @ssh_cmd);
76 logmsg
('err', "illegal VMID") if $vmid !~ m/^\d+$/;
77 $vmid = int ($vmid); # remove leading zeros
79 my $storecfg = PVE
::Storage
::config
();
81 my $conffile = PVE
::QemuServer
::config_file
($vmid);
83 my $delayed_interrupt = 0;
85 $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = $SIG{PIPE
} = sub {
86 logmsg
('err', "received interrupt - delayed");
87 $delayed_interrupt = 1;
94 local $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = sub {
95 $delayed_interrupt = 0;
96 logmsg
('err', "received interrupt");
97 die "interrupted by signal\n";
99 local $SIG{PIPE
} = sub {
100 $delayed_interrupt = 0;
101 logmsg
('err', "received broken pipe interrupt");
102 die "interrupted by signal\n";
105 my $di = $delayed_interrupt;
106 $delayed_interrupt = 0;
108 die "interrupted by signal\n" if $di;
116 die "VM $vmid does not exist\n" if ! -f
$conffile;
118 # test ssh connection
119 my $cmd = [ @rem_ssh, '/bin/true' ];
120 eval { PVE
::Storage
::run_command
($cmd); };
121 die "Can't connect to destination address using public key\n" if $@;
123 # test if VM already exists
124 $cmd = [ @rem_ssh, $qm_cmd, 'status', $vmid ];
127 PVE
::Storage
::run_command
($cmd, outfunc
=> sub { $stat .= shift; });
129 die "can't query VM status on host '$host'\n" if $@;
131 die "VM $vmid already exists on destination host\n" if $stat !~ m/^unknown$/;
135 my ($conf, $rhash, $running) = @_;
137 logmsg
('info', "copying disk images");
145 # get list from PVE::Storage (for unused volumes)
146 my $dl = PVE
::Storage
::vdisk_list
($storecfg, undef, $vmid);
147 PVE
::Storage
::foreach_volid
($dl, sub {
148 my ($volid, $sid, $volname) = @_;
150 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
152 return if $scfg->{shared
};
154 $volhash->{$volid} = 1;
157 # and add used,owned/non-shared disks (just to be sure we have all)
160 PVE
::QemuServer
::foreach_drive
($conf, sub {
161 my ($ds, $drive) = @_;
163 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
165 my $volid = $drive->{file
};
168 die "cant migrate local file/device '$volid'\n" if $volid =~ m
|^/|;
170 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
172 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
174 return if $scfg->{shared
};
178 my ($path, $owner) = PVE
::Storage
::path
($storecfg, $volid);
180 die "can't migrate volume '$volid' - owned by other VM (owner = VM $owner)\n"
181 if !$owner || ($owner != $vmid);
183 $volhash->{$volid} = 1;
186 if ($running && !$sharedvm) {
187 die "can't do online migration - VM uses local disks\n";
190 # do some checks first
191 foreach my $volid (keys %$volhash) {
192 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
193 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
195 die "can't migrate '$volid' - storagy type '$scfg->{type}' not supported\n"
196 if $scfg->{type
} ne 'dir';
199 foreach my $volid (keys %$volhash) {
200 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
201 push @{$rhash->{volumes
}}, $volid;
202 PVE
::Storage
::storage_migrate
($storecfg, $volid, $host, $sid);
206 die "Failed to sync data - $@" if $@;
210 my ($remhost, $lport, $rport) = @_;
212 my $cmd = [@ssh_cmd, '-o', 'BatchMode=yes',
213 '-L', "$lport:localhost:$rport", $remhost,
216 my $tunnel = PVE
::Storage
::fork_command_pipe
($cmd);
218 my $reader = $tunnel->{reader
};
222 PVE
::Storage
::run_with_timeout
(60, sub { $helo = <$reader>; });
223 die "no reply\n" if !$helo;
224 die "got strange reply from mtunnel ('$helo')\n"
225 if $helo !~ m/^tunnel online$/;
230 PVE
::Storage
::finish_command_pipe
($tunnel);
231 die "can't open migration tunnel - $err";
239 my $writer = $tunnel->{writer
};
242 PVE
::Storage
::run_with_timeout
(30, sub {
243 print $writer "quit\n";
249 PVE
::Storage
::finish_command_pipe
($tunnel);
255 my ($conf, $rhash, $running) = @_;
257 logmsg
('info', "starting migration of VM $vmid to host '$host'");
260 $loc_res = 1 if $conf->{hostusb
};
261 $loc_res = 1 if $conf->{hostpci
};
262 $loc_res = 1 if $conf->{serial
};
263 $loc_res = 1 if $conf->{parallel
};
267 die "can't migrate VM which uses local devices\n";
269 logmsg
('info', "migrating VM which uses local devices");
273 # set migrate lock in config file
274 $rhash->{clearlock
} = 1;
276 my $settings = { lock => 'migrate' };
277 PVE
::QemuServer
::change_config_nolock
($vmid, $settings, {}, 1);
279 # copy config to remote host
281 my $cmd = [ @scp_cmd, $conffile, "root\@$host:$conffile"];
282 PVE
::Storage
::run_command
($cmd);
283 $rhash->{conffile
} = 1;
285 die "Failed to copy config file - $@" if $@;
287 sync_disks
($conf, $rhash, $running);
291 my ($conf, $rhash) = shift;
293 logmsg
('info', "starting VM on remote host '$host'");
297 ## start on remote host
298 my $cmd = [@rem_ssh, $qm_cmd, '--skiplock', 'start', $vmid, '--incoming', 'tcp'];
300 PVE
::Storage
::run_command
($cmd, outfunc
=> sub {
303 if ($line =~ m/^migration listens on port (\d+)$/) {
308 die "unable to detect remote migration port\n" if !$rport;
310 logmsg
('info', "starting migration tunnel");
311 ## create tunnel to remote port
312 my $lport = PVE
::QemuServer
::next_migrate_port
();
313 $rhash->{tunnel
} = fork_tunnel
($host, $lport, $rport);
315 logmsg
('info', "starting online/live migration");
320 PVE
::QemuServer
::vm_monitor_command
($vmid, "migrate -d \"tcp:localhost:$lport\"");
325 my $stat = PVE::QemuServer::vm_monitor_command ($vmid, "info migrate
", 1);
326 if ($stat =~ m/^Migration status: (active|completed|failed|cancelled)$/im) {
329 if ($stat ne $lstat) {
330 if ($ms eq 'active') {
331 my ($trans, $rem, $total) = (0, 0, 0);
332 $trans = $1 if $stat =~ m/^transferred ram: (\d+) kbytes$/im;
333 $rem = $1 if $stat =~ m/^remaining ram: (\d+) kbytes$/im;
334 $total = $1 if $stat =~ m/^total ram: (\d+) kbytes$/im;
336 logmsg ('info', "migration status
: $ms (transferred
${trans
}KB
, " .
337 "remaining
${rem
}KB
), total
${total
}KB
)");
339 logmsg ('info', "migration status
: $ms");
343 if ($ms eq 'completed') {
344 my $delay = time() - $start;
346 my $mbps = sprintf "%.2f", $conf->{memory}/$delay;
347 logmsg ('info', "migration speed
: $mbps MB
/s
");
351 if ($ms eq 'failed' || $ms eq 'cancelled') {
355 last if $ms ne 'active';
357 die "unable to parse migration status
'$stat' - aborting
\n";
365 my $starttime = time();
367 # lock config during migration
368 PVE::QemuServer::lock_config ($vmid, sub {
370 eval_int (\&prepare);
373 my $conf = PVE::QemuServer::load_config($vmid);
375 PVE::QemuServer::check_lock ($conf);
378 if (PVE::QemuServer::check_running ($vmid)) {
379 die "cant migrate running VM without
--online
\n" if !$opt_online;
384 eval_int (sub { phase1 ($conf, $rhash, $running); });
388 if ($rhash->{clearlock}) {
389 my $unset = { lock => 1 };
390 eval { PVE::QemuServer::change_config_nolock ($vmid, {}, $unset, 1) };
391 logmsg ('err', $@) if $@;
393 if ($rhash->{conffile}) {
394 my $cmd = [ @rem_ssh, '/bin/rm', '-f', $conffile ];
395 eval { PVE::Storage::run_command ($cmd); };
396 logmsg ('err', $@) if $@;
398 if ($rhash->{volumes}) {
399 foreach my $volid (@{$rhash->{volumes}}) {
400 logmsg ('err', "found stale volume copy
'$volid' on host
'$host'");
407 # vm is now owned by other host
408 my $volids = $rhash->{volumes};
413 eval_int (sub { phase2 ($conf, $rhash); });
417 if ($rhash->{tunnel}) {
418 eval_int (sub { finish_tunnel ($rhash->{tunnel}) });
420 logmsg ('err', "stopping tunnel failed
- $@");
425 # always stop local VM - no interrupts possible
426 eval { PVE::QemuServer::vm_stop ($vmid, 1); };
428 logmsg ('err', "stopping vm failed
- $@");
434 logmsg ('err', "online migrate failure
- $err");
438 # finalize -- clear migrate lock
440 my $cmd = [@rem_ssh, $qm_cmd, 'unlock', $vmid ];
441 PVE::Storage::run_command ($cmd);
444 logmsg ('err', "failed to clear migrate
lock - $@");
450 # destroy local copies
451 foreach my $volid (@$volids) {
452 eval_int (sub { PVE::Storage::vdisk_free ($storecfg, $volid); });
456 logmsg ('err', "removing
local copy of
'$volid' failed
- $err");
459 last if $err =~ /^interrupted by signal$/;
466 my $delay = time () - $starttime;
467 my $mins = int ($delay/60);
468 my $secs = $delay - $mins*60;
469 my $hours = int ($mins/60);
470 $mins = $mins - $hours*60;
472 my $duration = sprintf "%02d:%02d:%02d", $hours, $mins, $secs;
475 logmsg ('err', $err) if $err;
476 logmsg ('info', "migration aborted
");
481 logmsg ('info', "migration finished with problems
(duration
$duration)");
485 logmsg ('info', "migration finished successfuly
(duration
$duration)");
493 qmigrate - utility for VM migration between hardware nodes (kvm/qemu)
497 qmigrate [--online] [--verbose] destination_address VMID