* [PATCH common v5 01/28] tools: move version_cmp() helper from qemu-server
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH common v5 02/28] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
` (26 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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 v5.
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] 29+ messages in thread* [PATCH common v5 02/28] rest handler: handle: respect schema's 'type-property' when resolving type
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
2026-10-19 14:45 ` [PATCH common v5 01/28] tools: move version_cmp() helper from qemu-server Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH cluster v5 03/28] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
` (25 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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 v5.
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] 29+ messages in thread* [PATCH cluster v5 03/28] cluster: move pvecfg node version helpers from qemu-server
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
2026-10-19 14:45 ` [PATCH common v5 01/28] tools: move version_cmp() helper from qemu-server Fiona Ebner
2026-10-19 14:45 ` [PATCH common v5 02/28] rest handler: handle: respect schema's 'type-property' when resolving type Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 04/28] manager: add missing PVE::HA::Resources import Fiona Ebner
` (24 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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 v5.
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] 29+ messages in thread* [PATCH ha-manager v5 04/28] manager: add missing PVE::HA::Resources import
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (2 preceding siblings ...)
2026-10-19 14:45 ` [PATCH cluster v5 03/28] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 05/28] update crm commands: align logging with next state started/stopped Fiona Ebner
` (23 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v5.
src/PVE/HA/Manager.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 5840a76..e91151a 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -10,6 +10,7 @@ use PVE::Tools;
use PVE::HA::Groups;
use PVE::HA::Helpers;
use PVE::HA::NodeStatus;
+use PVE::HA::Resources;
use PVE::HA::Rules;
use PVE::HA::Rules::NodeAffinity qw(get_node_affinity);
use PVE::HA::Rules::ResourceAffinity
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH ha-manager v5 05/28] update crm commands: align logging with next state started/stopped
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (3 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 04/28] manager: add missing PVE::HA::Resources import Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 06/28] manager: factor out helper to check if requested resource motion is valid Fiona Ebner
` (22 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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 e91151a..fa0c796 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -682,12 +682,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] 29+ messages in thread* [PATCH ha-manager v5 06/28] manager: factor out helper to check if requested resource motion is valid
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (4 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 05/28] update crm commands: align logging with next state started/stopped Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 07/28] next state stopped: align logging for motion command with next_state_started() Fiona Ebner
` (21 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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 fa0c796..e0b47e9 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -667,6 +667,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) = @_;
@@ -681,17 +699,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");
@@ -1309,15 +1318,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;
}
@@ -1433,17 +1435,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] 29+ messages in thread* [PATCH ha-manager v5 07/28] next state stopped: align logging for motion command with next_state_started()
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (5 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 06/28] manager: factor out helper to check if requested resource motion is valid Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 08/28] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
` (20 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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 e0b47e9..b3bf064 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1320,6 +1320,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] 29+ messages in thread* [PATCH ha-manager v5 08/28] next state {stopped,started}: factor out helper to handle motion command
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (6 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 07/28] next state stopped: align logging for motion command with next_state_started() Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 09/28] lrm: resource migration: support extra migration options Fiona Ebner
` (19 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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 b3bf064..7c7b8ff 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1300,6 +1300,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) = @_;
@@ -1317,11 +1335,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') {
@@ -1435,16 +1450,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] 29+ messages in thread* [PATCH ha-manager v5 09/28] lrm: resource migration: support extra migration options
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (7 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 08/28] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 10/28] change service state: support hash as a parameter value Fiona Ebner
` (18 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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] 29+ messages in thread* [PATCH ha-manager v5 10/28] change service state: support hash as a parameter value
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (8 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 09/28] lrm: resource migration: support extra migration options Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 11/28] next state: handle motion command: support migration options Fiona Ebner
` (17 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 UTC (permalink / raw)
To: pve-devel
Required for the migration options.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v5.
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 7c7b8ff..8e3d838 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;
@@ -534,7 +535,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] 29+ messages in thread* [PATCH ha-manager v5 11/28] next state: handle motion command: support migration options
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (9 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 10/28] change service state: support hash as a parameter value Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 12/28] resources: add migrate_json_properties() method Fiona Ebner
` (16 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
---
No changes in v5.
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 8e3d838..f5592e6 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -1313,10 +1313,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] 29+ messages in thread* [PATCH ha-manager v5 12/28] resources: add migrate_json_properties() method
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (10 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 11/28] next state: handle motion command: support migration options Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 13/28] queue resource motion: support migration options Fiona Ebner
` (15 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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 v5:
* Add required include for PVE::Cluster already in this patch.
* Only construct properties hash once.
src/PVE/HA/Resources.pm | 21 +++++++++++++++++++++
src/PVE/HA/Resources/PVECT.pm | 17 +++++++++++++++++
src/PVE/HA/Resources/PVEVM.pm | 17 +++++++++++++++++
src/PVE/HA/Sim/Resources.pm | 6 ++++++
4 files changed, 61 insertions(+)
diff --git a/src/PVE/HA/Resources.pm b/src/PVE/HA/Resources.pm
index 656cb19..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;
@@ -103,6 +104,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 +174,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..de7c5b8 100644
--- a/src/PVE/HA/Resources/PVECT.pm
+++ b/src/PVE/HA/Resources/PVECT.pm
@@ -22,6 +22,17 @@ BEGIN {
use base qw(PVE::HA::Resources);
+my $ct_migrate_json_properties;
+# 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..
+my sub init_migrate_json_properties {
+ 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->%*;
+ $ct_migrate_json_properties = $combined;
+}
+
sub type {
return 'ct';
}
@@ -174,4 +185,10 @@ sub get_static_stats_from_config {
};
}
+sub migrate_json_properties {
+ my ($class) = @_;
+ init_migrate_json_properties() if !defined($ct_migrate_json_properties);
+ return $ct_migrate_json_properties
+}
+
1;
diff --git a/src/PVE/HA/Resources/PVEVM.pm b/src/PVE/HA/Resources/PVEVM.pm
index 3b61113..914f406 100644
--- a/src/PVE/HA/Resources/PVEVM.pm
+++ b/src/PVE/HA/Resources/PVEVM.pm
@@ -22,6 +22,17 @@ BEGIN {
use base qw(PVE::HA::Resources);
+my $vm_migrate_json_properties;
+# 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..
+my sub init_migrate_json_properties {
+ 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->%*;
+ $vm_migrate_json_properties = $combined;
+}
+
sub type {
return 'vm';
}
@@ -194,4 +205,10 @@ sub get_static_stats_from_config {
};
}
+sub migrate_json_properties {
+ my ($class) = @_;
+ init_migrate_json_properties() if !defined($vm_migrate_json_properties);
+ return $vm_migrate_json_properties;
+}
+
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] 29+ messages in thread* [PATCH ha-manager v5 13/28] queue resource motion: support migration options
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (11 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 12/28] resources: add migrate_json_properties() method Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 14/28] crm command: support JSON-style migrate command Fiona Ebner
` (14 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
No changes in v5.
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 f5592e6..b8d0e3e 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -296,7 +296,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 {
@@ -611,7 +611,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()->@* };
@@ -642,18 +642,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];
}
}
@@ -705,7 +727,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] 29+ messages in thread* [PATCH ha-manager v5 14/28] crm command: support JSON-style migrate command
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (12 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 13/28] queue resource motion: support migration options Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 15/28] api: resources: migration: support additional migration options Fiona Ebner
` (13 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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 v5:
* Drop unused variables in handle_crm_motion_command().
* Add handle_crm_motion_command() helper.
* Use elsif instead of if+next.
* Rename 'kind' to 'task' for consistency.
src/PVE/HA/Manager.pm | 66 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 56 insertions(+), 10 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index b8d0e3e..8d14ca7 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -712,6 +712,56 @@ 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, $ss) = $self->@{qw(haenv 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;
+}
+
+my sub handle_crm_json_command {
+ my ($self, $cmd) = @_;
+
+ my $haenv = $self->{haenv};
+
+ my $command_info = eval { JSON::decode_json($cmd) };
+ if (my $err = $@) {
+ $haenv->log('err', "unable to decode JSON command: '$cmd' - $err");
+ next;
+ }
+
+ my $task = $command_info->{task};
+ if (!defined($task)) {
+ $haenv->log('err', "unable to handle JSON command without 'task': '$cmd'");
+ } elsif ($task 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, $task, $sid, $node, $options);
+ } else {
+ $haenv->log('err', "unable to handle unknown JSON command: '$cmd'");
+ }
+
+ return;
+}
+
# read new crm commands and save them into crm master status
sub update_crm_commands {
my ($self) = @_;
@@ -723,16 +773,12 @@ 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.
+ handle_crm_json_command($self, $cmd);
+ } elsif ($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] 29+ messages in thread* [PATCH ha-manager v5 15/28] api: resources: migration: support additional migration options
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (13 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 14/28] crm command: support JSON-style migrate command Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 16/28] test: add test for " Fiona Ebner
` (12 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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!
The pve-manager version in the check must be adapted upon applying!
Changes in v5:
* Add comment about why PVE::HA::Env::PVE2 must be included.
* Rename JSON CRM command check helper functions to include 'motion'.
* Bump required pve-manager version.
* Rename 'kind' to 'task' for consistency.
src/PVE/API2/HA/Resources.pm | 110 ++++++++++++++++++++++++++++++-----
1 file changed, 96 insertions(+), 14 deletions(-)
diff --git a/src/PVE/API2/HA/Resources.pm b/src/PVE/API2/HA/Resources.pm
index b96572b..feaa29e 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; # The resource plugins are registered 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_motion_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, 23)) {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+my sub json_crm_motion_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_motion_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_motion_command_is_applicable($sid, $req_node, $param)) {
+ $crm_command = JSON::to_json(
+ {
+ task => '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;
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH ha-manager v5 16/28] test: add test for migration options
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (14 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 15/28] api: resources: migration: support additional migration options Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:45 ` [PATCH qemu-server v5 17/28] helpers: move version_cmp() helper to pve-common Fiona Ebner
` (11 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 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>
---
Changes in v5:
* Rename 'kind' to 'task' for consistency.
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..0a3d5f7 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 = {
+ task => $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] 29+ messages in thread* [PATCH qemu-server v5 17/28] helpers: move version_cmp() helper to pve-common
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (15 preceding siblings ...)
2026-10-19 14:45 ` [PATCH ha-manager v5 16/28] test: add test for " Fiona Ebner
@ 2026-10-19 14:45 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 18/28] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
` (10 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Build dependency and dependency bump for pve-common needed!
No changes in v5.
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] 29+ messages in thread* [PATCH qemu-server v5 18/28] helpers: move pvecfg node version helpers to pve-cluster
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (16 preceding siblings ...)
2026-10-19 14:45 ` [PATCH qemu-server v5 17/28] helpers: move version_cmp() helper to pve-common Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 19/28] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (9 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
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 96035a75..279ed70b 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] 29+ messages in thread* [PATCH qemu-server v5 19/28] api: migrate: allow forwarding certain migration properties to HA
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (17 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 18/28] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 20/28] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
` (8 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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>
---
No changes in v5.
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] 29+ messages in thread* [PATCH qemu-server v5 20/28] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (18 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 19/28] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 21/28] qm: mtunnel: reply when a command is unknown Fiona Ebner
` (7 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
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] 29+ messages in thread* [PATCH qemu-server v5 21/28] qm: mtunnel: reply when a command is unknown
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (19 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 20/28] fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA migration Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 22/28] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
` (6 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
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] 29+ messages in thread* [PATCH qemu-server v5 22/28] qm: mtunnel: add 'conf' command to do strict configuration parsing
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (20 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 21/28] qm: mtunnel: reply when a command is unknown Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH qemu-server v5 23/28] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
` (5 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
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] 29+ messages in thread* [PATCH qemu-server v5 23/28] migration: intra-cluster: check config can be parsed on target node
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (21 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 22/28] qm: mtunnel: add 'conf' command to do strict configuration parsing Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH container v5 24/28] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
` (4 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 UTC (permalink / raw)
To: pve-devel
For remote migration, we already check that the config can be parsed
on the target. Do the same for intra-cluster migration, to avoid
issues like [0] for future new settings, with lines being unexpectedly
and relatively silently dropped (there are warnings in the target's
system logs, but nothing else).
Unfortunately, before commit "qm: mtunnel: reply when a command is
unknown", which is part of the same patch series, when a command is
unknown, mtunnel did not reply at all. Therefore, check that the
target node is recent enough to support the new mtunnel command.
[0]: https://bugzilla.proxmox.com/show_bug.cgi?id=7341
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
The pve-manager version in the check must be adapted upon applying!
Changes in v5:
* Bump required pve-manager 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 279ed70b..aba59475 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, 23)) {
+ # 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.23");
+ }
+ } 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] 29+ messages in thread* [PATCH container v5 24/28] api: migrate: allow forwarding certain migration properties to HA
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (22 preceding siblings ...)
2026-10-19 14:46 ` [PATCH qemu-server v5 23/28] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH container v5 25/28] pct: introduce mtunnel command Fiona Ebner
` (3 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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>
---
No changes in v5.
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] 29+ messages in thread* [PATCH container v5 25/28] pct: introduce mtunnel command
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (23 preceding siblings ...)
2026-10-19 14:46 ` [PATCH container v5 24/28] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH container v5 26/28] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
` (2 subsequent siblings)
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
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] 29+ messages in thread* [PATCH container v5 26/28] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (24 preceding siblings ...)
2026-10-19 14:46 ` [PATCH container v5 25/28] pct: introduce mtunnel command Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH container v5 27/28] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
2026-10-19 14:46 ` [PATCH manager v5 28/28] ui: migrate: add checkbox to allow skipping newly enforced config check Fiona Ebner
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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 v5.
debian/control | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/debian/control b/debian/control
index ef57b13..b3a6341 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] 29+ messages in thread* [PATCH container v5 27/28] migration: intra-cluster: check config can be parsed on target node
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (25 preceding siblings ...)
2026-10-19 14:46 ` [PATCH container v5 26/28] d/control: bump versioned build dependency for libpve-common-perl to 9.0.12 Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
2026-10-19 14:46 ` [PATCH manager v5 28/28] ui: migrate: add checkbox to allow skipping newly enforced config check Fiona Ebner
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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!
The pve-manager version in the check must be adapted upon applying!
Changes in v5:
* Bump required pve-manager 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..27517c4 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, 23)) {
+ # 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.23");
+ }
+ } 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] 29+ messages in thread* [PATCH manager v5 28/28] ui: migrate: add checkbox to allow skipping newly enforced config check
2026-10-19 14:45 [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration Fiona Ebner
` (26 preceding siblings ...)
2026-10-19 14:46 ` [PATCH container v5 27/28] migration: intra-cluster: check config can be parsed on target node Fiona Ebner
@ 2026-10-19 14:46 ` Fiona Ebner
27 siblings, 0 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:46 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.
No changes in v5.
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] 29+ messages in thread