Analogous to the qm mtunnel commands, pct mtunnel creates a tunnel between the two migration nodes, that can be used to execute various commands during the migration process.
This tunnel has been implemented using v2 of our tunnel protocol. It supports the migrate-hook as well as the query-migrate-hook command. The migrate-hook command executes the respective hook script by forking from the tunnel process. This enables us to evade timeouts resulting from long running hook scripts, as well as hookscripts doing weird stuff with STDOUT. query-migrate-hook can be used to get information about the currently running migration-hook. It returns the output of the command in the case of a successful run / error. If the migrate-hook is still running then it returns information about the running migration-hook. In future patches it might be wise to move some basic funtionality used in the tunnel to its own class, since this functionality is used in more places already and could be shared. This seemed out of scope for this patch though. Signed-off-by: Stefan Hanreich <s.hanre...@proxmox.com> --- src/PVE/CLI/pct.pm | 230 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/src/PVE/CLI/pct.pm b/src/PVE/CLI/pct.pm index 23793ee..4adb155 100755 --- a/src/PVE/CLI/pct.pm +++ b/src/PVE/CLI/pct.pm @@ -6,6 +6,7 @@ use warnings; use Fcntl; use File::Copy 'copy'; use POSIX; +use JSON; use PVE::CLIHandler; use PVE::Cluster; @@ -803,6 +804,233 @@ __PACKAGE__->register_method ({ return undef; }}); +__PACKAGE__->register_method ({ + name => 'mtunnel', + path => 'mtunnel', + method => 'POST', + description => "Used by qmigrate - do not use manually.", + parameters => { + additionalProperties => 0, + properties => {}, + }, + returns => { type => 'null'}, + code => sub { + my ($param) = @_; + + if (!PVE::Cluster::check_cfs_quorum(1)) { + print "no quorum\n"; + return; + } + + my $tunnel_write = sub { + my $text = shift; + chomp $text; + print "$text\n"; + *STDOUT->flush(); + }; + + $tunnel_write->("tunnel online"); + $tunnel_write->("ver 2"); + + my $state = { + quit => 0, + }; + + my $cmd_desc = { + quit => {}, + 'query-migrate-hook' => {}, + 'migrate-hook' => { + properties => { + vmid => get_standard_option('pve-vmid'), + source => get_standard_option('pve-node'), + target => get_standard_option('pve-node'), + phase => { + type => 'string', + description => 'The phase of the hook (either pre or post)', + }, + } + }, + }; + + my $cmd_handlers = { + 'quit' => sub { + $state->{quit} = 1; + return; + }, + 'query-migrate-hook' => sub { + if (!$state->{migrate_hook}) { + die "No migration hook running!" + } + + if (!waitpid($state->{migrate_hook}->{pid}, POSIX::WNOHANG)) { + return { + status => 'running', + pid => $state->{migrate_hook}->{pid}, + } + } + + my $reader = $state->{migrate_hook}->{output}; + my $output = ""; + + while (my $line = <$reader>) { + $output .= $line; + } + + close $state->{migrate_hook}->{output}; + delete $state->{migrate_hook}; + + my $status = ($? == 0) + ? 'finished' + : 'error'; + + return { + status => $status, + output => $output, + }; + }, + 'migrate-hook' => sub { + if ($state->{migrate_hook}) { + die "Migrate Hook is already running!"; + } + + my $params = shift; + + my $vmid = $params->{vmid}; + my $phase = $params->{phase}; + my $source = $params->{source}; + my $target = $params->{target}; + + my $config_node = ($phase eq 'pre') + ? $source + : $target; + + eval { + my $conf = PVE::LXC::Config->load_config($vmid, $config_node); + + pipe(my $reader, my $writer); + + my $pid = fork(); + die "Could not fork new process!" if !defined $pid; + + if ($pid == 0) { + # child + close $reader; + + $ENV{PVE_MIGRATED_FROM} = $source; + + eval { + PVE::GuestHelpers::exec_hookscript( + $conf, + $vmid, + "$phase-migrate", + 1, + { + output => ">&" . fileno($writer), + errfunc => sub { + my $line = shift; + print $writer "STDERR: " . $line; + }, + } + ); + }; + my $err = $@; + + close $writer; + + if ($err) { + POSIX::_exit(1); + } + + POSIX::_exit(0); + } + + close $writer; + + $state->{migrate_hook} = { + output => $reader, + pid => $pid, + }; + }; + if ($@) { + chomp $@; + die "ERR: $phase-migrate hook failed - $@"; + } else { + return {} + } + }, + }; + + my $reply_err = sub { + my ($msg) = @_; + + my $reply = JSON::encode_json({ + success => JSON::false, + msg => $msg, + }); + + $tunnel_write->($reply); + }; + + my $reply_ok = sub { + my ($res) = @_; + + $res->{success} = JSON::true; + + my $reply = JSON::encode_json($res); + + $tunnel_write->($reply); + }; + + while (my $line = <STDIN>) { + if ($state->{quit}) { + if ($state->{migrate_hook}->{output}) { + close $state->{migrate_hook}->{output}; + } + + last; + } + + chomp $line; + + # untaint, we validate below if needed + ($line) = $line =~ /^(.*)$/; + my $parsed = eval {JSON::decode_json($line)}; + if ($@) { + $reply_err->("failed to parse command - $@"); + next; + } + + my $cmd = delete $parsed->{cmd}; + + if (!defined($cmd)) { + $reply_err->("'cmd' missing"); + } elsif (my $handler = $cmd_handlers->{$cmd}) { + if (!$cmd_desc->{$cmd}) { + $reply_err->("unknown command '$cmd' given"); + next; + } + + eval { + PVE::JSONSchema::validate($parsed, $cmd_desc->{$cmd}); + }; + if ($@) { + $reply_err->("invalid payload format for $cmd' command - $@"); + next; + } + + eval { + my $res = $handler->($parsed); + $reply_ok->($res); + }; + $reply_err->("failed to handle '$cmd' command - $@") if $@; + } else { + $reply_err->("unknown command '$cmd' given"); + } + } + + return; + }}); + our $cmddef = { list=> [ 'PVE::API2::LXC', 'vmlist', [], { node => $nodename }, sub { my $res = shift; @@ -874,6 +1102,8 @@ our $cmddef = { rescan => [ __PACKAGE__, 'rescan', []], cpusets => [ __PACKAGE__, 'cpusets', []], fstrim => [ __PACKAGE__, 'fstrim', ['vmid']], + + mtunnel => [ __PACKAGE__, 'mtunnel', []], }; 1; -- 2.30.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel