#!/usr/bin/perl -w use strict; use Getopt::Long; use PVE::SafeSyslog; use IO::Select; use IPC::Open3; use IPC::Open2; use PVE::Cluster; use PVE::QemuServer; use PVE::Storage; use POSIX qw(strftime); # fimxe: adopt for new cluster filestem die "not implemented - fixme!"; # fixme: kvm > 88 has more migration options and verbose status initlog('qmigrate'); PVE::Cluster::cfs_update(); sub print_usage { my $msg = shift; print STDERR "ERROR: $msg\n" if $msg; print STDERR "USAGE: qmigrate [--online] [--verbose]\n"; print STDERR " destination_address VMID\n"; exit (-1); } # fixme: bwlimit ? my $opt_online; my $opt_verbose; sub logmsg { my ($level, $msg) = @_; chomp $msg; return if !$msg; my $tstr = strftime ("%b %d %H:%M:%S", localtime); syslog ($level, $msg); foreach my $line (split (/\n/, $msg)) { print STDOUT "$tstr $line\n"; } \*STDOUT->flush(); } if (!GetOptions ('online' => \$opt_online, 'verbose' => \$opt_verbose)) { print_usage (); } if (scalar (@ARGV) != 2) { print_usage (); } my $host = shift; my $vmid = shift; # blowfish is a fast block cipher, much faster then 3des my @ssh_opts = ('-c', 'blowfish', '-o', 'BatchMode=yes'); my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts); my @rem_ssh = (@ssh_cmd, "root\@$host"); my @scp_cmd = ('/usr/bin/scp', @ssh_opts); my $qm_cmd = '/usr/sbin/qm'; $ENV{RSYNC_RSH} = join (' ', @ssh_cmd); logmsg ('err', "illegal VMID") if $vmid !~ m/^\d+$/; $vmid = int ($vmid); # remove leading zeros my $storecfg = PVE::Storage::config(); my $conffile = PVE::QemuServer::config_file ($vmid); my $delayed_interrupt = 0; $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub { logmsg ('err', "received interrupt - delayed"); $delayed_interrupt = 1; }; sub eval_int { my ($func) = @_; eval { local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { $delayed_interrupt = 0; logmsg ('err', "received interrupt"); die "interrupted by signal\n"; }; local $SIG{PIPE} = sub { $delayed_interrupt = 0; logmsg ('err', "received broken pipe interrupt"); die "interrupted by signal\n"; }; my $di = $delayed_interrupt; $delayed_interrupt = 0; die "interrupted by signal\n" if $di; &$func(); }; } sub prepare { die "VM $vmid does not exist\n" if ! -f $conffile; # test ssh connection my $cmd = [ @rem_ssh, '/bin/true' ]; eval { PVE::Storage::run_command ($cmd); }; die "Can't connect to destination address using public key\n" if $@; # test if VM already exists $cmd = [ @rem_ssh, $qm_cmd, 'status', $vmid ]; my $stat = ''; eval { PVE::Storage::run_command ($cmd, outfunc => sub { $stat .= shift; }); }; die "can't query VM status on host '$host'\n" if $@; die "VM $vmid already exists on destination host\n" if $stat !~ m/^unknown$/; } sub sync_disks { my ($conf, $rhash, $running) = @_; logmsg ('info', "copying disk images"); my $res = []; eval { my $volhash = {}; # get list from PVE::Storage (for unused volumes) my $dl = PVE::Storage::vdisk_list ($storecfg, undef, $vmid); PVE::Storage::foreach_volid ($dl, sub { my ($volid, $sid, $volname) = @_; my $scfg = PVE::Storage::storage_config ($storecfg, $sid); return if $scfg->{shared}; $volhash->{$volid} = 1; }); # and add used,owned/non-shared disks (just to be sure we have all) my $sharedvm = 1; PVE::QemuServer::foreach_drive($conf, sub { my ($ds, $drive) = @_; return if PVE::QemuServer::drive_is_cdrom ($drive); my $volid = $drive->{file}; return if !$volid; die "cant migrate local file/device '$volid'\n" if $volid =~ m|^/|; my ($sid, $volname) = PVE::Storage::parse_volume_id ($volid); my $scfg = PVE::Storage::storage_config ($storecfg, $sid); return if $scfg->{shared}; $sharedvm = 0; my ($path, $owner) = PVE::Storage::path ($storecfg, $volid); die "can't migrate volume '$volid' - owned by other VM (owner = VM $owner)\n" if !$owner || ($owner != $vmid); $volhash->{$volid} = 1; }); if ($running && !$sharedvm) { die "can't do online migration - VM uses local disks\n"; } # do some checks first foreach my $volid (keys %$volhash) { my ($sid, $volname) = PVE::Storage::parse_volume_id ($volid); my $scfg = PVE::Storage::storage_config ($storecfg, $sid); die "can't migrate '$volid' - storagy type '$scfg->{type}' not supported\n" if $scfg->{type} ne 'dir'; } foreach my $volid (keys %$volhash) { my ($sid, $volname) = PVE::Storage::parse_volume_id ($volid); push @{$rhash->{volumes}}, $volid; PVE::Storage::storage_migrate ($storecfg, $volid, $host, $sid); } }; die "Failed to sync data - $@" if $@; } sub fork_tunnel { my ($remhost, $lport, $rport) = @_; my $cmd = [@ssh_cmd, '-o', 'BatchMode=yes', '-L', "$lport:localhost:$rport", $remhost, 'qm', 'mtunnel' ]; my $tunnel = PVE::Storage::fork_command_pipe ($cmd); my $reader = $tunnel->{reader}; my $helo; eval { PVE::Storage::run_with_timeout (60, sub { $helo = <$reader>; }); die "no reply\n" if !$helo; die "got strange reply from mtunnel ('$helo')\n" if $helo !~ m/^tunnel online$/; }; my $err = $@; if ($err) { PVE::Storage::finish_command_pipe ($tunnel); die "can't open migration tunnel - $err"; } return $tunnel; } sub finish_tunnel { my $tunnel = shift; my $writer = $tunnel->{writer}; eval { PVE::Storage::run_with_timeout (30, sub { print $writer "quit\n"; $writer->flush(); }); }; my $err = $@; PVE::Storage::finish_command_pipe ($tunnel); die $err if $err; } sub phase1 { my ($conf, $rhash, $running) = @_; logmsg ('info', "starting migration of VM $vmid to host '$host'"); my $loc_res = 0; $loc_res = 1 if $conf->{hostusb}; $loc_res = 1 if $conf->{hostpci}; $loc_res = 1 if $conf->{serial}; $loc_res = 1 if $conf->{parallel}; if ($loc_res) { if ($running) { die "can't migrate VM which uses local devices\n"; } else { logmsg ('info', "migrating VM which uses local devices"); } } # set migrate lock in config file $rhash->{clearlock} = 1; my $settings = { lock => 'migrate' }; PVE::QemuServer::change_config_nolock ($vmid, $settings, {}, 1); # copy config to remote host eval { my $cmd = [ @scp_cmd, $conffile, "root\@$host:$conffile"]; PVE::Storage::run_command ($cmd); $rhash->{conffile} = 1; }; die "Failed to copy config file - $@" if $@; sync_disks ($conf, $rhash, $running); }; sub phase2 { my ($conf, $rhash) = shift; logmsg ('info', "starting VM on remote host '$host'"); my $rport; ## start on remote host my $cmd = [@rem_ssh, $qm_cmd, '--skiplock', 'start', $vmid, '--incoming', 'tcp']; PVE::Storage::run_command ($cmd, outfunc => sub { my $line = shift; if ($line =~ m/^migration listens on port (\d+)$/) { $rport = $1; } }); die "unable to detect remote migration port\n" if !$rport; logmsg ('info', "starting migration tunnel"); ## create tunnel to remote port my $lport = PVE::QemuServer::next_migrate_port (); $rhash->{tunnel} = fork_tunnel ($host, $lport, $rport); logmsg ('info', "starting online/live migration"); # start migration my $start = time(); PVE::QemuServer::vm_monitor_command ($vmid, "migrate -d \"tcp:localhost:$lport\""); my $lstat = ''; while (1) { sleep (2); my $stat = PVE::QemuServer::vm_monitor_command ($vmid, "info migrate", 1); if ($stat =~ m/^Migration status: (active|completed|failed|cancelled)$/im) { my $ms = $1; if ($stat ne $lstat) { if ($ms eq 'active') { my ($trans, $rem, $total) = (0, 0, 0); $trans = $1 if $stat =~ m/^transferred ram: (\d+) kbytes$/im; $rem = $1 if $stat =~ m/^remaining ram: (\d+) kbytes$/im; $total = $1 if $stat =~ m/^total ram: (\d+) kbytes$/im; logmsg ('info', "migration status: $ms (transferred ${trans}KB, " . "remaining ${rem}KB), total ${total}KB)"); } else { logmsg ('info', "migration status: $ms"); } } if ($ms eq 'completed') { my $delay = time() - $start; if ($delay > 0) { my $mbps = sprintf "%.2f", $conf->{memory}/$delay; logmsg ('info', "migration speed: $mbps MB/s"); } } if ($ms eq 'failed' || $ms eq 'cancelled') { die "aborting\n" } last if $ms ne 'active'; } else { die "unable to parse migration status '$stat' - aborting\n"; } $lstat = $stat; }; } my $errors; my $starttime = time(); # lock config during migration PVE::QemuServer::lock_config ($vmid, sub { eval_int (\&prepare); die $@ if $@; my $conf = PVE::QemuServer::load_config($vmid); PVE::QemuServer::check_lock ($conf); my $running = 0; if (PVE::QemuServer::check_running ($vmid)) { die "cant migrate running VM without --online\n" if !$opt_online; $running = 1; } my $rhash = {}; eval_int (sub { phase1 ($conf, $rhash, $running); }); my $err = $@; if ($err) { if ($rhash->{clearlock}) { my $unset = { lock => 1 }; eval { PVE::QemuServer::change_config_nolock ($vmid, {}, $unset, 1) }; logmsg ('err', $@) if $@; } if ($rhash->{conffile}) { my $cmd = [ @rem_ssh, '/bin/rm', '-f', $conffile ]; eval { PVE::Storage::run_command ($cmd); }; logmsg ('err', $@) if $@; } if ($rhash->{volumes}) { foreach my $volid (@{$rhash->{volumes}}) { logmsg ('err', "found stale volume copy '$volid' on host '$host'"); } } die $err; } # vm is now owned by other host my $volids = $rhash->{volumes}; if ($running) { $rhash = {}; eval_int (sub { phase2 ($conf, $rhash); }); my $err = $@; # always kill tunnel if ($rhash->{tunnel}) { eval_int (sub { finish_tunnel ($rhash->{tunnel}) }); if ($@) { logmsg ('err', "stopping tunnel failed - $@"); $errors = 1; } } # always stop local VM - no interrupts possible eval { PVE::QemuServer::vm_stop ($vmid, 1); }; if ($@) { logmsg ('err', "stopping vm failed - $@"); $errors = 1; } if ($err) { $errors = 1; logmsg ('err', "online migrate failure - $err"); } } # finalize -- clear migrate lock eval_int (sub { my $cmd = [@rem_ssh, $qm_cmd, 'unlock', $vmid ]; PVE::Storage::run_command ($cmd); }); if ($@) { logmsg ('err', "failed to clear migrate lock - $@"); $errors = 1; } unlink $conffile; # destroy local copies foreach my $volid (@$volids) { eval_int (sub { PVE::Storage::vdisk_free ($storecfg, $volid); }); my $err = $@; if ($err) { logmsg ('err', "removing local copy of '$volid' failed - $err"); $errors = 1; last if $err =~ /^interrupted by signal$/; } } }); my $err = $@; my $delay = time () - $starttime; my $mins = int ($delay/60); my $secs = $delay - $mins*60; my $hours = int ($mins/60); $mins = $mins - $hours*60; my $duration = sprintf "%02d:%02d:%02d", $hours, $mins, $secs; if ($err) { logmsg ('err', $err) if $err; logmsg ('info', "migration aborted"); exit (-1); } if ($errors) { logmsg ('info', "migration finished with problems (duration $duration)"); exit (-1); } logmsg ('info', "migration finished successfuly (duration $duration)"); exit (0); __END__ =head1 NAME qmigrate - utility for VM migration between hardware nodes (kvm/qemu) =head1 SYNOPSIS qmigrate [--online] [--verbose] destination_address VMID =head1 DESCRIPTION no info available.