From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id E5A841FF138 for ; Tue, 21 Jul 2026 13:59:15 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4C5D22152C; Tue, 21 Jul 2026 13:59:04 +0200 (CEST) From: Erik Fastermann To: pve-devel@lists.proxmox.com Subject: [RFC qemu-server 4/4] remote migrate: add precondition check endpoint Date: Tue, 21 Jul 2026 13:58:27 +0200 Message-ID: <20260721115827.163442-5-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.049 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: GFYKEMX5LDVEYQOKDCCEITHLOX22HYS5 X-Message-ID-Hash: GFYKEMX5LDVEYQOKDCCEITHLOX22HYS5 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: Add a remote_migrate_vm_precondition endpoint that runs the remote migration precondition checks without starting a migration and returns them as a list of {severity, code, message} findings, so callers such as the web UI can surface blockers and warnings before the user commits to a migration. It reuses the same validate_remote_migrate_preconditions helper the migrate endpoint uses, so the precheck cannot drift from what is actually enforced. The endpoint is POST rather than GET, on its own path, to keep the remote API token out of request URLs and the access log. Suggested-by: Dominik Csapak Signed-off-by: Erik Fastermann --- src/PVE/API2/Qemu.pm | 156 ++++++++++++++++++++++++++++++------------- 1 file changed, 111 insertions(+), 45 deletions(-) diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm index 9b487457..ade1451e 100644 --- a/src/PVE/API2/Qemu.pm +++ b/src/PVE/API2/Qemu.pm @@ -1042,6 +1042,53 @@ sub assert_scsi_feature_compatibility { } } +my $remote_migrate_vm_properties = { + 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', + }, + }, +}; + my sub validate_remote_migrate_preconditions { my ($param, $findings) = @_; @@ -5837,61 +5884,80 @@ __PACKAGE__->register_method({ }); __PACKAGE__->register_method({ - name => 'remote_migrate_vm', - path => '{vmid}/remote_migrate', + name => 'remote_migrate_vm_precondition', + path => '{vmid}/remote_migrate_precondition', method => 'POST', protected => 1, proxyto => 'node', - description => - "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!", + description => "Get preconditions for remote migration.", 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, + parameters => { $remote_migrate_vm_properties->%* }, + returns => { + type => "array", + items => { + description => "List of findings.", + type => "object", + properties => { + severity => { + description => "Severity of the finding.", + type => 'string', + enum => [qw(error warning)], }, - ), - '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', + code => { + description => "Machine readable code of the finding.", + type => 'string', + enum => [qw( + load-vm-config + vm-locked + vm-ha-configured + remote-conn + target-vmid-exists + storage-missing + storage-no-images + vm-replicated + offline-migration-vm-running + online-migration-vm-not-running + custom-cpu-validation + custom-cpu-mismatch + storage-mapping + )], + }, + message => { + description => "Human readable message.", + type => 'string', + }, + storage => get_standard_option( + 'pve-storage-id', + { + description => "Optional associated storage.", + optional => 1, + }, + ), }, }, }, + code => sub { + my ($param) = @_; + my $findings = []; + validate_remote_migrate_preconditions($param, $findings); + return $findings; + }, +}); + +__PACKAGE__->register_method({ + name => 'remote_migrate_vm', + path => '{vmid}/remote_migrate', + method => 'POST', + protected => 1, + proxyto => 'node', + description => + "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!", + permissions => { + check => ['perm', '/vms/{vmid}', ['VM.Migrate']], + }, + parameters => { $remote_migrate_vm_properties->%* }, returns => { type => 'string', description => "the task ID.", -- 2.47.3