package PVE::OpenVZMigrate; use strict; use warnings; use PVE::AbstractMigrate; use File::Basename; use File::Copy; use PVE::Tools; use PVE::INotify; use PVE::Cluster; use PVE::Storage; use PVE::OpenVZ; use base qw(PVE::AbstractMigrate); # fixme: lock VM on target node sub lock_vm { my ($self, $vmid, $code, @param) = @_; return PVE::OpenVZ::lock_container($vmid, undef, $code, @param); } sub prepare { my ($self, $vmid) = @_; my $online = $self->{opts}->{online}; $self->{storecfg} = PVE::Storage::config(); $self->{vzconf} = PVE::OpenVZ::read_global_vz_config(), # test is VM exist my $conf = $self->{vmconf} = PVE::OpenVZ::load_config($vmid); my $path = PVE::OpenVZ::get_privatedir($conf, $vmid); my ($vtype, $volid) = PVE::Storage::path_to_volume_id($self->{storecfg}, $path); my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1) if $volid; die "can't determine assigned storage\n" if !$storage; # check if storage is available on both nodes my $scfg = PVE::Storage::storage_check_node($self->{storecfg}, $storage); PVE::Storage::storage_check_node($self->{storecfg}, $storage, $self->{node}); # we simply use the backup dir to store temporary dump files # Note: this is on shared storage if the storage is 'shared' $self->{dumpdir} = PVE::Storage::get_backup_dir($self->{storecfg}, $storage); PVE::Storage::activate_volumes($self->{storecfg}, [ $volid ]); $self->{storage} = $storage; $self->{privatedir} = $path; $self->{rootdir} = PVE::OpenVZ::get_rootdir($conf, $vmid); $self->{shared} = $scfg->{shared}; my $running = 0; if (PVE::OpenVZ::check_running($vmid)) { die "cant migrate running container without --online\n" if !$online; $running = 1; } # fixme: test if VM uses local resources # test ssh connection my $cmd = [ @{$self->{rem_ssh}}, '/bin/true' ]; eval { $self->cmd_quiet($cmd); }; die "Can't connect to destination address using public key\n" if $@; if ($running) { # test if OpenVZ is running $cmd = [ @{$self->{rem_ssh}}, '/etc/init.d/vz status' ]; eval { $self->cmd_quiet($cmd); }; die "OpenVZ is not running on the target machine\n" if $@; # test if CPT modules are loaded for online migration die "vzcpt module is not loaded\n" if ! -f '/proc/cpt'; $cmd = [ @{$self->{rem_ssh}}, 'test -f /proc/rst' ]; eval { $self->cmd_quiet($cmd); }; die "vzrst module is not loaded on the target machine\n" if $@; } # fixme: do we want to test if IPs exists on target node? return $running; } sub phase1 { my ($self, $vmid) = @_; $self->log('info', "starting migration of CT $self->{vmid} to node '$self->{node}' ($self->{nodeip})"); my $conf = $self->{vmconf}; if ($self->{running}) { $self->log('info', "container is running - using online migration"); } my $cmd = [ @{$self->{rem_ssh}}, 'mkdir', '-p', $self->{rootdir} ]; $self->cmd_quiet($cmd, errmsg => "Failed to make container root directory"); my $privatedir = $self->{privatedir}; if (!$self->{shared}) { $cmd = [ @{$self->{rem_ssh}}, 'mkdir', '-p', $privatedir ]; $self->cmd_quiet($cmd, errmsg => "Failed to make container private directory"); $self->{undo_private} = $privatedir; $self->log('info', "starting rsync phase 1"); my $basedir = dirname($privatedir); $cmd = [ @{$self->{rsync_cmd}}, '--sparse', $privatedir, "root\@$self->{nodeip}:$basedir" ]; $self->cmd($cmd, errmsg => "Failed to sync container private area"); } else { $self->log('info', "container data is on shared storage '$self->{storage}'"); } my $conffile = PVE::OpenVZ::config_file($vmid); my $newconffile = PVE::OpenVZ::config_file($vmid, $self->{node}); my $srccfgdir = dirname($conffile); my $newcfgdir = dirname($newconffile); foreach my $s (PVE::OpenVZ::SCRIPT_EXT) { my $scriptfn = "${vmid}.$s"; my $srcfn = "$srccfgdir/$scriptfn"; next if ! -f $srcfn; my $dstfn = "$newcfgdir/$scriptfn"; copy($srcfn, $dstfn) || die "copy '$srcfn' to '$dstfn' failed - $!\n"; } if ($self->{running}) { # fixme: save state and quota $self->log('info', "start live migration - suspending container"); $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--suspend' ]; $self->cmd_quiet($cmd, errmsg => "Failed to suspend container"); $self->{undo_suspend} = 1; $self->log('info', "dump container state"); $self->{dumpfile} = "$self->{dumpdir}/dump.$vmid"; $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--dump', '--dumpfile', $self->{dumpfile} ]; $self->cmd_quiet($cmd, errmsg => "Failed to dump container state"); if (!$self->{shared}) { $self->log('info', "copy dump file to target node"); $self->{undo_copy_dump} = 1; $cmd = [ @{$self->{scp_cmd}}, $self->{dumpfile}, "root\@$self->{nodeip}:$self->{dumpfile}"]; $self->cmd_quiet($cmd, errmsg => "Failed to copy dump file"); $self->log('info', "starting rsync (2nd pass)"); my $basedir = dirname($privatedir); $cmd = [ @{$self->{rsync_cmd}}, $privatedir, "root\@$self->{nodeip}:$basedir" ]; $self->cmd($cmd, errmsg => "Failed to sync container private area"); } } else { if (PVE::OpenVZ::check_mounted($conf, $vmid)) { $self->log('info', "unmounting container"); $cmd = [ 'vzctl', '--skiplock', 'umount', $vmid ]; $self->cmd_quiet($cmd, errmsg => "Failed to umount container"); } } my $disk_quota = $conf->{disk_quota}->{value}; if (!defined($disk_quota) || ($disk_quota != 0)) { $disk_quota = $self->{disk_quota} = 1; $self->log('info', "dump 2nd level quota"); $self->{quotadumpfile} = "$self->{dumpdir}/quotadump.$vmid"; $cmd = "vzdqdump $vmid -U -G -T > " . PVE::Tools::shellquote($self->{quotadumpfile}); $self->cmd_quiet($cmd, errmsg => "Failed to dump 2nd level quota"); if (!$self->{shared}) { $self->log('info', "copy 2nd level quota to target node"); $self->{undo_copy_quota_dump} = 1; $cmd = [@{$self->{scp_cmd}}, $self->{quotadumpfile}, "root\@$self->{nodeip}:$self->{quotadumpfile}"]; $self->cmd_quiet($cmd, errmsg => "Failed to copy 2nd level quota dump"); } } # everythin copied - make sure container is stoped # fixme_ do we need to start on the other node first? if ($self->{running}) { delete $self->{undo_suspend}; $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--kill' ]; $self->cmd_quiet($cmd, errmsg => "Failed to kill container"); $cmd = [ 'vzctl', '--skiplock', 'umount', $vmid ]; sleep(1); # hack: wait - else there are open files $self->cmd_quiet($cmd, errmsg => "Failed to umount container"); } # move config die "Failed to move config to node '$self->{node}' - rename failed: $!\n" if !rename($conffile, $newconffile); } sub phase1_cleanup { my ($self, $vmid, $err) = @_; $self->log('info', "aborting phase 1 - cleanup resources"); my $conf = $self->{vmconf}; if ($self->{undo_suspend}) { my $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--resume' ]; $self->cmd_logerr($cmd, errmsg => "Failed to resume container"); } if ($self->{undo_private}) { $self->log('info', "removing copied files on target node"); my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-rf', $self->{undo_private} ]; $self->cmd_logerr($cmd, errmsg => "Failed to remove copied files"); } # fixme: that seem to be very dangerous and not needed #my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-rf', $self->{rootdir} ]; #eval { $self->cmd_quiet($cmd); }; my $newconffile = PVE::OpenVZ::config_file($vmid, $self->{node}); my $newcfgdir = dirname($newconffile); foreach my $s (PVE::OpenVZ::SCRIPT_EXT) { my $scriptfn = "${vmid}.$s"; my $dstfn = "$newcfgdir/$scriptfn"; if (-f $dstfn) { $self->log('err', "unlink '$dstfn' failed - $!") if !unlink $dstfn; } } } sub init_target_vm { my ($self, $vmid) = @_; my $conf = $self->{vmconf}; $self->log('info', "initialize container on remote node '$self->{node}'"); my $cmd = [ @{$self->{rem_ssh}}, 'vzctl', '--quiet', 'set', $vmid, '--applyconfig_map', 'name', '--save' ]; $self->cmd_quiet($cmd, errmsg => "Failed to apply config on target node"); if ($self->{disk_quota}) { $self->log('info', "initializing remote quota"); $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'quotainit', $vmid]; $self->cmd_quiet($cmd, errmsg => "Failed to initialize quota"); $self->log('info', "turn on remote quota"); $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'quotaon', $vmid]; $self->cmd_quiet($cmd, errmsg => "Failed to turn on quota"); $self->log('info', "load 2nd level quota"); $cmd = [ @{$self->{rem_ssh}}, "(vzdqload $vmid -U -G -T < " . PVE::Tools::shellquote($self->{quotadumpfile}) . " && vzquota reload2 $vmid)"]; $self->cmd_quiet($cmd, errmsg => "Failed to load 2nd level quota"); if (!$self->{running}) { $self->log('info', "turn off remote quota"); $cmd = [ @{$self->{rem_ssh}}, 'vzquota', 'off', $vmid]; $self->cmd_quiet($cmd, errmsg => "Failed to turn off quota"); } } } sub phase2 { my ($self, $vmid) = @_; my $conf = $self->{vmconf}; $self->{target_initialized} = 1; init_target_vm($self, $vmid); $self->log('info', "starting container on remote node '$self->{node}'"); $self->log('info', "restore container state"); $self->{dumpfile} = "$self->{dumpdir}/dump.$vmid"; my $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'restore', $vmid, '--undump', '--dumpfile', $self->{dumpfile}, '--skip_arpdetect' ]; $self->cmd_quiet($cmd, errmsg => "Failed to restore container"); $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'restore', $vmid, '--resume' ]; $self->cmd_quiet($cmd, errmsg => "Failed to resume container"); } sub phase3 { my ($self, $vmid) = @_; if (!$self->{target_initialized}) { init_target_vm($self, $vmid); } } sub phase3_cleanup { my ($self, $vmid, $err) = @_; my $conf = $self->{vmconf}; if (!$self->{shared}) { # destroy local container data $self->log('info', "removing container files on local node"); my $cmd = [ 'rm', '-rf', $self->{privatedir} ]; $self->cmd_logerr($cmd); } if ($self->{disk_quota}) { my $cmd = [ 'vzquota', 'drop', $vmid]; $self->cmd_logerr($cmd, errmsg => "Failed to drop local quota"); } } sub final_cleanup { my ($self, $vmid) = @_; $self->log('info', "start final cleanup"); my $conf = $self->{vmconf}; unlink($self->{quotadumpfile}) if $self->{quotadumpfile}; unlink($self->{dumpfile}) if $self->{dumpfile}; if ($self->{undo_copy_dump} && $self->{dumpfile}) { my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-f', $self->{dumpfile} ]; $self->cmd_logerr($cmd, errmsg => "Failed to remove dump file"); } if ($self->{undo_copy_quota_dump} && $self->{quotadumpfile}) { my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-f', $self->{quotadumpfile} ]; $self->cmd_logerr($cmd, errmsg => "Failed to remove 2nd level quota dump file"); } } 1;