2011-09-14 12:02:08 +02:00
package PVE::QemuMigrate ;
2011-09-09 12:10:29 +02:00
2011-08-23 07:47:04 +02:00
use strict ;
2011-09-14 12:02:08 +02:00
use warnings ;
2011-12-07 06:36:20 +01:00
use PVE::AbstractMigrate ;
2011-09-14 12:02:08 +02:00
use IO::File ;
2011-08-23 07:47:04 +02:00
use IPC::Open2 ;
2011-09-14 12:02:08 +02:00
use PVE::INotify ;
use PVE::Cluster ;
2011-08-23 07:47:04 +02:00
use PVE::Storage ;
2011-09-14 12:02:08 +02:00
use PVE::QemuServer ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
use base qw( PVE::AbstractMigrate ) ;
2011-08-23 07:47:04 +02:00
2011-09-09 12:10:29 +02:00
sub fork_command_pipe {
2011-12-07 11:25:20 +01:00
my ( $ self , $ cmd ) = @ _ ;
2011-09-12 12:26:00 +02:00
2011-09-09 12:10:29 +02:00
my $ reader = IO::File - > new ( ) ;
my $ writer = IO::File - > new ( ) ;
my $ orig_pid = $$ ;
my $ cpid ;
eval { $ cpid = open2 ( $ reader , $ writer , @$ cmd ) ; } ;
my $ err = $@ ;
# catch exec errors
if ( $ orig_pid != $$ ) {
2011-12-07 11:25:20 +01:00
$ self - > log ( 'err' , "can't fork command pipe\n" ) ;
2011-09-12 12:26:00 +02:00
POSIX:: _exit ( 1 ) ;
kill ( 'KILL' , $$ ) ;
2011-09-09 12:10:29 +02:00
}
die $ err if $ err ;
return { writer = > $ writer , reader = > $ reader , pid = > $ cpid } ;
}
2011-09-12 12:26:00 +02:00
sub finish_command_pipe {
2011-12-07 11:25:20 +01:00
my ( $ self , $ cmdpipe ) = @ _ ;
2011-09-09 12:10:29 +02:00
my $ writer = $ cmdpipe - > { writer } ;
my $ reader = $ cmdpipe - > { reader } ;
$ writer - > close ( ) ;
$ reader - > close ( ) ;
my $ cpid = $ cmdpipe - > { pid } ;
kill ( 15 , $ cpid ) if kill ( 0 , $ cpid ) ;
waitpid ( $ cpid , 0 ) ;
}
sub run_with_timeout {
my ( $ timeout , $ code , @ param ) = @ _ ;
die "got timeout\n" if $ timeout <= 0 ;
my $ prev_alarm ;
my $ sigcount = 0 ;
my $ res ;
eval {
local $ SIG { ALRM } = sub { $ sigcount + + ; die "got timeout\n" ; } ;
local $ SIG { PIPE } = sub { $ sigcount + + ; die "broken pipe\n" } ;
local $ SIG { __DIE__ } ; # see SA bug 4631
$ prev_alarm = alarm ( $ timeout ) ;
$ res = & $ code ( @ param ) ;
alarm ( 0 ) ; # avoid race conditions
} ;
my $ err = $@ ;
2011-09-12 12:26:00 +02:00
2011-09-09 12:10:29 +02:00
alarm ( $ prev_alarm ) if defined ( $ prev_alarm ) ;
die "unknown error" if $ sigcount && ! $ err ; # seems to happen sometimes
die $ err if $ err ;
return $ res ;
}
2011-08-23 07:47:04 +02:00
sub fork_tunnel {
2011-12-07 06:36:20 +01:00
my ( $ self , $ nodeip , $ lport , $ rport ) = @ _ ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
my $ cmd = [ @ { $ self - > { rem_ssh } } , '-L' , "$lport:localhost:$rport" ,
2011-08-23 07:47:04 +02:00
'qm' , 'mtunnel' ] ;
2011-09-12 12:26:00 +02:00
2011-12-07 11:25:20 +01:00
my $ tunnel = $ self - > fork_command_pipe ( $ cmd ) ;
2011-08-23 07:47:04 +02:00
my $ reader = $ tunnel - > { reader } ;
my $ helo ;
2011-09-12 12:26:00 +02:00
eval {
run_with_timeout ( 60 , sub { $ helo = <$reader> ; } ) ;
2011-08-23 07:47:04 +02:00
die "no reply\n" if ! $ helo ;
2011-09-09 12:10:29 +02:00
die "no quorum on target node\n" if $ helo =~ m/^no quorum$/ ;
2011-09-12 12:26:00 +02:00
die "got strange reply from mtunnel ('$helo')\n"
2011-08-23 07:47:04 +02:00
if $ helo !~ m/^tunnel online$/ ;
} ;
my $ err = $@ ;
if ( $ err ) {
2011-12-07 11:25:20 +01:00
$ self - > finish_command_pipe ( $ tunnel ) ;
2011-08-23 07:47:04 +02:00
die "can't open migration tunnel - $err" ;
}
return $ tunnel ;
}
2011-09-12 12:26:00 +02:00
sub finish_tunnel {
2011-12-07 06:36:20 +01:00
my ( $ self , $ tunnel ) = @ _ ;
2011-08-23 07:47:04 +02:00
my $ writer = $ tunnel - > { writer } ;
2011-09-12 12:26:00 +02:00
eval {
2011-09-09 12:10:29 +02:00
run_with_timeout ( 30 , sub {
2011-08-23 07:47:04 +02:00
print $ writer "quit\n" ;
$ writer - > flush ( ) ;
2011-09-12 12:26:00 +02:00
} ) ;
2011-08-23 07:47:04 +02:00
} ;
my $ err = $@ ;
2011-09-12 12:26:00 +02:00
2011-12-07 11:25:20 +01:00
$ self - > finish_command_pipe ( $ tunnel ) ;
2011-09-12 12:26:00 +02:00
2011-08-23 07:47:04 +02:00
die $ err if $ err ;
}
2011-12-07 06:36:20 +01:00
sub lock_vm {
my ( $ self , $ vmid , $ code , @ param ) = @ _ ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
return PVE::QemuServer:: lock_config ( $ vmid , $ code , @ param ) ;
}
2011-11-25 08:05:36 +01:00
2011-12-07 06:36:20 +01:00
sub prepare {
my ( $ self , $ vmid ) = @ _ ;
2011-11-25 08:05:36 +01:00
2011-12-07 06:36:20 +01:00
my $ online = $ self - > { opts } - > { online } ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
$ self - > { storecfg } = PVE::Storage:: config ( ) ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
# test is VM exist
my $ conf = $ self - > { vmconf } = PVE::QemuServer:: load_config ( $ vmid ) ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
PVE::QemuServer:: check_lock ( $ conf ) ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
my $ running = 0 ;
if ( my $ pid = PVE::QemuServer:: check_running ( $ vmid ) ) {
die "cant migrate running VM without --online\n" if ! $ online ;
$ running = $ pid ;
2011-09-14 12:02:08 +02:00
}
2011-12-07 06:36:20 +01:00
if ( my $ loc_res = PVE::QemuServer:: check_local_resources ( $ conf , 1 ) ) {
if ( $ self - > { running } || ! $ self - > { opts } - > { force } ) {
die "can't migrate VM which uses local devices\n" ;
} else {
$ self - > log ( 'info' , "migrating VM which uses local devices" ) ;
}
2011-09-14 12:02:08 +02:00
}
2011-11-25 08:05:36 +01:00
# activate volumes
my $ vollist = PVE::QemuServer:: get_vm_volumes ( $ conf ) ;
2011-12-07 06:36:20 +01:00
PVE::Storage:: activate_volumes ( $ self - > { storecfg } , $ vollist ) ;
# fixme: check if storage is available on both nodes
2011-09-14 12:02:08 +02:00
# test ssh connection
2011-12-07 06:36:20 +01:00
my $ cmd = [ @ { $ self - > { rem_ssh } } , '/bin/true' ] ;
eval { $ self - > cmd_quiet ( $ cmd ) ; } ;
2011-09-14 12:02:08 +02:00
die "Can't connect to destination address using public key\n" if $@ ;
2011-11-25 08:05:36 +01:00
2011-12-07 06:36:20 +01:00
return $ running ;
2011-09-14 12:02:08 +02:00
}
sub sync_disks {
2011-12-07 06:36:20 +01:00
my ( $ self , $ vmid ) = @ _ ;
$ self - > log ( 'info' , "copying disk images" ) ;
2011-09-14 12:02:08 +02:00
2011-12-07 06:36:20 +01:00
my $ conf = $ self - > { vmconf } ;
$ self - > { volumes } = [] ;
2011-09-14 12:02:08 +02:00
my $ res = [] ;
eval {
my $ volhash = { } ;
my $ cdromhash = { } ;
# get list from PVE::Storage (for unused volumes)
2011-12-07 06:36:20 +01:00
my $ dl = PVE::Storage:: vdisk_list ( $ self - > { storecfg } , undef , $ vmid ) ;
2011-09-14 12:02:08 +02:00
PVE::Storage:: foreach_volid ( $ dl , sub {
my ( $ volid , $ sid , $ volname ) = @ _ ;
2011-12-07 06:36:20 +01:00
# check if storage is available on both nodes
my $ scfg = PVE::Storage:: storage_check_node ( $ self - > { storecfg } , $ sid ) ;
PVE::Storage:: storage_check_node ( $ self - > { storecfg } , $ sid , $ self - > { node } ) ;
2011-09-14 12:02:08 +02:00
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 ) = @ _ ;
my $ volid = $ drive - > { file } ;
return if ! $ volid ;
die "cant migrate local file/device '$volid'\n" if $ volid =~ m | ^ / | ;
if ( PVE::QemuServer:: drive_is_cdrom ( $ drive ) ) {
die "cant migrate local cdrom drive\n" if $ volid eq 'cdrom' ;
return if $ volid eq 'none' ;
$ cdromhash - > { $ volid } = 1 ;
}
my ( $ sid , $ volname ) = PVE::Storage:: parse_volume_id ( $ volid ) ;
2011-12-07 06:36:20 +01:00
# check if storage is available on both nodes
my $ scfg = PVE::Storage:: storage_check_node ( $ self - > { storecfg } , $ sid ) ;
PVE::Storage:: storage_check_node ( $ self - > { storecfg } , $ sid , $ self - > { node } ) ;
2011-09-14 12:02:08 +02:00
return if $ scfg - > { shared } ;
die "can't migrate local cdrom '$volid'\n" if $ cdromhash - > { $ volid } ;
$ sharedvm = 0 ;
2011-12-07 06:36:20 +01:00
my ( $ path , $ owner ) = PVE::Storage:: path ( $ self - > { storecfg } , $ volid ) ;
2011-09-14 12:02:08 +02:00
die "can't migrate volume '$volid' - owned by other VM (owner = VM $owner)\n"
2011-12-07 06:36:20 +01:00
if ! $ owner || ( $ owner != $ self - > { vmid } ) ;
2011-09-14 12:02:08 +02:00
$ volhash - > { $ volid } = 1 ;
} ) ;
2011-12-07 06:36:20 +01:00
if ( $ self - > { running } && ! $ sharedvm ) {
2011-09-14 12:02:08 +02:00
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 ) ;
2011-12-07 06:36:20 +01:00
my $ scfg = PVE::Storage:: storage_config ( $ self - > { storecfg } , $ sid ) ;
2011-09-14 12:02:08 +02:00
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 ) ;
2011-12-07 06:36:20 +01:00
push @ { $ self - > { volumes } } , $ volid ;
PVE::Storage:: storage_migrate ( $ self - > { storecfg } , $ volid , $ self - > { nodeip } , $ sid ) ;
2011-09-14 12:02:08 +02:00
}
} ;
die "Failed to sync data - $@" if $@ ;
}
2011-08-23 07:47:04 +02:00
sub phase1 {
2011-12-07 06:36:20 +01:00
my ( $ self , $ vmid ) = @ _ ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "starting migration of VM $vmid to node '$self->{node}' ($self->{nodeip})" ) ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
my $ conf = $ self - > { vmconf } ;
2011-08-23 07:47:04 +02:00
# set migrate lock in config file
2011-12-07 06:36:20 +01:00
PVE::QemuServer:: change_config_nolock ( $ vmid , { lock = > 'migrate' } , { } , 1 ) ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
sync_disks ( $ self , $ vmid ) ;
2011-09-09 12:10:29 +02:00
# move config to remote node
2011-12-07 06:36:20 +01:00
my $ conffile = PVE::QemuServer:: config_file ( $ vmid ) ;
my $ newconffile = PVE::QemuServer:: config_file ( $ vmid , $ self - > { node } ) ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
die "Failed to move config to node '$self->{node}' - rename failed: $!\n"
2011-09-09 12:10:29 +02:00
if ! rename ( $ conffile , $ newconffile ) ;
2011-08-23 07:47:04 +02:00
} ;
2011-12-07 06:36:20 +01:00
sub phase1_cleanup {
my ( $ self , $ vmid , $ err ) = @ _ ;
$ self - > log ( 'info' , "aborting phase 1 - cleanup resources" ) ;
my $ unset = { lock = > 1 } ;
eval { PVE::QemuServer:: change_config_nolock ( $ vmid , { } , $ unset , 1 ) } ;
if ( my $ err = $@ ) {
$ self - > log ( 'err' , $ err ) ;
}
if ( $ self - > { volumes } ) {
foreach my $ volid ( @ { $ self - > { volumes } } ) {
$ self - > log ( 'err' , "found stale volume copy '$volid' on node '$self->{node}'" ) ;
# fixme: try to remove ?
}
}
}
2011-08-23 07:47:04 +02:00
sub phase2 {
2011-12-07 06:36:20 +01:00
my ( $ self , $ vmid ) = @ _ ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
my $ conf = $ self - > { vmconf } ;
2011-12-07 11:25:20 +01:00
$ self - > log ( 'info' , "starting VM $vmid on remote node '$self->{node}'" ) ;
2011-08-23 07:47:04 +02:00
my $ rport ;
2011-09-12 12:26:00 +02:00
## start on remote node
2011-12-07 06:36:20 +01:00
my $ cmd = [ @ { $ self - > { rem_ssh } } , 'qm' , 'start' ,
$ vmid , '--stateuri' , 'tcp' , '--skiplock' ] ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
$ self - > cmd ( $ cmd , outfunc = > sub {
2011-08-23 07:47:04 +02:00
my $ line = shift ;
if ( $ line =~ m/^migration listens on port (\d+)$/ ) {
$ rport = $ 1 ;
}
} ) ;
die "unable to detect remote migration port\n" if ! $ rport ;
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "starting migration tunnel" ) ;
2011-09-09 12:10:29 +02:00
2011-08-23 07:47:04 +02:00
## create tunnel to remote port
2011-09-09 12:10:29 +02:00
my $ lport = PVE::QemuServer:: next_migrate_port ( ) ;
2011-12-07 06:36:20 +01:00
$ self - > { tunnel } = $ self - > fork_tunnel ( $ self - > { nodeip } , $ lport , $ rport ) ;
2011-08-23 07:47:04 +02:00
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "starting online/live migration" ) ;
2011-08-23 07:47:04 +02:00
# start migration
my $ start = time ( ) ;
2011-12-07 06:36:20 +01:00
PVE::QemuServer:: vm_monitor_command ( $ vmid , "migrate -d \"tcp:localhost:$lport\"" , 1 ) ;
2011-08-23 07:47:04 +02:00
my $ lstat = '' ;
while ( 1 ) {
sleep ( 2 ) ;
2011-12-07 06:36:20 +01:00
my $ stat = PVE::QemuServer:: vm_monitor_command ( $ vmid , "info migrate" , 1 ) ;
2011-08-23 07:47:04 +02:00
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 ;
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "migration status: $ms (transferred ${trans}KB, " .
"remaining ${rem}KB), total ${total}KB)" ) ;
2011-08-23 07:47:04 +02:00
} else {
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "migration status: $ms" ) ;
2011-08-23 07:47:04 +02:00
}
}
if ( $ ms eq 'completed' ) {
my $ delay = time ( ) - $ start ;
if ( $ delay > 0 ) {
my $ mbps = sprintf "%.2f" , $ conf - > { memory } / $ delay ;
2011-12-07 06:36:20 +01:00
$ self - > log ( 'info' , "migration speed: $mbps MB/s" ) ;
2011-08-23 07:47:04 +02:00
}
}
2011-12-07 06:36:20 +01:00
2011-08-23 07:47:04 +02:00
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 ;
} ;
}
2011-12-07 06:36:20 +01:00
sub phase3 {
my ( $ self , $ vmid ) = @ _ ;
my $ volids = $ self - > { volumes } ;
# destroy local copies
foreach my $ volid ( @$ volids ) {
eval { PVE::Storage:: vdisk_free ( $ self - > { storecfg } , $ volid ) ; } ;
if ( my $ err = $@ ) {
$ self - > log ( 'err' , "removing local copy of '$volid' failed - $err" ) ;
$ self - > { errors } = 1 ;
last if $ err =~ /^interrupted by signal$/ ;
}
}
if ( $ self - > { tunnel } ) {
eval { finish_tunnel ( $ self , $ self - > { tunnel } ) ; } ;
if ( my $ err = $@ ) {
$ self - > log ( 'err' , $ err ) ;
$ self - > { errors } = 1 ;
}
}
}
sub phase3_cleanup {
my ( $ self , $ vmid , $ err ) = @ _ ;
my $ conf = $ self - > { vmconf } ;
# always stop local VM
eval { PVE::QemuServer:: vm_stop ( $ self - > { storecfg } , $ vmid , 1 , 1 ) ; } ;
if ( my $ err = $@ ) {
$ self - > log ( 'err' , "stopping vm failed - $err" ) ;
$ self - > { errors } = 1 ;
}
# always deactivate volumes - avoid lvm LVs to be active on several nodes
eval {
my $ vollist = PVE::QemuServer:: get_vm_volumes ( $ conf ) ;
PVE::Storage:: deactivate_volumes ( $ self - > { storecfg } , $ vollist ) ;
} ;
if ( my $ err = $@ ) {
$ self - > log ( 'err' , $ err ) ;
$ self - > { errors } = 1 ;
}
# clear migrate lock
my $ cmd = [ @ { $ self - > { rem_ssh } } , 'qm' , 'unlock' , $ vmid ] ;
$ self - > cmd_logerr ( $ cmd , errmsg = > "failed to clear migrate lock" ) ;
}
sub final_cleanup {
my ( $ self , $ vmid ) = @ _ ;
# nothing to do
}
1 ;