same as in qemu-server, the following should be squashed into this 
patch/commit:

----8<----
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 4e21be4..3573b59 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -2870,7 +2870,7 @@ __PACKAGE__->register_method({
                    print "received command '$cmd'\n";
                    eval {
                        if ($cmd_desc->{$cmd}) {
-                           PVE::JSONSchema::validate($cmd_desc->{$cmd}, 
$parsed);
+                           PVE::JSONSchema::validate($parsed, 
$cmd_desc->{$cmd});
                        } else {
                            $parsed = {};
                        }
---->8----

On September 28, 2022 2:50 pm, Fabian Grünbichler wrote:
> modelled after the VM migration, but folded into a single commit since
> the actual migration changes are a lot smaller here.
> 
> Signed-off-by: Fabian Grünbichler <f.gruenbich...@proxmox.com>
> ---
> 
> Notes:
>     v6:
>     - check for Sys.Incoming in mtunnel API endpoint
>     - mark as experimental
>     - test_mp fix for non-snapshot calls
>     
>     new in v5 - PoC to ensure helpers and abstractions are re-usable
>     
>     requires bumped pve-storage to avoid tainted issue
> 
>  src/PVE/API2/LXC.pm    | 635 +++++++++++++++++++++++++++++++++++++++++
>  src/PVE/LXC/Migrate.pm | 245 +++++++++++++---
>  2 files changed, 838 insertions(+), 42 deletions(-)
> 
> diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
> index 589f96f..4e21be4 100644
> --- a/src/PVE/API2/LXC.pm
> +++ b/src/PVE/API2/LXC.pm
> @@ -3,6 +3,8 @@ package PVE::API2::LXC;
>  use strict;
>  use warnings;
>  
> +use Socket qw(SOCK_STREAM);
> +
>  use PVE::SafeSyslog;
>  use PVE::Tools qw(extract_param run_command);
>  use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
> @@ -1089,6 +1091,174 @@ __PACKAGE__->register_method ({
>      }});
>  
>  
> +__PACKAGE__->register_method({
> +    name => 'remote_migrate_vm',
> +    path => '{vmid}/remote_migrate',
> +    method => 'POST',
> +    protected => 1,
> +    proxyto => 'node',
> +    description => "Migrate the container to another cluster. Creates a new 
> migration task. EXPERIMENTAL feature!",
> +    permissions => {
> +     check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
> +    },
> +    parameters => {
> +     additionalProperties => 0,
> +     properties => {
> +         node => get_standard_option('pve-node'),
> +         vmid => get_standard_option('pve-vmid', { completion => 
> \&PVE::LXC::complete_ctid }),
> +         'target-vmid' => get_standard_option('pve-vmid', { optional => 1 }),
> +         'target-endpoint' => get_standard_option('proxmox-remote', {
> +             description => "Remote target endpoint",
> +         }),
> +         online => {
> +             type => 'boolean',
> +             description => "Use online/live migration.",
> +             optional => 1,
> +         },
> +         restart => {
> +             type => 'boolean',
> +             description => "Use restart migration",
> +             optional => 1,
> +         },
> +         timeout => {
> +             type => 'integer',
> +             description => "Timeout in seconds for shutdown for restart 
> migration",
> +             optional => 1,
> +             default => 180,
> +         },
> +         delete => {
> +             type => 'boolean',
> +             description => "Delete the original CT and related data after 
> successful migration. By default the original CT is kept on the source 
> cluster in a stopped state.",
> +             optional => 1,
> +             default => 0,
> +         },
> +         'target-storage' => get_standard_option('pve-targetstorage', {
> +             optional => 0,
> +         }),
> +         'target-bridge' => {
> +             type => 'string',
> +             description => "Mapping from source to target bridges. 
> Providing only a single bridge ID maps all source bridges to that bridge. 
> Providing the special value '1' will map each source bridge to itself.",
> +             format => 'bridge-pair-list',
> +         },
> +         bwlimit => {
> +             description => "Override I/O bandwidth limit (in KiB/s).",
> +             optional => 1,
> +             type => 'number',
> +             minimum => '0',
> +             default => 'migrate limit from datacenter or storage config',
> +         },
> +     },
> +    },
> +    returns => {
> +     type => 'string',
> +     description => "the task ID.",
> +    },
> +    code => sub {
> +     my ($param) = @_;
> +
> +     my $rpcenv = PVE::RPCEnvironment::get();
> +     my $authuser = $rpcenv->get_user();
> +
> +     my $source_vmid = extract_param($param, 'vmid');
> +     my $target_endpoint = extract_param($param, 'target-endpoint');
> +     my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
> +
> +     my $delete = extract_param($param, 'delete') // 0;
> +
> +     PVE::Cluster::check_cfs_quorum();
> +
> +     # test if CT exists
> +     my $conf = PVE::LXC::Config->load_config($source_vmid);
> +     PVE::LXC::Config->check_lock($conf);
> +
> +     # try to detect errors early
> +     if (PVE::LXC::check_running($source_vmid)) {
> +         die "can't migrate running container without --online or 
> --restart\n"
> +             if !$param->{online} && !$param->{restart};
> +     }
> +
> +     raise_param_exc({ vmid => "cannot migrate HA-managed CT to remote 
> cluster" })
> +         if PVE::HA::Config::vm_is_ha_managed($source_vmid);
> +
> +     my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', 
> $target_endpoint);
> +
> +     # TODO: move this as helper somewhere appropriate?
> +     my $conn_args = {
> +         protocol => 'https',
> +         host => $remote->{host},
> +         port => $remote->{port} // 8006,
> +         apitoken => $remote->{apitoken},
> +     };
> +
> +     my $fp;
> +     if ($fp = $remote->{fingerprint}) {
> +         $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
> +     }
> +
> +     print "Establishing API connection with remote at '$remote->{host}'\n";
> +
> +     my $api_client = PVE::APIClient::LWP->new(%$conn_args);
> +
> +     if (!defined($fp)) {
> +         my $cert_info = 
> $api_client->get("/nodes/localhost/certificates/info");
> +         foreach my $cert (@$cert_info) {
> +             my $filename = $cert->{filename};
> +             next if $filename ne 'pveproxy-ssl.pem' && $filename ne 
> 'pve-ssl.pem';
> +             $fp = $cert->{fingerprint} if !$fp || $filename eq 
> 'pveproxy-ssl.pem';
> +         }
> +         $conn_args->{cached_fingerprints} = { uc($fp) => 1 }
> +             if defined($fp);
> +     }
> +
> +     my $storecfg = PVE::Storage::config();
> +     my $target_storage = extract_param($param, 'target-storage');
> +     my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 
> 'pve-storage-id') };
> +     raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" 
> })
> +         if $@;
> +
> +     my $target_bridge = extract_param($param, 'target-bridge');
> +     my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 
> 'pve-bridge-id') };
> +     raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
> +         if $@;
> +
> +     die "remote migration requires explicit storage mapping!\n"
> +         if $storagemap->{identity};
> +
> +     $param->{storagemap} = $storagemap;
> +     $param->{bridgemap} = $bridgemap;
> +     $param->{remote} = {
> +         conn => $conn_args, # re-use fingerprint for tunnel
> +         client => $api_client,
> +         vmid => $target_vmid,
> +     };
> +     $param->{migration_type} = 'websocket';
> +     $param->{delete} = $delete if $delete;
> +
> +     my $cluster_status = $api_client->get("/cluster/status");
> +     my $target_node;
> +     foreach my $entry (@$cluster_status) {
> +         next if $entry->{type} ne 'node';
> +         if ($entry->{local}) {
> +             $target_node = $entry->{name};
> +             last;
> +         }
> +     }
> +
> +     die "couldn't determine endpoint's node name\n"
> +         if !defined($target_node);
> +
> +     my $realcmd = sub {
> +         PVE::LXC::Migrate->migrate($target_node, $remote->{host}, 
> $source_vmid, $param);
> +     };
> +
> +     my $worker = sub {
> +         return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, 
> $realcmd);
> +     };
> +
> +     return $rpcenv->fork_worker('vzmigrate', $source_vmid, $authuser, 
> $worker);
> +    }});
> +
> +
>  __PACKAGE__->register_method({
>      name => 'migrate_vm',
>      path => '{vmid}/migrate',
> @@ -2318,4 +2488,469 @@ __PACKAGE__->register_method({
>       return PVE::GuestHelpers::config_with_pending_array($conf, 
> $pending_delete_hash);
>      }});
>  
> +__PACKAGE__->register_method({
> +    name => 'mtunnel',
> +    path => '{vmid}/mtunnel',
> +    method => 'POST',
> +    protected => 1,
> +    description => 'Migration tunnel endpoint - only for internal use by CT 
> migration.',
> +    permissions => {
> +     check =>
> +     [ 'and',
> +       ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
> +       ['perm', '/', [ 'Sys.Incoming' ]],
> +     ],
> +     description => "You need 'VM.Allocate' permissions on '/vms/{vmid}' and 
> Sys.Incoming" .
> +                    " on '/'. Further permission checks happen during the 
> actual migration.",
> +    },
> +    parameters => {
> +     additionalProperties => 0,
> +     properties => {
> +         node => get_standard_option('pve-node'),
> +         vmid => get_standard_option('pve-vmid'),
> +         storages => {
> +             type => 'string',
> +             format => 'pve-storage-id-list',
> +             optional => 1,
> +             description => 'List of storages to check permission and 
> availability. Will be checked again for all actually used storages during 
> migration.',
> +         },
> +         bridges => {
> +             type => 'string',
> +             format => 'pve-bridge-id-list',
> +             optional => 1,
> +             description => 'List of network bridges to check availability. 
> Will be checked again for actually used bridges during migration.',
> +         },
> +     },
> +    },
> +    returns => {
> +     additionalProperties => 0,
> +     properties => {
> +         upid => { type => 'string' },
> +         ticket => { type => 'string' },
> +         socket => { type => 'string' },
> +     },
> +    },
> +    code => sub {
> +     my ($param) = @_;
> +
> +     my $rpcenv = PVE::RPCEnvironment::get();
> +     my $authuser = $rpcenv->get_user();
> +
> +     my $node = extract_param($param, 'node');
> +     my $vmid = extract_param($param, 'vmid');
> +
> +     my $storages = extract_param($param, 'storages');
> +     my $bridges = extract_param($param, 'bridges');
> +
> +     my $nodename = PVE::INotify::nodename();
> +
> +     raise_param_exc({ node => "node needs to be 'localhost' or local 
> hostname '$nodename'" })
> +         if $node ne 'localhost' && $node ne $nodename;
> +
> +     $node = $nodename;
> +
> +     my $storecfg = PVE::Storage::config();
> +     foreach my $storeid (PVE::Tools::split_list($storages)) {
> +         $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, 
> $storeid, $node);
> +     }
> +
> +     foreach my $bridge (PVE::Tools::split_list($bridges)) {
> +         PVE::Network::read_bridge_mtu($bridge);
> +     }
> +
> +     PVE::Cluster::check_cfs_quorum();
> +
> +     my $socket_addr = "/run/pve/ct-$vmid.mtunnel";
> +
> +     my $lock = 'create';
> +     eval { PVE::LXC::Config->create_and_lock_config($vmid, 0, $lock); };
> +
> +     raise_param_exc({ vmid => "unable to create empty CT config - $@"})
> +         if $@;
> +
> +     my $realcmd = sub {
> +         my $state = {
> +             storecfg => PVE::Storage::config(),
> +             lock => $lock,
> +             vmid => $vmid,
> +         };
> +
> +         my $run_locked = sub {
> +             my ($code, $params) = @_;
> +             return PVE::LXC::Config->lock_config($state->{vmid}, sub {
> +                 my $conf = PVE::LXC::Config->load_config($state->{vmid});
> +
> +                 $state->{conf} = $conf;
> +
> +                 die "Encountered wrong lock - aborting mtunnel command 
> handling.\n"
> +                     if $state->{lock} && !PVE::LXC::Config->has_lock($conf, 
> $state->{lock});
> +
> +                 return $code->($params);
> +             });
> +         };
> +
> +         my $cmd_desc = {
> +             config => {
> +                 conf => {
> +                     type => 'string',
> +                     description => 'Full CT config, adapted for target 
> cluster/node',
> +                 },
> +                 'firewall-config' => {
> +                     type => 'string',
> +                     description => 'CT firewall config',
> +                     optional => 1,
> +                 },
> +             },
> +             ticket => {
> +                 path => {
> +                     type => 'string',
> +                     description => 'socket path for which the ticket should 
> be valid. must be known to current mtunnel instance.',
> +                 },
> +             },
> +             quit => {
> +                 cleanup => {
> +                     type => 'boolean',
> +                     description => 'remove CT config and volumes, aborting 
> migration',
> +                     default => 0,
> +                 },
> +             },
> +             'disk-import' => 
> $PVE::StorageTunnel::cmd_schema->{'disk-import'},
> +             'query-disk-import' => 
> $PVE::StorageTunnel::cmd_schema->{'query-disk-import'},
> +             bwlimit => $PVE::StorageTunnel::cmd_schema->{bwlimit},
> +         };
> +
> +         my $cmd_handlers = {
> +             'version' => sub {
> +                 # compared against other end's version
> +                 # bump/reset for breaking changes
> +                 # bump/bump for opt-in changes
> +                 return {
> +                     api => $PVE::LXC::Migrate::WS_TUNNEL_VERSION,
> +                     age => 0,
> +                 };
> +             },
> +             'config' => sub {
> +                 my ($params) = @_;
> +
> +                 # parse and write out VM FW config if given
> +                 if (my $fw_conf = $params->{'firewall-config'}) {
> +                     my ($path, $fh) = 
> PVE::Tools::tempfile_contents($fw_conf, 700);
> +
> +                     my $empty_conf = {
> +                         rules => [],
> +                         options => {},
> +                         aliases => {},
> +                         ipset => {} ,
> +                         ipset_comments => {},
> +                     };
> +                     my $cluster_fw_conf = 
> PVE::Firewall::load_clusterfw_conf();
> +
> +                     # TODO: add flag for strict parsing?
> +                     # TODO: add import sub that does all this given raw 
> content?
> +                     my $vmfw_conf = 
> PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, $empty_conf, 
> 'vm');
> +                     $vmfw_conf->{vmid} = $state->{vmid};
> +                     PVE::Firewall::save_vmfw_conf($state->{vmid}, 
> $vmfw_conf);
> +
> +                     $state->{cleanup}->{fw} = 1;
> +                 }
> +
> +                 my $conf_fn = "incoming/lxc/$state->{vmid}.conf";
> +                 my $new_conf = PVE::LXC::Config::parse_pct_config($conf_fn, 
> $params->{conf}, 1);
> +                 delete $new_conf->{lock};
> +                 delete $new_conf->{digest};
> +
> +                 my $unprivileged = delete $new_conf->{unprivileged};
> +                 my $arch = delete $new_conf->{arch};
> +
> +                 # TODO handle properly?
> +                 delete $new_conf->{snapshots};
> +                 delete $new_conf->{parent};
> +                 delete $new_conf->{pending};
> +                 delete $new_conf->{lxc};
> +
> +                 PVE::LXC::Config->remove_lock($state->{vmid}, 'create');
> +
> +                 eval {
> +                     my $conf = {
> +                         unprivileged => $unprivileged,
> +                         arch => $arch,
> +                     };
> +                     PVE::LXC::check_ct_modify_config_perm(
> +                         $rpcenv,
> +                         $authuser,
> +                         $state->{vmid},
> +                         undef,
> +                         $conf,
> +                         $new_conf,
> +                         undef,
> +                         $unprivileged,
> +                     );
> +                     my $errors = PVE::LXC::Config->update_pct_config(
> +                         $state->{vmid},
> +                         $conf,
> +                         0,
> +                         $new_conf,
> +                         [],
> +                         [],
> +                     );
> +                     raise_param_exc($errors) if scalar(keys %$errors);
> +                     PVE::LXC::Config->write_config($state->{vmid}, $conf);
> +                     PVE::LXC::update_lxc_config($vmid, $conf);
> +                 };
> +                 if (my $err = $@) {
> +                     # revert to locked previous config
> +                     my $conf = 
> PVE::LXC::Config->load_config($state->{vmid});
> +                     $conf->{lock} = 'create';
> +                     PVE::LXC::Config->write_config($state->{vmid}, $conf);
> +
> +                     die $err;
> +                 }
> +
> +                 my $conf = PVE::LXC::Config->load_config($state->{vmid});
> +                 $conf->{lock} = 'migrate';
> +                 PVE::LXC::Config->write_config($state->{vmid}, $conf);
> +
> +                 $state->{lock} = 'migrate';
> +
> +                 return;
> +             },
> +             'bwlimit' => sub {
> +                 my ($params) = @_;
> +                 return PVE::StorageTunnel::handle_bwlimit($params);
> +             },
> +             'disk-import' => sub {
> +                 my ($params) = @_;
> +
> +                 $check_storage_access_migrate->(
> +                     $rpcenv,
> +                     $authuser,
> +                     $state->{storecfg},
> +                     $params->{storage},
> +                     $node
> +                 );
> +
> +                 $params->{unix} = "/run/pve/ct-$state->{vmid}.storage";
> +
> +                 return PVE::StorageTunnel::handle_disk_import($state, 
> $params);
> +             },
> +             'query-disk-import' => sub {
> +                 my ($params) = @_;
> +
> +                 return PVE::StorageTunnel::handle_query_disk_import($state, 
> $params);
> +             },
> +             'unlock' => sub {
> +                 PVE::LXC::Config->remove_lock($state->{vmid}, 
> $state->{lock});
> +                 delete $state->{lock};
> +                 return;
> +             },
> +             'start' => sub {
> +                 PVE::LXC::vm_start(
> +                     $state->{vmid},
> +                     $state->{conf},
> +                     0
> +                 );
> +
> +                 return;
> +             },
> +             'stop' => sub {
> +                 PVE::LXC::vm_stop($state->{vmid}, 1, 10, 1);
> +                 return;
> +             },
> +             'ticket' => sub {
> +                 my ($params) = @_;
> +
> +                 my $path = $params->{path};
> +
> +                 die "Not allowed to generate ticket for unknown socket 
> '$path'\n"
> +                     if !defined($state->{sockets}->{$path});
> +
> +                 return { ticket => 
> PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
> +             },
> +             'quit' => sub {
> +                 my ($params) = @_;
> +
> +                 if ($params->{cleanup}) {
> +                     if ($state->{cleanup}->{fw}) {
> +                         PVE::Firewall::remove_vmfw_conf($state->{vmid});
> +                     }
> +
> +                     for my $volid (keys $state->{cleanup}->{volumes}->%*) {
> +                         print "freeing volume '$volid' as part of 
> cleanup\n";
> +                         eval { PVE::Storage::vdisk_free($state->{storecfg}, 
> $volid) };
> +                         warn $@ if $@;
> +                     }
> +
> +                     PVE::LXC::destroy_lxc_container(
> +                         $state->{storecfg},
> +                         $state->{vmid},
> +                         $state->{conf},
> +                         undef,
> +                         0,
> +                     );
> +                 }
> +
> +                 print "switching to exit-mode, waiting for client to 
> disconnect\n";
> +                 $state->{exit} = 1;
> +                 return;
> +             },
> +         };
> +
> +         $run_locked->(sub {
> +             my $socket_addr = "/run/pve/ct-$state->{vmid}.mtunnel";
> +             unlink $socket_addr;
> +
> +             $state->{socket} = IO::Socket::UNIX->new(
> +                 Type => SOCK_STREAM(),
> +                 Local => $socket_addr,
> +                 Listen => 1,
> +             );
> +
> +             $state->{socket_uid} = getpwnam('www-data')
> +                 or die "Failed to resolve user 'www-data' to numeric UID\n";
> +             chown $state->{socket_uid}, -1, $socket_addr;
> +         });
> +
> +         print "mtunnel started\n";
> +
> +         my $conn = eval { PVE::Tools::run_with_timeout(300, sub { 
> $state->{socket}->accept() }) };
> +         if ($@) {
> +             warn "Failed to accept tunnel connection - $@\n";
> +
> +             warn "Removing tunnel socket..\n";
> +             unlink $state->{socket};
> +
> +             warn "Removing temporary VM config..\n";
> +             $run_locked->(sub {
> +                 PVE::LXC::destroy_config($state->{vmid});
> +             });
> +
> +             die "Exiting mtunnel\n";
> +         }
> +
> +         $state->{conn} = $conn;
> +
> +         my $reply_err = sub {
> +             my ($msg) = @_;
> +
> +             my $reply = JSON::encode_json({
> +                 success => JSON::false,
> +                 msg => $msg,
> +             });
> +             $conn->print("$reply\n");
> +             $conn->flush();
> +         };
> +
> +         my $reply_ok = sub {
> +             my ($res) = @_;
> +
> +             $res->{success} = JSON::true;
> +             my $reply = JSON::encode_json($res);
> +             $conn->print("$reply\n");
> +             $conn->flush();
> +         };
> +
> +         while (my $line = <$conn>) {
> +             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 ($state->{exit}) {
> +                 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd 
> not possible");
> +                 next;
> +             } elsif (my $handler = $cmd_handlers->{$cmd}) {
> +                 print "received command '$cmd'\n";
> +                 eval {
> +                     if ($cmd_desc->{$cmd}) {
> +                         PVE::JSONSchema::validate($cmd_desc->{$cmd}, 
> $parsed);
> +                     } else {
> +                         $parsed = {};
> +                     }
> +                     my $res = $run_locked->($handler, $parsed);
> +                     $reply_ok->($res);
> +                 };
> +                 $reply_err->("failed to handle '$cmd' command - $@")
> +                     if $@;
> +             } else {
> +                 $reply_err->("unknown command '$cmd' given");
> +             }
> +         }
> +
> +         if ($state->{exit}) {
> +             print "mtunnel exited\n";
> +         } else {
> +             die "mtunnel exited unexpectedly\n";
> +         }
> +     };
> +
> +     my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, 
> "/socket/$socket_addr");
> +     my $upid = $rpcenv->fork_worker('vzmtunnel', $vmid, $authuser, 
> $realcmd);
> +
> +     return {
> +         ticket => $ticket,
> +         upid => $upid,
> +         socket => $socket_addr,
> +     };
> +    }});
> +
> +__PACKAGE__->register_method({
> +    name => 'mtunnelwebsocket',
> +    path => '{vmid}/mtunnelwebsocket',
> +    method => 'GET',
> +    permissions => {
> +     description => "You need to pass a ticket valid for the selected 
> socket. Tickets can be created via the mtunnel API call, which will check 
> permissions accordingly.",
> +        user => 'all', # check inside
> +    },
> +    description => 'Migration tunnel endpoint for websocket upgrade - only 
> for internal use by VM migration.',
> +    parameters => {
> +     additionalProperties => 0,
> +     properties => {
> +         node => get_standard_option('pve-node'),
> +         vmid => get_standard_option('pve-vmid'),
> +         socket => {
> +             type => "string",
> +             description => "unix socket to forward to",
> +         },
> +         ticket => {
> +             type => "string",
> +             description => "ticket return by initial 'mtunnel' API call, or 
> retrieved via 'ticket' tunnel command",
> +         },
> +     },
> +    },
> +    returns => {
> +     type => "object",
> +     properties => {
> +         port => { type => 'string', optional => 1 },
> +         socket => { type => 'string', optional => 1 },
> +     },
> +    },
> +    code => sub {
> +     my ($param) = @_;
> +
> +     my $rpcenv = PVE::RPCEnvironment::get();
> +     my $authuser = $rpcenv->get_user();
> +
> +     my $nodename = PVE::INotify::nodename();
> +     my $node = extract_param($param, 'node');
> +
> +     raise_param_exc({ node => "node needs to be 'localhost' or local 
> hostname '$nodename'" })
> +         if $node ne 'localhost' && $node ne $nodename;
> +
> +     my $vmid = $param->{vmid};
> +     # check VM exists
> +     PVE::LXC::Config->load_config($vmid);
> +
> +     my $socket = $param->{socket};
> +     PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, 
> "/socket/$socket");
> +
> +     return { socket => $socket };
> +    }});
>  1;
> diff --git a/src/PVE/LXC/Migrate.pm b/src/PVE/LXC/Migrate.pm
> index 2ef1cce..a0ab65e 100644
> --- a/src/PVE/LXC/Migrate.pm
> +++ b/src/PVE/LXC/Migrate.pm
> @@ -17,6 +17,9 @@ use PVE::Replication;
>  
>  use base qw(PVE::AbstractMigrate);
>  
> +# compared against remote end's minimum version
> +our $WS_TUNNEL_VERSION = 2;
> +
>  sub lock_vm {
>      my ($self, $vmid, $code, @param) = @_;
>  
> @@ -28,6 +31,7 @@ sub prepare {
>  
>      my $online = $self->{opts}->{online};
>      my $restart= $self->{opts}->{restart};
> +    my $remote = $self->{opts}->{remote};
>  
>      $self->{storecfg} = PVE::Storage::config();
>  
> @@ -44,6 +48,7 @@ sub prepare {
>      }
>      $self->{was_running} = $running;
>  
> +    my $storages = {};
>      PVE::LXC::Config->foreach_volume_full($conf, { include_unused => 1 }, 
> sub {
>       my ($ms, $mountpoint) = @_;
>  
> @@ -70,7 +75,7 @@ sub prepare {
>       die "content type 'rootdir' is not available on storage '$storage'\n"
>           if !$scfg->{content}->{rootdir};
>  
> -     if ($scfg->{shared}) {
> +     if ($scfg->{shared} && !$remote) {
>           # PVE::Storage::activate_storage checks this for non-shared storages
>           my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
>           warn "Used shared storage '$storage' is not online on source 
> node!\n"
> @@ -83,18 +88,63 @@ sub prepare {
>           $targetsid = PVE::JSONSchema::map_id($self->{opts}->{storagemap}, 
> $storage);
>       }
>  
> -     my $target_scfg = 
> PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node});
> +     if (!$remote) {
> +         my $target_scfg = 
> PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node});
> +
> +         die "$volid: content type 'rootdir' is not available on storage 
> '$targetsid'\n"
> +             if !$target_scfg->{content}->{rootdir};
> +     }
>  
> -     die "$volid: content type 'rootdir' is not available on storage 
> '$targetsid'\n"
> -         if !$target_scfg->{content}->{rootdir};
> +     $storages->{$targetsid} = 1;
>      });
>  
>      # todo: 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 ($remote) {
> +     # test & establish websocket connection
> +     my $bridges = map_bridges($conf, $self->{opts}->{bridgemap}, 1);
> +
> +     my $remote = $self->{opts}->{remote};
> +     my $conn = $remote->{conn};
> +
> +     my $log = sub {
> +         my ($level, $msg) = @_;
> +         $self->log($level, $msg);
> +     };
> +
> +     my $websocket_url = 
> "https://$conn->{host}:$conn->{port}/api2/json/nodes/$self->{node}/lxc/$remote->{vmid}/mtunnelwebsocket";
> +     my $url = "/nodes/$self->{node}/lxc/$remote->{vmid}/mtunnel";
> +
> +     my $tunnel_params = {
> +         url => $websocket_url,
> +     };
> +
> +     my $storage_list = join(',', keys %$storages);
> +     my $bridge_list = join(',', keys %$bridges);
> +
> +     my $req_params = {
> +         storages => $storage_list,
> +         bridges => $bridge_list,
> +     };
> +
> +     my $tunnel = PVE::Tunnel::fork_websocket_tunnel($conn, $url, 
> $req_params, $tunnel_params, $log);
> +     my $min_version = $tunnel->{version} - $tunnel->{age};
> +     $self->log('info', "local WS tunnel version: $WS_TUNNEL_VERSION");
> +     $self->log('info', "remote WS tunnel version: $tunnel->{version}");
> +     $self->log('info', "minimum required WS tunnel version: $min_version");
> +     die "Remote tunnel endpoint not compatible, upgrade required\n"
> +         if $WS_TUNNEL_VERSION < $min_version;
> +      die "Remote tunnel endpoint too old, upgrade required\n"
> +         if $WS_TUNNEL_VERSION > $tunnel->{version};
> +
> +     $self->log('info', "websocket tunnel started\n");
> +     $self->{tunnel} = $tunnel;
> +    } else {
> +     # 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 $@;
> +    }
>  
>      # in restart mode, we shutdown the container before migrating
>      if ($restart && $running) {
> @@ -113,6 +163,8 @@ sub prepare {
>  sub phase1 {
>      my ($self, $vmid) = @_;
>  
> +    my $remote = $self->{opts}->{remote};
> +
>      $self->log('info', "starting migration of CT $self->{vmid} to node 
> '$self->{node}' ($self->{nodeip})");
>  
>      my $conf = $self->{vmconf};
> @@ -147,7 +199,7 @@ sub phase1 {
>  
>       my $targetsid = $sid;
>  
> -     if ($scfg->{shared}) {
> +     if ($scfg->{shared} && !$remote) {
>           $self->log('info', "volume '$volid' is on shared storage '$sid'")
>               if !$snapname;
>           return;
> @@ -155,7 +207,8 @@ sub phase1 {
>           $targetsid = PVE::JSONSchema::map_id($self->{opts}->{storagemap}, 
> $sid);
>       }
>  
> -     PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node});
> +     PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node})
> +         if !$remote;
>  
>       my $bwlimit = $self->get_bwlimit($sid, $targetsid);
>  
> @@ -192,6 +245,9 @@ sub phase1 {
>  
>       eval {
>           &$test_volid($volid, $snapname);
> +
> +         die "remote migration with snapshots not supported yet\n"
> +             if $remote && $snapname;
>       };
>  
>       &$log_error($@, $volid) if $@;
> @@ -201,7 +257,7 @@ sub phase1 {
>      my @sids = PVE::Storage::storage_ids($self->{storecfg});
>      foreach my $storeid (@sids) {
>       my $scfg = PVE::Storage::storage_config($self->{storecfg}, $storeid);
> -     next if $scfg->{shared};
> +     next if $scfg->{shared} && !$remote;
>       next if !PVE::Storage::storage_check_enabled($self->{storecfg}, 
> $storeid, undef, 1);
>  
>       # get list from PVE::Storage (for unreferenced volumes)
> @@ -211,10 +267,12 @@ sub phase1 {
>  
>       # check if storage is available on target node
>       my $targetsid = PVE::JSONSchema::map_id($self->{opts}->{storagemap}, 
> $storeid);
> -     my $target_scfg = 
> PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node});
> +     if (!$remote) {
> +         my $target_scfg = 
> PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, 
> $self->{node});
>  
> -     die "content type 'rootdir' is not available on storage '$targetsid'\n"
> -         if !$target_scfg->{content}->{rootdir};
> +         die "content type 'rootdir' is not available on storage 
> '$targetsid'\n"
> +             if !$target_scfg->{content}->{rootdir};
> +     }
>  
>       PVE::Storage::foreach_volid($dl, sub {
>           my ($volid, $sid, $volname) = @_;
> @@ -240,12 +298,21 @@ sub phase1 {
>           my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
>           my $scfg =  PVE::Storage::storage_config($self->{storecfg}, $sid);
>  
> -         my $migratable = ($scfg->{type} eq 'dir') || ($scfg->{type} eq 
> 'zfspool')
> -             || ($scfg->{type} eq 'lvmthin') || ($scfg->{type} eq 'lvm')
> -             || ($scfg->{type} eq 'btrfs');
> +         # TODO move to storage plugin layer?
> +         my $migratable_storages = [
> +             'dir',
> +             'zfspool',
> +             'lvmthin',
> +             'lvm',
> +             'btrfs',
> +         ];
> +         if ($remote) {
> +             push @$migratable_storages, 'cifs';
> +             push @$migratable_storages, 'nfs';
> +         }
>  
>           die "storage type '$scfg->{type}' not supported\n"
> -             if !$migratable;
> +             if !grep { $_ eq $scfg->{type} } @$migratable_storages;
>  
>           # image is a linked clone on local storage, se we can't migrate.
>           if (my $basename = (PVE::Storage::parse_volname($self->{storecfg}, 
> $volid))[3]) {
> @@ -280,7 +347,10 @@ sub phase1 {
>  
>      my $rep_cfg = PVE::ReplicationConfig->new();
>  
> -    if (my $jobcfg = $rep_cfg->find_local_replication_job($vmid, 
> $self->{node})) {
> +    if ($remote) {
> +     die "cannot remote-migrate replicated VM\n"
> +         if $rep_cfg->check_for_existing_jobs($vmid, 1);
> +    } elsif (my $jobcfg = $rep_cfg->find_local_replication_job($vmid, 
> $self->{node})) {
>       die "can't live migrate VM with replicated volumes\n" if 
> $self->{running};
>       my $start_time = time();
>       my $logfunc = sub { my ($msg) = @_;  $self->log('info', $msg); };
> @@ -291,7 +361,6 @@ sub phase1 {
>      my $opts = $self->{opts};
>      foreach my $volid (keys %$volhash) {
>       next if $rep_volumes->{$volid};
> -     my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
>       push @{$self->{volumes}}, $volid;
>  
>       # JSONSchema and get_bandwidth_limit use kbps - storage_migrate bps
> @@ -301,22 +370,39 @@ sub phase1 {
>       my $targetsid = $volhash->{$volid}->{targetsid};
>  
>       my $new_volid = eval {
> -         my $storage_migrate_opts = {
> -             'ratelimit_bps' => $bwlimit,
> -             'insecure' => $opts->{migration_type} eq 'insecure',
> -             'with_snapshots' => $volhash->{$volid}->{snapshots},
> -             'allow_rename' => 1,
> -         };
> -
> -         my $logfunc = sub { $self->log('info', $_[0]); };
> -         return PVE::Storage::storage_migrate(
> -             $self->{storecfg},
> -             $volid,
> -             $self->{ssh_info},
> -             $targetsid,
> -             $storage_migrate_opts,
> -             $logfunc,
> -         );
> +         if ($remote) {
> +             my $log = sub {
> +                 my ($level, $msg) = @_;
> +                 $self->log($level, $msg);
> +             };
> +
> +             return PVE::StorageTunnel::storage_migrate(
> +                 $self->{tunnel},
> +                 $self->{storecfg},
> +                 $volid,
> +                 $self->{vmid},
> +                 $remote->{vmid},
> +                 $volhash->{$volid},
> +                 $log,
> +             );
> +         } else {
> +             my $storage_migrate_opts = {
> +                 'ratelimit_bps' => $bwlimit,
> +                 'insecure' => $opts->{migration_type} eq 'insecure',
> +                 'with_snapshots' => $volhash->{$volid}->{snapshots},
> +                 'allow_rename' => 1,
> +             };
> +
> +             my $logfunc = sub { $self->log('info', $_[0]); };
> +             return PVE::Storage::storage_migrate(
> +                 $self->{storecfg},
> +                 $volid,
> +                 $self->{ssh_info},
> +                 $targetsid,
> +                 $storage_migrate_opts,
> +                 $logfunc,
> +             );
> +         }
>       };
>  
>       if (my $err = $@) {
> @@ -346,13 +432,38 @@ sub phase1 {
>      my $vollist = PVE::LXC::Config->get_vm_volumes($conf);
>      PVE::Storage::deactivate_volumes($self->{storecfg}, $vollist);
>  
> -    # transfer replication state before moving config
> -    $self->transfer_replication_state() if $rep_volumes;
> -    PVE::LXC::Config->update_volume_ids($conf, $self->{volume_map});
> -    PVE::LXC::Config->write_config($vmid, $conf);
> -    PVE::LXC::Config->move_config_to_node($vmid, $self->{node});
> +    if ($remote) {
> +     my $remote_conf = PVE::LXC::Config->load_config($vmid);
> +     PVE::LXC::Config->update_volume_ids($remote_conf, $self->{volume_map});
> +
> +     my $bridges = map_bridges($remote_conf, $self->{opts}->{bridgemap});
> +     for my $target (keys $bridges->%*) {
> +         for my $nic (keys $bridges->{$target}->%*) {
> +             $self->log('info', "mapped: $nic from 
> $bridges->{$target}->{$nic} to $target");
> +         }
> +     }
> +     my $conf_str = PVE::LXC::Config::write_pct_config("remote", 
> $remote_conf);
> +
> +     # TODO expose in PVE::Firewall?
> +     my $vm_fw_conf_path = "/etc/pve/firewall/$vmid.fw";
> +     my $fw_conf_str;
> +     $fw_conf_str = PVE::Tools::file_get_contents($vm_fw_conf_path)
> +         if -e $vm_fw_conf_path;
> +     my $params = {
> +         conf => $conf_str,
> +         'firewall-config' => $fw_conf_str,
> +     };
> +
> +     PVE::Tunnel::write_tunnel($self->{tunnel}, 10, 'config', $params);
> +    } else {
> +     # transfer replication state before moving config
> +     $self->transfer_replication_state() if $rep_volumes;
> +     PVE::LXC::Config->update_volume_ids($conf, $self->{volume_map});
> +     PVE::LXC::Config->write_config($vmid, $conf);
> +     PVE::LXC::Config->move_config_to_node($vmid, $self->{node});
> +     $self->switch_replication_job_target() if $rep_volumes;
> +    }
>      $self->{conf_migrated} = 1;
> -    $self->switch_replication_job_target() if $rep_volumes;
>  }
>  
>  sub phase1_cleanup {
> @@ -366,6 +477,12 @@ sub phase1_cleanup {
>           # fixme: try to remove ?
>       }
>      }
> +
> +    if ($self->{opts}->{remote}) {
> +     # cleans up remote volumes
> +     PVE::Tunnel::finish_tunnel($self->{tunnel}, 1);
> +     delete $self->{tunnel};
> +    }
>  }
>  
>  sub phase3 {
> @@ -373,6 +490,9 @@ sub phase3 {
>  
>      my $volids = $self->{volumes};
>  
> +    # handled below in final_cleanup
> +    return if $self->{opts}->{remote};
> +
>      # destroy local copies
>      foreach my $volid (@$volids) {
>       eval { PVE::Storage::vdisk_free($self->{storecfg}, $volid); };
> @@ -401,6 +521,24 @@ sub final_cleanup {
>           my $skiplock = 1;
>           PVE::LXC::vm_start($vmid, $self->{vmconf}, $skiplock);
>       }
> +    } elsif ($self->{opts}->{remote}) {
> +     eval { PVE::Tunnel::write_tunnel($self->{tunnel}, 10, 'unlock') };
> +     $self->log('err', "Failed to clear migrate lock - $@\n") if $@;
> +
> +     if ($self->{opts}->{restart} && $self->{was_running}) {
> +         $self->log('info', "start container on target node");
> +         PVE::Tunnel::write_tunnel($self->{tunnel}, 60, 'start');
> +     }
> +     if ($self->{opts}->{delete}) {
> +         PVE::LXC::destroy_lxc_container(
> +             PVE::Storage::config(),
> +             $vmid,
> +             PVE::LXC::Config->load_config($vmid),
> +             undef,
> +             0,
> +         );
> +     }
> +     PVE::Tunnel::finish_tunnel($self->{tunnel});
>      } else {
>       my $cmd = [ @{$self->{rem_ssh}}, 'pct', 'unlock', $vmid ];
>       $self->cmd_logerr($cmd, errmsg => "failed to clear migrate lock");
> @@ -413,7 +551,30 @@ sub final_cleanup {
>           $self->cmd($cmd);
>       }
>      }
> +}
> +
> +sub map_bridges {
> +    my ($conf, $map, $scan_only) = @_;
> +
> +    my $bridges = {};
> +
> +    foreach my $opt (keys %$conf) {
> +     next if $opt !~ m/^net\d+$/;
> +
> +     next if !$conf->{$opt};
> +     my $d = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
> +     next if !$d || !$d->{bridge};
> +
> +     my $target_bridge = PVE::JSONSchema::map_id($map, $d->{bridge});
> +     $bridges->{$target_bridge}->{$opt} = $d->{bridge};
> +
> +     next if $scan_only;
> +
> +     $d->{bridge} = $target_bridge;
> +     $conf->{$opt} = PVE::LXC::Config->print_lxc_network($d);
> +    }
>  
> +    return $bridges;
>  }
>  
>  1;
> -- 
> 2.30.2
> 
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to