public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v2 pve-container 1/2] add pct mtunnel command to the CLI
Date: Thu,  6 Oct 2022 14:44:41 +0200	[thread overview]
Message-ID: <20221006124447.120701-3-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20221006124447.120701-1-s.hanreich@proxmox.com>

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@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




  parent reply	other threads:[~2022-10-06 12:44 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-10-06 12:44 [pve-devel] [PATCH v2 pve-container/qemu-server/pve-docs/pve-guest-common 0/8] Add pre/post-migrate hooks Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 pve-guest-common 1/1] Add run_params to exec_hookscript function Stefan Hanreich
2022-10-06 12:44 ` Stefan Hanreich [this message]
2022-10-06 12:44 ` [pve-devel] [PATCH v2 pve-container 2/2] add migration hooks to container migration process Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 pve-docs 1/3] Add pre/post-migrate events to hookscript example Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 pve-docs 2/3] Add hookscript section to container documentation Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 pve-docs 3/3] Add pre/post-migrate section to VM hookscript documentation Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 qemu-server 1/2] add migrate-hook and query-migrate-hook commands to CLI Stefan Hanreich
2022-10-06 12:44 ` [pve-devel] [PATCH v2 qemu-server 2/2] add migration hooks to VM migration process Stefan Hanreich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20221006124447.120701-3-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal