From: Erik Fastermann <e.fastermann@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Erik Fastermann <e.fastermann@proxmox.com>
Subject: [RFC qemu-server 3/4] qm: remote-migrate: call API endpoint directly
Date: Tue, 21 Jul 2026 13:58:26 +0200 [thread overview]
Message-ID: <20260721115827.163442-4-e.fastermann@proxmox.com> (raw)
In-Reply-To: <20260721115827.163442-1-e.fastermann@proxmox.com>
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 <e.fastermann@proxmox.com>
---
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
next prev parent reply other threads:[~2026-07-21 11:58 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-21 11:58 [RFC qemu-server 0/4] remote migrate: extract preconditions and add check endpoint Erik Fastermann
2026-07-21 11:58 ` [RFC qemu-server 1/4] remote migrate: drop ineffective fingerprint auto-detection Erik Fastermann
2026-07-21 11:58 ` [RFC qemu-server 2/4] remote migrate: collect preconditions as structured findings Erik Fastermann
2026-07-21 11:58 ` Erik Fastermann [this message]
2026-07-21 11:58 ` [RFC qemu-server 4/4] remote migrate: add precondition check endpoint Erik Fastermann
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=20260721115827.163442-4-e.fastermann@proxmox.com \
--to=e.fastermann@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