* [PATCH qemu v2 1/9] patches/extra: allow mirror job to use non-root block node
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 2/9] partially fix #7299: fix regression with guest IO limits applying to mirror job Fiona Ebner
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
Cherry-pick commit bb70b73cd5 ("blockdev: mirror: do not require a
root block node") from upstream master in preparation to fix the
regression from issue #7299: the I/O limits intended for the guest
only wrongly apply to blockdev mirror.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v2:
* Cherry-pick the version that landed upstream.
...d-support-for-sync-bitmap-mode-never.patch | 10 +-
...check-for-bitmap-mode-without-bitmap.patch | 2 +-
.../0006-mirror-move-some-checks-to-qmp.patch | 2 +-
...ror-do-not-require-a-root-block-node.patch | 96 +++++++++++++++++++
...add-the-zeroinit-block-driver-filter.patch | 2 +-
...le-posix-make-locking-optiono-on-cre.patch | 2 +-
...ckup-Proxmox-backup-patches-for-QEMU.patch | 4 +-
...k-driver-to-map-backup-archives-into.patch | 2 +-
...igrate-dirty-bitmap-state-via-savevm.patch | 2 +-
.../0035-block-add-alloc-track-driver.patch | 2 +-
.../0036-PVE-backup-add-fleecing-option.patch | 2 +-
...ment-backup-access-setup-and-teardow.patch | 2 +-
debian/patches/series | 1 +
13 files changed, 113 insertions(+), 16 deletions(-)
create mode 100644 debian/patches/extra/0007-blockdev-mirror-do-not-require-a-root-block-node.patch
diff --git a/debian/patches/bitmap-mirror/0001-drive-mirror-add-support-for-sync-bitmap-mode-never.patch b/debian/patches/bitmap-mirror/0001-drive-mirror-add-support-for-sync-bitmap-mode-never.patch
index 8b0740a093..0306c3eb62 100644
--- a/debian/patches/bitmap-mirror/0001-drive-mirror-add-support-for-sync-bitmap-mode-never.patch
+++ b/debian/patches/bitmap-mirror/0001-drive-mirror-add-support-for-sync-bitmap-mode-never.patch
@@ -229,7 +229,7 @@ index 089856f4a8..e6f645e0f2 100644
base_read_only, errp);
if (!job) {
diff --git a/blockdev.c b/blockdev.c
-index 6e86c6262f..b68dfef260 100644
+index 0f70973326..aa39048dd5 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -2859,6 +2859,9 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs,
@@ -289,7 +289,7 @@ index 6e86c6262f..b68dfef260 100644
target_is_zero, on_source_error, on_target_error, unmap,
filter_node_name, copy_mode, errp);
}
-@@ -3111,6 +3138,8 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
+@@ -3117,6 +3144,8 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
blockdev_mirror_common(arg->job_id, bs, target_bs,
arg->replaces, arg->sync,
@@ -298,7 +298,7 @@ index 6e86c6262f..b68dfef260 100644
backing_mode, target_is_zero,
arg->has_speed, arg->speed,
arg->has_granularity, arg->granularity,
-@@ -3130,6 +3159,8 @@ void qmp_blockdev_mirror(const char *job_id,
+@@ -3136,6 +3165,8 @@ void qmp_blockdev_mirror(const char *job_id,
const char *device, const char *target,
const char *replaces,
MirrorSyncMode sync,
@@ -307,7 +307,7 @@ index 6e86c6262f..b68dfef260 100644
bool has_speed, int64_t speed,
bool has_granularity, uint32_t granularity,
bool has_buf_size, int64_t buf_size,
-@@ -3168,7 +3199,8 @@ void qmp_blockdev_mirror(const char *job_id,
+@@ -3182,7 +3213,8 @@ void qmp_blockdev_mirror(const char *job_id,
}
blockdev_mirror_common(job_id, bs, target_bs,
@@ -333,7 +333,7 @@ index e7c8f1a856..d5aa68caeb 100644
BlockdevOnError on_source_error,
BlockdevOnError on_target_error,
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 199efc1e00..28d9c1e98e 100644
+index 48600206ec..03005334e5 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -2280,6 +2280,15 @@
diff --git a/debian/patches/bitmap-mirror/0003-mirror-add-check-for-bitmap-mode-without-bitmap.patch b/debian/patches/bitmap-mirror/0003-mirror-add-check-for-bitmap-mode-without-bitmap.patch
index dadb0e6f85..773139a4ab 100644
--- a/debian/patches/bitmap-mirror/0003-mirror-add-check-for-bitmap-mode-without-bitmap.patch
+++ b/debian/patches/bitmap-mirror/0003-mirror-add-check-for-bitmap-mode-without-bitmap.patch
@@ -16,7 +16,7 @@ Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
1 file changed, 3 insertions(+)
diff --git a/blockdev.c b/blockdev.c
-index b68dfef260..878e47a321 100644
+index aa39048dd5..1d0494a090 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -2952,6 +2952,9 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs,
diff --git a/debian/patches/bitmap-mirror/0006-mirror-move-some-checks-to-qmp.patch b/debian/patches/bitmap-mirror/0006-mirror-move-some-checks-to-qmp.patch
index 98c3f7b93d..abf1f03770 100644
--- a/debian/patches/bitmap-mirror/0006-mirror-move-some-checks-to-qmp.patch
+++ b/debian/patches/bitmap-mirror/0006-mirror-move-some-checks-to-qmp.patch
@@ -62,7 +62,7 @@ index 0f56ad1f54..75563e6e75 100644
if (bitmap_mode != BITMAP_SYNC_MODE_NEVER) {
diff --git a/blockdev.c b/blockdev.c
-index 878e47a321..d9575c8367 100644
+index 1d0494a090..9da3f5b8bb 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -2931,7 +2931,36 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs,
diff --git a/debian/patches/extra/0007-blockdev-mirror-do-not-require-a-root-block-node.patch b/debian/patches/extra/0007-blockdev-mirror-do-not-require-a-root-block-node.patch
new file mode 100644
index 0000000000..70109f219b
--- /dev/null
+++ b/debian/patches/extra/0007-blockdev-mirror-do-not-require-a-root-block-node.patch
@@ -0,0 +1,96 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Fiona Ebner <f.ebner@proxmox.com>
+Date: Tue, 6 Oct 2026 13:42:17 +0200
+Subject: [PATCH] blockdev: mirror: do not require a root block node
+
+The mirror block job currently only operates on a root block node. It
+might be desired to use a throttle node as the root node for limiting
+the guest IO, without also limiting mirror operations.
+
+There is no obvious reason why mirror should only be able to operate
+on a root block node.
+
+When there is no explicit @replaces argument, implicit filters on top
+are skipped. Allowing mirror to operate on non-root nodes does not
+change that fact and keeps the behavior consistent with the
+documentation for @replaces.
+
+Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
+Message-ID: <20261006114240.478523-3-f.ebner@proxmox.com>
+Reviewed-by: Eric Blake <eblake@redhat.com>
+Signed-off-by: Kevin Wolf <kwolf@redhat.com>
+(cherry picked from commit bb70b73cd55f0110fb2b18c9ad84fe5b2fea0534)
+Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
+---
+ blockdev.c | 20 +++++++++++++++++---
+ qapi/block-core.json | 4 ++--
+ 2 files changed, 19 insertions(+), 5 deletions(-)
+
+diff --git a/blockdev.c b/blockdev.c
+index 6e86c6262f..0f70973326 100644
+--- a/blockdev.c
++++ b/blockdev.c
+@@ -2987,13 +2987,19 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
+ bool target_is_zero;
+ int ret;
+
+- bs = qmp_get_root_bs(arg->device, errp);
++ bdrv_graph_rdlock_main_loop();
++ bs = bdrv_lookup_bs(arg->device, arg->device, errp);
+ if (!bs) {
++ bdrv_graph_rdunlock_main_loop();
++ return;
++ }
++ if (!bdrv_is_inserted(bs)) {
++ error_setg(errp, "Device has no medium");
++ bdrv_graph_rdunlock_main_loop();
+ return;
+ }
+
+ /* Early check to avoid creating target */
+- bdrv_graph_rdlock_main_loop();
+ if (bdrv_op_is_blocked(bs, BLOCK_OP_TYPE_MIRROR_SOURCE, errp)) {
+ bdrv_graph_rdunlock_main_loop();
+ return;
+@@ -3150,10 +3156,18 @@ void qmp_blockdev_mirror(const char *job_id,
+ BlockMirrorBackingMode backing_mode = MIRROR_LEAVE_BACKING_CHAIN;
+ int ret;
+
+- bs = qmp_get_root_bs(device, errp);
++ bdrv_graph_rdlock_main_loop();
++ bs = bdrv_lookup_bs(device, device, errp);
+ if (!bs) {
++ bdrv_graph_rdunlock_main_loop();
+ return;
+ }
++ if (!bdrv_is_inserted(bs)) {
++ error_setg(errp, "Device has no medium");
++ bdrv_graph_rdunlock_main_loop();
++ return;
++ }
++ bdrv_graph_rdunlock_main_loop();
+
+ target_bs = bdrv_lookup_bs(target, target, errp);
+ if (!target_bs) {
+diff --git a/qapi/block-core.json b/qapi/block-core.json
+index 199efc1e00..48600206ec 100644
+--- a/qapi/block-core.json
++++ b/qapi/block-core.json
+@@ -2253,7 +2253,7 @@
+ # @job-id: identifier for the newly-created block job. If omitted,
+ # the device name will be used. (Since 2.7)
+ #
+-# @device: the device name or node-name of a root node whose writes
++# @device: the device name or node-name of a block node whose writes
+ # should be mirrored.
+ #
+ # @target: the target of the new image. If the file exists, or if it
+@@ -2583,7 +2583,7 @@
+ # @job-id: identifier for the newly-created block job. If omitted,
+ # the device name will be used. (Since 2.7)
+ #
+-# @device: The device name or node-name of a root node whose writes
++# @device: The device name or node-name of a block node whose writes
+ # should be mirrored.
+ #
+ # @target: the id or node-name of the block device to mirror to. This
diff --git a/debian/patches/pve/0017-PVE-block-add-the-zeroinit-block-driver-filter.patch b/debian/patches/pve/0017-PVE-block-add-the-zeroinit-block-driver-filter.patch
index c88df54d03..9f910164a6 100644
--- a/debian/patches/pve/0017-PVE-block-add-the-zeroinit-block-driver-filter.patch
+++ b/debian/patches/pve/0017-PVE-block-add-the-zeroinit-block-driver-filter.patch
@@ -247,7 +247,7 @@ index 0000000000..036edb17f5
+
+block_init(bdrv_zeroinit_init);
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 28d9c1e98e..07d16c4a04 100644
+index 03005334e5..20f5d21ef4 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -3377,7 +3377,7 @@
diff --git a/debian/patches/pve/0020-PVE-Up-Config-file-posix-make-locking-optiono-on-cre.patch b/debian/patches/pve/0020-PVE-Up-Config-file-posix-make-locking-optiono-on-cre.patch
index a6a1076be7..f4f499c349 100644
--- a/debian/patches/pve/0020-PVE-Up-Config-file-posix-make-locking-optiono-on-cre.patch
+++ b/debian/patches/pve/0020-PVE-Up-Config-file-posix-make-locking-optiono-on-cre.patch
@@ -119,7 +119,7 @@ index 2c29ee7819..73e7eb3cd2 100644
};
return raw_co_create(&options, errp);
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 07d16c4a04..a12a1a2747 100644
+index 20f5d21ef4..eecd8e1d7d 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -5130,6 +5130,10 @@
diff --git a/debian/patches/pve/0027-PVE-Backup-Proxmox-backup-patches-for-QEMU.patch b/debian/patches/pve/0027-PVE-Backup-Proxmox-backup-patches-for-QEMU.patch
index e5d76c54c0..82891d7ea9 100644
--- a/debian/patches/pve/0027-PVE-Backup-Proxmox-backup-patches-for-QEMU.patch
+++ b/debian/patches/pve/0027-PVE-Backup-Proxmox-backup-patches-for-QEMU.patch
@@ -165,7 +165,7 @@ index 69ade0534f..77abc58cb1 100644
+ hmp_handle_error(mon, error);
+}
diff --git a/blockdev.c b/blockdev.c
-index d9575c8367..38e24e12bd 100644
+index 9da3f5b8bb..3a9f557f5b 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -37,6 +37,7 @@
@@ -1685,7 +1685,7 @@ index 0000000000..177fb851b4
+ return ret;
+}
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index a12a1a2747..e66c455f6e 100644
+index eecd8e1d7d..eb814b3b8d 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -952,6 +952,248 @@
diff --git a/debian/patches/pve/0029-PVE-Add-PBS-block-driver-to-map-backup-archives-into.patch b/debian/patches/pve/0029-PVE-Add-PBS-block-driver-to-map-backup-archives-into.patch
index 0dbc77f807..e1d93ba6ef 100644
--- a/debian/patches/pve/0029-PVE-Add-PBS-block-driver-to-map-backup-archives-into.patch
+++ b/debian/patches/pve/0029-PVE-Add-PBS-block-driver-to-map-backup-archives-into.patch
@@ -361,7 +361,7 @@ index 91de400570..b1cbaa100e 100644
summary_info += {'libdaxctl support': libdaxctl}
summary_info += {'libcbor support': libcbor}
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index e66c455f6e..ca18144902 100644
+index eb814b3b8d..1001dbc96b 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -3615,6 +3615,7 @@
diff --git a/debian/patches/pve/0031-PVE-Migrate-dirty-bitmap-state-via-savevm.patch b/debian/patches/pve/0031-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
index f14c756faa..5860c2f4f9 100644
--- a/debian/patches/pve/0031-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
+++ b/debian/patches/pve/0031-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
@@ -200,7 +200,7 @@ index 177fb851b4..7575abab7c 100644
ret->pbs_masterkey = true;
ret->backup_max_workers = true;
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index ca18144902..91efb9922e 100644
+index 1001dbc96b..ce072f5756 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1112,6 +1112,11 @@
diff --git a/debian/patches/pve/0035-block-add-alloc-track-driver.patch b/debian/patches/pve/0035-block-add-alloc-track-driver.patch
index b54317dd3e..e6071c18cd 100644
--- a/debian/patches/pve/0035-block-add-alloc-track-driver.patch
+++ b/debian/patches/pve/0035-block-add-alloc-track-driver.patch
@@ -449,7 +449,7 @@ index d023753091..a777c8079c 100644
out:
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 91efb9922e..80ffcc0547 100644
+index ce072f5756..a0275b4b28 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -3608,7 +3608,8 @@
diff --git a/debian/patches/pve/0036-PVE-backup-add-fleecing-option.patch b/debian/patches/pve/0036-PVE-backup-add-fleecing-option.patch
index dbcf3bb65f..5023d57372 100644
--- a/debian/patches/pve/0036-PVE-backup-add-fleecing-option.patch
+++ b/debian/patches/pve/0036-PVE-backup-add-fleecing-option.patch
@@ -429,7 +429,7 @@ index 7575abab7c..8b83465ebd 100644
return ret;
}
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 80ffcc0547..62d7fb4e7a 100644
+index a0275b4b28..cb5cbe9402 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1054,6 +1054,10 @@
diff --git a/debian/patches/pve/0042-PVE-backup-implement-backup-access-setup-and-teardow.patch b/debian/patches/pve/0042-PVE-backup-implement-backup-access-setup-and-teardow.patch
index ddd7b1ca3e..a348dfd902 100644
--- a/debian/patches/pve/0042-PVE-backup-implement-backup-access-setup-and-teardow.patch
+++ b/debian/patches/pve/0042-PVE-backup-implement-backup-access-setup-and-teardow.patch
@@ -740,7 +740,7 @@ index 0000000000..9ebeef7c8f
+
+#endif /* PVE_BACKUP_H */
diff --git a/qapi/block-core.json b/qapi/block-core.json
-index 62d7fb4e7a..5e50776664 100644
+index cb5cbe9402..13f9d66430 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1128,6 +1128,9 @@
diff --git a/debian/patches/series b/debian/patches/series
index 1828cca017..1807e4a66e 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -4,6 +4,7 @@ extra/0003-block-vmdk-tolerate-known-seSparse-reserved1-bit.patch
extra/0004-hw-scsi-lsi53c895a-gracefully-handle-re-entrant-DMA.patch
extra/0005-qapi-virtio-Restore-backend-features-in-x-query-virt.patch
extra/0006-qapi-monitor-Handle-missing-vhost-user-protocol-feat.patch
+extra/0007-blockdev-mirror-do-not-require-a-root-block-node.patch
bitmap-mirror/0001-drive-mirror-add-support-for-sync-bitmap-mode-never.patch
bitmap-mirror/0002-drive-mirror-add-support-for-conditional-and-always-.patch
bitmap-mirror/0003-mirror-add-check-for-bitmap-mode-without-bitmap.patch
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 2/9] partially fix #7299: fix regression with guest IO limits applying to mirror job
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu v2 1/9] patches/extra: allow mirror job to use non-root block node Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 3/9] partially fix #7299: migrate: fix regression with guest IO limits applying to NBD export Fiona Ebner
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
Since the switch to -blockdev was made in Proxmox VE 9, the root block
node is now always a throttle node for optionally limiting the guest
IO. Mirror jobs are accidentally affected by a set limit too, because
the throttle root block node was passed as the source of the mirror
job. Since pve-qemu-kvm 11.1.4, non-root nodes can be used for the
mirror job too. Use the node node below throttle as the mirror source.
During mirror completion, the throttle node on top is also drained by
QEMU (which temporarily ignores the throttling).
For migration, the dirty bitmap also needs to be created on the node
below throttle.
For the tests, it is necessary to mock the 'query-block' QMP command
for the blockdev module.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
The QEMU binary version in the runs_at_least_qemu_version() check
needs to be adapted when applying, to the pve-qemu version which
actually does allow non-root nodes as a mirror source.
Changes in v2:
* Bump version in version check.
src/PVE/QemuMigrate.pm | 8 ++++--
src/PVE/QemuServer/BlockJob.pm | 42 +++++++++++++++++++++++++++---
src/test/MigrationTest/Shared.pm | 35 +++++++++++++++++++++++++
src/test/run_qemu_migrate_tests.pl | 8 +++---
4 files changed, 83 insertions(+), 10 deletions(-)
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 8176afe5..f177f262 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -628,7 +628,9 @@ sub handle_replication {
# start tracking before replication to get full delta + a few duplicates
$self->log('info', "$drive: start tracking writes using block-dirty-bitmap '$bitmap'");
- mon_cmd($vmid, 'block-dirty-bitmap-add', node => "drive-$drive", name => $bitmap);
+ my $block_node_name =
+ PVE::QemuServer::BlockJob::get_mirror_source_node($vmid, "drive-$drive");
+ mon_cmd($vmid, 'block-dirty-bitmap-add', node => "$block_node_name", name => $bitmap);
# other info comes from target node in phase 2
$self->{target_drive}->{$drive}->{bitmap} = $bitmap;
@@ -820,9 +822,11 @@ sub cleanup_bitmaps {
my $bitmap = $self->{target_drive}->{$drive}->{bitmap};
next if !$bitmap;
$self->log('info', "$drive: removing block-dirty-bitmap '$bitmap'");
+ my $block_node_name =
+ PVE::QemuServer::BlockJob::get_mirror_source_node($self->{vmid}, "drive-$drive");
mon_cmd(
$self->{vmid}, 'block-dirty-bitmap-remove',
- node => "drive-$drive",
+ node => "$block_node_name",
name => $bitmap,
);
}
diff --git a/src/PVE/QemuServer/BlockJob.pm b/src/PVE/QemuServer/BlockJob.pm
index 493dda33..2e4e275e 100644
--- a/src/PVE/QemuServer/BlockJob.pm
+++ b/src/PVE/QemuServer/BlockJob.pm
@@ -13,7 +13,9 @@ use PVE::Storage;
use PVE::QemuServer::Agent qw(qga_check_running);
use PVE::QemuServer::Blockdev;
use PVE::QemuServer::Drive qw(checked_volume_format);
+use PVE::QemuServer::Machine;
use PVE::QemuServer::Monitor qw(mon_cmd qmp_cmd vm_qmp_peer);
+use PVE::QemuServer::QMPHelpers;
use PVE::QemuServer::RunState;
# If the job was started with auto-dismiss=false, it's necessary to dismiss it manually. Using this
@@ -244,12 +246,44 @@ sub monitor {
}
}
+=pod
+
+=head3 get_mirror_source_node
+
+ my $node_name = get_mirror_source_node($vmid, $device_id);
+
+Return the node for the drive device C<$device_id> that should serve as the mirror source.
+
+=cut
+
+sub get_mirror_source_node {
+ my ($vmid, $device_id) = @_;
+
+ # Before QEMU binary 11.1.4, blockdev-mirror requires a root node.
+ if (!PVE::QemuServer::QMPHelpers::runs_at_least_qemu_version($vmid, 11, 1, 4)) {
+ return $device_id;
+ }
+
+ # The switch to -blockdev happened with machine version 10.0.
+ my $machine_type = PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
+ if (!PVE::QemuServer::Machine::is_machine_version_at_least($machine_type, 10, 0)) {
+ return $device_id;
+ }
+
+ # Avoid throttle limits intended for the guest applying to the mirror job (bug #7299).
+ return PVE::QemuServer::Blockdev::get_node_name_below_throttle(
+ vm_qmp_peer($vmid), $device_id, 1,
+ );
+}
+
my sub common_mirror_qmp_options {
- my ($device_id, $qemu_target, $src_bitmap, $bwlimit) = @_;
+ my ($vmid, $device_id, $qemu_target, $src_bitmap, $bwlimit) = @_;
+
+ my $source_node_name = get_mirror_source_node($vmid, $device_id);
my $opts = {
timeout => 10,
- device => "$device_id",
+ device => "$source_node_name",
sync => "full",
target => $qemu_target,
'auto-dismiss' => JSON::false,
@@ -306,7 +340,7 @@ sub qemu_drive_mirror {
$qemu_target = $is_zero_initialized ? "zeroinit:$dst_path" : $dst_path;
}
- my $opts = common_mirror_qmp_options($device_id, $qemu_target, $src_bitmap, $bwlimit);
+ my $opts = common_mirror_qmp_options($vmid, $device_id, $qemu_target, $src_bitmap, $bwlimit);
$opts->{mode} = "existing";
$opts->{format} = $format if $format;
@@ -500,7 +534,7 @@ sub blockdev_mirror {
};
my $qmp_opts = common_mirror_qmp_options(
- $device_id, $target_node_name, $source->{bitmap}, $options->{bwlimit},
+ $vmid, $device_id, $target_node_name, $source->{bitmap}, $options->{bwlimit},
);
$qmp_opts->{'job-id'} = "$jobid";
diff --git a/src/test/MigrationTest/Shared.pm b/src/test/MigrationTest/Shared.pm
index a51e1692..c69a141a 100644
--- a/src/test/MigrationTest/Shared.pm
+++ b/src/test/MigrationTest/Shared.pm
@@ -159,6 +159,41 @@ $qemu_server_module->mock(
},
);
+our $qemu_server_blockdev_module = Test::MockModule->new("PVE::QemuServer::Blockdev");
+$qemu_server_blockdev_module->mock(
+ mon_cmd => sub {
+ my ($vmid, $command, %params) = @_;
+
+ if ($command eq 'query-block') {
+ my $vm_config = PVE::QemuConfig->load_config($vmid);
+ my $res = [];
+ PVE::QemuConfig->foreach_volume(
+ $vm_config,
+ sub {
+ my ($ds, $drive) = @_;
+ my $entry = {
+ qdev => "$ds",
+ inserted => {
+ drv => 'throttle',
+ 'node-name' => "drive-$ds",
+ children => [
+ {
+ 'node-name' => "f-$ds",
+ child => 'file',
+ },
+ ],
+ },
+ };
+ push($res->@*, $entry);
+ },
+ );
+ return $res;
+ } else {
+ die "mon_cmd (mocked) - implement me: $command";
+ }
+ },
+);
+
our $qemu_server_ovmf_module = Test::MockModule->new("PVE::QemuServer::OVMF");
$qemu_server_ovmf_module->mock(
get_efivars_size => sub {
diff --git a/src/test/run_qemu_migrate_tests.pl b/src/test/run_qemu_migrate_tests.pl
index fc2f1375..22a1febb 100755
--- a/src/test/run_qemu_migrate_tests.pl
+++ b/src/test/run_qemu_migrate_tests.pl
@@ -1418,8 +1418,8 @@ my $tests = [
target_volids => local_volids_for_vm(105),
expected_calls => {
%{$replicated_expected_calls_online},
- 'block-dirty-bitmap-add-drive-scsi0' => 1,
- 'block-dirty-bitmap-add-drive-ide0' => 1,
+ 'block-dirty-bitmap-add-f-scsi0' => 1,
+ 'block-dirty-bitmap-add-f-ide0' => 1,
},
expected => {
source_volids => local_volids_for_vm(105),
@@ -1467,8 +1467,8 @@ my $tests = [
target_volids => local_volids_for_vm(105),
expected_calls => {
%{$replicated_expected_calls_online},
- 'block-dirty-bitmap-add-drive-scsi0' => 1,
- 'block-dirty-bitmap-add-drive-ide0' => 1,
+ 'block-dirty-bitmap-add-f-scsi0' => 1,
+ 'block-dirty-bitmap-add-f-ide0' => 1,
},
expected => {
source_volids => local_volids_for_vm(105),
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 3/9] partially fix #7299: migrate: fix regression with guest IO limits applying to NBD export
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu v2 1/9] patches/extra: allow mirror job to use non-root block node Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 2/9] partially fix #7299: fix regression with guest IO limits applying to mirror job Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 4/9] blockdev: attach: support explicitly attaching as writeable Fiona Ebner
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
Related to commit "partially fix #7299: fix regression with guest IO
limits applying to mirror job", but taking care of the NBD export on
the target side. Since the switch to -blockdev with version 10.0,
write limits intended for the guest would apply to NBD exports as
well. This is because the top throttle node was exported. Export the
node below instead to avoid the issue.
For the test, change to using the version of query-block from
the MigrationTest::Shared module to make it work.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v2.
src/PVE/QemuServer.pm | 13 ++++++++++++-
src/test/MigrationTest/QmMock.pm | 7 -------
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 8149184e..1f7cc79e 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -5819,7 +5819,18 @@ sub vm_start_nolock {
my $drivestr = $nbd->{$opt}->{drivestr};
my $volid = $nbd->{$opt}->{volid};
- my $block_node = $block_info->{$opt}->{inserted}->{'node-name'};
+ my $top = $block_info->{$opt}->{inserted};
+ die "no block node found for drive '$opt'\n" if !$top;
+ my $block_node = $top->{'node-name'};
+ my $machine_type = PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
+ if (PVE::QemuServer::Machine::is_machine_version_at_least($machine_type, 10, 0)) {
+ # Since the switch to -blockdev with machine version 10, the top node is a throttle
+ # node. Use the node below it to avoid the guest limits applying to the NBD export.
+ die "top node for drive '$opt' is not a throttle node" if $top->{drv} ne 'throttle';
+ my $children = { map { $_->{child} => $_ } $top->{children}->@* };
+ $block_node = $children->{file}->{'node-name'}
+ or die "drive '$opt': throttle node without file child node name\n";
+ }
mon_cmd(
$vmid,
diff --git a/src/test/MigrationTest/QmMock.pm b/src/test/MigrationTest/QmMock.pm
index 8f8899dc..aa5fac4f 100644
--- a/src/test/MigrationTest/QmMock.pm
+++ b/src/test/MigrationTest/QmMock.pm
@@ -50,8 +50,6 @@ my sub mocked_mon_cmd {
return;
} elsif ($command eq 'block-export-add') {
return;
- } elsif ($command eq 'query-block') {
- return [];
} elsif ($command eq 'qom-set') {
return;
} elsif ($command eq 'query-version') {
@@ -70,11 +68,6 @@ $inotify_module->mock(
},
);
-my $qemu_server_blockdev_module = Test::MockModule->new("PVE::QemuServer::Blockdev");
-$qemu_server_blockdev_module->mock(
- mon_cmd => \&mocked_mon_cmd,
-);
-
my $qemu_server_helpers_module = Test::MockModule->new("PVE::QemuServer::Helpers");
$qemu_server_helpers_module->mock(
vm_running_locally => sub {
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 4/9] blockdev: attach: support explicitly attaching as writeable
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (2 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 3/9] partially fix #7299: migrate: fix regression with guest IO limits applying to NBD export Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 5/9] blockdev: implement reopen function Fiona Ebner
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
If the read-only option is explicitly set to 0, override the drive
configuration and attach the drive as writeable. This is useful for
making exports and mirror targets below throttle writeable even if the
drive itself is configured as read-only.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v2.
src/PVE/QemuServer/Blockdev.pm | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/PVE/QemuServer/Blockdev.pm b/src/PVE/QemuServer/Blockdev.pm
index c2989a37..ef038c7e 100644
--- a/src/PVE/QemuServer/Blockdev.pm
+++ b/src/PVE/QemuServer/Blockdev.pm
@@ -202,7 +202,9 @@ sub get_node_name_below_throttle {
my sub read_only_json_option {
my ($drive, $options) = @_;
- return json_bool($drive->{ro} || drive_is_cdrom($drive) || $options->{'read-only'});
+ return json_bool($options->{'read-only'}) if defined($options->{'read-only'});
+
+ return json_bool($drive->{ro} || drive_is_cdrom($drive));
}
# Common blockdev options that need to be set across the whole throttle->fmt->file chain.
@@ -571,8 +573,8 @@ Parameters:
=item C<< $options->{'no-throttle'} >>: Do not insert a throttle node as the top node.
-=item C<< $options->{'read-only'} >>: Attach the image as read-only irrespective of the
-configuration in C<$drive>.
+=item C<< $options->{'read-only'} >>: Attach the image as read-only or as writeable irrespective of
+the configuration in C<$drive>.
=item C<< $options->{size} >>: Attach the image with this virtual size. Must be smaller than the
actual size of the image. The image format must be C<raw>.
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 5/9] blockdev: implement reopen function
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (3 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 4/9] blockdev: attach: support explicitly attaching as writeable Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 6/9] partially fix #7836: allow mirror for drive with read-only flag when using blockdev Fiona Ebner
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
Similar to the attach() function, but for re-opening an already
attached block node. Currently limited to non-throttle nodes.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v2.
src/PVE/QemuServer/Blockdev.pm | 68 ++++++++++++++++++++++++++++------
1 file changed, 57 insertions(+), 11 deletions(-)
diff --git a/src/PVE/QemuServer/Blockdev.pm b/src/PVE/QemuServer/Blockdev.pm
index ef038c7e..6678bc33 100644
--- a/src/PVE/QemuServer/Blockdev.pm
+++ b/src/PVE/QemuServer/Blockdev.pm
@@ -548,6 +548,33 @@ my sub blockdev_add {
=pod
+=head3 generate_drive_blockdev_for_peer
+
+Helper for the C<attach> and C<reopen> functions, see C<attach> for the parameter descriptions.
+Returns the generated blockdev and the QMP peer information.
+
+=cut
+
+my sub generate_drive_blockdev_for_peer {
+ my ($storecfg, $id, $drive, $options) = @_;
+
+ my $qmp_peer = $options->{qsd} ? qsd_qmp_peer($id) : vm_qmp_peer($id);
+
+ my $machine_version;
+ if ($options->{qsd}) { # qemu-storage-daemon runs with the installed binary version
+ $machine_version =
+ 'pc-i440fx-' . PVE::QemuServer::Machine::latest_installed_machine_version();
+ } else {
+ $machine_version = PVE::QemuServer::Machine::get_current_qemu_machine($id);
+ }
+
+ my $blockdev = generate_drive_blockdev($storecfg, $drive, $machine_version, $options);
+
+ return ($blockdev, $qmp_peer);
+}
+
+=pod
+
=head3 attach
my ($node_name, $read_only) = attach($storecfg, $id, $drive, $options);
@@ -596,17 +623,7 @@ state image.
sub attach {
my ($storecfg, $id, $drive, $options) = @_;
- my $qmp_peer = $options->{qsd} ? qsd_qmp_peer($id) : vm_qmp_peer($id);
-
- my $machine_version;
- if ($options->{qsd}) { # qemu-storage-daemon runs with the installed binary version
- $machine_version =
- 'pc-i440fx-' . PVE::QemuServer::Machine::latest_installed_machine_version();
- } else {
- $machine_version = PVE::QemuServer::Machine::get_current_qemu_machine($id);
- }
-
- my $blockdev = generate_drive_blockdev($storecfg, $drive, $machine_version, $options);
+ my ($blockdev, $qmp_peer) = generate_drive_blockdev_for_peer($storecfg, $id, $drive, $options);
my $throttle_group_id;
if (parse_top_node_name($blockdev->{'node-name'})) { # device top nodes need a throttle group
@@ -727,6 +744,35 @@ sub detach_fleecing_block_nodes {
}
}
+=pod
+
+=head3 reopen
+
+ reopen($storecfg, $id, $drive, $options);
+
+Re-open the already attached drive C<$drive> of the VM C<$id> while considering the additional
+options C<$options>. See C<attach> for the description of parameters and options. The C<no-throttle>
+option is currently required to be set.
+
+=cut
+
+sub reopen {
+ my ($storecfg, $id, $drive, $options) = @_;
+
+ # Will need additional consideration regarding the throttle group objects.
+ die "reopen for a throttle node is not implemented yet\n" if !$options->{'no-throttle'};
+
+ my ($blockdev, $qmp_peer) = generate_drive_blockdev_for_peer($storecfg, $id, $drive, $options);
+
+ if ($blockdev->{driver} eq 'zeroinit') { # zero-init does not support re-open, defer to child
+ $blockdev = $blockdev->{file};
+ }
+
+ qmp_cmd($qmp_peer, 'blockdev-reopen', options => [$blockdev]);
+
+ return;
+}
+
sub resize {
my ($vmid, $deviceid, $storecfg, $volid, $size) = @_;
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 6/9] partially fix #7836: allow mirror for drive with read-only flag when using blockdev
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (4 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 5/9] blockdev: implement reopen function Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 7/9] qm: schema: declare 'nbdstop' command as being for internal use only Fiona Ebner
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
The switch to -blockdev in Proxmox VE 9 introduced a regression,
making clone of a VM with a read-only drive fail. The reason is that
the target node is also attached as read-only and thus cannot serve as
the mirror target. To fix the issue, open the mirror target as
writeable initially and in case the VM switched to the new drive,
re-open as read-only later. Note that the drive device is always
read-only for the guest, because the top throttle node is.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v2:
* Use a callback directly rather than passing data around and calling
a new helper function.
* In v1 the reopen was done directly after 'job-complete', but this
might be racy. Reopen only after the job actually finished.
src/PVE/QemuServer/BlockJob.pm | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/src/PVE/QemuServer/BlockJob.pm b/src/PVE/QemuServer/BlockJob.pm
index 2e4e275e..bedcdc9d 100644
--- a/src/PVE/QemuServer/BlockJob.pm
+++ b/src/PVE/QemuServer/BlockJob.pm
@@ -121,7 +121,13 @@ sub monitor {
my $complete = defined($jobs->{$job_id}->{complete}) && $vanished;
if ($complete || ($vanished && $completion eq 'auto')) {
print "$job_id: $op-job finished\n";
- delete $jobs->{$job_id};
+ my $job_info = delete $jobs->{$job_id};
+
+ if ($completion ne 'cancel' && $job_info->{'reopen-after-switch'}) {
+ eval { $job_info->{'reopen-after-switch'}->(); };
+ warn "reopen after switch: $@" if $@;
+ }
+
next;
}
@@ -521,6 +527,14 @@ sub blockdev_mirror {
}
}
+ # If the drive is configured as read-only, attach the mirror target as writeable initially and
+ # re-open it as read-only later. For the guest, the drive will be seen as read-only if the
+ # throttle node is, even in case mirror switches to the new target.
+ my $dest_is_read_only = $dest_drive->{ro};
+ if ($dest_is_read_only) {
+ $attach_dest_opts->{'read-only'} = 0;
+ }
+
# Note that if 'aio' is not explicitly set, i.e. default, it can change if source and target
# don't both allow or both not allow 'io_uring' as the default.
my ($target_node_name) =
@@ -533,6 +547,23 @@ sub blockdev_mirror {
'target-node-name' => $target_node_name,
};
+ if ($dest_is_read_only) {
+ my $reopen_opts = { $attach_dest_opts->%* };
+ delete($reopen_opts->{'read-only'}); # remove the override for re-opening later
+
+ $jobs->{$jobid}->{'reopen-after-switch'} = sub {
+ eval {
+ PVE::QemuServer::Blockdev::reopen($storecfg, $vmid, $dest_drive, $reopen_opts);
+ };
+ if (my $err = $@) {
+ # For the guest, the drive will be seen as read-only if the throttle node already
+ # is, so a failure here is not too bad.
+ my $drive_id = PVE::QemuServer::Drive::get_drive_id($dest_drive);
+ print "$drive_id: failed to re-apply read-only flag below throttle - $err\n";
+ }
+ };
+ }
+
my $qmp_opts = common_mirror_qmp_options(
$vmid, $device_id, $target_node_name, $source->{bitmap}, $options->{bwlimit},
);
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 7/9] qm: schema: declare 'nbdstop' command as being for internal use only
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (5 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 6/9] partially fix #7836: allow mirror for drive with read-only flag when using blockdev Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 8/9] migrate: log messages from remote nbdstop command Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 9/9] partially fix #7836: allow migration for drive with read-only flag when using blockdev Fiona Ebner
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
Declare the 'qm nbdstop' command as being for internal use only. There
is no way to start an NBD server via the CLI/API, so the command only
makes sense in the context of migration when issued by the Proxmox VE
stack itself.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v2.
src/PVE/CLI/qm.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index f62fe036..c4f9c64b 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -414,7 +414,7 @@ __PACKAGE__->register_method({
name => 'nbdstop',
path => 'nbdstop',
method => 'PUT',
- description => "Stop embedded nbd server.",
+ description => "Stop embedded nbd server. For internal use only!",
parameters => {
additionalProperties => 0,
properties => {
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 8/9] migrate: log messages from remote nbdstop command
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (6 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 7/9] qm: schema: declare 'nbdstop' command as being for internal use only Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
2026-11-11 12:01 ` [PATCH qemu-server v2 9/9] partially fix #7836: allow migration for drive with read-only flag when using blockdev Fiona Ebner
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
The output was always ignored since the introduction of the command in
2016 with commit b74cad8a ("add live storage migration with vm
migration"). But it's nicer to show messages from the remote side to
detect failures and for debugging purposes.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
No changes in v2.
src/PVE/QemuMigrate.pm | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index f177f262..9bca321a 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -1743,7 +1743,12 @@ sub phase3_cleanup {
my $cmd = build_ssh_command($self->{rem_ssh}, ['qm', 'nbdstop', $vmid]);
eval {
- PVE::Tools::run_command($cmd, outfunc => sub { }, errfunc => sub { });
+ PVE::Tools::run_command(
+ $cmd,
+ logfunc => sub {
+ $self->log('info', "[$self->{node}] $_[0]");
+ },
+ );
};
if (my $err = $@) {
$self->log('err', $err);
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH qemu-server v2 9/9] partially fix #7836: allow migration for drive with read-only flag when using blockdev
2026-11-11 12:01 [PATCH-SERIES qemu/qemu-server v2 0/9] mirror: fix regressions with blockdev and allow migration with 'ro' flag Fiona Ebner
` (7 preceding siblings ...)
2026-11-11 12:01 ` [PATCH qemu-server v2 8/9] migrate: log messages from remote nbdstop command Fiona Ebner
@ 2026-11-11 12:01 ` Fiona Ebner
8 siblings, 0 replies; 10+ messages in thread
From: Fiona Ebner @ 2026-11-11 12:01 UTC (permalink / raw)
To: pve-devel
A read-only block node cannot serve as a writeable block export for
the NBD mirror target. With -blockdev, the issue can be resolved by
re-opening the node below throttle as writeable, exporting it, and
then re-opening it as read-only after finishing the migration.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
Changes in v2:
* Improve error handling.
* Fix typo in comment.
* Terminate warning messages with newlines.
src/PVE/API2/Qemu.pm | 7 ++++--
src/PVE/CLI/qm.pm | 26 +++++++++++++++++++--
src/PVE/QemuServer.pm | 9 ++++++++
src/PVE/QemuServer/BlockJob.pm | 42 ++++++++++++++++++++++++++++++++++
4 files changed, 80 insertions(+), 4 deletions(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 7830d09b..15391ff0 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -48,7 +48,6 @@ use PVE::QemuServer::MetaInfo;
use PVE::QemuServer::Network;
use PVE::QemuServer::OVMF;
use PVE::QemuServer::PCI;
-use PVE::QemuServer::QMPHelpers;
use PVE::QemuServer::RNG;
use PVE::QemuServer::RunState;
use PVE::QemuServer::USB;
@@ -7145,7 +7144,11 @@ __PACKAGE__->register_method({
return;
},
'nbdstop' => sub {
- PVE::QemuServer::QMPHelpers::nbd_stop($state->{vmid});
+ PVE::QemuServer::BlockJob::nbd_stop_after_mirror(
+ $state->{storecfg},
+ $state->{vmid},
+ $state->{conf},
+ );
return;
},
'resume' => sub {
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index c4f9c64b..f3f02ef8 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -428,8 +428,30 @@ __PACKAGE__->register_method({
my $vmid = $param->{vmid};
- eval { PVE::QemuServer::QMPHelpers::nbd_stop($vmid) };
- warn $@ if $@;
+ my $need_nbd_stop = 1;
+
+ eval {
+ PVE::QemuConfig->lock_config(
+ $vmid,
+ sub {
+ my $storecfg = PVE::Storage::config();
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ $need_nbd_stop = 0; # nbd_stop_after_mirror() handles it.
+ PVE::QemuServer::BlockJob::nbd_stop_after_mirror(
+ $storecfg, $vmid, $conf,
+ );
+ },
+ );
+ };
+ if (my $err = $@) {
+ warn $err;
+ if ($need_nbd_stop) {
+ # Fall back to just stopping the NBD server without checking disks.
+ warn "nbdstop: skipping disk check\n";
+ eval { PVE::QemuServer::QMPHelpers::nbd_stop($vmid) };
+ warn $@ if $@;
+ }
+ }
return;
},
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 1f7cc79e..17bf88b3 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -5819,6 +5819,10 @@ sub vm_start_nolock {
my $drivestr = $nbd->{$opt}->{drivestr};
my $volid = $nbd->{$opt}->{volid};
+ my $drive = parse_drive($opt, $drivestr);
+ die "$opt: internal error - unable to parse drive '$drivestr'\n" if !$drive;
+ my $drive_is_readonly = $drive->{ro};
+
my $top = $block_info->{$opt}->{inserted};
die "no block node found for drive '$opt'\n" if !$top;
my $block_node = $top->{'node-name'};
@@ -5830,6 +5834,11 @@ sub vm_start_nolock {
my $children = { map { $_->{child} => $_ } $top->{children}->@* };
$block_node = $children->{file}->{'node-name'}
or die "drive '$opt': throttle node without file child node name\n";
+
+ if ($drive_is_readonly) { # re-open the node below throttle as writeable
+ my $reopen_opts = { 'no-throttle' => 1, 'read-only' => 0 };
+ PVE::QemuServer::Blockdev::reopen($storecfg, $vmid, $drive, $reopen_opts);
+ }
}
mon_cmd(
diff --git a/src/PVE/QemuServer/BlockJob.pm b/src/PVE/QemuServer/BlockJob.pm
index bedcdc9d..fe87d976 100644
--- a/src/PVE/QemuServer/BlockJob.pm
+++ b/src/PVE/QemuServer/BlockJob.pm
@@ -614,4 +614,46 @@ sub mirror {
}
}
+sub nbd_stop_after_mirror {
+ my ($storecfg, $vmid, $conf) = @_;
+
+ my $exports = [];
+
+ eval {
+ my $machine_type = PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
+ if (PVE::QemuServer::Machine::is_machine_version_at_least($machine_type, 10, 0)) {
+ $exports = mon_cmd($vmid, 'query-block-exports');
+ }
+ };
+ warn "stop nbd: unable to query block exports - $@" if $@;
+
+ PVE::QemuServer::QMPHelpers::nbd_stop($vmid);
+
+ for my $export ($exports->@*) {
+ my ($opt) = $export->{id} =~ m/^drive-(.*)$/;
+ if (!defined($opt)) {
+ warn "stop nbd: got unexpected export ID '$export->{id}'\n";
+ } elsif (!PVE::QemuServer::Drive::is_valid_drivename($opt)) {
+ warn "stop nbd: got unexpected export ID '$export->{id}' - not a drive key\n";
+ } else {
+ my $drive = PVE::QemuServer::Drive::parse_drive($opt, $conf->{$opt});
+ if (!$drive) {
+ warn "stop nbd: $opt: unable to parse drive '$conf->{$opt}'\n";
+ next;
+ }
+ if ($drive->{ro}) {
+ # The node below throttle was opened as writeable to be able to serve as a target
+ # NBD export for mirror. Re-open as read-only now. Failure is not too important,
+ # because the guest sees the drive as read-only anyways, because the top throttle
+ # node is.
+ my $reopen_opts = { 'no-throttle' => 1 };
+ eval { PVE::QemuServer::Blockdev::reopen($storecfg, $vmid, $drive, $reopen_opts); };
+ print "drive-$opt failed to re-apply read-only flag below throttle - $@\n" if $@;
+ }
+ }
+ }
+
+ return;
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread