From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 4DFCB1FF138 for ; Tue, 21 Jul 2026 13:58:39 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D66E0214F8; Tue, 21 Jul 2026 13:58:33 +0200 (CEST) From: Erik Fastermann To: pve-devel@lists.proxmox.com Subject: [RFC qemu-server 3/4] qm: remote-migrate: call API endpoint directly Date: Tue, 21 Jul 2026 13:58:26 +0200 Message-ID: <20260721115827.163442-4-e.fastermann@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260721115827.163442-1-e.fastermann@proxmox.com> References: <20260721115827.163442-1-e.fastermann@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 2 AWL -0.053 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 1.274 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 Message-ID-Hash: H7Q6KIV6LQHOI3VCW4IPIEDPLDXGGRE3 X-Message-ID-Hash: H7Q6KIV6LQHOI3VCW4IPIEDPLDXGGRE3 X-MailFrom: efastermann@ruth.proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Erik Fastermann X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The command wrapper only ran a subset of the precondition checks the endpoint now performs itself. Register the 'remote-migrate' command against PVE::API2::Qemu directly, as done for 'migrate' and the other commands, and drop the wrapper. Also remove the now unused PVE::APIClient::LWP import. Signed-off-by: Erik Fastermann --- src/PVE/CLI/qm.pm | 128 +--------------------------------------------- 1 file changed, 1 insertion(+), 127 deletions(-) diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm index b903c1f1..ec87972c 100755 --- a/src/PVE/CLI/qm.pm +++ b/src/PVE/CLI/qm.pm @@ -15,7 +15,6 @@ use POSIX qw(strftime); use Term::ReadLine; use URI::Escape; -use PVE::APIClient::LWP; use PVE::Cluster; use PVE::Exception qw(raise_param_exc); use PVE::GuestHelpers; @@ -172,131 +171,6 @@ __PACKAGE__->register_method({ }, }); -__PACKAGE__->register_method({ - name => 'remote_migrate_vm', - path => 'remote_migrate_vm', - method => 'POST', - description => - "Migrate virtual machine to a remote 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::QemuServer::complete_vmid }), - '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 if VM is running. Ignored if VM is stopped.", - optional => 1, - }, - delete => { - type => 'boolean', - description => - "Delete the original VM and related data after successful migration. By default the original VM is kept on the source cluster in a stopped state.", - optional => 1, - default => 0, - }, - 'target-storage' => get_standard_option( - 'pve-targetstorage', - { - completion => \&PVE::QemuServer::complete_migration_storage, - 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 => 'integer', - 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 = $param->{vmid}; - my $target_endpoint = $param->{'target-endpoint'}; - my $target_vmid = $param->{'target-vmid'} // $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}, - }; - - $conn_args->{cached_fingerprints} = { uc($remote->{fingerprint}) => 1 } - if defined($remote->{fingerprint}); - - my $api_client = PVE::APIClient::LWP->new(%$conn_args); - my $resources = $api_client->get("/cluster/resources", { type => 'vm' }); - if (grep { defined($_->{vmid}) && $_->{vmid} eq $target_vmid } @$resources) { - raise_param_exc( - { - target_vmid => - "Guest with ID '$target_vmid' already exists on remote cluster", - }, - ); - } - - my $storages = $api_client->get("/nodes/localhost/storage", { enabled => 1 }); - - my $storecfg = PVE::Storage::config(); - my $target_storage = $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 $check_remote_storage = sub { - my ($storage) = @_; - my $found = [grep { $_->{storage} eq $storage } @$storages]; - die "remote: storage '$storage' does not exist (or missing permission)!\n" - if !@$found; - - $found = @$found[0]; - - my $content_types = [PVE::Tools::split_list($found->{content})]; - die "remote: storage '$storage' cannot store images\n" - if !grep { $_ eq 'images' } @$content_types; - }; - - foreach my $target_sid (values %{ $storagemap->{entries} }) { - $check_remote_storage->($target_sid); - } - - $check_remote_storage->($storagemap->{default}) - if $storagemap->{default}; - - return PVE::API2::Qemu->remote_migrate_vm($param); - }, -}); - __PACKAGE__->register_method({ name => 'status', path => 'status', @@ -1361,7 +1235,7 @@ our $cmddef = { migrate => ["PVE::API2::Qemu", 'migrate_vm', ['vmid', 'target'], {%node}, $upid_exit], 'remote-migrate' => [ - __PACKAGE__, + "PVE::API2::Qemu", 'remote_migrate_vm', ['vmid', 'target-vmid', 'target-endpoint'], {%node}, -- 2.47.3