From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 4E69491363 for ; Thu, 6 Oct 2022 14:44:57 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0F3A81C66F for ; Thu, 6 Oct 2022 14:44:57 +0200 (CEST) Received: from lana.proxmox.com (unknown [94.136.29.99]) by firstgate.proxmox.com (Proxmox) with ESMTP for ; Thu, 6 Oct 2022 14:44:54 +0200 (CEST) Received: by lana.proxmox.com (Postfix, from userid 10043) id BADDA2C2326; Thu, 6 Oct 2022 14:44:48 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Thu, 6 Oct 2022 14:44:41 +0200 Message-Id: <20221006124447.120701-3-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221006124447.120701-1-s.hanreich@proxmox.com> References: <20221006124447.120701-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.402 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods NO_DNS_FOR_FROM 0.001 Envelope sender has no MX or A DNS records RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [pct.pm] Subject: [pve-devel] [PATCH v2 pve-container 1/2] add pct mtunnel command to the CLI X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 06 Oct 2022 12:44:57 -0000 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 --- 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 = ) { + 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