public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH-SERIES many v5 00/28] migration: strict config check for intra-cluster migration
@ 2026-10-19 14:45 Fiona Ebner
  2026-10-19 14:45 ` [PATCH common v5 01/28] tools: move version_cmp() helper from qemu-server Fiona Ebner
                   ` (27 more replies)
  0 siblings, 28 replies; 29+ messages in thread
From: Fiona Ebner @ 2026-10-19 14:45 UTC (permalink / raw)
  To: pve-devel

Changes in v5 (thanks @Dano!):
* Add PVE::Cluster include in patch "resources: add
  migrate_json_properties() method".
* Initialize plugin-specific migrate json properties once on module
  load.
* Drop unused variables in handle_crm_motion_command().
* Factor out JSON command handling into helper function.
* Add missing PVE::HA::Resources include for Manager.pm.
* Rename 'kind' with 'task' in CRM JSON command handling for
  consistency.
* Add comment why PVE::HA::Env::PVE2 is included in the resources API
  module.
* Clarify that functions for JSON CRM command support currently only
  deal with migration command.
* Add patch for missing PVE::HA::Resources import for Manager.pm.
* Bump pve-manager version in version checks.

Changes in v4 (thanks @Dano!):
* Rebase on master and update pve-manager version in checks.
* Split out adding log line into separate patch.
* Add check_resource_motion_valid() helper.
* Remove unnecessary 'sort'.
* Improve some names.
* Pass 'migrate-options' consistent with other params for
  queue_resource_command().
* Add cfs_update() call before checking node versions.
* Improve warnings in API endpoint and ensure they are also logged to
  CLI callers.
* Only submit command as JSON if actually required.
* Import raise_param_exc in PVE::API2::HA::Resources.
* Better separate extra options in log output.
* Add UI checkbox to allow opting out of guest configuration check.
* Use canonical => 1 when encoding JSON for log consistency with
  multiple options.
* Add HA manager test case.
* Mention missing dependency requirements in cover letter.

Changes in v3:
* Add patches to teach HA to pass along certain migration parameters
  and fix #7053 by passing along 'with-conntrack-state'.
* Avoid re-using root-only 'force' flag, add a new 'skip-config-check'
  flag that can also be used by regular users.
* Use pvecfg node version to detect if a target supports config
  checking via mtunnel.

Changes in v2:
* add patches for container
* log when skipping strict configuration check

First part
==========

Additional migration options can now be passed from the guest API
endpoints to the HA stack. From the HA resource migrate API endpoint,
the options are passed all the way to the LRM and resource plugins,
which will then use it again when invoking the resource's migrate API
endpoint itself.

The migration API endpoints for containers and VMs use different
parameters. For example, the endpoint for containers does not have
the 'with-conntrack-state' option. To keep the schema for the more
abstract endpoint for HA resources clean, separate by type using a
oneOf schema and let the resource plugins declare additional
parameters via a migrate_json_properties() function.

Known issue: because of cyclic dependencies, the PVE::API2::{LXC,QEMU}
modules can't be included by the respective resource plugin, when the
build happens in a PVE_GENERATING_DOCS environment. Because of this,
the extra migration options for the HA endpoint won't show up in the
docs.

To pass these along to CRM, support for JSON CRM commands is
introduced, with only the migration command being supported for now.
If not all nodes are recent enough to support the new-style JSON CRM
command, produce a warning and fall-back to the previous behavior
ignoring the additional migration options.

Initially, this is used to fix #7053 by passing along the
'with-conntrack-state' migration option.

Second part
===========

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 settings being
unexpectedly ignored on the target, and in the case of qemu-server
even relatively silently dropped (there are warnings in the target's
system logs). In case of containers, an example is migrating a
container with a mountpoint with 'keepattrs' to a node with a too old
pve-container version, resulting in the mountpoint not being mounted
on the target.

Unfortunately, before patch "qm: mtunnel: reply when a command is
unknown", when a command is unknown, mtunnel did not reply at all.
Therefore, check the target node's version to see if it is new enough
to support the new mtunnel command.

Useful to pull in early
=======================

pvecfg node version helpers moved to pve-cluster rather than just
pve-guest-common, because David needs them in pve-cluster too [1].

[0]: https://bugzilla.proxmox.com/show_bug.cgi?id=7341
[1]: https://lore.proxmox.com/pve-devel/20260724142528.109453-1-d.riley@proxmox.com/

Patch 20/27 qm: mtunnel: reply when a command is unknown would also
be nice to have.

Dependencies
============

Dependency bump qemu-server,pve-container -> common,pve-cluster needed.
pve-cluster depends on new common too.

Dependency bump ha-manger -> qemu-server,pve-container,pve-cluster
needed.

qemu-server also build-depends on new common and pve-cluster.

pve-manager needs to depend on new pve-container, qemu-server and
ha-manager so the node version checking makes sense.

common:

Fiona Ebner (2):
  tools: move version_cmp() helper from qemu-server
  rest handler: handle: respect schema's 'type-property' when resolving
    type

 src/PVE/JSONSchema.pm    |  10 +++
 src/PVE/RESTHandler.pm   |  21 ++++--
 src/PVE/Tools.pm         |  26 +++++++
 test/get-options-test.pl | 150 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 200 insertions(+), 7 deletions(-)


cluster:

Fiona Ebner (1):
  cluster: move pvecfg node version helpers from qemu-server

 src/PVE/Cluster.pm | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)


ha-manager:

Fiona Ebner (13):
  manager: add missing PVE::HA::Resources import
  update crm commands: align logging with next state started/stopped
  manager: factor out helper to check if requested resource motion is
    valid
  next state stopped: align logging for motion command with
    next_state_started()
  next state {stopped,started}: factor out helper to handle motion
    command
  lrm: resource migration: support extra migration options
  change service state: support hash as a parameter value
  next state: handle motion command: support migration options
  resources: add migrate_json_properties() method
  queue resource motion: support migration options
  crm command: support JSON-style migrate command
  api: resources: migration: support additional migration options
  test: add test for migration options

 src/PVE/API2/HA/Resources.pm                  | 110 ++++++++--
 src/PVE/HA/LRM.pm                             |  16 +-
 src/PVE/HA/Manager.pm                         | 193 +++++++++++++-----
 src/PVE/HA/Resources.pm                       |  23 ++-
 src/PVE/HA/Resources/PVECT.pm                 |  22 +-
 src/PVE/HA/Resources/PVEVM.pm                 |  22 +-
 src/PVE/HA/Sim/Hardware.pm                    |  25 ++-
 src/PVE/HA/Sim/Resources.pm                   |  19 +-
 src/PVE/HA/Sim/Resources/VirtCT.pm            |  14 +-
 src/PVE/HA/Sim/Resources/VirtFail.pm          |   6 +-
 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 +
 .../test-node-affinity-nonstrict7/log.expect  |   2 +-
 .../test-node-affinity-strict7/log.expect     |   4 +-
 .../test-relocate-to-inactive-node/log.expect |   1 +
 src/test/test-service-stopped3/log.expect     |   1 +
 22 files changed, 584 insertions(+), 91 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


qemu-server:

Fiona Ebner (7):
  helpers: move version_cmp() helper to pve-common
  helpers: move pvecfg node version helpers to pve-cluster
  api: migrate: allow forwarding certain migration properties to HA
  fix #7053: api: migrate: pass 'with-conntrack-state' flag to HA
    migration
  qm: mtunnel: reply when a command is unknown
  qm: mtunnel: add 'conf' command to do strict configuration parsing
  migration: intra-cluster: check config can be parsed on target node

 src/PVE/API2/Qemu.pm                      | 160 +++++++++++++---------
 src/PVE/CLI/qm.pm                         |  15 ++
 src/PVE/QemuMigrate.pm                    |  34 ++++-
 src/PVE/QemuServer/Drive.pm               |   3 +-
 src/PVE/QemuServer/Helpers.pm             |  51 +------
 src/PVE/QemuServer/Machine.pm             |  13 +-
 src/PVE/QemuServer/QMPHelpers.pm          |   5 +-
 src/test/MigrationTest/QemuMigrateMock.pm |  11 +-
 8 files changed, 160 insertions(+), 132 deletions(-)


container:

Fiona Ebner (4):
  api: migrate: allow forwarding certain migration properties to HA
  pct: introduce mtunnel command
  d/control: bump versioned build dependency for libpve-common-perl to
    9.0.12
  migration: intra-cluster: check config can be parsed on target node

 debian/control         |   2 +-
 src/PVE/API2/LXC.pm    | 107 +++++++++++++++++++++++++++--------------
 src/PVE/CLI/pct.pm     |  57 ++++++++++++++++++++++
 src/PVE/LXC/Migrate.pm |  33 +++++++++++++
 4 files changed, 161 insertions(+), 38 deletions(-)


manager:

Fiona Ebner (1):
  ui: migrate: add checkbox to allow skipping newly enforced config
    check

 www/manager6/window/Migrate.js | 39 ++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)


Summary over all repositories:
  40 files changed, 1174 insertions(+), 268 deletions(-)

-- 
Generated by git-murpp 0.5.0




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [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

end of thread, other threads:[~2026-10-19 14:50 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH cluster v5 03/28] cluster: move pvecfg node version helpers from qemu-server Fiona Ebner
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 ` [PATCH ha-manager v5 05/28] update crm commands: align logging with next state started/stopped 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
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 ` [PATCH ha-manager v5 08/28] next state {stopped,started}: factor out helper to handle motion command Fiona Ebner
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 ` [PATCH ha-manager v5 10/28] change service state: support hash as a parameter value Fiona Ebner
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 ` [PATCH ha-manager v5 12/28] resources: add migrate_json_properties() method Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 13/28] queue resource motion: support migration options Fiona Ebner
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 ` [PATCH ha-manager v5 15/28] api: resources: migration: support additional migration options Fiona Ebner
2026-10-19 14:45 ` [PATCH ha-manager v5 16/28] test: add test for " Fiona Ebner
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 ` [PATCH qemu-server v5 18/28] helpers: move pvecfg node version helpers to pve-cluster Fiona Ebner
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 ` [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 ` [PATCH qemu-server v5 21/28] qm: mtunnel: reply when a command is unknown 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
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 ` [PATCH container v5 24/28] api: migrate: allow forwarding certain migration properties to HA Fiona Ebner
2026-10-19 14:46 ` [PATCH container v5 25/28] pct: introduce mtunnel command 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
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal