* [PATCH common v3 01/21] tools: move version_cmp() helper from qemu-server
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH common v3 02/21] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
` (19 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
To be re-used from pve-cluster for node version checking.
This helper seems generic enough to warrant inclusion in the
PVE::Tools module. There is no module like PVE::Version or similar yet
and a module for the single helper seems overkill.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/Tools.pm | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index 8e7646d..18e1dd2 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -1647,4 +1647,30 @@ sub is_deeply {
return 1;
}
+# gets in pairs the versions you want to compares, i.e.:
+# ($a-major, $b-major, $a-minor, $b-minor, $a-extra, $b-extra, ...)
+# returns 0 if same, -1 if $a is older than $b, +1 if $a is newer than $b
+sub version_cmp {
+ my @versions = @_;
+
+ my $size = scalar(@versions);
+
+ return 0 if $size == 0;
+
+ if ($size & 1) {
+ my (undef, $fn, $line) = caller(0);
+ die "cannot compare odd count of versions, called from $fn:$line\n";
+ }
+
+ for (my $i = 0; $i < $size; $i += 2) {
+ my ($left, $right) = splice(@versions, 0, 2);
+ $left //= 0;
+ $right //= 0;
+
+ return 1 if $left > $right;
+ return -1 if $left < $right;
+ }
+ return 0;
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH common v3 02/21] rest handler: handle: respect schema's 'type-property' when resolving type
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
2026-09-18 16:08 ` [PATCH common v3 01/21] tools: move version_cmp() helper from qemu-server Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH cluster v3 03/21] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
` (18 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
For the HA resources 'migrate' endpoint, the plan is to use a oneOf
schema with the type parameter being 'resource-type' and a
resolve_type() function resolving the resource type based on the
service ID, which consists of the resource type and the numerical ID
of the resource. Resolve the type property instead of hard-coding
'type' and add a test case modeled after the above use case.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/JSONSchema.pm | 10 +++
src/PVE/RESTHandler.pm | 21 ++++--
test/get-options-test.pl | 150 +++++++++++++++++++++++++++++++++++++++
3 files changed, 174 insertions(+), 7 deletions(-)
diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
index 34f2949..4b4842d 100644
--- a/src/PVE/JSONSchema.pm
+++ b/src/PVE/JSONSchema.pm
@@ -2254,6 +2254,16 @@ sub get_object_property_schema($schema, $key, $object_data = undef) {
return;
}
+# Get the type property of an object-like schema. Returns undef if there is no type property.
+sub get_object_type_property($schema) {
+ if (my $all_of = $schema->{allOf}) {
+ for my $subschema ($all_of->@*) {
+ return $subschema->{'type-property'} if defined($subschema->{'type-property'});
+ }
+ }
+ return $schema->{'type-property'};
+}
+
# $schema: an object-like schema
# $sub: sub($key, $schema, $one_of_instance_info) ->
# () empty list -> continue
diff --git a/src/PVE/RESTHandler.pm b/src/PVE/RESTHandler.pm
index dc6f329..316ce3a 100644
--- a/src/PVE/RESTHandler.pm
+++ b/src/PVE/RESTHandler.pm
@@ -522,14 +522,21 @@ sub handle {
# Type resolution must happen before normalization, since normalization needs to know
# the schema of values, for which the type must already be known, otherwise the oneOf
# variants will be ignored and normalization silently skips over parameters.
- # The property name is fixed, matching the 'type-property' SectionConfig generates.
my $resolve_type_hook = $info->{resolve_type};
- if ($resolve_type_hook && ref($param) eq 'HASH' && !defined($param->{type})) {
- # The callback only inspects the parameters, so hand it a clone.
- my $resolved_type = $resolve_type_hook->(clone($param));
- # NOTE: the resolved type is passed on to the method's code, which must tolerate it.
- if (defined($resolved_type)) {
- $param->{type} = $resolved_type;
+ if ($resolve_type_hook && ref($param) eq 'HASH') {
+ my $type_property;
+ if (PVE::JSONSchema::is_object_like_schema($schema)) {
+ $type_property = PVE::JSONSchema::get_object_type_property($schema);
+ }
+ # TODO enforce that 'type-property' is defined instead of using a default?
+ $type_property //= 'type';
+ if (!defined($param->{$type_property})) {
+ # The callback only inspects the parameters, so hand it a clone.
+ my $resolved_type = $resolve_type_hook->(clone($param));
+ # NOTE: the resolved type is passed on to the method's code, which must tolerate it.
+ if (defined($resolved_type)) {
+ $param->{$type_property} = $resolved_type;
+ }
}
}
diff --git a/test/get-options-test.pl b/test/get-options-test.pl
index 6eeee37..154cd11 100755
--- a/test/get-options-test.pl
+++ b/test/get-options-test.pl
@@ -753,6 +753,156 @@ package DirectOneOf {
}
}
+package OneOfWithCustomResolvedTypeProperty {
+ usebase;
+
+ sub desc($class) {
+ "oneOf as top-level with a custom 'type-property' that is resolved via 'resolve_type'";
+ }
+
+ sub schema($class) {
+ my $implicit_type_discriminator = {
+ type => 'string',
+ description => 'A property from which the type can be deduced.',
+ enum => ['deduce-type-one', 'deduce-type-two'],
+ };
+ return {
+ 'type-property' => 'custom-type',
+ 'type-property-schema' => {
+ type => 'string',
+ description => 'The type.',
+ enum => ['one', 'two'],
+ },
+ oneOf => [
+ {
+ 'instance-type' => 'one',
+ additionalProperties => 0,
+ properties => {
+ 'implicit-type-discriminator' => $implicit_type_discriminator,
+ 'prop-one' => {
+ optional => 1,
+ type => 'string',
+ description => 'a or b',
+ enum => ['a', 'b'],
+ },
+ },
+ },
+ {
+ 'instance-type' => 'two',
+ additionalProperties => 0,
+ properties => {
+ 'implicit-type-discriminator' => $implicit_type_discriminator,
+ 'prop-two' => {
+ type => 'number',
+ description => 'number',
+ minimum => 3,
+ maximum => 100,
+ },
+ },
+ },
+ ],
+ };
+ }
+
+ sub additional_method_info($class) {
+ return (
+ resolve_type => sub {
+ my ($param) = @_;
+ return 'one' if $param->{'implicit-type-discriminator'} eq 'deduce-type-one';
+ return 'two' if $param->{'implicit-type-discriminator'} eq 'deduce-type-two';
+ die "unable to resolve type\n";
+ },
+ );
+ }
+
+ sub long_usage_str($class, $prefix) {
+ "USAGE: $prefix --custom-type <string> [OPTIONS]\n"
+ . " --custom-type <one | two>\n"
+ . "\t The type.\n" . "\n"
+ . " Conditional options:\n" . "\n"
+ . " [custom-type=one]\n" . "\n"
+ . " --implicit-type-discriminator <deduce-type-one | deduce-type-two>\n"
+ . "\t A property from which the type can be deduced.\n" . "\n"
+ . " --prop-one <a | b>\n"
+ . "\t a or b\n" . "\n"
+ . " [custom-type=two]\n" . "\n"
+ . " --implicit-type-discriminator <deduce-type-one | deduce-type-two>\n"
+ . "\t A property from which the type can be deduced.\n" . "\n"
+ . " --prop-two <number> (3 - 100)\n"
+ . "\t number\n" . "\n";
+ }
+
+ sub invocations($class) {
+ return (
+ {
+ desc => "implicit-type-discriminator parameter works",
+ args => [qw(--implicit-type-discriminator deduce-type-one)],
+ expected => {
+ 'custom-type' => 'one',
+ 'implicit-type-discriminator' => 'deduce-type-one',
+ },
+ },
+ {
+ desc => "valid options parse",
+ args => [qw(--implicit-type-discriminator deduce-type-one --prop-one a)],
+ expected => {
+ 'custom-type' => 'one',
+ 'implicit-type-discriminator' => 'deduce-type-one',
+ 'prop-one' => 'a',
+ },
+ },
+ {
+ desc => "invalid options are rejected",
+ args => [qw(--implicit-type-discriminator deduce-type-one --prop-invalid a)],
+ error => "400 unable to parse option\n",
+ },
+ {
+ desc => "optional arg_param is optional",
+ args => [qw(--implicit-type-discriminator deduce-type-one)],
+ arg_param => [qw(prop-one)],
+ expected => {
+ 'custom-type' => 'one',
+ 'implicit-type-discriminator' => 'deduce-type-one',
+ },
+ },
+ {
+ desc => "optional arg_param is functional",
+ args => [qw(--implicit-type-discriminator deduce-type-one b)],
+ arg_param => [qw(prop-one)],
+ expected => {
+ 'custom-type' => 'one',
+ 'implicit-type-discriminator' => 'deduce-type-one',
+ 'prop-one' => 'b',
+ },
+ },
+ {
+ desc => "mandatory arg_param is mandatory",
+ args => [qw(--implicit-type-discriminator deduce-type-two)],
+ arg_param => [qw(prop-two)],
+ error => "400 not enough arguments\n",
+ },
+ {
+ desc => "mandatory arg_param is is functional",
+ args => [qw(--implicit-type-discriminator deduce-type-two 33)],
+ arg_param => [qw(prop-two)],
+ expected => {
+ 'custom-type' => 'two',
+ 'implicit-type-discriminator' => 'deduce-type-two',
+ 'prop-two' => 33,
+ },
+ },
+ {
+ desc => "explicit type takes precedence",
+ args => [qw(--implicit-type-discriminator deduce-type-two --custom-type one)],
+ expected => {
+ 'custom-type' => 'one',
+ 'implicit-type-discriminator' => 'deduce-type-two',
+ },
+ },
+ );
+ }
+}
+
package OneOfArrayVsScalar {
usebase;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH cluster v3 03/21] cluster: move pvecfg node version helpers from qemu-server
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
2026-09-18 16:08 ` [PATCH common v3 01/21] tools: move version_cmp() helper from qemu-server Fiona Ebner
2026-09-18 16:08 ` [PATCH common v3 02/21] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 04/21] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
` (17 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
For the pvecfg version helpers, add a node_pvecfg_version_at_least()
function for convenience.
In preparation to check the node version for container migration and
from HA manager too.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/Cluster.pm | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index 034b78c..784ea02 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -929,4 +929,34 @@ sub cfs_rename_db_unsafe {
rename $dbfile, "$dbfile.$ctime.bak" or warn "failed to rename old config database - $!\n";
}
+sub get_node_pvecfg_version {
+ my ($node) = @_;
+
+ my $nodes_version_info = get_node_kv('version-info', $node);
+ return if !$nodes_version_info->{$node};
+
+ my $version_info = decode_json($nodes_version_info->{$node});
+ return $version_info->{version};
+}
+
+sub pvecfg_min_version {
+ my ($verstr, $major, $minor, $release) = @_;
+
+ return 0 if !$verstr;
+
+ if ($verstr =~ m/^(\d+)\.(\d+)(?:[.-](\d+))?/) {
+ return 1 if PVE::Tools::version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
+ return 0;
+ }
+
+ die "internal error: cannot check version of invalid string '$verstr'";
+}
+
+sub node_pvecfg_version_at_least {
+ my ($node, $major, $minor, $release) = @_;
+
+ my $version = get_node_pvecfg_version($node);
+ return $version && pvecfg_min_version($version, $major, $minor, $release);
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 04/21] next state {stopped,started}: factor out helper to handle motion command
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (2 preceding siblings ...)
2026-09-18 16:08 ` [PATCH cluster v3 03/21] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 05/21] lrm: resource migration: support extra migration options Fiona Ebner
` (16 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Deduplicate the code before extending it for supporting migration
options. The only intended functional change is an additional log line
with the motion command and target in case of next_state_stopped().
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/Manager.pm | 55 +++++++++----------
.../test-relocate-to-inactive-node/log.expect | 1 +
src/test/test-service-stopped3/log.expect | 1 +
3 files changed, 27 insertions(+), 30 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 5840a76..e0c1858 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1290,6 +1290,29 @@ sub next_state_migrate_relocate {
}
}
+my sub next_state_handle_motion_command {
+ my ($self, $cmd, $sid, $sd) = @_;
+
+ my $haenv = $self->{haenv};
+ my $ns = $self->{ns};
+
+ my $target = shift @{ $sd->{cmd} };
+ if (!$ns->node_is_online($target)) {
+ $haenv->log('err', "ignore service '$sid' $cmd request - node '$target' not online");
+ } elsif ($sd->{node} eq $target) {
+ $haenv->log(
+ 'info',
+ "ignore service '$sid' $cmd request - service already on node '$target'",
+ );
+ } else {
+ $haenv->log('info', "$cmd service '$sid' to node '$target'");
+ &$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
+ return 1;
+ }
+
+ return;
+}
+
sub next_state_stopped {
my ($self, $sid, $cd, $sd, $lrm_res) = @_;
@@ -1307,17 +1330,7 @@ sub next_state_stopped {
my $cmd = shift @{ $sd->{cmd} };
if ($cmd eq 'migrate' || $cmd eq 'relocate') {
- my $target = shift @{ $sd->{cmd} };
- if (!$ns->node_is_online($target)) {
- $haenv->log('err',
- "ignore service '$sid' $cmd request - node '$target' not online");
- } elsif ($sd->{node} eq $target) {
- $haenv->log(
- 'info',
- "ignore service '$sid' $cmd request - service already on node '$target'",
- );
- } else {
- &$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
+ if (next_state_handle_motion_command($self, $cmd, $sid, $sd)) {
return;
}
} elsif ($cmd eq 'stop') {
@@ -1431,25 +1444,7 @@ sub next_state_started {
my $cmd = shift @{ $sd->{cmd} };
if ($cmd eq 'migrate' || $cmd eq 'relocate') {
- my $target = shift @{ $sd->{cmd} };
- if (!$ns->node_is_online($target)) {
- $haenv->log(
- 'err',
- "ignore service '$sid' $cmd request - node '$target' not online",
- );
- } elsif ($sd->{node} eq $target) {
- $haenv->log(
- 'info',
- "ignore service '$sid' $cmd request - service already on node '$target'",
- );
- } else {
- $haenv->log('info', "$cmd service '$sid' to node '$target'");
- &$change_service_state(
- $self, $sid, $cmd,
- node => $sd->{node},
- target => $target,
- );
- }
+ next_state_handle_motion_command($self, $cmd, $sid, $sd);
} elsif ($cmd eq 'stop') {
my $timeout = shift @{ $sd->{cmd} };
if ($timeout == 0) {
diff --git a/src/test/test-relocate-to-inactive-node/log.expect b/src/test/test-relocate-to-inactive-node/log.expect
index 266fb48..8c9d892 100644
--- a/src/test/test-relocate-to-inactive-node/log.expect
+++ b/src/test/test-relocate-to-inactive-node/log.expect
@@ -21,6 +21,7 @@ info 25 node3/lrm: status change wait_for_agent_lock => active
info 40 node1/crm: service 'vm:103': state changed from 'request_stop' to 'stopped'
info 120 cmdlist: execute service vm:103 relocate node2
info 120 node1/crm: got crm command: relocate vm:103 node2
+info 120 node1/crm: relocate service 'vm:103' to node 'node2'
info 120 node1/crm: service 'vm:103': state changed from 'stopped' to 'relocate' (node = node3, target = node2)
info 123 node2/lrm: got lock 'ha_agent_node2_lock'
info 123 node2/lrm: status change wait_for_agent_lock => active
diff --git a/src/test/test-service-stopped3/log.expect b/src/test/test-service-stopped3/log.expect
index e08b54c..d4737a7 100644
--- a/src/test/test-service-stopped3/log.expect
+++ b/src/test/test-service-stopped3/log.expect
@@ -21,6 +21,7 @@ info 25 node3/lrm: status change wait_for_agent_lock => active
info 40 node1/crm: service 'fa:1501': state changed from 'request_stop' to 'stopped'
info 120 cmdlist: execute service fa:1501 migrate node2
info 120 node1/crm: got crm command: migrate fa:1501 node2
+info 120 node1/crm: migrate service 'fa:1501' to node 'node2'
info 120 node1/crm: service 'fa:1501': state changed from 'stopped' to 'migrate' (node = node3, target = node2)
info 123 node2/lrm: got lock 'ha_agent_node2_lock'
info 123 node2/lrm: status change wait_for_agent_lock => active
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 05/21] lrm: resource migration: support extra migration options
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (3 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 04/21] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 06/21] change service state: support hash as a parameter value Fiona Ebner
` (15 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
The options will be passed from the guest API endpoints through the HA
stack, all the way to the resource plugins.
In a following commit, the resource plugins will forward parts of the
schema of the resource-specific migration endpoint to the HA resource
endpoint. In particular, forwarding the 'with-conntrack-state' option
will be used to fix bug #7053.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/LRM.pm | 15 ++++++++++-----
src/PVE/HA/Resources.pm | 2 +-
src/PVE/HA/Resources/PVECT.pm | 5 ++++-
src/PVE/HA/Resources/PVEVM.pm | 5 ++++-
src/PVE/HA/Sim/Resources.pm | 2 +-
src/PVE/HA/Sim/Resources/VirtCT.pm | 2 +-
src/PVE/HA/Sim/Resources/VirtFail.pm | 2 +-
7 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/src/PVE/HA/LRM.pm b/src/PVE/HA/LRM.pm
index 72e37e6..5fc66eb 100644
--- a/src/PVE/HA/LRM.pm
+++ b/src/PVE/HA/LRM.pm
@@ -749,14 +749,19 @@ sub manage_resources {
# intermediate step for optional better node selection on stop -> start request state change
next if $request_state eq 'request_start';
+ my $params = {
+ 'target' => $sd->{target},
+ 'timeout' => $sd->{timeout},
+ };
+ if (defined($sd->{'migrate-options'})) {
+ $params->{'migrate-options'} = $sd->{'migrate-options'};
+ }
+
$self->queue_resource_command(
$sid,
$sd->{uid},
$request_state,
- {
- 'target' => $sd->{target},
- 'timeout' => $sd->{timeout},
- },
+ $params,
);
}
@@ -1040,7 +1045,7 @@ sub exec_resource_agent {
my $online = ($cmd eq 'migrate') ? 1 : 0;
- my $res = $plugin->migrate($haenv, $id, $target, $online);
+ my $res = $plugin->migrate($haenv, $id, $target, $online, $params->{'migrate-options'});
# something went wrong if service is still on this node
if (!$res) {
diff --git a/src/PVE/HA/Resources.pm b/src/PVE/HA/Resources.pm
index df7c1ff..656cb19 100644
--- a/src/PVE/HA/Resources.pm
+++ b/src/PVE/HA/Resources.pm
@@ -154,7 +154,7 @@ sub shutdown {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
die "implement in subclass";
}
diff --git a/src/PVE/HA/Resources/PVECT.pm b/src/PVE/HA/Resources/PVECT.pm
index 177b907..3d8a5b9 100644
--- a/src/PVE/HA/Resources/PVECT.pm
+++ b/src/PVE/HA/Resources/PVECT.pm
@@ -106,7 +106,7 @@ sub shutdown {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
my $nodename = $haenv->nodename();
@@ -117,6 +117,9 @@ sub migrate {
online => 0, # we cannot migrate CT (yet) online, only relocate
};
+ $migrate_options //= {};
+ $params->{$_} = $migrate_options->{$_} for sort keys $migrate_options->%*;
+
# always relocate container for now
if ($class->check_running($haenv, $id)) {
$class->shutdown($haenv, $id);
diff --git a/src/PVE/HA/Resources/PVEVM.pm b/src/PVE/HA/Resources/PVEVM.pm
index 8753271..964f374 100644
--- a/src/PVE/HA/Resources/PVEVM.pm
+++ b/src/PVE/HA/Resources/PVEVM.pm
@@ -107,7 +107,7 @@ sub shutdown {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
my $nodename = $haenv->nodename();
@@ -123,6 +123,9 @@ sub migrate {
online => $online,
};
+ $migrate_options //= {};
+ $params->{$_} = $migrate_options->{$_} for sort keys $migrate_options->%*;
+
# explicitly shutdown if $online isn't true (relocate)
if (!$online && $class->check_running($haenv, $id)) {
$class->shutdown($haenv, $id);
diff --git a/src/PVE/HA/Sim/Resources.pm b/src/PVE/HA/Sim/Resources.pm
index 9b2f3b6..a464c62 100644
--- a/src/PVE/HA/Sim/Resources.pm
+++ b/src/PVE/HA/Sim/Resources.pm
@@ -88,7 +88,7 @@ sub check_running {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
my $sid = $class->type() . ":$id";
my $nodename = $haenv->nodename();
diff --git a/src/PVE/HA/Sim/Resources/VirtCT.pm b/src/PVE/HA/Sim/Resources/VirtCT.pm
index 3f6df85..ac61486 100644
--- a/src/PVE/HA/Sim/Resources/VirtCT.pm
+++ b/src/PVE/HA/Sim/Resources/VirtCT.pm
@@ -17,7 +17,7 @@ sub exists {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
my $sid = "ct:$id";
my $nodename = $haenv->nodename();
diff --git a/src/PVE/HA/Sim/Resources/VirtFail.pm b/src/PVE/HA/Sim/Resources/VirtFail.pm
index ea7a87a..71bd282 100644
--- a/src/PVE/HA/Sim/Resources/VirtFail.pm
+++ b/src/PVE/HA/Sim/Resources/VirtFail.pm
@@ -84,7 +84,7 @@ sub shutdown {
}
sub migrate {
- my ($class, $haenv, $id, $target, $online) = @_;
+ my ($class, $haenv, $id, $target, $online, $migrate_options) = @_;
my ($migrate_failure_count, $limit_to_node) = ($decode_id->($id))[1, 4];
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 06/21] change service state: support hash as a parameter value
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (4 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 05/21] lrm: resource migration: support extra migration options Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 07/21] next state: handle motion command: support migration options Fiona Ebner
` (14 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Required for the migration options.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/Manager.pm | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index e0c1858..d61e50c 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -533,7 +533,11 @@ my $change_service_state = sub {
foreach my $k (sort keys %params) {
my $v = $params{$k};
$text_state .= ", " if $text_state;
- $text_state .= "$k = $v";
+ if (ref($v) eq 'HASH') {
+ $text_state .= "$k = " . JSON::encode_json($v);
+ } else {
+ $text_state .= "$k = $v";
+ }
$sd->{$k} = $v;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 07/21] next state: handle motion command: support migration options
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (5 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 06/21] change service state: support hash as a parameter value Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 08/21] queue resource motion: " Fiona Ebner
` (13 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/Manager.pm | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index d61e50c..1d9ff36 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1301,6 +1301,7 @@ my sub next_state_handle_motion_command {
my $ns = $self->{ns};
my $target = shift @{ $sd->{cmd} };
+ my $migrate_options = shift @{ $sd->{cmd} };
if (!$ns->node_is_online($target)) {
$haenv->log('err', "ignore service '$sid' $cmd request - node '$target' not online");
} elsif ($sd->{node} eq $target) {
@@ -1310,7 +1311,16 @@ my sub next_state_handle_motion_command {
);
} else {
$haenv->log('info', "$cmd service '$sid' to node '$target'");
- &$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
+
+ my $params = {
+ node => $sd->{node},
+ target => $target,
+ };
+ if ($migrate_options && scalar(keys $migrate_options->%*) > 0) {
+ $params->{'migrate-options'} = $migrate_options;
+ }
+
+ $change_service_state->($self, $sid, $cmd, $params->%*);
return 1;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 08/21] queue resource motion: support migration options
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (6 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 07/21] next state: handle motion command: support migration options Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 09/21] crm command: support JSON-style migrate command Fiona Ebner
` (12 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/Manager.pm | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 1d9ff36..555ea0d 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -294,7 +294,7 @@ sub load_balance {
);
$haenv->log('info', "auto rebalance - $task $sid to $target ($imbalance_change_str)");
- $self->queue_resource_motion($cmd, $task, $sid, $target);
+ $self->queue_resource_motion($cmd, $task, $sid, $target, {});
}
sub cleanup {
@@ -609,7 +609,7 @@ sub read_lrm_status {
}
sub queue_resource_motion {
- my ($self, $cmd, $task, $sid, $target) = @_;
+ my ($self, $cmd, $task, $sid, $target, $migrate_options) = @_;
my ($haenv, $sc, $ss, $ns, $compiled_rules) = $self->@{qw(haenv sc ss ns compiled_rules)};
my $online_nodes = { map { $_ => 1 } $self->{ns}->list_online_nodes()->@* };
@@ -640,7 +640,7 @@ sub queue_resource_motion {
}
$haenv->log('info', "got crm command: $cmd");
- $ss->{$sid}->{cmd} = [$task, $target];
+ $ss->{$sid}->{cmd} = [$task, $target, $migrate_options];
for my $csid (@$dependent_resources) {
next if $ss->{$csid}->{node} && $ss->{$csid}->{node} eq $target;
@@ -651,7 +651,7 @@ sub queue_resource_motion {
"crm command '$cmd' - $task service '$csid' to node '$target'"
. " (service '$csid' in positive affinity with service '$sid')",
);
- $ss->{$csid}->{cmd} = [$task, $target];
+ $ss->{$csid}->{cmd} = [$task, $target, $migrate_options];
}
}
@@ -693,7 +693,7 @@ sub update_crm_commands {
"ignore crm command - service already on target node: $cmd",
);
} else {
- $self->queue_resource_motion($cmd, $task, $sid, $node);
+ $self->queue_resource_motion($cmd, $task, $sid, $node, {});
}
}
} else {
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 09/21] crm command: support JSON-style migrate command
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (7 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 08/21] queue resource motion: " Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH ha-manager v3 10/21] api: resources: migration: support additional migration options Fiona Ebner
` (11 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
To pass along opaque migration options from the resource-specific
migration API endpoint to the resource plugin implementation, the
options will be encoded as JSON. Instead of just adding an extra
positional argument with those, support having the full CRM command be
recorded as JSON in the queued commands file.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/HA/Manager.pm | 73 +++++++++++++++++++++++++++++++++----------
1 file changed, 56 insertions(+), 17 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 555ea0d..25026d6 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -4,6 +4,7 @@ use strict;
use warnings;
use Digest::MD5 qw(md5_base64);
+use JSON qw();
use PVE::Tools;
@@ -670,6 +671,36 @@ sub any_resource_motion_queued_or_running {
return 0;
}
+my sub crm_cmd_migrate_relocate {
+ my ($self, $task, $sid, $node, $options) = @_;
+
+ my $cmd = "$task $sid $node";
+
+ if (defined($options) && scalar(keys($options->%*))) {
+ $cmd .= ' ' . JSON::encode_json($options);
+ }
+
+ my ($haenv, $ms, $ns, $sc, $ss) = $self->@{qw(haenv ms ns sc ss)};
+
+ if (my $sd = $ss->{$sid}) {
+ if (!$ns->node_is_online($node)) {
+ $haenv->log('err', "crm command error - node not online: $cmd");
+ } else {
+ if ($node eq $sd->{node}) {
+ $haenv->log(
+ 'info', "ignore crm command - service already on target node: $cmd",
+ );
+ } else {
+ $self->queue_resource_motion($cmd, $task, $sid, $node, $options);
+ }
+ }
+ } else {
+ $haenv->log('err', "crm command error - no such service: $cmd");
+ }
+
+ return;
+}
+
# read new crm commands and save them into crm master status
sub update_crm_commands {
my ($self) = @_;
@@ -681,25 +712,33 @@ sub update_crm_commands {
foreach my $cmd (split(/\n/, $cmdlist)) {
chomp $cmd;
- if ($cmd =~ m/^(migrate|relocate)\s+(\S+)\s+(\S+)$/) {
- my ($task, $sid, $node) = ($1, $2, $3);
- if (my $sd = $ss->{$sid}) {
- if (!$ns->node_is_online($node)) {
- $haenv->log('err', "crm command error - node not online: $cmd");
- } else {
- if ($node eq $sd->{node}) {
- $haenv->log(
- 'info',
- "ignore crm command - service already on target node: $cmd",
- );
- } else {
- $self->queue_resource_motion($cmd, $task, $sid, $node, {});
- }
- }
- } else {
- $haenv->log('err', "crm command error - no such service: $cmd");
+ if ($cmd =~ m/^{/) {
+ # New-style JSON-encoded command. Currently only 'migrate' is supported, allowing for
+ # additional options.
+
+ my $command_info = eval { JSON::decode_json($cmd) };
+ if (my $err = $@) {
+ $haenv->log('err', "unable to decode command as JSON: '$cmd' - $err");
+ next;
}
+ my $kind = $command_info->{kind};
+ if (defined($kind) && $kind eq 'migrate') {
+ my ($sid, $node, $options) = $command_info->@{qw(sid node options)};
+ if (!defined($sid) || !defined($node)) {
+ $haenv->log('err', "'migrate' JSON command without sid or node");
+ next;
+ }
+ crm_cmd_migrate_relocate($self, $kind, $sid, $node, $options);
+ } else {
+ $haenv->log('err', "unable to handle unknown JSON command: '$cmd'");
+ }
+
+ next;
+ }
+
+ if ($cmd =~ m/^(migrate|relocate)\s+(\S+)\s+(\S+)$/) {
+ crm_cmd_migrate_relocate($self, $1, $2, $3, {});
} elsif ($cmd =~ m/^stop\s+(\S+)\s+(\S+)$/) {
my ($sid, $timeout) = ($1, $2);
if (my $sd = $ss->{$sid}) {
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH ha-manager v3 10/21] api: resources: migration: support additional migration options
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (8 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 09/21] crm command: support JSON-style migrate command Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:21 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 11/21] helpers: move version_cmp() helper to pve-common Fiona Ebner
` (10 subsequent siblings)
20 siblings, 1 reply; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Additional migration options can now be passed from the guest API
endpoints to the HA stack. From the HA resource API endpoint, the
options are passed all the way to the resource plugins.
Check that all nodes are recent enough to support the new-style JSON
CRM command, otherwise produce a warning and fall-back to the previous
behavior ignoring the additional migration options.
The migration API endpoints for containers and VMs use different
parameters. For example, the endpoint for containers does not have
the 'with-conntrack-state' option. To keep the schema for the more
abstract endpoint for HA resources clean, separate by type using a
oneOf schema and let the resource plugins declare additional
parameters via a migrate_json_properties() function.
Initially, this will be used to fix #7053 and pass along the
'with-conntrack-state' migration option.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
The node version might need to be adapted when applying!
New in v3.
src/PVE/API2/HA/Resources.pm | 99 ++++++++++++++++++++++++++++++-----
src/PVE/HA/Resources/PVECT.pm | 10 ++++
src/PVE/HA/Resources/PVEVM.pm | 10 ++++
3 files changed, 105 insertions(+), 14 deletions(-)
diff --git a/src/PVE/API2/HA/Resources.pm b/src/PVE/API2/HA/Resources.pm
index b96572b..6d808ca 100644
--- a/src/PVE/API2/HA/Resources.pm
+++ b/src/PVE/API2/HA/Resources.pm
@@ -3,6 +3,8 @@ package PVE::API2::HA::Resources;
use strict;
use warnings;
+use JSON qw();
+
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::Cluster;
@@ -12,6 +14,7 @@ use HTTP::Status qw(:constants);
use Storable qw(dclone);
use PVE::JSONSchema qw(get_standard_option);
use PVE::RPCEnvironment;
+use PVE::SafeSyslog;
use PVE::RESTHandler;
@@ -34,6 +37,34 @@ my $api_copy_config = sub {
return $scfg;
};
+my $common_migrate_json_properties = {
+ sid => get_standard_option(
+ 'pve-ha-resource-or-vm-id',
+ { completion => \&PVE::HA::Tools::complete_sid },
+ ),
+ node => get_standard_option(
+ 'pve-node',
+ {
+ completion => \&PVE::Cluster::complete_migration_target,
+ description => "Target node.",
+ },
+ ),
+};
+
+my sub json_crm_command_is_supported {
+ my $version_info = PVE::Cluster::get_node_kv('version-info');
+ for my $node (sort keys $version_info->%*) {
+ my $node_info = eval { JSON::decode_json($version_info->{$node}); };
+ if (my $err = $@) { # warn, but continue
+ syslog('warn', "cannot parse version info for $node as JSON - $err");
+ } elsif (!PVE::Cluster::pvecfg_min_version($node_info->{version}, 9, 2, 21)) {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
sub check_service_state {
my ($sid, $req_state) = @_;
@@ -336,20 +367,34 @@ __PACKAGE__->register_method({
check => ['perm', '/', ['Sys.Console']],
},
parameters => {
- additionalProperties => 0,
- properties => {
- sid => get_standard_option(
- 'pve-ha-resource-or-vm-id',
- { completion => \&PVE::HA::Tools::complete_sid },
- ),
- node => get_standard_option(
- 'pve-node',
- {
- completion => \&PVE::Cluster::complete_migration_target,
- description => "Target node.",
- },
- ),
+ 'type-property' => 'resource-type',
+ 'type-property-schema' => {
+ type => 'string',
+ description => 'The resource type. Automatically determined from the service ID.',
+ enum => ['ct', 'vm'],
},
+ oneOf => [
+ {
+ 'instance-type' => 'ct',
+ additionalProperties => 0,
+ properties => PVE::HA::Resources::PVECT::migrate_json_properties(
+ $common_migrate_json_properties),
+ },
+ {
+ 'instance-type' => 'vm',
+ additionalProperties => 0,
+ properties => PVE::HA::Resources::PVEVM::migrate_json_properties(
+ $common_migrate_json_properties),
+ },
+ ],
+ },
+ resolve_type => sub {
+ my ($param) = @_;
+ my $type = (PVE::HA::Config::parse_sid($param->{sid}))[1];
+ if (!($type eq 'ct' || $type eq 'vm')) {
+ die "Unknown service type '$type' for 'migrate' endpoint schema.\n";
+ }
+ return $type;
},
returns => {
type => 'object',
@@ -403,12 +448,38 @@ __PACKAGE__->register_method({
my ($sid, $type, $name) = PVE::HA::Config::parse_sid(extract_param($param, 'sid'));
my $req_node = extract_param($param, 'node');
+ my $resource_type = extract_param($param, 'resource-type');
+ if ($resource_type ne $type) {
+ raise_param_exc({
+ 'resource-type' => "$resource_type does not match type of $sid" });
+ }
+
+ # The rest of the parameters in $param are migration options passed along to the plugin.
PVE::HA::Config::service_is_ha_managed($sid);
check_service_state($sid);
- PVE::HA::Config::queue_crm_commands("migrate $sid $req_node");
+ my $crm_command;
+ if (json_crm_command_is_supported()) {
+ $crm_command = JSON::encode_json({
+ kind => 'migrate',
+ sid => $sid,
+ node => $req_node,
+ options => $param,
+ });
+ } else {
+ if (scalar(keys $param->%*) > 0) {
+ syslog(
+ 'warn',
+ "$sid migration to $req_node: ignoring additional migration options"
+ . " - not supported by CRM on all nodes",
+ );
+ }
+ $crm_command = "migrate $sid $req_node";
+ }
+
+ PVE::HA::Config::queue_crm_commands($crm_command);
$result->{sid} = $sid;
$result->{'requested-node'} = $req_node;
diff --git a/src/PVE/HA/Resources/PVECT.pm b/src/PVE/HA/Resources/PVECT.pm
index 3d8a5b9..17dd697 100644
--- a/src/PVE/HA/Resources/PVECT.pm
+++ b/src/PVE/HA/Resources/PVECT.pm
@@ -174,4 +174,14 @@ sub get_static_stats_from_config {
};
}
+# FIXME: the extra properties are missing from the docs, since the PVE::API::LXC module can't be
+# included in that environment, because of cyclic dependencies..
+sub migrate_json_properties {
+ my ($props) = @_;
+ my $extra_migrate_props = eval { PVE::API2::LXC::ha_migrate_json_properties() } // {};
+ die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
+ $props->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
+ return $props;
+}
+
1;
diff --git a/src/PVE/HA/Resources/PVEVM.pm b/src/PVE/HA/Resources/PVEVM.pm
index 964f374..52a48e4 100644
--- a/src/PVE/HA/Resources/PVEVM.pm
+++ b/src/PVE/HA/Resources/PVEVM.pm
@@ -194,4 +194,14 @@ sub get_static_stats_from_config {
};
}
+# FIXME: the extra properties are missing from the docs, since the PVE::API::QEMU module can't be
+# included in that environment, because of cyclic dependencies..
+sub migrate_json_properties {
+ my ($props) = @_;
+ my $extra_migrate_props = eval { PVE::API2::Qemu::ha_migrate_json_properties() } // {};
+ die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
+ $props->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
+ return $props;
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 11/21] helpers: move version_cmp() helper to pve-common
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (9 preceding siblings ...)
2026-09-18 16:08 ` [PATCH ha-manager v3 10/21] api: resources: migration: support additional migration options Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 12/21] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
` (9 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/QemuServer/Helpers.pm | 30 ++----------------------------
src/PVE/QemuServer/Machine.pm | 13 +++++--------
src/PVE/QemuServer/QMPHelpers.pm | 5 +++--
3 files changed, 10 insertions(+), 38 deletions(-)
diff --git a/src/PVE/QemuServer/Helpers.pm b/src/PVE/QemuServer/Helpers.pm
index dd17eef5..a39e4abf 100644
--- a/src/PVE/QemuServer/Helpers.pm
+++ b/src/PVE/QemuServer/Helpers.pm
@@ -228,39 +228,13 @@ sub min_version {
my ($verstr, $major, $minor, $pve) = @_;
if ($verstr =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:\+pve(\d+))?/) {
- return 1 if version_cmp($1, $major, $2, $minor, $4, $pve) >= 0;
+ return 1 if PVE::Tools::version_cmp($1, $major, $2, $minor, $4, $pve) >= 0;
return 0;
}
die "internal error: cannot check version of invalid string '$verstr'";
}
-# gets in pairs the versions you want to compares, i.e.:
-# ($a-major, $b-major, $a-minor, $b-minor, $a-extra, $b-extra, ...)
-# returns 0 if same, -1 if $a is older than $b, +1 if $a is newer than $b
-sub version_cmp {
- my @versions = @_;
-
- my $size = scalar(@versions);
-
- return 0 if $size == 0;
-
- if ($size & 1) {
- my (undef, $fn, $line) = caller(0);
- die "cannot compare odd count of versions, called from $fn:$line\n";
- }
-
- for (my $i = 0; $i < $size; $i += 2) {
- my ($left, $right) = splice(@versions, 0, 2);
- $left //= 0;
- $right //= 0;
-
- return 1 if $left > $right;
- return -1 if $left < $right;
- }
- return 0;
-}
-
sub config_aware_timeout {
my ($config, $memory, $is_suspended) = @_;
my $timeout = 30;
@@ -313,7 +287,7 @@ sub pvecfg_min_version {
return 0 if !$verstr;
if ($verstr =~ m/^(\d+)\.(\d+)(?:[.-](\d+))?/) {
- return 1 if version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
+ return 1 if PVE::Tools::version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
return 0;
}
diff --git a/src/PVE/QemuServer/Machine.pm b/src/PVE/QemuServer/Machine.pm
index 35161d1f..2e8e57b4 100644
--- a/src/PVE/QemuServer/Machine.pm
+++ b/src/PVE/QemuServer/Machine.pm
@@ -3,6 +3,8 @@ package PVE::QemuServer::Machine;
use strict;
use warnings;
+use PVE::Tools;
+
use PVE::QemuServer::Helpers;
use PVE::QemuServer::MetaInfo;
use PVE::QemuServer::Monitor;
@@ -298,13 +300,8 @@ sub machine_version_cmp {
my ($major_a, $minor_a, $pve_a) = extract_version_parts($machine_type_a);
my ($major_b, $minor_b, $pve_b) = extract_version_parts($machine_type_b);
- return PVE::QemuServer::Helpers::version_cmp(
- $major_a,
- $major_b,
- $minor_a,
- $minor_b,
- $pve_a,
- $pve_b,
+ return PVE::Tools::version_cmp(
+ $major_a, $major_b, $minor_a, $minor_b, $pve_a, $pve_b,
);
}
@@ -345,7 +342,7 @@ sub can_run_pve_machine_version {
my $pvever = $3;
$kvmversion =~ m/(\d+)\.(\d+)/;
- return 0 if PVE::QemuServer::Helpers::version_cmp($1, $major, $2, $minor) < 0;
+ return 0 if PVE::Tools::version_cmp($1, $major, $2, $minor) < 0;
# if $pvever is missing or 0, we definitely support it as long as we didn't
# fail the QEMU version check above
diff --git a/src/PVE/QemuServer/QMPHelpers.pm b/src/PVE/QemuServer/QMPHelpers.pm
index f474c123..f83b41e2 100644
--- a/src/PVE/QemuServer/QMPHelpers.pm
+++ b/src/PVE/QemuServer/QMPHelpers.pm
@@ -2,7 +2,8 @@ package PVE::QemuServer::QMPHelpers;
use v5.36;
-use PVE::QemuServer::Helpers;
+use PVE::Tools;
+
use PVE::QemuServer::Monitor qw(mon_cmd);
use base 'Exporter';
@@ -59,7 +60,7 @@ sub runs_at_least_qemu_version($vmid, $major, $minor, $extra = undef) {
die "could not query currently running version for VM $vmid\n" if !defined($v);
$v = $v->{qemu};
- return PVE::QemuServer::Helpers::version_cmp(
+ return PVE::Tools::version_cmp(
$v->{major}, $major, $v->{minor}, $minor, $v->{micro}, $extra,
) >= 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 12/21] helpers: move pvecfg node version helpers to pve-cluster
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (10 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 11/21] helpers: move version_cmp() helper to pve-common Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 13/21] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (8 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
To prepare for pve-container and pve-ha-manager to check node
versions too.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/QemuMigrate.pm | 7 +------
src/PVE/QemuServer/Helpers.pm | 23 -----------------------
2 files changed, 1 insertion(+), 29 deletions(-)
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 8da6f15d..080490f0 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -1394,14 +1394,9 @@ sub phase2 {
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
- my $target_version = PVE::QemuServer::Helpers::get_node_pvecfg_version($self->{node});
-
my $ticket_port = undef;
# Check if target is new enough for having the port encoded in the proxy ticket.
- if (
- $target_version
- && PVE::QemuServer::Helpers::pvecfg_min_version($target_version, 9, 1, 9)
- ) {
+ if (PVE::Cluster::node_pvecfg_version_at_least($self->{node}, 9, 1, 9)) {
$ticket_port = $spice_port;
}
diff --git a/src/PVE/QemuServer/Helpers.pm b/src/PVE/QemuServer/Helpers.pm
index a39e4abf..2e9535c7 100644
--- a/src/PVE/QemuServer/Helpers.pm
+++ b/src/PVE/QemuServer/Helpers.pm
@@ -271,29 +271,6 @@ sub config_aware_timeout {
return $timeout;
}
-sub get_node_pvecfg_version {
- my ($node) = @_;
-
- my $nodes_version_info = PVE::Cluster::get_node_kv('version-info', $node);
- return if !$nodes_version_info->{$node};
-
- my $version_info = decode_json($nodes_version_info->{$node});
- return $version_info->{version};
-}
-
-sub pvecfg_min_version {
- my ($verstr, $major, $minor, $release) = @_;
-
- return 0 if !$verstr;
-
- if ($verstr =~ m/^(\d+)\.(\d+)(?:[.-](\d+))?/) {
- return 1 if PVE::Tools::version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
- return 0;
- }
-
- die "internal error: cannot check version of invalid string '$verstr'";
-}
-
sub parse_number_sets {
my ($set) = @_;
my $res = [];
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 13/21] api: migrate: allow forwarding certain migration properties to HA
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (11 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 12/21] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 14/21] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
` (7 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Empty for now, but there is a 'skip-config-check' option planned that
will be forwarded. And the existing 'with-conntrack-state' option will
be forwarded too, to fix bug #7053.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/API2/Qemu.pm | 146 ++++++++++++++++++++++++-------------------
1 file changed, 83 insertions(+), 63 deletions(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 71247eec..b5a850a3 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -80,6 +80,77 @@ BEGIN {
use base qw(PVE::RESTHandler);
+my $migrate_json_properties = {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
+ target => get_standard_option(
+ 'pve-node',
+ {
+ description => "Target node.",
+ completion => \&PVE::Cluster::complete_migration_target,
+ },
+ ),
+ online => {
+ type => 'boolean',
+ description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
+ optional => 1,
+ },
+ force => {
+ type => 'boolean',
+ description =>
+ "Allow to migrate VMs which use local devices. Only root may use this option.",
+ optional => 1,
+ },
+ migration_type => {
+ type => 'string',
+ enum => ['secure', 'insecure'],
+ description =>
+ "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
+ optional => 1,
+ },
+ migration_network => {
+ type => 'string',
+ format => 'CIDR',
+ description => "CIDR of the (sub) network that is used for migration.",
+ optional => 1,
+ },
+ "with-local-disks" => {
+ type => 'boolean',
+ description => "Enable live storage migration for local disk",
+ optional => 1,
+ },
+ targetstorage => get_standard_option(
+ 'pve-targetstorage',
+ {
+ completion => \&PVE::QemuServer::complete_migration_storage,
+ },
+ ),
+ bwlimit => {
+ description => "Override I/O bandwidth limit (in KiB/s).",
+ optional => 1,
+ type => 'integer',
+ minimum => '0',
+ default => 'migrate limit from datacenter or storage config',
+ },
+ 'with-conntrack-state' => {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description => 'Whether to migrate conntrack entries for running VMs.',
+ },
+};
+
+# Properties that will be forwarded via the HA stack to the LRM, which will then use them for its
+# own invocation of the migration API endpoint. The other properties are not forwarded and lost for
+# HA migrations with many being hard-coded for the LRM invocation.
+my $ha_migrate_props = {};
+
+sub ha_migrate_json_properties {
+ my $forwarded_props = {};
+ $forwarded_props->{$_} = $migrate_json_properties->{$_} for keys $ha_migrate_props->%*;
+ return $forwarded_props;
+}
+
my $opt_force_description =
"Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.";
@@ -5489,67 +5560,7 @@ __PACKAGE__->register_method({
},
parameters => {
additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- vmid =>
- get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
- target => get_standard_option(
- 'pve-node',
- {
- description => "Target node.",
- completion => \&PVE::Cluster::complete_migration_target,
- },
- ),
- online => {
- type => 'boolean',
- description =>
- "Use online/live migration if VM is running. Ignored if VM is stopped.",
- optional => 1,
- },
- force => {
- type => 'boolean',
- description =>
- "Allow to migrate VMs which use local devices. Only root may use this option.",
- optional => 1,
- },
- migration_type => {
- type => 'string',
- enum => ['secure', 'insecure'],
- description =>
- "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
- optional => 1,
- },
- migration_network => {
- type => 'string',
- format => 'CIDR',
- description => "CIDR of the (sub) network that is used for migration.",
- optional => 1,
- },
- "with-local-disks" => {
- type => 'boolean',
- description => "Enable live storage migration for local disk",
- optional => 1,
- },
- targetstorage => get_standard_option(
- 'pve-targetstorage',
- {
- completion => \&PVE::QemuServer::complete_migration_storage,
- },
- ),
- bwlimit => {
- description => "Override I/O bandwidth limit (in KiB/s).",
- optional => 1,
- type => 'integer',
- minimum => '0',
- default => 'migrate limit from datacenter or storage config',
- },
- 'with-conntrack-state' => {
- type => 'boolean',
- optional => 1,
- default => 0,
- description => 'Whether to migrate conntrack entries for running VMs.',
- },
- },
+ properties => $migrate_json_properties,
},
returns => {
type => 'string',
@@ -5641,9 +5652,18 @@ __PACKAGE__->register_method({
my $hacmd = sub {
my $upid = shift;
- print "Requesting HA migration for VM $vmid to node $target\n";
-
+ my $extra_opts = '';
my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
+ for my $prop (sort keys $ha_migrate_props->%*) {
+ if (defined(my $value = $param->{$prop})) {
+ push($cmd->@*, "--$prop", $value);
+ $extra_opts .= ',';
+ $extra_opts .= " $prop=$value";
+ }
+ }
+
+ print "Requesting HA migration for VM $vmid to node ${target}${extra_opts}\n";
+
run_command($cmd);
return;
};
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 14/21] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (12 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 13/21] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 15/21] qm: mtunnel: reply when a command is unknown Fiona Ebner
` (6 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
The HA stack will pass it along to the LRM, which will then invoke the
API call with the flag again.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/API2/Qemu.pm | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index b5a850a3..56b6c500 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -143,7 +143,9 @@ my $migrate_json_properties = {
# Properties that will be forwarded via the HA stack to the LRM, which will then use them for its
# own invocation of the migration API endpoint. The other properties are not forwarded and lost for
# HA migrations with many being hard-coded for the LRM invocation.
-my $ha_migrate_props = {};
+my $ha_migrate_props = {
+ 'with-conntrack-state' => 1,
+};
sub ha_migrate_json_properties {
my $forwarded_props = {};
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 15/21] qm: mtunnel: reply when a command is unknown
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (13 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 14/21] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 16/21] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
` (5 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Otherwise, the other endpoint cannot distinguish between an unknown
command and a command which takes a long time.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v3:
* move earlier in series, so it can be applied independently.
src/PVE/CLI/qm.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index b903c1f1..85fe993f 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -482,6 +482,8 @@ __PACKAGE__->register_method({
} else {
$tunnel_write->("ERR: resume failed - VM $vmid not running");
}
+ } else {
+ $tunnel_write->("ERR: unknown command '$line'");
}
}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 16/21] qm: mtunnel: add 'conf' command to do strict configuration parsing
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (14 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 15/21] qm: mtunnel: reply when a command is unknown Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH qemu-server v3 17/21] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
` (4 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Will be requested by the source of the migration before the
configuration is moved, so there is a parameter for the node where
the configuration resides.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v3.
src/PVE/CLI/qm.pm | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index 85fe993f..d0374ff3 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -18,6 +18,7 @@ use URI::Escape;
use PVE::APIClient::LWP;
use PVE::Cluster;
use PVE::Exception qw(raise_param_exc);
+use PVE::File;
use PVE::GuestHelpers;
use PVE::GuestImport::OVF;
use PVE::INotify;
@@ -482,6 +483,18 @@ __PACKAGE__->register_method({
} else {
$tunnel_write->("ERR: resume failed - VM $vmid not running");
}
+ } elsif ($line =~ /^config (\d+) (\S+)$/) {
+ my ($vmid, $node) = ($1, $2);
+ eval {
+ my $conf_fn = PVE::QemuConfig->config_file($vmid, $node);
+ my $raw = PVE::File::file_get_contents($conf_fn);
+ PVE::QemuServer::parse_vm_config($conf_fn, $raw, 1);
+ };
+ if (my $err = $@) {
+ $tunnel_write->("ERR: strict config check for target node failed - $err");
+ } else {
+ $tunnel_write->("OK");
+ }
} else {
$tunnel_write->("ERR: unknown command '$line'");
}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH qemu-server v3 17/21] migration: intra-cluster: check config can be parsed on target node
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (15 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 16/21] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH container v3 18/21] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (3 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
For remote migration, we already check that the config can be parsed
on the target. Do the same for intra-cluster migration, to avoid
issues like [0] for future new settings, with lines being unexpectedly
and relatively silently dropped (there are warnings in the target's
system logs, but nothing else).
Unfortunately, before commit "qm: mtunnel: reply when a command is
unknown", which is part of the same patch series, when a command is
unknown, mtunnel did not reply at all. Therefore, check that the
target node is recent enough to support the new mtunnel command.
[0]: https://bugzilla.proxmox.com/show_bug.cgi?id=7341
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
The node version might need to be adapted when applying!
Changes in v3:
* add skip-config-check flag rather than re-using force.
* check node version to decide if too old or not.
src/PVE/API2/Qemu.pm | 10 ++++++++++
src/PVE/QemuMigrate.pm | 24 +++++++++++++++++++++++
src/test/MigrationTest/QemuMigrateMock.pm | 11 ++++++++++-
3 files changed, 44 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 56b6c500..937f8cae 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -138,6 +138,15 @@ my $migrate_json_properties = {
default => 0,
description => 'Whether to migrate conntrack entries for running VMs.',
},
+ 'skip-config-check' => {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description =>
+ 'For intra-cluster migration. Skip checking if the configuration can be parsed'
+ . ' successfully by the target. Using this flag can lead to dropped'
+ . ' configuration options if the target is too old to parse them.',
+ },
};
# Properties that will be forwarded via the HA stack to the LRM, which will then use them for its
@@ -145,6 +154,7 @@ my $migrate_json_properties = {
# HA migrations with many being hard-coded for the LRM invocation.
my $ha_migrate_props = {
'with-conntrack-state' => 1,
+ 'skip-config-check' => 1,
};
sub ha_migrate_json_properties {
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 080490f0..fff57eab 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -359,6 +359,30 @@ sub prepare {
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 (!$self->{opts}->{'skip-config-check'}) {
+ if (PVE::Cluster::node_pvecfg_version_at_least($self->{node}, 9, 2, 21)) {
+ # Fork a short-lived tunnel for checking the config. Later, the proper tunnel with
+ # SSH forwarding info is forked.
+ my $tunnel = $self->fork_tunnel();
+ # Compared to remote migration, which also does volume activation, this only
+ # strictly parses the config, so no too large timeout is needed.
+ eval {
+ my $nodename = PVE::INotify::nodename();
+ PVE::Tunnel::write_tunnel($tunnel, 30, "config $vmid $nodename");
+ };
+ my $err = $@;
+
+ eval { PVE::Tunnel::finish_tunnel($tunnel); };
+ $self->log('warn', "failed to finish tunnel in prepare() - $@") if $@;
+
+ die "$err - use 'skip-config-check' flag to migrate regardless\n" if $err;
+ } else {
+ $self->log('info', "skipping config check - target pve-manager version < 9.2.21");
+ }
+ } else {
+ $self->log('info', "skipping config check - override option is set");
+ }
}
return $running;
diff --git a/src/test/MigrationTest/QemuMigrateMock.pm b/src/test/MigrationTest/QemuMigrateMock.pm
index 3a483817..ac198acb 100644
--- a/src/test/MigrationTest/QemuMigrateMock.pm
+++ b/src/test/MigrationTest/QemuMigrateMock.pm
@@ -65,6 +65,10 @@ $tunnel_module->mock(
my $vmid = $1;
die "resuming wrong VM '$vmid'\n" if $vmid ne $test_vmid;
return;
+ } elsif ($command =~ m/^config (\d+) (\S+)$/) {
+ my ($vmid, $node) = ($1, $2);
+ die "check config for wrong VM '$vmid'\n" if $vmid ne $test_vmid;
+ return;
}
die "write_tunnel (mocked) - implement me: $command\n";
},
@@ -73,7 +77,12 @@ $tunnel_module->mock(
my $qemu_migrate_module = Test::MockModule->new("PVE::QemuMigrate");
$qemu_migrate_module->mock(
fork_tunnel => sub {
- die "fork_tunnel (mocked) - implement me\n"; # currently no call should lead here
+ return {
+ writer => "mocked",
+ reader => "mocked",
+ pid => 123456,
+ version => 1,
+ };
},
start_remote_tunnel => sub {
my ($self, $raddr, $rport, $ruri, $unix_socket_info) = @_;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH container v3 18/21] api: migrate: allow forwarding certain migration properties to HA
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (16 preceding siblings ...)
2026-09-18 16:08 ` [PATCH qemu-server v3 17/21] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH container v3 19/21] pct: introduce mtunnel command Fiona Ebner
` (2 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
From: Fiona Ebner <phi@faeony.eu>
Empty for now, but there is a 'skip-config-check' option planned that
will be forwarded. And this is also done for consistency with VMs,
where the existing 'with-conntrack-state' option will be forwarded
too.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/API2/LXC.pm | 94 +++++++++++++++++++++++++++------------------
1 file changed, 57 insertions(+), 37 deletions(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 5f94d5a..c89837e 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -48,6 +48,53 @@ BEGIN {
}
}
+my $migrate_json_properties = {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
+ target => get_standard_option(
+ 'pve-node',
+ {
+ description => "Target node.",
+ completion => \&PVE::Cluster::complete_migration_target,
+ },
+ ),
+ 'target-storage' => get_standard_option('pve-targetstorage'),
+ 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,
+ },
+ bwlimit => {
+ description => "Override I/O bandwidth limit (in KiB/s).",
+ optional => 1,
+ type => 'number',
+ minimum => '0',
+ default => 'migrate limit from datacenter or storage config',
+ },
+};
+
+# Properties that will be forwarded via the HA stack to the LRM, which will then use them for its
+# own invocation of the migration API endpoint. The other properties are not forwarded and lost for
+# HA migrations with many being hard-coded for the LRM invocation.
+my $ha_migrate_props = {};
+
+sub ha_migrate_json_properties {
+ my $forwarded_props = {};
+ $forwarded_props->{$_} = $migrate_json_properties->{$_} for keys $ha_migrate_props->%*;
+ return $forwarded_props;
+}
+
my sub assert_not_restore_from_external {
my ($archive, $storage_cfg) = @_;
@@ -1633,42 +1680,7 @@ __PACKAGE__->register_method({
},
parameters => {
additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- vmid =>
- get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
- target => get_standard_option(
- 'pve-node',
- {
- description => "Target node.",
- completion => \&PVE::Cluster::complete_migration_target,
- },
- ),
- 'target-storage' => get_standard_option('pve-targetstorage'),
- 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,
- },
- bwlimit => {
- description => "Override I/O bandwidth limit (in KiB/s).",
- optional => 1,
- type => 'number',
- minimum => '0',
- default => 'migrate limit from datacenter or storage config',
- },
- },
+ properties => $migrate_json_properties,
},
returns => {
type => 'string',
@@ -1732,10 +1744,18 @@ __PACKAGE__->register_method({
my $upid = shift;
my $service = "ct:$vmid";
+ my $extra_opts = '';
my $cmd = ['ha-manager', 'migrate', $service, $target];
+ for my $prop (sort keys $ha_migrate_props->%*) {
+ if (defined(my $value = $param->{$prop})) {
+ push($cmd->@*, "--$prop", $value);
+ $extra_opts .= ',';
+ $extra_opts .= " $prop=$value";
+ }
+ }
- print "Requesting HA migration for CT $vmid to node $target\n";
+ print "Requesting HA migration for CT $vmid to node ${target}${extra_opts}\n";
PVE::Tools::run_command($cmd);
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH container v3 19/21] pct: introduce mtunnel command
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (17 preceding siblings ...)
2026-09-18 16:08 ` [PATCH container v3 18/21] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH container v3 20/21] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
2026-09-18 16:08 ` [PATCH container v3 21/21] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
Similar to what we already have in qemu-server. There is a 'config'
command used for checking that the configuration can be understood by
the migration target.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v3.
src/PVE/CLI/pct.pm | 57 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/src/PVE/CLI/pct.pm b/src/PVE/CLI/pct.pm
index ceb0bea..00f9d48 100755
--- a/src/PVE/CLI/pct.pm
+++ b/src/PVE/CLI/pct.pm
@@ -12,6 +12,7 @@ use PVE::CLIHandler;
use PVE::Cluster;
use PVE::CpuSet;
use PVE::Exception qw(raise_param_exc);
+use PVE::File;
use PVE::GuestHelpers;
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
@@ -1053,6 +1054,60 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'mtunnel',
+ path => 'mtunnel',
+ method => 'POST',
+ description => "For internal use by intra-cluster migration only.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ if (!PVE::Cluster::check_cfs_quorum(1)) {
+ print "no quorum\n";
+ return;
+ }
+
+ my $tunnel_write = sub {
+ my $text = shift;
+ chomp $text;
+ print "$text\n";
+ *STDOUT->flush();
+ };
+
+ $tunnel_write->("tunnel online");
+ $tunnel_write->("ver 1");
+
+ while (my $line = <STDIN>) {
+ chomp $line;
+ if ($line =~ /^quit$/) {
+ $tunnel_write->("OK");
+ last;
+ } elsif ($line =~ /^config (\d+) (\S+)$/) {
+ my ($vmid, $node) = ($1, $2);
+ eval {
+ my $conf_fn = PVE::LXC::Config->config_file($vmid, $node);
+ my $raw = PVE::File::file_get_contents($conf_fn);
+ PVE::LXC::Config::parse_pct_config($conf_fn, $raw, 1);
+ };
+ if (my $err = $@) {
+ $tunnel_write->("ERR: strict config check for target node failed - $err");
+ } else {
+ $tunnel_write->("OK");
+ }
+ } else {
+ $tunnel_write->("ERR: unknown command '$line'");
+ }
+ }
+
+ return;
+ },
+});
+
our $cmddef = {
list => [
'PVE::API2::LXC',
@@ -1193,6 +1248,8 @@ our $cmddef = {
rescan => [__PACKAGE__, 'rescan', []],
cpusets => [__PACKAGE__, 'cpusets', []],
fstrim => [__PACKAGE__, 'fstrim', ['vmid']],
+
+ mtunnel => [__PACKAGE__, 'mtunnel', []],
};
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH container v3 20/21] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (18 preceding siblings ...)
2026-09-18 16:08 ` [PATCH container v3 19/21] pct: introduce mtunnel command Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
2026-09-18 16:08 ` [PATCH container v3 21/21] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
The PVE::File module was introduced with libpve-common-perl=9.0.12.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v3.
debian/control | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/debian/control b/debian/control
index 238bffc..9a1af21 100644
--- a/debian/control
+++ b/debian/control
@@ -6,7 +6,7 @@ Build-Depends: debhelper-compat (= 13),
dh-apparmor,
libpve-access-control (>= 8.0.0~),
libpve-cluster-perl,
- libpve-common-perl (>= 8.1.0),
+ libpve-common-perl (>= 9.0.12),
libpve-guest-common-perl (>= 5.1.0),
libpve-rs-perl (>= 0.11~),
libpve-storage-perl,
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH container v3 21/21] migration: intra-cluster: check config can be parsed on target node
2026-09-18 16:08 [PATCH-SERIES common/cluster/ha-manager/qemu-server/container v3 00/21] migration: strict config check for intra-cluster migration Fiona Ebner
` (19 preceding siblings ...)
2026-09-18 16:08 ` [PATCH container v3 20/21] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
@ 2026-09-18 16:08 ` Fiona Ebner
20 siblings, 0 replies; 23+ messages in thread
From: Fiona Ebner @ 2026-09-18 16:08 UTC (permalink / raw)
To: pve-devel
For remote migration, we already check that the config can be parsed
on the target. Do the same for intra-cluster migration, to avoid
issues with future new settings unexpectedly being ignored if the
target is too old. For example, migrating a container with a
mountpoint with 'keepattrs' to a node with a too old pve-container
version, results in the mountpoint not being mounted on the target.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
The node version might need to be adapted when applying!
Changes in v3:
* add skip-config-check flag rather than re-using force.
* check node version to decide if too old or not.
src/PVE/API2/LXC.pm | 13 ++++++++++++-
src/PVE/LXC/Migrate.pm | 30 ++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index c89837e..c5e3b04 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -82,12 +82,23 @@ my $migrate_json_properties = {
minimum => '0',
default => 'migrate limit from datacenter or storage config',
},
+ 'skip-config-check' => {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description =>
+ 'For intra-cluster migration. Skip checking if the configuration can be parsed'
+ . ' successfully by the target. Using this flag can lead to ignored'
+ . ' configuration options if the target is too old to parse them.',
+ },
};
# Properties that will be forwarded via the HA stack to the LRM, which will then use them for its
# own invocation of the migration API endpoint. The other properties are not forwarded and lost for
# HA migrations with many being hard-coded for the LRM invocation.
-my $ha_migrate_props = {};
+my $ha_migrate_props = {
+ 'skip-config-check' => 1,
+};
sub ha_migrate_json_properties {
my $forwarded_props = {};
diff --git a/src/PVE/LXC/Migrate.pm b/src/PVE/LXC/Migrate.pm
index d243d90..a8d3a3d 100644
--- a/src/PVE/LXC/Migrate.pm
+++ b/src/PVE/LXC/Migrate.pm
@@ -159,6 +159,36 @@ sub prepare {
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 (!$self->{opts}->{'skip-config-check'}) {
+ if (PVE::Cluster::node_pvecfg_version_at_least($self->{node}, 9, 2, 21)) {
+ # Fork a short-lived tunnel for checking the config. Later, the proper tunnel with
+ # SSH forwarding info is forked.
+ my $tunnel = PVE::Tunnel::fork_ssh_tunnel(
+ $self->{rem_ssh},
+ ['pct', 'mtunnel'],
+ undef,
+ sub {
+ my ($level, $msg) = @_;
+ $self->log($level, $msg);
+ },
+ );
+ eval {
+ my $nodename = PVE::INotify::nodename();
+ PVE::Tunnel::write_tunnel($tunnel, 30, "config $vmid $nodename");
+ };
+ my $err = $@;
+
+ eval { PVE::Tunnel::finish_tunnel($tunnel); };
+ $self->log('warn', "failed to finish tunnel in prepare() - $@") if $@;
+
+ die "$err - use 'skip-config-check' flag to migrate regardless\n" if $err;
+ } else {
+ $self->log('info', "skipping config check - target pve-manager version < 9.2.21");
+ }
+ } else {
+ $self->log('info', "skipping config check - override option is set");
+ }
}
# in restart mode, we shutdown the container before migrating
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread