* [PATCH common v4 01/27] tools: move version_cmp() helper from qemu-server
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH common v4 02/27] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
` (25 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
No changes in v4.
src/PVE/Tools.pm | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index 331de40..347ae50 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -1658,4 +1658,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] 31+ messages in thread* [PATCH common v4 02/27] rest handler: handle: respect schema's 'type-property' when resolving type
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
2026-10-15 12:50 ` [PATCH common v4 01/27] tools: move version_cmp() helper from qemu-server Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH cluster v4 03/27] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
` (24 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
No changes in v4.
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] 31+ messages in thread* [PATCH cluster v4 03/27] cluster: move pvecfg node version helpers from qemu-server
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
2026-10-15 12:50 ` [PATCH common v4 01/27] tools: move version_cmp() helper from qemu-server Fiona Ebner
2026-10-15 12:50 ` [PATCH common v4 02/27] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 04/27] update crm commands: align logging with next state started/stopped Fiona Ebner
` (23 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Dependency bump for pve-common needed!
No changes in v4.
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] 31+ messages in thread* [PATCH ha-manager v4 04/27] update crm commands: align logging with next state started/stopped
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (2 preceding siblings ...)
2026-10-15 12:50 ` [PATCH cluster v4 03/27] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 05/27] manager: factor out helper to check if requested resource motion is valid Fiona Ebner
` (22 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
In preparation to factor out a common helper for checking whether a
resource motion is valid.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v4.
src/PVE/HA/Manager.pm | 4 ++--
src/test/test-node-affinity-nonstrict7/log.expect | 2 +-
src/test/test-node-affinity-strict7/log.expect | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 5840a76..0aef118 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -681,12 +681,12 @@ sub update_crm_commands {
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");
+ $haenv->log('err', "ignore crm command '$cmd' - node '$node' not online");
} else {
if ($node eq $sd->{node}) {
$haenv->log(
'info',
- "ignore crm command - service already on target node: $cmd",
+ "ignore crm command '$cmd' - service already on node '$node'",
);
} else {
$self->queue_resource_motion($cmd, $task, $sid, $node);
diff --git a/src/test/test-node-affinity-nonstrict7/log.expect b/src/test/test-node-affinity-nonstrict7/log.expect
index 54e824e..fe9423b 100644
--- a/src/test/test-node-affinity-nonstrict7/log.expect
+++ b/src/test/test-node-affinity-nonstrict7/log.expect
@@ -32,7 +32,7 @@ err 120 node1/crm: crm command 'migrate vm:101 node1' error - service 'vm
info 220 cmdlist: execute service vm:101 migrate node2
err 220 node1/crm: crm command 'migrate vm:101 node2' error - service 'vm:101' is not allowed on node 'node2'
info 320 cmdlist: execute service vm:101 migrate node3
-info 320 node1/crm: ignore crm command - service already on target node: migrate vm:101 node3
+info 320 node1/crm: ignore crm command 'migrate vm:101 node3' - service already on node 'node3'
info 420 cmdlist: execute service vm:102 migrate node3
info 420 node1/crm: got crm command: migrate vm:102 node3
info 420 node1/crm: migrate service 'vm:102' to node 'node3'
diff --git a/src/test/test-node-affinity-strict7/log.expect b/src/test/test-node-affinity-strict7/log.expect
index ae8e43f..21cfb76 100644
--- a/src/test/test-node-affinity-strict7/log.expect
+++ b/src/test/test-node-affinity-strict7/log.expect
@@ -32,11 +32,11 @@ err 120 node1/crm: crm command 'migrate vm:101 node1' error - service 'vm
info 220 cmdlist: execute service vm:101 migrate node2
err 220 node1/crm: crm command 'migrate vm:101 node2' error - service 'vm:101' is not allowed on node 'node2'
info 320 cmdlist: execute service vm:101 migrate node3
-info 320 node1/crm: ignore crm command - service already on target node: migrate vm:101 node3
+info 320 node1/crm: ignore crm command 'migrate vm:101 node3' - service already on node 'node3'
info 420 cmdlist: execute service vm:102 migrate node3
err 420 node1/crm: crm command 'migrate vm:102 node3' error - service 'vm:102' is not allowed on node 'node3'
info 520 cmdlist: execute service vm:102 migrate node2
-info 520 node1/crm: ignore crm command - service already on target node: migrate vm:102 node2
+info 520 node1/crm: ignore crm command 'migrate vm:102 node2' - service already on node 'node2'
info 620 cmdlist: execute service vm:102 migrate node1
info 620 node1/crm: got crm command: migrate vm:102 node1
info 620 node1/crm: migrate service 'vm:102' to node 'node1'
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 05/27] manager: factor out helper to check if requested resource motion is valid
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (3 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 04/27] update crm commands: align logging with next state started/stopped Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 06/27] next state stopped: align logging for motion command with next_state_started() Fiona Ebner
` (21 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Avoids some duplication. No functional change is intended.
Suggested-by: Daniel Kral <d.kral@proxmox.com>
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v4:
* Split into its own patch and also use for CRM check.
src/PVE/HA/Manager.pm | 55 +++++++++++++++++++------------------------
1 file changed, 24 insertions(+), 31 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 0aef118..b5da4b7 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -666,6 +666,24 @@ sub any_resource_motion_queued_or_running {
return 0;
}
+my sub check_resource_motion_valid {
+ my ($self, $target, $sd, $logprefix) = @_;
+
+ my ($haenv, $ns) = $self->@{qw(haenv ns)};
+
+ if (!$ns->node_is_online($target)) {
+ $haenv->log('err', "$logprefix - node '$target' not online");
+ return 0;
+ }
+
+ if ($target eq $sd->{node}) {
+ $haenv->log('info', "$logprefix - service already on node '$target'");
+ return 0;
+ }
+
+ return 1;
+}
+
# read new crm commands and save them into crm master status
sub update_crm_commands {
my ($self) = @_;
@@ -680,17 +698,8 @@ sub update_crm_commands {
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', "ignore crm command '$cmd' - node '$node' not online");
- } else {
- if ($node eq $sd->{node}) {
- $haenv->log(
- 'info',
- "ignore crm command '$cmd' - service already on node '$node'",
- );
- } else {
- $self->queue_resource_motion($cmd, $task, $sid, $node);
- }
+ if (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
+ $self->queue_resource_motion($cmd, $task, $sid, $node);
}
} else {
$haenv->log('err', "crm command error - no such service: $cmd");
@@ -1308,15 +1317,8 @@ sub next_state_stopped {
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 {
+ my $logprefix = "ignore service '$sid' $cmd request";
+ if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
&$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
return;
}
@@ -1432,17 +1434,8 @@ sub next_state_started {
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 {
+ my $logprefix = "ignore service '$sid' $cmd request";
+ if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
$haenv->log('info', "$cmd service '$sid' to node '$target'");
&$change_service_state(
$self, $sid, $cmd,
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 06/27] next state stopped: align logging for motion command with next_state_started()
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (4 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 05/27] manager: factor out helper to check if requested resource motion is valid Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 07/27] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
` (20 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
In preparation to factor out the code for handling a motion command
in next_state_{started,stopped}() into a common helper.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v4:
* Split into its own patch.
src/PVE/HA/Manager.pm | 1 +
src/test/test-relocate-to-inactive-node/log.expect | 1 +
src/test/test-service-stopped3/log.expect | 1 +
3 files changed, 3 insertions(+)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index b5da4b7..dfcdca2 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1319,6 +1319,7 @@ sub next_state_stopped {
my $target = shift @{ $sd->{cmd} };
my $logprefix = "ignore service '$sid' $cmd request";
if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
+ $haenv->log('info', "$cmd service '$sid' to node '$target'");
&$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
return;
}
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] 31+ messages in thread* [PATCH ha-manager v4 07/27] next state {stopped,started}: factor out helper to handle motion command
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (5 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 06/27] next state stopped: align logging for motion command with next_state_started() Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 08/27] lrm: resource migration: support extra migration options Fiona Ebner
` (19 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Deduplicate the code before extending it for supporting migration
options. No functional change is intended.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v4:
* Patch was split and check_resource_motion_valid() introduced
as a separate patch.
src/PVE/HA/Manager.pm | 37 ++++++++++++++++++++++---------------
1 file changed, 22 insertions(+), 15 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index dfcdca2..eecf598 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1299,6 +1299,24 @@ sub next_state_migrate_relocate {
}
}
+# Calls change_service_state() with $state=$cmd if successful. Returns a boolean indicating success.
+my sub handle_resource_motion_command {
+ my ($self, $cmd, $sid, $sd) = @_;
+
+ my $haenv = $self->{haenv};
+ my $ns = $self->{ns};
+
+ my $target = shift $sd->{cmd}->@*;
+ my $logprefix = "ignore service '$sid' $cmd request";
+ if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
+ $haenv->log('info', "$cmd service '$sid' to node '$target'");
+ $change_service_state->($self, $sid, $cmd, node => $sd->{node}, target => $target);
+ return 1;
+ }
+
+ return 0;
+}
+
sub next_state_stopped {
my ($self, $sid, $cd, $sd, $lrm_res) = @_;
@@ -1316,11 +1334,8 @@ sub next_state_stopped {
my $cmd = shift @{ $sd->{cmd} };
if ($cmd eq 'migrate' || $cmd eq 'relocate') {
- my $target = shift @{ $sd->{cmd} };
- my $logprefix = "ignore service '$sid' $cmd request";
- if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
- $haenv->log('info', "$cmd service '$sid' to node '$target'");
- &$change_service_state($self, $sid, $cmd, node => $sd->{node}, target => $target);
+ # If the motion command is handled, calls change_service_state() with $state=$cmd.
+ if (handle_resource_motion_command($self, $cmd, $sid, $sd)) {
return;
}
} elsif ($cmd eq 'stop') {
@@ -1434,16 +1449,8 @@ sub next_state_started {
my $cmd = shift @{ $sd->{cmd} };
if ($cmd eq 'migrate' || $cmd eq 'relocate') {
- my $target = shift @{ $sd->{cmd} };
- my $logprefix = "ignore service '$sid' $cmd request";
- if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
- $haenv->log('info', "$cmd service '$sid' to node '$target'");
- &$change_service_state(
- $self, $sid, $cmd,
- node => $sd->{node},
- target => $target,
- );
- }
+ # If the motion command is handled, calls change_service_state() with $state=$cmd.
+ handle_resource_motion_command($self, $cmd, $sid, $sd);
} elsif ($cmd eq 'stop') {
my $timeout = shift @{ $sd->{cmd} };
if ($timeout == 0) {
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 08/27] lrm: resource migration: support extra migration options
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (6 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 07/27] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 09/27] change service state: support hash as a parameter value Fiona Ebner
` (18 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Changes in v4:
* Make tidy.
* Always apply migrate-options to $params hash like is done for other
params too.
* Avoid unnecessary sort.
* Also pass migrate options along in simulated plugins.
src/PVE/HA/LRM.pm | 16 ++++++++--------
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 | 5 ++---
src/PVE/HA/Sim/Resources/VirtFail.pm | 6 +++---
7 files changed, 23 insertions(+), 18 deletions(-)
diff --git a/src/PVE/HA/LRM.pm b/src/PVE/HA/LRM.pm
index 72e37e6..4aec85b 100644
--- a/src/PVE/HA/LRM.pm
+++ b/src/PVE/HA/LRM.pm
@@ -749,14 +749,14 @@ 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 = {
+ 'migrate-options' => $sd->{'migrate-options'},
+ 'target' => $sd->{target},
+ 'timeout' => $sd->{timeout},
+ };
+
$self->queue_resource_command(
- $sid,
- $sd->{uid},
- $request_state,
- {
- 'target' => $sd->{target},
- 'timeout' => $sd->{timeout},
- },
+ $sid, $sd->{uid}, $request_state, $params,
);
}
@@ -1040,7 +1040,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..a8fe42e 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 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..3b61113 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 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..ecf3af4 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();
@@ -29,8 +29,7 @@ sub migrate {
$online = 0;
}
- return $class->SUPER::migrate($haenv, $id, $target, $online);
-
+ return $class->SUPER::migrate($haenv, $id, $target, $online, $migrate_options);
}
1;
diff --git a/src/PVE/HA/Sim/Resources/VirtFail.pm b/src/PVE/HA/Sim/Resources/VirtFail.pm
index ea7a87a..9eb86b4 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];
@@ -97,7 +97,7 @@ sub migrate {
$tries->{migrate}->{$id} = 0; # reset counts
- return $class->SUPER::migrate($haenv, $id, $target, $online);
-
+ return $class->SUPER::migrate($haenv, $id, $target, $online, $migrate_options);
}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 09/27] change service state: support hash as a parameter value
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (7 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 08/27] lrm: resource migration: support extra migration options Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 10/27] next state: handle motion command: support migration options Fiona Ebner
` (17 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Required for the migration options.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v4:
* Encode with canonical => 1.
src/PVE/HA/Manager.pm | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index eecf598..c1611ef 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;
@@ -533,7 +534,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::to_json($v, { canonical => 1 });
+ } else {
+ $text_state .= "$k = $v";
+ }
$sd->{$k} = $v;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 10/27] next state: handle motion command: support migration options
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (8 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 09/27] change service state: support hash as a parameter value Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 11/27] resources: add migrate_json_properties() method Fiona Ebner
` (16 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
---
Changes in v4:
* Style fixes suggested by Daniel.
* Add Daniel's R-b.
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 c1611ef..9fca61a 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1312,10 +1312,20 @@ my sub handle_resource_motion_command {
my $ns = $self->{ns};
my $target = shift $sd->{cmd}->@*;
+ my $migrate_options = shift $sd->{cmd}->@*;
my $logprefix = "ignore service '$sid' $cmd request";
if (check_resource_motion_valid($self, $target, $sd, $logprefix)) {
$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] 31+ messages in thread* [PATCH ha-manager v4 11/27] resources: add migrate_json_properties() method
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (9 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 10/27] next state: handle motion command: support migration options Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-16 9:51 ` Daniel Kral
2026-10-15 12:50 ` [PATCH ha-manager v4 12/27] queue resource motion: support migration options Fiona Ebner
` (15 subsequent siblings)
26 siblings, 1 reply; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Will be used by the API schema for resource-type specific migration
options. When migrating dependent resources, the migration options
will be filtered according to the resource type, so the property
schema is also needed there.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Dependency bump for pve-container and qemu-server needed!
Changes in v4:
* Split out from other patch and turn into proper method, because
this is needed for filtering properties when migrating a dependent
resource.
src/PVE/HA/Resources.pm | 20 ++++++++++++++++++++
src/PVE/HA/Resources/PVECT.pm | 11 +++++++++++
src/PVE/HA/Resources/PVEVM.pm | 11 +++++++++++
src/PVE/HA/Sim/Resources.pm | 6 ++++++
4 files changed, 48 insertions(+)
diff --git a/src/PVE/HA/Resources.pm b/src/PVE/HA/Resources.pm
index 656cb19..57fde00 100644
--- a/src/PVE/HA/Resources.pm
+++ b/src/PVE/HA/Resources.pm
@@ -103,6 +103,20 @@ EODESC
},
};
+our $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.",
+ },
+ ),
+};
+
sub verify_name {
my ($class, $name) = @_;
@@ -159,6 +173,12 @@ sub migrate {
die "implement in subclass";
}
+sub migrate_json_properties {
+ my ($class) = @_;
+
+ die "implement in subclass";
+}
+
sub config_file {
my ($class, $id, $nodename) = @_;
diff --git a/src/PVE/HA/Resources/PVECT.pm b/src/PVE/HA/Resources/PVECT.pm
index a8fe42e..67bab7a 100644
--- a/src/PVE/HA/Resources/PVECT.pm
+++ b/src/PVE/HA/Resources/PVECT.pm
@@ -174,4 +174,15 @@ sub get_static_stats_from_config {
};
}
+# FIXME: the extra properties are missing from the docs, since the PVE::API2::LXC module can't be
+# included in the generating docs environment, because of cyclic dependencies..
+sub migrate_json_properties {
+ my ($class) = @_;
+ my $combined = { $PVE::HA::Resources::common_migrate_json_properties->%* };
+ my $extra_migrate_props = eval { PVE::API2::LXC::ha_migrate_json_properties() } // {};
+ die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
+ $combined->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
+ return $combined;
+}
+
1;
diff --git a/src/PVE/HA/Resources/PVEVM.pm b/src/PVE/HA/Resources/PVEVM.pm
index 3b61113..76b927c 100644
--- a/src/PVE/HA/Resources/PVEVM.pm
+++ b/src/PVE/HA/Resources/PVEVM.pm
@@ -194,4 +194,15 @@ sub get_static_stats_from_config {
};
}
+# FIXME: the extra properties are missing from the docs, since the PVE::API2::QEMU module can't be
+# included in the generating docs environment, because of cyclic dependencies..
+sub migrate_json_properties {
+ my ($class) = @_;
+ my $combined = { $PVE::HA::Resources::common_migrate_json_properties->%* };
+ my $extra_migrate_props = eval { PVE::API2::Qemu::ha_migrate_json_properties() } // {};
+ die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
+ $combined->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
+ return $combined;
+}
+
1;
diff --git a/src/PVE/HA/Sim/Resources.pm b/src/PVE/HA/Sim/Resources.pm
index a464c62..730bfaa 100644
--- a/src/PVE/HA/Sim/Resources.pm
+++ b/src/PVE/HA/Sim/Resources.pm
@@ -122,6 +122,12 @@ sub migrate {
return defined($ss->{$sid}) ? 0 : 1;
}
+sub migrate_json_properties {
+ my ($class) = @_;
+
+ return $PVE::HA::Resources::common_migrate_json_properties;
+}
+
sub remove_locks {
my ($self, $haenv, $id, $locks, $service_node) = @_;
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH ha-manager v4 11/27] resources: add migrate_json_properties() method
2026-10-15 12:50 ` [PATCH ha-manager v4 11/27] resources: add migrate_json_properties() method Fiona Ebner
@ 2026-10-16 9:51 ` Daniel Kral
0 siblings, 0 replies; 31+ messages in thread
From: Daniel Kral @ 2026-10-16 9:51 UTC (permalink / raw)
To: Fiona Ebner, pve-devel
On Thu Oct 15, 2026 at 2:50 PM CEST, Fiona Ebner wrote:
> Will be used by the API schema for resource-type specific migration
> options. When migrating dependent resources, the migration options
> will be filtered according to the resource type, so the property
> schema is also needed there.
>
> Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
> ---
>
> Dependency bump for pve-container and qemu-server needed!
>
> Changes in v4:
> * Split out from other patch and turn into proper method, because
> this is needed for filtering properties when migrating a dependent
> resource.
>
> src/PVE/HA/Resources.pm | 20 ++++++++++++++++++++
> src/PVE/HA/Resources/PVECT.pm | 11 +++++++++++
> src/PVE/HA/Resources/PVEVM.pm | 11 +++++++++++
> src/PVE/HA/Sim/Resources.pm | 6 ++++++
> 4 files changed, 48 insertions(+)
>
> diff --git a/src/PVE/HA/Resources.pm b/src/PVE/HA/Resources.pm
> index 656cb19..57fde00 100644
> --- a/src/PVE/HA/Resources.pm
> +++ b/src/PVE/HA/Resources.pm
> @@ -103,6 +103,20 @@ EODESC
> },
> };
>
> +our $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,
missing 'use PVE::Cluster' statement
> + description => "Target node.",
> + },
> + ),
> +};
> +
> sub verify_name {
> my ($class, $name) = @_;
>
> @@ -159,6 +173,12 @@ sub migrate {
> die "implement in subclass";
> }
>
> +sub migrate_json_properties {
> + my ($class) = @_;
> +
> + die "implement in subclass";
> +}
> +
> sub config_file {
> my ($class, $id, $nodename) = @_;
>
> diff --git a/src/PVE/HA/Resources/PVECT.pm b/src/PVE/HA/Resources/PVECT.pm
> index a8fe42e..67bab7a 100644
> --- a/src/PVE/HA/Resources/PVECT.pm
> +++ b/src/PVE/HA/Resources/PVECT.pm
> @@ -174,4 +174,15 @@ sub get_static_stats_from_config {
> };
> }
>
> +# FIXME: the extra properties are missing from the docs, since the PVE::API2::LXC module can't be
> +# included in the generating docs environment, because of cyclic dependencies..
> +sub migrate_json_properties {
> + my ($class) = @_;
> + my $combined = { $PVE::HA::Resources::common_migrate_json_properties->%* };
> + my $extra_migrate_props = eval { PVE::API2::LXC::ha_migrate_json_properties() } // {};
> + die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
> + $combined->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
> + return $combined;
> +}
Not a serious blocker at all, but since we use migrate_json_properties()
in Manager as well now and the return value won't change within one
runtime, this could be a static variable initialized at startup whose
reference is returned here, so we don't need to create a new hash for
every dependent HA resource type in queue_resource_motion() in the next
patch.
> +
> 1;
> diff --git a/src/PVE/HA/Resources/PVEVM.pm b/src/PVE/HA/Resources/PVEVM.pm
> index 3b61113..76b927c 100644
> --- a/src/PVE/HA/Resources/PVEVM.pm
> +++ b/src/PVE/HA/Resources/PVEVM.pm
> @@ -194,4 +194,15 @@ sub get_static_stats_from_config {
> };
> }
>
> +# FIXME: the extra properties are missing from the docs, since the PVE::API2::QEMU module can't be
> +# included in the generating docs environment, because of cyclic dependencies..
> +sub migrate_json_properties {
> + my ($class) = @_;
> + my $combined = { $PVE::HA::Resources::common_migrate_json_properties->%* };
> + my $extra_migrate_props = eval { PVE::API2::Qemu::ha_migrate_json_properties() } // {};
> + die $@ if $@ && !$ENV{PVE_GENERATING_DOCS};
> + $combined->{$_} = $extra_migrate_props->{$_} for keys $extra_migrate_props->%*;
> + return $combined;
> +}
Same here
> +
> 1;
> diff --git a/src/PVE/HA/Sim/Resources.pm b/src/PVE/HA/Sim/Resources.pm
> index a464c62..730bfaa 100644
> --- a/src/PVE/HA/Sim/Resources.pm
> +++ b/src/PVE/HA/Sim/Resources.pm
> @@ -122,6 +122,12 @@ sub migrate {
> return defined($ss->{$sid}) ? 0 : 1;
> }
>
> +sub migrate_json_properties {
> + my ($class) = @_;
> +
> + return $PVE::HA::Resources::common_migrate_json_properties;
> +}
> +
> sub remove_locks {
> my ($self, $haenv, $id, $locks, $service_node) = @_;
>
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH ha-manager v4 12/27] queue resource motion: support migration options
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (10 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 11/27] resources: add migrate_json_properties() method Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH ha-manager v4 13/27] crm command: support JSON-style migrate command Fiona Ebner
` (14 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
For dependent resource, it is necessary to filter out migration
properties that are not applicable to the resource's type.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v4:
* Apply filtered migration options to resource of different type.
src/PVE/HA/Manager.pm | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 9fca61a..c28d7fa 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -295,7 +295,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 {
@@ -610,7 +610,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()->@* };
@@ -641,18 +641,40 @@ sub queue_resource_motion {
}
$haenv->log('info', "got crm command: $cmd");
- $ss->{$sid}->{cmd} = [$task, $target];
+ $ss->{$sid}->{cmd} = [$task, $target, $migrate_options];
+
+ # For dependent resources of a different type, filter unapplicable options.
+ my $_migrate_options_for_type = {};
+ my $migrate_options_for_type = sub {
+ my ($type) = @_;
+ return $_migrate_options_for_type->{$type}
+ if defined($_migrate_options_for_type->{$type});
+
+ my $plugin = PVE::HA::Resources->lookup($type);
+ my $applicable_properties = $plugin->migrate_json_properties();
+
+ $_migrate_options_for_type->{$type} = {};
+ for my $key (keys $migrate_options->%*) {
+ next if !exists($applicable_properties->{$key});
+ $_migrate_options_for_type->{$type}->{$key} = $migrate_options->{$key};
+ }
+
+ return $_migrate_options_for_type->{$type};
+ };
for my $csid (@$dependent_resources) {
next if $ss->{$csid}->{node} && $ss->{$csid}->{node} eq $target;
next if $ss->{$csid}->{target} && $ss->{$csid}->{target} eq $target;
+ my $type = ($haenv->parse_sid($csid))[1];
+ my $applicable_migrate_options = $migrate_options_for_type->($type);
+
$haenv->log(
'info',
"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, $applicable_migrate_options];
}
}
@@ -704,7 +726,7 @@ sub update_crm_commands {
my ($task, $sid, $node) = ($1, $2, $3);
if (my $sd = $ss->{$sid}) {
if (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
- $self->queue_resource_motion($cmd, $task, $sid, $node);
+ $self->queue_resource_motion($cmd, $task, $sid, $node, {});
}
} else {
$haenv->log('err', "crm command error - no such service: $cmd");
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH ha-manager v4 13/27] crm command: support JSON-style migrate command
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (11 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 12/27] queue resource motion: support migration options Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-16 9:53 ` Daniel Kral
2026-10-15 12:50 ` [PATCH ha-manager v4 14/27] api: resources: migration: support additional migration options Fiona Ebner
` (13 subsequent siblings)
26 siblings, 1 reply; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Changes in v4:
* Encode JSON with canonical => 1.
* Improve error messages.
src/PVE/HA/Manager.pm | 57 +++++++++++++++++++++++++++++++++++++------
1 file changed, 49 insertions(+), 8 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index c28d7fa..4e7fab5 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -711,6 +711,28 @@ my sub check_resource_motion_valid {
return 1;
}
+my sub handle_crm_motion_command {
+ my ($self, $task, $sid, $node, $options) = @_;
+
+ my $cmd = "$task $sid $node";
+
+ if (defined($options) && scalar(keys($options->%*))) {
+ $cmd .= ' ' . JSON::to_json($options, { canonical => 1 });
+ }
+
+ my ($haenv, $ms, $ns, $sc, $ss) = $self->@{qw(haenv ms ns sc ss)};
+
+ if (my $sd = $ss->{$sid}) {
+ if (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
+ $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) = @_;
@@ -722,16 +744,35 @@ 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 (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
- $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 JSON command: '$cmd' - $err");
+ next;
}
+ my $kind = $command_info->{kind};
+ if (!defined($kind)) {
+ $haenv->log('err', "unable to handle JSON command without 'kind': '$cmd'");
+ } elsif ($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;
+ }
+ handle_crm_motion_command($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+)$/) {
+ handle_crm_motion_command($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] 31+ messages in thread* Re: [PATCH ha-manager v4 13/27] crm command: support JSON-style migrate command
2026-10-15 12:50 ` [PATCH ha-manager v4 13/27] crm command: support JSON-style migrate command Fiona Ebner
@ 2026-10-16 9:53 ` Daniel Kral
0 siblings, 0 replies; 31+ messages in thread
From: Daniel Kral @ 2026-10-16 9:53 UTC (permalink / raw)
To: Fiona Ebner, pve-devel
On Thu Oct 15, 2026 at 2:50 PM CEST, Fiona Ebner wrote:
> 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>
> ---
>
> Changes in v4:
> * Encode JSON with canonical => 1.
> * Improve error messages.
>
> src/PVE/HA/Manager.pm | 57 +++++++++++++++++++++++++++++++++++++------
> 1 file changed, 49 insertions(+), 8 deletions(-)
>
> diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
> index c28d7fa..4e7fab5 100644
> --- a/src/PVE/HA/Manager.pm
> +++ b/src/PVE/HA/Manager.pm
> @@ -711,6 +711,28 @@ my sub check_resource_motion_valid {
> return 1;
> }
>
> +my sub handle_crm_motion_command {
> + my ($self, $task, $sid, $node, $options) = @_;
> +
> + my $cmd = "$task $sid $node";
> +
> + if (defined($options) && scalar(keys($options->%*))) {
> + $cmd .= ' ' . JSON::to_json($options, { canonical => 1 });
> + }
> +
> + my ($haenv, $ms, $ns, $sc, $ss) = $self->@{qw(haenv ms ns sc ss)};
nit: $ms, $ns, and $sc are unused here
> +
> + if (my $sd = $ss->{$sid}) {
> + if (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
> + $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) = @_;
> @@ -722,16 +744,35 @@ 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 (check_resource_motion_valid($self, $node, $sd, "ignore crm command '$cmd'")) {
> - $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 JSON command: '$cmd' - $err");
> + next;
> }
>
> + my $kind = $command_info->{kind};
> + if (!defined($kind)) {
> + $haenv->log('err', "unable to handle JSON command without 'kind': '$cmd'");
> + } elsif ($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;
> + }
> + handle_crm_motion_command($self, $kind, $sid, $node, $options);
nit: only noticed now, but 'kind' should probably also be 'task' here as
we call it like that everywhere else... Even though with variants
'kind' is more common, so no hard feelings here.
> + } else {
> + $haenv->log('err', "unable to handle unknown JSON command: '$cmd'");
> + }
> +
> + next;
> + }
> +
> + if ($cmd =~ m/^(migrate|relocate)\s+(\S+)\s+(\S+)$/) {
nit: should probably be an elsif here?
> + handle_crm_motion_command($self, $1, $2, $3, {});
> } elsif ($cmd =~ m/^stop\s+(\S+)\s+(\S+)$/) {
> my ($sid, $timeout) = ($1, $2);
> if (my $sd = $ss->{$sid}) {
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH ha-manager v4 14/27] api: resources: migration: support additional migration options
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (12 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 13/27] crm command: support JSON-style migrate command Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-16 9:54 ` Daniel Kral
2026-10-15 12:50 ` [PATCH ha-manager v4 15/27] test: add test for " Fiona Ebner
` (12 subsequent siblings)
26 siblings, 1 reply; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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() method.
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>
---
Dependency bump for pve-cluster needed!
Changes in v4:
* Add cfs_update() call.
* Import raise_param_exc().
* Ensure error messages are also visible on the CLI.
* Only use JSON-style command if required to avoid fallout in
scenarios with old manager that fails to report its version properly
* Document why continuing when the version-info for a node cannot be
parsed is better than aborting.
src/PVE/API2/HA/Resources.pm | 110 ++++++++++++++++++++++++++++++-----
src/PVE/HA/Resources.pm | 1 +
2 files changed, 97 insertions(+), 14 deletions(-)
diff --git a/src/PVE/API2/HA/Resources.pm b/src/PVE/API2/HA/Resources.pm
index b96572b..6a97b2f 100644
--- a/src/PVE/API2/HA/Resources.pm
+++ b/src/PVE/API2/HA/Resources.pm
@@ -3,15 +3,20 @@ package PVE::API2::HA::Resources;
use strict;
use warnings;
+use JSON qw();
+
+use PVE::Exception qw(raise_param_exc);
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::Cluster;
use PVE::HA::Config;
+use PVE::HA::Env::PVE2;
use PVE::HA::Resources;
use HTTP::Status qw(:constants);
use Storable qw(dclone);
use PVE::JSONSchema qw(get_standard_option);
use PVE::RPCEnvironment;
+use PVE::RESTEnvironment qw(log_warn);
use PVE::RESTHandler;
@@ -34,6 +39,49 @@ my $api_copy_config = sub {
return $scfg;
};
+my sub migrate_json_properties {
+ my ($type) = @_;
+ my $plugin = PVE::HA::Resources->lookup($type);
+ return $plugin->migrate_json_properties();
+}
+
+my sub json_crm_command_is_supported {
+ PVE::Cluster::cfs_update();
+ 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 = $@) {
+ # An updated cluster is the usual situation. An error here should not affect the usual
+ # situation. The unusual situation, where the master is too old and unable to report its
+ # version properly is rare. Submitting a JSON CRM to such a master will lead to warnings
+ # and the command being ignored. Note that migration without extra parameters also still
+ # works.
+ log_warn("failed check for migration options support - cannot parse version info for"
+ . " node '$node' - $err");
+ } elsif (!PVE::Cluster::pvecfg_min_version($node_info->{version}, 9, 2, 22)) {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+my sub json_crm_command_is_applicable {
+ my ($sid, $req_node, $param) = @_;
+
+ # FIXME: MAJOR VERSION: use JSON variant even if there are no extra params.
+ if (scalar(keys $param->%*) > 0) {
+ if (json_crm_command_is_supported()) {
+ return 1;
+ } else {
+ log_warn("$sid migration to $req_node: ignoring additional migration options - not"
+ . " supported by CRM on all nodes");
+ }
+ }
+
+ return 0;
+}
+
sub check_service_state {
my ($sid, $req_state) = @_;
@@ -336,20 +384,32 @@ __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 => $resource_type_enum,
},
+ oneOf => [
+ {
+ 'instance-type' => 'ct',
+ additionalProperties => 0,
+ properties => migrate_json_properties('ct'),
+ },
+ {
+ 'instance-type' => 'vm',
+ additionalProperties => 0,
+ properties => migrate_json_properties('vm'),
+ },
+ ],
+ },
+ 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 +463,34 @@ __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' => "value '$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_applicable($sid, $req_node, $param)) {
+ $crm_command = JSON::to_json(
+ {
+ kind => 'migrate',
+ sid => $sid,
+ node => $req_node,
+ options => $param,
+ },
+ { canonical => 1 },
+ );
+ } else {
+ $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.pm b/src/PVE/HA/Resources.pm
index 57fde00..ad603dc 100644
--- a/src/PVE/HA/Resources.pm
+++ b/src/PVE/HA/Resources.pm
@@ -3,6 +3,7 @@ package PVE::HA::Resources;
use strict;
use warnings;
+use PVE::Cluster;
use PVE::JSONSchema qw(get_standard_option);
use PVE::SectionConfig;
use PVE::HA::Tools;
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH ha-manager v4 14/27] api: resources: migration: support additional migration options
2026-10-15 12:50 ` [PATCH ha-manager v4 14/27] api: resources: migration: support additional migration options Fiona Ebner
@ 2026-10-16 9:54 ` Daniel Kral
0 siblings, 0 replies; 31+ messages in thread
From: Daniel Kral @ 2026-10-16 9:54 UTC (permalink / raw)
To: Fiona Ebner, pve-devel
On Thu Oct 15, 2026 at 2:50 PM CEST, Fiona Ebner wrote:
> 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() method.
>
> 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>
> ---
>
> Dependency bump for pve-cluster needed!
>
> Changes in v4:
> * Add cfs_update() call.
> * Import raise_param_exc().
> * Ensure error messages are also visible on the CLI.
> * Only use JSON-style command if required to avoid fallout in
> scenarios with old manager that fails to report its version properly
> * Document why continuing when the version-info for a node cannot be
> parsed is better than aborting.
>
> src/PVE/API2/HA/Resources.pm | 110 ++++++++++++++++++++++++++++++-----
> src/PVE/HA/Resources.pm | 1 +
> 2 files changed, 97 insertions(+), 14 deletions(-)
>
> diff --git a/src/PVE/API2/HA/Resources.pm b/src/PVE/API2/HA/Resources.pm
> index b96572b..6a97b2f 100644
> --- a/src/PVE/API2/HA/Resources.pm
> +++ b/src/PVE/API2/HA/Resources.pm
> @@ -3,15 +3,20 @@ package PVE::API2::HA::Resources;
> use strict;
> use warnings;
>
> +use JSON qw();
> +
> +use PVE::Exception qw(raise_param_exc);
> use PVE::SafeSyslog;
> use PVE::Tools qw(extract_param);
> use PVE::Cluster;
> use PVE::HA::Config;
> +use PVE::HA::Env::PVE2;
Why depend on PVE2 here?
> use PVE::HA::Resources;
> use HTTP::Status qw(:constants);
> use Storable qw(dclone);
> use PVE::JSONSchema qw(get_standard_option);
> use PVE::RPCEnvironment;
> +use PVE::RESTEnvironment qw(log_warn);
>
> use PVE::RESTHandler;
>
> @@ -34,6 +39,49 @@ my $api_copy_config = sub {
> return $scfg;
> };
>
> +my sub migrate_json_properties {
> + my ($type) = @_;
> + my $plugin = PVE::HA::Resources->lookup($type);
> + return $plugin->migrate_json_properties();
> +}
> +
> +my sub json_crm_command_is_supported {
nit: should probably include _motion to specify that it only checks
whether the migrate JSON command is supported
> + PVE::Cluster::cfs_update();
> + 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 = $@) {
> + # An updated cluster is the usual situation. An error here should not affect the usual
> + # situation. The unusual situation, where the master is too old and unable to report its
> + # version properly is rare. Submitting a JSON CRM to such a master will lead to warnings
> + # and the command being ignored. Note that migration without extra parameters also still
> + # works.
> + log_warn("failed check for migration options support - cannot parse version info for"
> + . " node '$node' - $err");
> + } elsif (!PVE::Cluster::pvecfg_min_version($node_info->{version}, 9, 2, 22)) {
> + return 0;
> + }
> + }
> +
> + return 1;
> +}
> +
> +my sub json_crm_command_is_applicable {
nit: should probably include _motion to specify that it only checks
whether the migrate JSON command is applicable
> + my ($sid, $req_node, $param) = @_;
> +
> + # FIXME: MAJOR VERSION: use JSON variant even if there are no extra params.
> + if (scalar(keys $param->%*) > 0) {
> + if (json_crm_command_is_supported()) {
> + return 1;
> + } else {
> + log_warn("$sid migration to $req_node: ignoring additional migration options - not"
> + . " supported by CRM on all nodes");
> + }
> + }
> +
> + return 0;
> +}
> +
> sub check_service_state {
> my ($sid, $req_state) = @_;
>
> @@ -336,20 +384,32 @@ __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 => $resource_type_enum,
> },
> + oneOf => [
> + {
> + 'instance-type' => 'ct',
> + additionalProperties => 0,
> + properties => migrate_json_properties('ct'),
> + },
> + {
> + 'instance-type' => 'vm',
> + additionalProperties => 0,
> + properties => migrate_json_properties('vm'),
> + },
> + ],
> + },
> + 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 +463,34 @@ __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' => "value '$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_applicable($sid, $req_node, $param)) {
> + $crm_command = JSON::to_json(
> + {
> + kind => 'migrate',
> + sid => $sid,
> + node => $req_node,
> + options => $param,
> + },
> + { canonical => 1 },
> + );
> + } else {
> + $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.pm b/src/PVE/HA/Resources.pm
> index 57fde00..ad603dc 100644
> --- a/src/PVE/HA/Resources.pm
> +++ b/src/PVE/HA/Resources.pm
> @@ -3,6 +3,7 @@ package PVE::HA::Resources;
> use strict;
> use warnings;
>
> +use PVE::Cluster;
Oh here it is :-)
> use PVE::JSONSchema qw(get_standard_option);
> use PVE::SectionConfig;
> use PVE::HA::Tools;
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH ha-manager v4 15/27] test: add test for migration options
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (13 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 14/27] api: resources: migration: support additional migration options Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 16/27] helpers: move version_cmp() helper to pve-common Fiona Ebner
` (11 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Tests that the new JSON-style migrate command works, and that
migration options are passed down the HA stack to the LRM and
resource plugin. Tests that migration options are also used for
dependent resources, but filtered by resource type.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v4.
src/PVE/HA/Sim/Hardware.pm | 25 ++-
src/PVE/HA/Sim/Resources.pm | 13 +-
src/PVE/HA/Sim/Resources/VirtCT.pm | 11 ++
src/PVE/HA/Sim/Resources/VirtVM.pm | 11 ++
src/test/test-migrate-options/README | 19 ++
src/test/test-migrate-options/cmdlist | 7 +
src/test/test-migrate-options/hardware_status | 5 +
src/test/test-migrate-options/log.expect | 163 ++++++++++++++++++
src/test/test-migrate-options/manager_status | 1 +
src/test/test-migrate-options/rules_config | 3 +
src/test/test-migrate-options/service_config | 8 +
11 files changed, 262 insertions(+), 4 deletions(-)
create mode 100644 src/test/test-migrate-options/README
create mode 100644 src/test/test-migrate-options/cmdlist
create mode 100644 src/test/test-migrate-options/hardware_status
create mode 100644 src/test/test-migrate-options/log.expect
create mode 100644 src/test/test-migrate-options/manager_status
create mode 100644 src/test/test-migrate-options/rules_config
create mode 100644 src/test/test-migrate-options/service_config
diff --git a/src/PVE/HA/Sim/Hardware.pm b/src/PVE/HA/Sim/Hardware.pm
index e1a9ddd..743c20e 100644
--- a/src/PVE/HA/Sim/Hardware.pm
+++ b/src/PVE/HA/Sim/Hardware.pm
@@ -795,7 +795,7 @@ sub get_cfs_state {
# shutdown <node>
# restart-lrm <node>
# service <sid> <started|disabled|stopped|ignored>
-# service <sid> <migrate|relocate> <target>
+# service <sid> <migrate|relocate> <target> [<migrate-options>]
# service <sid> stop <timeout>
# service <sid> lock/unlock [lockname]
# service <sid> add <node> [<request-state=started>] [<running=0>]
@@ -939,11 +939,30 @@ sub sim_hardware_cmd {
$self->set_service_state($sid, $action);
} elsif ($action eq 'migrate' || $action eq 'relocate') {
+ my $target = $params[0];
+ my $migrate_options = $params[1];
die "sim_hardware_cmd: missing target node for '$action' command"
- if !$param;
+ if !$target;
- $self->queue_crm_commands_nolock("$action $sid $param");
+ my $crm_command_str;
+ if ($action eq 'migrate' && $migrate_options) {
+ my $crm_command = {
+ kind => $action,
+ sid => $sid,
+ node => $target,
+ options => {},
+ };
+ for my $migrate_option (split(',', $migrate_options)) {
+ my ($key, $val) = split('=', $migrate_option);
+ $crm_command->{options}->{$key} = $val;
+ }
+ $crm_command_str = JSON::to_json($crm_command, { canonical => 1 });
+ } else {
+ $crm_command_str = "$action $sid $target";
+ }
+
+ $self->queue_crm_commands_nolock($crm_command_str);
} elsif ($action eq 'stop') {
diff --git a/src/PVE/HA/Sim/Resources.pm b/src/PVE/HA/Sim/Resources.pm
index 730bfaa..16963f0 100644
--- a/src/PVE/HA/Sim/Resources.pm
+++ b/src/PVE/HA/Sim/Resources.pm
@@ -103,6 +103,11 @@ sub migrate {
return;
}
+ for my $key (sort keys $migrate_options->%*) {
+ my $val = $migrate_options->{$key};
+ $haenv->log('info', "service $sid migration - got option $key=$val");
+ }
+
# explicitly shutdown if $online isn't true (relocate)
if (!$online && $class->check_running($haenv, $id)) {
$haenv->log("info", "stopping service $sid (relocate)");
@@ -125,7 +130,13 @@ sub migrate {
sub migrate_json_properties {
my ($class) = @_;
- return $PVE::HA::Resources::common_migrate_json_properties;
+ my $props = { $PVE::HA::Resources::common_migrate_json_properties->%* };
+ $props->{'common'} = {
+ type => 'string',
+ optional => 1,
+ description => 'A migration property common to resource types.',
+ };
+ return $props;
}
sub remove_locks {
diff --git a/src/PVE/HA/Sim/Resources/VirtCT.pm b/src/PVE/HA/Sim/Resources/VirtCT.pm
index ecf3af4..1e66f83 100644
--- a/src/PVE/HA/Sim/Resources/VirtCT.pm
+++ b/src/PVE/HA/Sim/Resources/VirtCT.pm
@@ -32,4 +32,15 @@ sub migrate {
return $class->SUPER::migrate($haenv, $id, $target, $online, $migrate_options);
}
+sub migrate_json_properties {
+ my ($class) = @_;
+ my $props = { $class->SUPER::migrate_json_properties()->%* };
+ $props->{'ct-foo'} = {
+ type => 'string',
+ optional => 1,
+ description => 'A migration property specific to CTs.',
+ };
+ return $props;
+}
+
1;
diff --git a/src/PVE/HA/Sim/Resources/VirtVM.pm b/src/PVE/HA/Sim/Resources/VirtVM.pm
index 4d2852e..c3d61ab 100644
--- a/src/PVE/HA/Sim/Resources/VirtVM.pm
+++ b/src/PVE/HA/Sim/Resources/VirtVM.pm
@@ -16,4 +16,15 @@ sub exists {
return 1;
}
+sub migrate_json_properties {
+ my ($class) = @_;
+ my $props = { $class->SUPER::migrate_json_properties()->%* };
+ $props->{'vm-bar'} = {
+ type => 'string',
+ optional => 1,
+ description => 'A migration property specific to VMs.',
+ };
+ return $props;
+}
+
1;
diff --git a/src/test/test-migrate-options/README b/src/test/test-migrate-options/README
new file mode 100644
index 0000000..9ea0b0a
--- /dev/null
+++ b/src/test/test-migrate-options/README
@@ -0,0 +1,19 @@
+Test that migrate options are correctly passed down, also when migrating
+dependent resources.
+
+The test scenario is:
+- vm:101, ct:102, vm:103, ct:104, vm:105 and ct:106 are all currently running on
+ node1.
+- vm:101 and ct:102 are migrated to node2 with the a guest-type-specific
+ migration option and a common migration option.
+- vm:103, ct:104, vm:105 and ct:106 are kept together via a positive affinity
+ rule.
+- vm:103 is migrated to node2 with a vm-specific and a common migration option.
+- ct:104 is migrated to node3 with a ct-specific and a common migration option.
+
+The expected outcome is:
+- vm:101 and ct:102 end up on node2.
+- vm:103, ct:104, vm:105 and ct:106 all end up on node3.
+- The guest-type-specific migration option is passed down for the matching guest
+ type and the common migration options is always passed down, also when
+ migrating dependent resources.
diff --git a/src/test/test-migrate-options/cmdlist b/src/test/test-migrate-options/cmdlist
new file mode 100644
index 0000000..eb36afb
--- /dev/null
+++ b/src/test/test-migrate-options/cmdlist
@@ -0,0 +1,7 @@
+[
+ [ "power node1 on", "power node2 on", "power node3 on" ],
+ [ "service vm:101 migrate node2 vm-bar=one,common=first"],
+ [ "service ct:102 migrate node2 ct-foo=two,common=second"],
+ [ "service vm:103 migrate node2 vm-bar=three,common=third"],
+ [ "service ct:104 migrate node3 ct-foo=four,common=fourth"]
+]
diff --git a/src/test/test-migrate-options/hardware_status b/src/test/test-migrate-options/hardware_status
new file mode 100644
index 0000000..451beb1
--- /dev/null
+++ b/src/test/test-migrate-options/hardware_status
@@ -0,0 +1,5 @@
+{
+ "node1": { "power": "off", "network": "off" },
+ "node2": { "power": "off", "network": "off" },
+ "node3": { "power": "off", "network": "off" }
+}
diff --git a/src/test/test-migrate-options/log.expect b/src/test/test-migrate-options/log.expect
new file mode 100644
index 0000000..a0ca883
--- /dev/null
+++ b/src/test/test-migrate-options/log.expect
@@ -0,0 +1,163 @@
+info 0 hardware: starting simulation
+info 20 cmdlist: execute power node1 on
+info 20 node1/crm: status change startup => wait_for_quorum
+info 20 node1/lrm: status change startup => wait_for_agent_lock
+info 20 cmdlist: execute power node2 on
+info 20 node2/crm: status change startup => wait_for_quorum
+info 20 node2/lrm: status change startup => wait_for_agent_lock
+info 20 cmdlist: execute power node3 on
+info 20 node3/crm: status change startup => wait_for_quorum
+info 20 node3/lrm: status change startup => wait_for_agent_lock
+info 20 node1/crm: got lock 'ha_manager_lock'
+info 20 node1/crm: status change wait_for_quorum => master
+info 20 node1/crm: node 'node1': state changed from 'unknown' => 'online'
+info 20 node1/crm: node 'node2': state changed from 'unknown' => 'online'
+info 20 node1/crm: node 'node3': state changed from 'unknown' => 'online'
+info 20 node1/crm: adding new service 'ct:102' on node 'node1'
+info 20 node1/crm: adding new service 'ct:104' on node 'node1'
+info 20 node1/crm: adding new service 'ct:106' on node 'node1'
+info 20 node1/crm: adding new service 'vm:101' on node 'node1'
+info 20 node1/crm: adding new service 'vm:103' on node 'node1'
+info 20 node1/crm: adding new service 'vm:105' on node 'node1'
+info 20 node1/crm: service 'ct:102': state changed from 'request_start' to 'started' (node = node1)
+info 20 node1/crm: service 'ct:104': state changed from 'request_start' to 'started' (node = node1)
+info 20 node1/crm: service 'ct:106': state changed from 'request_start' to 'started' (node = node1)
+info 20 node1/crm: service 'vm:101': state changed from 'request_start' to 'started' (node = node1)
+info 20 node1/crm: service 'vm:103': state changed from 'request_start' to 'started' (node = node1)
+info 20 node1/crm: service 'vm:105': state changed from 'request_start' to 'started' (node = node1)
+info 21 node1/lrm: got lock 'ha_agent_node1_lock'
+info 21 node1/lrm: status change wait_for_agent_lock => active
+info 21 node1/lrm: starting service ct:102
+info 21 node1/lrm: service status ct:102 started
+info 21 node1/lrm: starting service ct:104
+info 21 node1/lrm: service status ct:104 started
+info 21 node1/lrm: starting service ct:106
+info 21 node1/lrm: service status ct:106 started
+info 21 node1/lrm: starting service vm:101
+info 21 node1/lrm: service status vm:101 started
+info 21 node1/lrm: starting service vm:103
+info 21 node1/lrm: service status vm:103 started
+info 21 node1/lrm: starting service vm:105
+info 21 node1/lrm: service status vm:105 started
+info 22 node2/crm: status change wait_for_quorum => slave
+info 24 node3/crm: status change wait_for_quorum => slave
+info 120 cmdlist: execute service vm:101 migrate node2 vm-bar=one,common=first
+info 120 node1/crm: got crm command: migrate vm:101 node2 {"common":"first","vm-bar":"one"}
+info 120 node1/crm: migrate service 'vm:101' to node 'node2'
+info 120 node1/crm: service 'vm:101': state changed from 'started' to 'migrate' (migrate-options = {"common":"first","vm-bar":"one"}, node = node1, target = node2)
+info 121 node1/lrm: service vm:101 - start migrate to node 'node2'
+info 121 node1/lrm: service vm:101 migration - got option common=first
+info 121 node1/lrm: service vm:101 migration - got option vm-bar=one
+info 121 node1/lrm: service vm:101 - end migrate to node 'node2'
+info 123 node2/lrm: got lock 'ha_agent_node2_lock'
+info 123 node2/lrm: status change wait_for_agent_lock => active
+info 140 node1/crm: service 'vm:101': state changed from 'migrate' to 'started' (node = node2)
+info 143 node2/lrm: starting service vm:101
+info 143 node2/lrm: service status vm:101 started
+info 220 cmdlist: execute service ct:102 migrate node2 ct-foo=two,common=second
+info 220 node1/crm: got crm command: migrate ct:102 node2 {"common":"second","ct-foo":"two"}
+info 220 node1/crm: migrate service 'ct:102' to node 'node2'
+info 220 node1/crm: service 'ct:102': state changed from 'started' to 'migrate' (migrate-options = {"common":"second","ct-foo":"two"}, node = node1, target = node2)
+warn 221 node1/lrm: unable to live migrate running container, fallback to relocate
+info 221 node1/lrm: service ct:102 - start relocate to node 'node2'
+info 221 node1/lrm: service ct:102 migration - got option common=second
+info 221 node1/lrm: service ct:102 migration - got option ct-foo=two
+info 221 node1/lrm: stopping service ct:102 (relocate)
+info 221 node1/lrm: service status ct:102 stopped
+info 221 node1/lrm: service ct:102 - end relocate to node 'node2'
+info 240 node1/crm: service 'ct:102': state changed from 'migrate' to 'started' (node = node2)
+info 243 node2/lrm: starting service ct:102
+info 243 node2/lrm: service status ct:102 started
+info 320 cmdlist: execute service vm:103 migrate node2 vm-bar=three,common=third
+info 320 node1/crm: got crm command: migrate vm:103 node2 {"common":"third","vm-bar":"three"}
+info 320 node1/crm: crm command 'migrate vm:103 node2 {"common":"third","vm-bar":"three"}' - migrate service 'ct:104' to node 'node2' (service 'ct:104' in positive affinity with service 'vm:103')
+info 320 node1/crm: crm command 'migrate vm:103 node2 {"common":"third","vm-bar":"three"}' - migrate service 'ct:106' to node 'node2' (service 'ct:106' in positive affinity with service 'vm:103')
+info 320 node1/crm: crm command 'migrate vm:103 node2 {"common":"third","vm-bar":"three"}' - migrate service 'vm:105' to node 'node2' (service 'vm:105' in positive affinity with service 'vm:103')
+info 320 node1/crm: migrate service 'ct:104' to node 'node2'
+info 320 node1/crm: service 'ct:104': state changed from 'started' to 'migrate' (migrate-options = {"common":"third"}, node = node1, target = node2)
+info 320 node1/crm: migrate service 'ct:106' to node 'node2'
+info 320 node1/crm: service 'ct:106': state changed from 'started' to 'migrate' (migrate-options = {"common":"third"}, node = node1, target = node2)
+info 320 node1/crm: migrate service 'vm:103' to node 'node2'
+info 320 node1/crm: service 'vm:103': state changed from 'started' to 'migrate' (migrate-options = {"common":"third","vm-bar":"three"}, node = node1, target = node2)
+info 320 node1/crm: migrate service 'vm:105' to node 'node2'
+info 320 node1/crm: service 'vm:105': state changed from 'started' to 'migrate' (migrate-options = {"common":"third","vm-bar":"three"}, node = node1, target = node2)
+warn 321 node1/lrm: unable to live migrate running container, fallback to relocate
+info 321 node1/lrm: service ct:104 - start relocate to node 'node2'
+info 321 node1/lrm: service ct:104 migration - got option common=third
+info 321 node1/lrm: stopping service ct:104 (relocate)
+info 321 node1/lrm: service status ct:104 stopped
+info 321 node1/lrm: service ct:104 - end relocate to node 'node2'
+warn 321 node1/lrm: unable to live migrate running container, fallback to relocate
+info 321 node1/lrm: service ct:106 - start relocate to node 'node2'
+info 321 node1/lrm: service ct:106 migration - got option common=third
+info 321 node1/lrm: stopping service ct:106 (relocate)
+info 321 node1/lrm: service status ct:106 stopped
+info 321 node1/lrm: service ct:106 - end relocate to node 'node2'
+info 321 node1/lrm: service vm:103 - start migrate to node 'node2'
+info 321 node1/lrm: service vm:103 migration - got option common=third
+info 321 node1/lrm: service vm:103 migration - got option vm-bar=three
+info 321 node1/lrm: service vm:103 - end migrate to node 'node2'
+info 321 node1/lrm: service vm:105 - start migrate to node 'node2'
+info 321 node1/lrm: service vm:105 migration - got option common=third
+info 321 node1/lrm: service vm:105 migration - got option vm-bar=three
+info 321 node1/lrm: service vm:105 - end migrate to node 'node2'
+info 340 node1/crm: service 'ct:104': state changed from 'migrate' to 'started' (node = node2)
+info 340 node1/crm: service 'ct:106': state changed from 'migrate' to 'started' (node = node2)
+info 340 node1/crm: service 'vm:103': state changed from 'migrate' to 'started' (node = node2)
+info 340 node1/crm: service 'vm:105': state changed from 'migrate' to 'started' (node = node2)
+info 343 node2/lrm: starting service ct:104
+info 343 node2/lrm: service status ct:104 started
+info 343 node2/lrm: starting service ct:106
+info 343 node2/lrm: service status ct:106 started
+info 343 node2/lrm: starting service vm:103
+info 343 node2/lrm: service status vm:103 started
+info 343 node2/lrm: starting service vm:105
+info 343 node2/lrm: service status vm:105 started
+info 420 cmdlist: execute service ct:104 migrate node3 ct-foo=four,common=fourth
+info 420 node1/crm: got crm command: migrate ct:104 node3 {"common":"fourth","ct-foo":"four"}
+info 420 node1/crm: crm command 'migrate ct:104 node3 {"common":"fourth","ct-foo":"four"}' - migrate service 'ct:106' to node 'node3' (service 'ct:106' in positive affinity with service 'ct:104')
+info 420 node1/crm: crm command 'migrate ct:104 node3 {"common":"fourth","ct-foo":"four"}' - migrate service 'vm:103' to node 'node3' (service 'vm:103' in positive affinity with service 'ct:104')
+info 420 node1/crm: crm command 'migrate ct:104 node3 {"common":"fourth","ct-foo":"four"}' - migrate service 'vm:105' to node 'node3' (service 'vm:105' in positive affinity with service 'ct:104')
+info 420 node1/crm: migrate service 'ct:104' to node 'node3'
+info 420 node1/crm: service 'ct:104': state changed from 'started' to 'migrate' (migrate-options = {"common":"fourth","ct-foo":"four"}, node = node2, target = node3)
+info 420 node1/crm: migrate service 'ct:106' to node 'node3'
+info 420 node1/crm: service 'ct:106': state changed from 'started' to 'migrate' (migrate-options = {"common":"fourth","ct-foo":"four"}, node = node2, target = node3)
+info 420 node1/crm: migrate service 'vm:103' to node 'node3'
+info 420 node1/crm: service 'vm:103': state changed from 'started' to 'migrate' (migrate-options = {"common":"fourth"}, node = node2, target = node3)
+info 420 node1/crm: migrate service 'vm:105' to node 'node3'
+info 420 node1/crm: service 'vm:105': state changed from 'started' to 'migrate' (migrate-options = {"common":"fourth"}, node = node2, target = node3)
+warn 423 node2/lrm: unable to live migrate running container, fallback to relocate
+info 423 node2/lrm: service ct:104 - start relocate to node 'node3'
+info 423 node2/lrm: service ct:104 migration - got option common=fourth
+info 423 node2/lrm: service ct:104 migration - got option ct-foo=four
+info 423 node2/lrm: stopping service ct:104 (relocate)
+info 423 node2/lrm: service status ct:104 stopped
+info 423 node2/lrm: service ct:104 - end relocate to node 'node3'
+warn 423 node2/lrm: unable to live migrate running container, fallback to relocate
+info 423 node2/lrm: service ct:106 - start relocate to node 'node3'
+info 423 node2/lrm: service ct:106 migration - got option common=fourth
+info 423 node2/lrm: service ct:106 migration - got option ct-foo=four
+info 423 node2/lrm: stopping service ct:106 (relocate)
+info 423 node2/lrm: service status ct:106 stopped
+info 423 node2/lrm: service ct:106 - end relocate to node 'node3'
+info 423 node2/lrm: service vm:103 - start migrate to node 'node3'
+info 423 node2/lrm: service vm:103 migration - got option common=fourth
+info 423 node2/lrm: service vm:103 - end migrate to node 'node3'
+info 423 node2/lrm: service vm:105 - start migrate to node 'node3'
+info 423 node2/lrm: service vm:105 migration - got option common=fourth
+info 423 node2/lrm: service vm:105 - end migrate to node 'node3'
+info 425 node3/lrm: got lock 'ha_agent_node3_lock'
+info 425 node3/lrm: status change wait_for_agent_lock => active
+info 440 node1/crm: service 'ct:104': state changed from 'migrate' to 'started' (node = node3)
+info 440 node1/crm: service 'ct:106': state changed from 'migrate' to 'started' (node = node3)
+info 440 node1/crm: service 'vm:103': state changed from 'migrate' to 'started' (node = node3)
+info 440 node1/crm: service 'vm:105': state changed from 'migrate' to 'started' (node = node3)
+info 445 node3/lrm: starting service ct:104
+info 445 node3/lrm: service status ct:104 started
+info 445 node3/lrm: starting service ct:106
+info 445 node3/lrm: service status ct:106 started
+info 445 node3/lrm: starting service vm:103
+info 445 node3/lrm: service status vm:103 started
+info 445 node3/lrm: starting service vm:105
+info 445 node3/lrm: service status vm:105 started
+info 1020 hardware: exit simulation - done
diff --git a/src/test/test-migrate-options/manager_status b/src/test/test-migrate-options/manager_status
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/src/test/test-migrate-options/manager_status
@@ -0,0 +1 @@
+{}
diff --git a/src/test/test-migrate-options/rules_config b/src/test/test-migrate-options/rules_config
new file mode 100644
index 0000000..d21bb86
--- /dev/null
+++ b/src/test/test-migrate-options/rules_config
@@ -0,0 +1,3 @@
+resource-affinity: must-stick-together
+ resources vm:103,ct:104,vm:105,ct:106
+ affinity positive
diff --git a/src/test/test-migrate-options/service_config b/src/test/test-migrate-options/service_config
new file mode 100644
index 0000000..44777b5
--- /dev/null
+++ b/src/test/test-migrate-options/service_config
@@ -0,0 +1,8 @@
+{
+ "vm:101": { "node": "node1", "state": "started" },
+ "ct:102": { "node": "node1", "state": "started" },
+ "vm:103": { "node": "node1", "state": "started" },
+ "ct:104": { "node": "node1", "state": "started" },
+ "vm:105": { "node": "node1", "state": "started" },
+ "ct:106": { "node": "node1", "state": "started" }
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH qemu-server v4 16/27] helpers: move version_cmp() helper to pve-common
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (14 preceding siblings ...)
2026-10-15 12:50 ` [PATCH ha-manager v4 15/27] test: add test for " Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 17/27] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
` (10 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Build dependency and dependency bump for pve-common needed!
Changes in v4:
* Also adapt new version_cmp() call in virtio_win_issues().
src/PVE/QemuServer/Drive.pm | 3 +--
src/PVE/QemuServer/Helpers.pm | 30 ++----------------------------
src/PVE/QemuServer/Machine.pm | 13 +++++--------
src/PVE/QemuServer/QMPHelpers.pm | 5 +++--
4 files changed, 11 insertions(+), 40 deletions(-)
diff --git a/src/PVE/QemuServer/Drive.pm b/src/PVE/QemuServer/Drive.pm
index 0b573717..9c66c76f 100644
--- a/src/PVE/QemuServer/Drive.pm
+++ b/src/PVE/QemuServer/Drive.pm
@@ -784,8 +784,7 @@ sub virtio_win_iso_issue {
my $cmp_with = sub {
my ($other) = @_;
- return PVE::QemuServer::Helpers::version_cmp(map { ($version[$_], $other->[$_]) }
- 0 .. 2);
+ return PVE::Tools::version_cmp(map { ($version[$_], $other->[$_]) } 0 .. 2);
};
for my $issue ($virtio_win_issues->@*) {
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] 31+ messages in thread* [PATCH qemu-server v4 17/27] helpers: move pvecfg node version helpers to pve-cluster
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (15 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 16/27] helpers: move version_cmp() helper to pve-common Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 18/27] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (9 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Build dependency and dependency bump for pve-cluster needed!
No changes in v4.
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 6605f5d8..168f6b01 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -1395,14 +1395,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] 31+ messages in thread* [PATCH qemu-server v4 18/27] api: migrate: allow forwarding certain migration properties to HA
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (16 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 17/27] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 19/27] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
` (8 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Changes in v4:
* Use parentheses when logging migration options.
src/PVE/API2/Qemu.pm | 148 +++++++++++++++++++++++++------------------
1 file changed, 85 insertions(+), 63 deletions(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index cfbc1f51..a0abc807 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.";
@@ -5520,67 +5591,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',
@@ -5672,9 +5683,20 @@ __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 .= ', ' if $extra_opts;
+ $extra_opts .= "$prop=$value";
+ }
+ }
+
+ my $msg = "Requesting HA migration for VM $vmid to node $target";
+ $msg .= " (${extra_opts})" if $extra_opts;
+ print "$msg\n";
+
run_command($cmd);
return;
};
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH qemu-server v4 19/27] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (17 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 18/27] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 20/27] qm: mtunnel: reply when a command is unknown Fiona Ebner
` (7 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
No changes in v4.
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 a0abc807..889f6aaf 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] 31+ messages in thread* [PATCH qemu-server v4 20/27] qm: mtunnel: reply when a command is unknown
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (18 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 19/27] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 21/27] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
` (6 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
No changes in v4.
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] 31+ messages in thread* [PATCH qemu-server v4 21/27] qm: mtunnel: add 'conf' command to do strict configuration parsing
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (19 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 20/27] qm: mtunnel: reply when a command is unknown Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH qemu-server v4 22/27] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
` (5 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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 v4.
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] 31+ messages in thread* [PATCH qemu-server v4 22/27] migration: intra-cluster: check config can be parsed on target node
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (20 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 21/27] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH container v4 23/27] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (4 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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>
---
Changes in v4:
* Chomp original error to avoid weird formatting.
* Adapt version.
src/PVE/API2/Qemu.pm | 10 +++++++++
src/PVE/QemuMigrate.pm | 27 +++++++++++++++++++++++
src/test/MigrationTest/QemuMigrateMock.pm | 11 ++++++++-
3 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 889f6aaf..244b1ea2 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 168f6b01..f14cb072 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -359,6 +359,33 @@ 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, 22)) {
+ # 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 $@;
+
+ if ($err) {
+ chomp($err);
+ die "$err - use 'skip-config-check' flag to migrate regardless\n";
+ }
+ } else {
+ $self->log('info', "skipping config check - target pve-manager version < 9.2.22");
+ }
+ } 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 c715a60e..a2e97315 100644
--- a/src/test/MigrationTest/QemuMigrateMock.pm
+++ b/src/test/MigrationTest/QemuMigrateMock.pm
@@ -66,6 +66,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";
},
@@ -74,7 +78,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] 31+ messages in thread* [PATCH container v4 23/27] api: migrate: allow forwarding certain migration properties to HA
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (21 preceding siblings ...)
2026-10-15 12:50 ` [PATCH qemu-server v4 22/27] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH container v4 24/27] pct: introduce mtunnel command Fiona Ebner
` (3 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 UTC (permalink / raw)
To: pve-devel
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>
---
Changes in v4:
* Use parentheses when logging migration options.
src/PVE/API2/LXC.pm | 96 ++++++++++++++++++++++++++++-----------------
1 file changed, 59 insertions(+), 37 deletions(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 5f94d5a..1b2c0e2 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,20 @@ __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 .= ', ' if $extra_opts;
+ $extra_opts .= "$prop=$value";
+ }
+ }
- print "Requesting HA migration for CT $vmid to node $target\n";
+ my $msg = "Requesting HA migration for CT $vmid to node $target";
+ $msg .= " (${extra_opts})" if $extra_opts;
+ print "$msg\n";
PVE::Tools::run_command($cmd);
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH container v4 24/27] pct: introduce mtunnel command
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (22 preceding siblings ...)
2026-10-15 12:50 ` [PATCH container v4 23/27] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:50 ` [PATCH container v4 25/27] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
` (2 subsequent siblings)
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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 v4.
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] 31+ messages in thread* [PATCH container v4 25/27] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (23 preceding siblings ...)
2026-10-15 12:50 ` [PATCH container v4 24/27] pct: introduce mtunnel command Fiona Ebner
@ 2026-10-15 12:50 ` Fiona Ebner
2026-10-15 12:51 ` [PATCH container v4 26/27] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
2026-10-15 12:51 ` [PATCH manager v4 27/27] ui: migrate: add checkbox to allow skipping newly enforced config check Fiona Ebner
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:50 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 v4.
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] 31+ messages in thread* [PATCH container v4 26/27] migration: intra-cluster: check config can be parsed on target node
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (24 preceding siblings ...)
2026-10-15 12:50 ` [PATCH container v4 25/27] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
@ 2026-10-15 12:51 ` Fiona Ebner
2026-10-15 12:51 ` [PATCH manager v4 27/27] ui: migrate: add checkbox to allow skipping newly enforced config check Fiona Ebner
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:51 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>
---
Dependency bump for pve-cluster needed!
Changes in v4:
* Chomp original error to avoid weird formatting.
* Adapt version.
src/PVE/API2/LXC.pm | 13 ++++++++++++-
src/PVE/LXC/Migrate.pm | 33 +++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 1b2c0e2..2f11b7e 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..e6e1117 100644
--- a/src/PVE/LXC/Migrate.pm
+++ b/src/PVE/LXC/Migrate.pm
@@ -159,6 +159,39 @@ 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, 22)) {
+ # 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 $@;
+
+ if ($err) {
+ chomp($err);
+ die "$err - use 'skip-config-check' flag to migrate regardless\n";
+ }
+ } else {
+ $self->log('info', "skipping config check - target pve-manager version < 9.2.22");
+ }
+ } 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] 31+ messages in thread* [PATCH manager v4 27/27] ui: migrate: add checkbox to allow skipping newly enforced config check
2026-10-15 12:50 [PATCH-SERIES many v4 00/27] migration: strict config check for intra-cluster migration Fiona Ebner
` (25 preceding siblings ...)
2026-10-15 12:51 ` [PATCH container v4 26/27] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
@ 2026-10-15 12:51 ` Fiona Ebner
26 siblings, 0 replies; 31+ messages in thread
From: Fiona Ebner @ 2026-10-15 12:51 UTC (permalink / raw)
To: pve-devel
Since qemu-server and pve-container commits "migration: intra-cluster:
check config can be parsed on target node", there is an early check
that the target can successfully parse the guest configuration. There
is an escape hatch to skip the check with the 'skip-config-check'
parameter, to force migration regardless. Allow using the escape hatch
from the UI too.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Should depend on new pve-container, qemu-server and ha-manager so the
node version checking makes sense.
New in v4.
www/manager6/window/Migrate.js | 39 ++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js
index 8cac54ea..6f48d131 100644
--- a/www/manager6/window/Migrate.js
+++ b/www/manager6/window/Migrate.js
@@ -31,6 +31,7 @@ Ext.define('PVE.window.Migrate', {
hasLocalResources: false,
withConntrackState: true,
bothHaveDbusVmstate: false,
+ configCheck: true,
},
},
formulas: {
@@ -150,6 +151,10 @@ Ext.define('PVE.window.Migrate', {
params['with-conntrack-state'] = 1;
}
+ if (!vm.get('migration.configCheck')) {
+ params['skip-config-check'] = 1;
+ }
+
Proxmox.Utils.API2Request({
params: params,
url:
@@ -468,6 +473,13 @@ Ext.define('PVE.window.Migrate', {
}
}
+ if (!vm.get('migration.configCheck')) {
+ const text = gettext(
+ 'Without the config check, an outdated target node might drop configuration settings it cannot parse.',
+ );
+ migration.preconditions.push({ text, severity: 'warning' });
+ }
+
vm.set('migration', migration);
},
checkLxcPreconditions: async function (resetMigrationPossible) {
@@ -563,6 +575,13 @@ Ext.define('PVE.window.Migrate', {
}
}
+ if (!vm.get('migration.configCheck')) {
+ const text = gettext(
+ 'Without the config check, an outdated target node might ignore configuration settings it cannot parse.',
+ );
+ migration.preconditions.push({ text, severity: 'warning' });
+ }
+
vm.set('migration', migration);
},
},
@@ -602,6 +621,26 @@ Ext.define('PVE.window.Migrate', {
value: '{setMigrationMode}',
},
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'configCheck',
+ fieldLabel: gettext('Config Check'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext(
+ 'Check that the target node can parse the guest configuration.',
+ ),
+ },
+ bind: {
+ value: '{migration.configCheck}',
+ },
+ listeners: {
+ change: {
+ fn: 'checkMigratePreconditions',
+ extraArg: true,
+ },
+ },
+ },
],
},
{
--
2.47.3
^ permalink raw reply related [flat|nested] 31+ messages in thread