[pve-devel] [PATCH v2 pve-container 1/2] add pct mtunnel command to the CLI

Stefan Hanreich s.hanreich at proxmox.com
Thu Oct 6 14:44:41 CEST 2022


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.hanreich at 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





More information about the pve-devel mailing list