[pve-devel] [PATCH qemu-server 15/31] introduce QemuMigrate::Helpers module
Fiona Ebner
f.ebner at proxmox.com
Wed Jun 25 17:56:38 CEST 2025
The QemuMigrate module is high-level and should not be called from
many other places, while also being an implementation of
AbstractMigrate, so using a separate module is much more natural.
Signed-off-by: Fiona Ebner <f.ebner at proxmox.com>
---
src/PVE/API2/Qemu.pm | 6 +-
src/PVE/Makefile | 1 +
src/PVE/QemuConfig.pm | 5 +-
src/PVE/QemuMigrate.pm | 5 +-
src/PVE/QemuMigrate/Helpers.pm | 146 ++++++++++++++++++++++
src/PVE/QemuMigrate/Makefile | 9 ++
src/PVE/QemuServer.pm | 136 +-------------------
src/test/MigrationTest/QemuMigrateMock.pm | 3 -
src/test/MigrationTest/QmMock.pm | 3 -
src/test/MigrationTest/Shared.pm | 7 ++
src/test/snapshot-test.pm | 11 +-
11 files changed, 186 insertions(+), 146 deletions(-)
create mode 100644 src/PVE/QemuMigrate/Helpers.pm
create mode 100644 src/PVE/QemuMigrate/Makefile
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 7f55998e..27426eab 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -45,6 +45,7 @@ use PVE::QemuServer::RNG;
use PVE::QemuServer::USB;
use PVE::QemuServer::Virtiofs qw(max_virtiofs);
use PVE::QemuMigrate;
+use PVE::QemuMigrate::Helpers;
use PVE::RPCEnvironment;
use PVE::AccessControl;
use PVE::INotify;
@@ -5167,7 +5168,7 @@ __PACKAGE__->register_method({
$res->{running} = PVE::QemuServer::check_running($vmid) ? 1 : 0;
my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
- PVE::QemuServer::check_local_resources($vmconf, $res->{running}, 1);
+ PVE::QemuMigrate::Helpers::check_local_resources($vmconf, $res->{running}, 1);
my $vga = PVE::QemuServer::parse_vga($vmconf->{vga});
if ($res->{running} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
@@ -5903,7 +5904,8 @@ __PACKAGE__->register_method({
if lc($snapname) eq 'pending';
my $vmconf = PVE::QemuConfig->load_config($vmid);
- PVE::QemuServer::check_non_migratable_resources($vmconf, $param->{vmstate}, 0);
+ PVE::QemuMigrate::Helpers::check_non_migratable_resources($vmconf, $param->{vmstate},
+ 0);
my $realcmd = sub {
PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
diff --git a/src/PVE/Makefile b/src/PVE/Makefile
index 01cf9df6..e0537b4a 100644
--- a/src/PVE/Makefile
+++ b/src/PVE/Makefile
@@ -16,4 +16,5 @@ install:
$(MAKE) -C API2 install
$(MAKE) -C CLI install
$(MAKE) -C QemuConfig install
+ $(MAKE) -C QemuMigrate install
$(MAKE) -C QemuServer install
diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm
index 957c875a..01104723 100644
--- a/src/PVE/QemuConfig.pm
+++ b/src/PVE/QemuConfig.pm
@@ -8,6 +8,7 @@ use Scalar::Util qw(blessed);
use PVE::AbstractConfig;
use PVE::INotify;
use PVE::JSONSchema;
+use PVE::QemuMigrate::Helpers;
use PVE::QemuServer::Agent;
use PVE::QemuServer::CPUConfig;
use PVE::QemuServer::Drive;
@@ -211,7 +212,7 @@ sub get_backup_volumes {
sub __snapshot_assert_no_blockers {
my ($class, $vmconf, $save_vmstate) = @_;
- PVE::QemuServer::check_non_migratable_resources($vmconf, $save_vmstate, 0);
+ PVE::QemuMigrate::Helpers::check_non_migratable_resources($vmconf, $save_vmstate, 0);
}
sub __snapshot_save_vmstate {
@@ -325,7 +326,7 @@ sub __snapshot_create_vol_snapshots_hook {
PVE::Storage::activate_volumes($storecfg, [$snap->{vmstate}]);
my $state_storage_id = PVE::Storage::parse_volume_id($snap->{vmstate});
- PVE::QemuServer::set_migration_caps($vmid, 1);
+ PVE::QemuMigrate::Helpers::set_migration_caps($vmid, 1);
mon_cmd($vmid, "savevm-start", statefile => $path);
print "saving VM state and RAM using storage '$state_storage_id'\n";
my $render_state = sub {
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 934d4350..4fd46a76 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -25,6 +25,7 @@ use PVE::Tools;
use PVE::Tunnel;
use PVE::QemuConfig;
+use PVE::QemuMigrate::Helpers;
use PVE::QemuServer::CPUConfig;
use PVE::QemuServer::Drive qw(checked_volume_format);
use PVE::QemuServer::Helpers qw(min_version);
@@ -239,7 +240,7 @@ sub prepare {
}
my ($loc_res, $mapped_res, $missing_mappings_by_node) =
- PVE::QemuServer::check_local_resources($conf, $running, 1);
+ PVE::QemuMigrate::Helpers::check_local_resources($conf, $running, 1);
my $blocking_resources = [];
for my $res ($loc_res->@*) {
if (!defined($mapped_res->{$res})) {
@@ -1235,7 +1236,7 @@ sub phase2 {
my $defaults = PVE::QemuServer::load_defaults();
$self->log('info', "set migration capabilities");
- eval { PVE::QemuServer::set_migration_caps($vmid) };
+ eval { PVE::QemuMigrate::Helpers::set_migration_caps($vmid) };
warn $@ if $@;
my $qemu_migrate_params = {};
diff --git a/src/PVE/QemuMigrate/Helpers.pm b/src/PVE/QemuMigrate/Helpers.pm
new file mode 100644
index 00000000..f191565a
--- /dev/null
+++ b/src/PVE/QemuMigrate/Helpers.pm
@@ -0,0 +1,146 @@
+package PVE::QemuMigrate::Helpers;
+
+use strict;
+use warnings;
+
+use JSON;
+
+use PVE::Cluster;
+use PVE::JSONSchema qw(parse_property_string);
+use PVE::Mapping::Dir;
+use PVE::Mapping::PCI;
+use PVE::Mapping::USB;
+
+use PVE::QemuServer::Monitor qw(mon_cmd);
+use PVE::QemuServer::Virtiofs;
+
+sub check_non_migratable_resources {
+ my ($conf, $state, $noerr) = @_;
+
+ my @blockers = ();
+ if ($state) {
+ push @blockers, "amd-sev" if $conf->{"amd-sev"};
+ push @blockers, "virtiofs" if PVE::QemuServer::Virtiofs::virtiofs_enabled($conf);
+ }
+
+ if (scalar(@blockers) && !$noerr) {
+ die "Cannot live-migrate, snapshot (with RAM), or hibernate a VM with: "
+ . join(', ', @blockers) . "\n";
+ }
+
+ return @blockers;
+}
+
+# test if VM uses local resources (to prevent migration)
+sub check_local_resources {
+ my ($conf, $state, $noerr) = @_;
+
+ my @loc_res = ();
+ my $mapped_res = {};
+
+ my @non_migratable_resources = check_non_migratable_resources($conf, $state, $noerr);
+ push(@loc_res, @non_migratable_resources);
+
+ my $nodelist = PVE::Cluster::get_nodelist();
+ my $pci_map = PVE::Mapping::PCI::config();
+ my $usb_map = PVE::Mapping::USB::config();
+ my $dir_map = PVE::Mapping::Dir::config();
+
+ my $missing_mappings_by_node = { map { $_ => [] } @$nodelist };
+
+ my $add_missing_mapping = sub {
+ my ($type, $key, $id) = @_;
+ for my $node (@$nodelist) {
+ my $entry;
+ if ($type eq 'pci') {
+ $entry = PVE::Mapping::PCI::get_node_mapping($pci_map, $id, $node);
+ } elsif ($type eq 'usb') {
+ $entry = PVE::Mapping::USB::get_node_mapping($usb_map, $id, $node);
+ } elsif ($type eq 'dir') {
+ $entry = PVE::Mapping::Dir::get_node_mapping($dir_map, $id, $node);
+ }
+ if (!scalar($entry->@*)) {
+ push @{ $missing_mappings_by_node->{$node} }, $key;
+ }
+ }
+ };
+
+ push @loc_res, "hostusb" if $conf->{hostusb}; # old syntax
+ push @loc_res, "hostpci" if $conf->{hostpci}; # old syntax
+
+ push @loc_res, "ivshmem" if $conf->{ivshmem};
+
+ foreach my $k (keys %$conf) {
+ if ($k =~ m/^usb/) {
+ my $entry = parse_property_string('pve-qm-usb', $conf->{$k});
+ next if $entry->{host} && $entry->{host} =~ m/^spice$/i;
+ if (my $name = $entry->{mapping}) {
+ $add_missing_mapping->('usb', $k, $name);
+ $mapped_res->{$k} = { name => $name };
+ }
+ }
+ if ($k =~ m/^hostpci/) {
+ my $entry = parse_property_string('pve-qm-hostpci', $conf->{$k});
+ if (my $name = $entry->{mapping}) {
+ $add_missing_mapping->('pci', $k, $name);
+ my $mapped_device = { name => $name };
+ $mapped_res->{$k} = $mapped_device;
+
+ if ($pci_map->{ids}->{$name}->{'live-migration-capable'}) {
+ $mapped_device->{'live-migration'} = 1;
+ # don't add mapped device with live migration as blocker
+ next;
+ }
+
+ # don't add mapped devices as blocker for offline migration but still iterate over
+ # all mappings above to collect on which nodes they are available.
+ next if !$state;
+ }
+ }
+ if ($k =~ m/^virtiofs/) {
+ my $entry = parse_property_string('pve-qm-virtiofs', $conf->{$k});
+ $add_missing_mapping->('dir', $k, $entry->{dirid});
+ $mapped_res->{$k} = { name => $entry->{dirid} };
+ }
+ # sockets are safe: they will recreated be on the target side post-migrate
+ next if $k =~ m/^serial/ && ($conf->{$k} eq 'socket');
+ push @loc_res, $k if $k =~ m/^(usb|hostpci|serial|parallel|virtiofs)\d+$/;
+ }
+
+ die "VM uses local resources\n" if scalar @loc_res && !$noerr;
+
+ return wantarray ? (\@loc_res, $mapped_res, $missing_mappings_by_node) : \@loc_res;
+}
+
+sub set_migration_caps {
+ my ($vmid, $savevm) = @_;
+
+ my $qemu_support = eval { mon_cmd($vmid, "query-proxmox-support") };
+
+ my $bitmap_prop = $savevm ? 'pbs-dirty-bitmap-savevm' : 'pbs-dirty-bitmap-migration';
+ my $dirty_bitmaps = $qemu_support->{$bitmap_prop} ? 1 : 0;
+
+ my $cap_ref = [];
+
+ my $enabled_cap = {
+ "auto-converge" => 1,
+ "xbzrle" => 1,
+ "dirty-bitmaps" => $dirty_bitmaps,
+ };
+
+ my $supported_capabilities = mon_cmd($vmid, "query-migrate-capabilities");
+
+ for my $supported_capability (@$supported_capabilities) {
+ push @$cap_ref,
+ {
+ capability => $supported_capability->{capability},
+ state => $enabled_cap->{ $supported_capability->{capability} }
+ ? JSON::true
+ : JSON::false,
+ };
+ }
+
+ mon_cmd($vmid, "migrate-set-capabilities", capabilities => $cap_ref);
+}
+
+1;
diff --git a/src/PVE/QemuMigrate/Makefile b/src/PVE/QemuMigrate/Makefile
new file mode 100644
index 00000000..c6e50d3f
--- /dev/null
+++ b/src/PVE/QemuMigrate/Makefile
@@ -0,0 +1,9 @@
+DESTDIR=
+PREFIX=/usr
+PERLDIR=$(PREFIX)/share/perl5
+
+SOURCES=Helpers.pm
+
+.PHONY: install
+install: $(SOURCES)
+ for i in $(SOURCES); do install -D -m 0644 $$i $(DESTDIR)$(PERLDIR)/PVE/QemuMigrate/$$i; done
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index d24dc7eb..3c612b44 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -53,6 +53,7 @@ use PVE::Tools
use PVE::QMPClient;
use PVE::QemuConfig;
use PVE::QemuConfig::NoWrite;
+use PVE::QemuMigrate::Helpers;
use PVE::QemuServer::Agent qw(qga_check_running);
use PVE::QemuServer::Helpers
qw(config_aware_timeout get_iscsi_initiator_name min_version kvm_user_version windows_version);
@@ -2254,104 +2255,6 @@ sub config_list {
return $res;
}
-sub check_non_migratable_resources {
- my ($conf, $state, $noerr) = @_;
-
- my @blockers = ();
- if ($state) {
- push @blockers, "amd-sev" if $conf->{"amd-sev"};
- push @blockers, "virtiofs" if PVE::QemuServer::Virtiofs::virtiofs_enabled($conf);
- }
-
- if (scalar(@blockers) && !$noerr) {
- die "Cannot live-migrate, snapshot (with RAM), or hibernate a VM with: "
- . join(', ', @blockers) . "\n";
- }
-
- return @blockers;
-}
-
-# test if VM uses local resources (to prevent migration)
-sub check_local_resources {
- my ($conf, $state, $noerr) = @_;
-
- my @loc_res = ();
- my $mapped_res = {};
-
- my @non_migratable_resources = check_non_migratable_resources($conf, $state, $noerr);
- push(@loc_res, @non_migratable_resources);
-
- my $nodelist = PVE::Cluster::get_nodelist();
- my $pci_map = PVE::Mapping::PCI::config();
- my $usb_map = PVE::Mapping::USB::config();
- my $dir_map = PVE::Mapping::Dir::config();
-
- my $missing_mappings_by_node = { map { $_ => [] } @$nodelist };
-
- my $add_missing_mapping = sub {
- my ($type, $key, $id) = @_;
- for my $node (@$nodelist) {
- my $entry;
- if ($type eq 'pci') {
- $entry = PVE::Mapping::PCI::get_node_mapping($pci_map, $id, $node);
- } elsif ($type eq 'usb') {
- $entry = PVE::Mapping::USB::get_node_mapping($usb_map, $id, $node);
- } elsif ($type eq 'dir') {
- $entry = PVE::Mapping::Dir::get_node_mapping($dir_map, $id, $node);
- }
- if (!scalar($entry->@*)) {
- push @{ $missing_mappings_by_node->{$node} }, $key;
- }
- }
- };
-
- push @loc_res, "hostusb" if $conf->{hostusb}; # old syntax
- push @loc_res, "hostpci" if $conf->{hostpci}; # old syntax
-
- push @loc_res, "ivshmem" if $conf->{ivshmem};
-
- foreach my $k (keys %$conf) {
- if ($k =~ m/^usb/) {
- my $entry = parse_property_string('pve-qm-usb', $conf->{$k});
- next if $entry->{host} && $entry->{host} =~ m/^spice$/i;
- if (my $name = $entry->{mapping}) {
- $add_missing_mapping->('usb', $k, $name);
- $mapped_res->{$k} = { name => $name };
- }
- }
- if ($k =~ m/^hostpci/) {
- my $entry = parse_property_string('pve-qm-hostpci', $conf->{$k});
- if (my $name = $entry->{mapping}) {
- $add_missing_mapping->('pci', $k, $name);
- my $mapped_device = { name => $name };
- $mapped_res->{$k} = $mapped_device;
-
- if ($pci_map->{ids}->{$name}->{'live-migration-capable'}) {
- $mapped_device->{'live-migration'} = 1;
- # don't add mapped device with live migration as blocker
- next;
- }
-
- # don't add mapped devices as blocker for offline migration but still iterate over
- # all mappings above to collect on which nodes they are available.
- next if !$state;
- }
- }
- if ($k =~ m/^virtiofs/) {
- my $entry = parse_property_string('pve-qm-virtiofs', $conf->{$k});
- $add_missing_mapping->('dir', $k, $entry->{dirid});
- $mapped_res->{$k} = { name => $entry->{dirid} };
- }
- # sockets are safe: they will recreated be on the target side post-migrate
- next if $k =~ m/^serial/ && ($conf->{$k} eq 'socket');
- push @loc_res, $k if $k =~ m/^(usb|hostpci|serial|parallel|virtiofs)\d+$/;
- }
-
- die "VM uses local resources\n" if scalar @loc_res && !$noerr;
-
- return wantarray ? (\@loc_res, $mapped_res, $missing_mappings_by_node) : \@loc_res;
-}
-
# check if used storages are available on all nodes (use by migrate)
sub check_storage_availability {
my ($storecfg, $conf, $node) = @_;
@@ -4508,37 +4411,6 @@ sub qemu_volume_snapshot_delete {
}
}
-sub set_migration_caps {
- my ($vmid, $savevm) = @_;
-
- my $qemu_support = eval { mon_cmd($vmid, "query-proxmox-support") };
-
- my $bitmap_prop = $savevm ? 'pbs-dirty-bitmap-savevm' : 'pbs-dirty-bitmap-migration';
- my $dirty_bitmaps = $qemu_support->{$bitmap_prop} ? 1 : 0;
-
- my $cap_ref = [];
-
- my $enabled_cap = {
- "auto-converge" => 1,
- "xbzrle" => 1,
- "dirty-bitmaps" => $dirty_bitmaps,
- };
-
- my $supported_capabilities = mon_cmd($vmid, "query-migrate-capabilities");
-
- for my $supported_capability (@$supported_capabilities) {
- push @$cap_ref,
- {
- capability => $supported_capability->{capability},
- state => $enabled_cap->{ $supported_capability->{capability} }
- ? JSON::true
- : JSON::false,
- };
- }
-
- mon_cmd($vmid, "migrate-set-capabilities", capabilities => $cap_ref);
-}
-
sub foreach_volid {
my ($conf, $func, @param) = @_;
@@ -5893,7 +5765,7 @@ sub vm_start_nolock {
}
if ($migratedfrom) {
- eval { set_migration_caps($vmid); };
+ eval { PVE::QemuMigrate::Helpers::set_migration_caps($vmid); };
warn $@ if $@;
if ($spice_port) {
@@ -6315,7 +6187,7 @@ sub vm_suspend {
die "cannot suspend to disk during backup\n"
if $is_backing_up && $includestate;
- check_non_migratable_resources($conf, $includestate, 0);
+ PVE::QemuMigrate::Helpers::check_non_migratable_resources($conf, $includestate, 0);
if ($includestate) {
$conf->{lock} = 'suspending';
@@ -6351,7 +6223,7 @@ sub vm_suspend {
PVE::Storage::activate_volumes($storecfg, [$vmstate]);
eval {
- set_migration_caps($vmid, 1);
+ PVE::QemuMigrate::Helpers::set_migration_caps($vmid, 1);
mon_cmd($vmid, "savevm-start", statefile => $path);
for (;;) {
my $state = mon_cmd($vmid, "query-savevm");
diff --git a/src/test/MigrationTest/QemuMigrateMock.pm b/src/test/MigrationTest/QemuMigrateMock.pm
index 1b95a2ff..56a1d777 100644
--- a/src/test/MigrationTest/QemuMigrateMock.pm
+++ b/src/test/MigrationTest/QemuMigrateMock.pm
@@ -167,9 +167,6 @@ $MigrationTest::Shared::qemu_server_module->mock(
qemu_drive_mirror_switch_to_active_mode => sub {
return;
},
- set_migration_caps => sub {
- return;
- },
vm_stop => sub {
$vm_stop_executed = 1;
delete $expected_calls->{'vm_stop'};
diff --git a/src/test/MigrationTest/QmMock.pm b/src/test/MigrationTest/QmMock.pm
index 69b9c2c9..3eaa131f 100644
--- a/src/test/MigrationTest/QmMock.pm
+++ b/src/test/MigrationTest/QmMock.pm
@@ -142,9 +142,6 @@ $MigrationTest::Shared::qemu_server_module->mock(
}
die "run_command (mocked) - implement me: ${cmd_msg}";
},
- set_migration_caps => sub {
- return;
- },
vm_migrate_alloc_nbd_disks => sub {
my $nbd =
$MigrationTest::Shared::qemu_server_module->original('vm_migrate_alloc_nbd_disks')
diff --git a/src/test/MigrationTest/Shared.pm b/src/test/MigrationTest/Shared.pm
index e29cd1df..a51e1692 100644
--- a/src/test/MigrationTest/Shared.pm
+++ b/src/test/MigrationTest/Shared.pm
@@ -135,6 +135,13 @@ $qemu_config_module->mock(
},
);
+our $qemu_migrate_helpers_module = Test::MockModule->new("PVE::QemuMigrate::Helpers");
+$qemu_migrate_helpers_module->mock(
+ set_migration_caps => sub {
+ return;
+ },
+);
+
our $qemu_server_cloudinit_module = Test::MockModule->new("PVE::QemuServer::Cloudinit");
$qemu_server_cloudinit_module->mock(
generate_cloudinitconfig => sub {
diff --git a/src/test/snapshot-test.pm b/src/test/snapshot-test.pm
index 1a8623c0..4fce87f1 100644
--- a/src/test/snapshot-test.pm
+++ b/src/test/snapshot-test.pm
@@ -390,6 +390,12 @@ sub qmp_cmd {
}
# END mocked PVE::QemuServer::Monitor methods
+#
+# BEGIN mocked PVE::QemuMigrate::Helpers methods
+
+sub set_migration_caps { } # ignored
+
+# END mocked PVE::QemuMigrate::Helpers methods
# BEGIN redefine PVE::QemuServer methods
@@ -429,13 +435,14 @@ sub vm_stop {
return;
}
-sub set_migration_caps { } # ignored
-
# END redefine PVE::QemuServer methods
PVE::Tools::run_command("rm -rf snapshot-working");
PVE::Tools::run_command("cp -a snapshot-input snapshot-working");
+my $qemu_migrate_helpers_module = Test::MockModule->new('PVE::QemuMigrate::Helpers');
+$qemu_migrate_helpers_module->mock('set_migration_caps', \&set_migration_caps);
+
my $qemu_helpers_module = Test::MockModule->new('PVE::QemuServer::Helpers');
$qemu_helpers_module->mock('vm_running_locally', \&vm_running_locally);
--
2.47.2
More information about the pve-devel
mailing list