From: Fiona Ebner <f.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH qemu-server v3 4/4] introduce dedicated module for snaphsot as volume chain handling
Date: Fri, 13 Feb 2026 17:52:56 +0100 [thread overview]
Message-ID: <20260213165307.177055-5-f.ebner@proxmox.com> (raw)
In-Reply-To: <20260213165307.177055-1-f.ebner@proxmox.com>
Get rid of the cyclic dependency between the Blockdev and BlockJob
modules.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
New in v3.
src/PVE/QemuServer.pm | 9 +-
src/PVE/QemuServer/Blockdev.pm | 329 +----------------------------
src/PVE/QemuServer/Makefile | 3 +-
src/PVE/QemuServer/VolumeChain.pm | 337 ++++++++++++++++++++++++++++++
4 files changed, 354 insertions(+), 324 deletions(-)
create mode 100644 src/PVE/QemuServer/VolumeChain.pm
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 39723c82..239b4a8c 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -95,6 +95,7 @@ use PVE::QemuServer::RunState;
use PVE::QemuServer::StateFile;
use PVE::QemuServer::USB;
use PVE::QemuServer::Virtiofs qw(max_virtiofs start_all_virtiofsd);
+use PVE::QemuServer::VolumeChain;
use PVE::QemuServer::DBusVMState;
my $have_ha_config;
@@ -4356,7 +4357,7 @@ sub qemu_volume_snapshot {
print "external qemu snapshot\n";
my $snapshots = PVE::Storage::volume_snapshot_info($storecfg, $volid);
my $parent_snap = $snapshots->{'current'}->{parent};
- PVE::QemuServer::Blockdev::blockdev_external_snapshot(
+ PVE::QemuServer::VolumeChain::blockdev_external_snapshot(
$storecfg, $vmid, $machine_version, $deviceid, $drive, $snap, $parent_snap,
);
} elsif ($do_snapshots_type eq 'storage') {
@@ -4414,7 +4415,7 @@ sub qemu_volume_snapshot_delete {
# improve-me: if firstsnap > child : commit, if firstsnap < child do a stream.
if (!$parentsnap) {
print "delete first snapshot $snap\n";
- PVE::QemuServer::Blockdev::blockdev_commit(
+ PVE::QemuServer::VolumeChain::blockdev_commit(
$storecfg,
$vmid,
$machine_version,
@@ -4426,7 +4427,7 @@ sub qemu_volume_snapshot_delete {
PVE::Storage::rename_snapshot($storecfg, $volid, $snap, $childsnap);
- PVE::QemuServer::Blockdev::blockdev_replace(
+ PVE::QemuServer::VolumeChain::blockdev_replace(
$storecfg,
$vmid,
$machine_version,
@@ -4439,7 +4440,7 @@ sub qemu_volume_snapshot_delete {
} else {
#intermediate snapshot, we always stream the snapshot to child snapshot
print "stream intermediate snapshot $snap to $childsnap\n";
- PVE::QemuServer::Blockdev::blockdev_stream(
+ PVE::QemuServer::VolumeChain::blockdev_stream(
$storecfg,
$vmid,
$machine_version,
diff --git a/src/PVE/QemuServer/Blockdev.pm b/src/PVE/QemuServer/Blockdev.pm
index 5846ac69..be907be8 100644
--- a/src/PVE/QemuServer/Blockdev.pm
+++ b/src/PVE/QemuServer/Blockdev.pm
@@ -5,19 +5,24 @@ use warnings;
use Digest::SHA;
use Fcntl qw(S_ISBLK S_ISCHR);
-use File::Basename qw(basename dirname);
use File::stat;
use JSON;
use PVE::JSONSchema qw(json_bool);
use PVE::Storage;
-use PVE::QemuServer::BlockJob;
use PVE::QemuServer::Drive qw(drive_is_cdrom);
use PVE::QemuServer::Helpers;
use PVE::QemuServer::Machine;
use PVE::QemuServer::Monitor qw(mon_cmd qmp_cmd);
+use base qw(Exporter);
+
+our @EXPORT_OK = qw(
+ generate_file_blockdev
+ generate_format_blockdev
+);
+
# gives ($host, $port, $export)
my $NBD_TCP_PATH_RE_3 = qr/nbd:(\S+):(\d+):exportname=(\S+)/;
my $NBD_UNIX_PATH_RE_2 = qr/nbd:unix:(\S+):exportname=(\S+)/;
@@ -113,7 +118,7 @@ sub get_block_info {
return $block_info;
}
-my sub get_node_name {
+sub get_node_name {
my ($type, $drive_id, $volid, $options) = @_;
return fleecing_node_name($type, $drive_id, $options) if $options->{fleecing};
@@ -254,7 +259,7 @@ my sub generate_blockdev_drive_cache {
};
}
-my sub generate_file_blockdev {
+sub generate_file_blockdev {
my ($storecfg, $drive, $machine_version, $options) = @_;
my $blockdev = {};
@@ -335,7 +340,7 @@ my sub generate_file_blockdev {
return $blockdev;
}
-my sub generate_format_blockdev {
+sub generate_format_blockdev {
my ($storecfg, $drive, $child, $options) = @_;
die "generate_format_blockdev called without volid/path\n" if !$drive->{file};
@@ -850,318 +855,4 @@ sub set_io_throttle {
}
}
-sub blockdev_external_snapshot {
- my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $snap, $parent_snap) = @_;
-
- print "Creating a new current volume with $snap as backing snap\n";
-
- my $volid = $drive->{file};
-
- #rename current to snap && preallocate add a new current file with reference to snap1 backing-file
- PVE::Storage::volume_snapshot($storecfg, $volid, $snap);
-
- #reopen current to snap
- blockdev_replace(
- $storecfg,
- $vmid,
- $machine_version,
- $deviceid,
- $drive,
- 'current',
- $snap,
- $parent_snap,
- );
-
- #be sure to add drive in write mode
- delete($drive->{ro});
-
- my $new_file_blockdev = generate_file_blockdev($storecfg, $drive);
- my $new_fmt_blockdev = generate_format_blockdev($storecfg, $drive, $new_file_blockdev);
-
- my $snap_file_blockdev =
- generate_file_blockdev($storecfg, $drive, $machine_version, { 'snapshot-name' => $snap });
- my $snap_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $snap_file_blockdev,
- { 'snapshot-name' => $snap },
- );
-
- #backing need to be forced to undef in blockdev, to avoid reopen of backing-file on blockdev-add
- $new_fmt_blockdev->{backing} = undef;
-
- mon_cmd($vmid, 'blockdev-add', %$new_fmt_blockdev);
-
- print "blockdev-snapshot: reopen current with $snap backing image\n";
- mon_cmd(
- $vmid, 'blockdev-snapshot',
- node => $snap_fmt_blockdev->{'node-name'},
- overlay => $new_fmt_blockdev->{'node-name'},
- );
-}
-
-sub blockdev_delete {
- my ($storecfg, $vmid, $drive, $file_blockdev, $fmt_blockdev, $snap) = @_;
-
- eval { detach($vmid, $fmt_blockdev->{'node-name'}); };
- warn "detaching block node for $file_blockdev->{filename} failed - $@" if $@;
-
- #delete the file (don't use vdisk_free as we don't want to delete all snapshot chain)
- print "delete old $file_blockdev->{filename}\n";
-
- my $storage_name = PVE::Storage::parse_volume_id($drive->{file});
-
- my $volid = $drive->{file};
- PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, 1);
-}
-
-my sub blockdev_relative_backing_file {
- my ($backing, $backed) = @_;
-
- my $backing_file = $backing->{filename};
- my $backed_file = $backed->{filename};
-
- if (dirname($backing_file) eq dirname($backed_file)) {
- # make backing file relative if in same directory
- return basename($backing_file);
- }
-
- return $backing_file;
-}
-
-sub blockdev_replace {
- my (
- $storecfg,
- $vmid,
- $machine_version,
- $deviceid,
- $drive,
- $src_snap,
- $target_snap,
- $parent_snap,
- ) = @_;
-
- print "blockdev replace $src_snap by $target_snap\n";
-
- my $volid = $drive->{file};
- my $drive_id = PVE::QemuServer::Drive::get_drive_id($drive);
-
- my $src_name_options = {};
- my $src_blockdev_name;
- if ($src_snap eq 'current') {
- # there might be other nodes on top like zeroinit, look up the current node below throttle
- $src_blockdev_name = get_node_name_below_throttle($vmid, $deviceid, 1);
- } else {
- $src_name_options = { 'snapshot-name' => $src_snap };
- $src_blockdev_name = get_node_name('fmt', $drive_id, $volid, $src_name_options);
- }
-
- my $target_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $target_snap },
- );
- my $target_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $target_file_blockdev,
- { 'snapshot-name' => $target_snap },
- );
-
- if ($target_snap eq 'current' || $src_snap eq 'current') {
- #rename from|to current
-
- #add backing to target
- if ($parent_snap) {
- my $parent_fmt_nodename =
- get_node_name('fmt', $drive_id, $volid, { 'snapshot-name' => $parent_snap });
- $target_fmt_blockdev->{backing} = $parent_fmt_nodename;
- }
- mon_cmd($vmid, 'blockdev-add', %$target_fmt_blockdev);
-
- #reopen the current throttlefilter nodename with the target fmt nodename
- my $throttle_blockdev =
- generate_throttle_blockdev($drive, $target_fmt_blockdev->{'node-name'}, {});
- mon_cmd($vmid, 'blockdev-reopen', options => [$throttle_blockdev]);
- } else {
- #intermediate snapshot
- mon_cmd($vmid, 'blockdev-add', %$target_fmt_blockdev);
-
- #reopen the parent node with the new target fmt backing node
- my $parent_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $parent_snap },
- );
- my $parent_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $parent_file_blockdev,
- { 'snapshot-name' => $parent_snap },
- );
- $parent_fmt_blockdev->{backing} = $target_fmt_blockdev->{'node-name'};
- mon_cmd($vmid, 'blockdev-reopen', options => [$parent_fmt_blockdev]);
-
- my $backing_file =
- blockdev_relative_backing_file($target_file_blockdev, $parent_file_blockdev);
-
- #change backing-file in qcow2 metadatas
- mon_cmd(
- $vmid, 'change-backing-file',
- device => $deviceid,
- 'image-node-name' => $parent_fmt_blockdev->{'node-name'},
- 'backing-file' => $backing_file,
- );
- }
-
- # delete old file|fmt nodes
- eval { detach($vmid, $src_blockdev_name); };
- warn "detaching block node for $src_snap failed - $@" if $@;
-}
-
-sub blockdev_commit {
- my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $src_snap, $target_snap) = @_;
-
- my $volid = $drive->{file};
- my $target_was_read_only;
-
- print "block-commit $src_snap to base:$target_snap\n";
-
- my $target_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $target_snap },
- );
- my $target_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $target_file_blockdev,
- { 'snapshot-name' => $target_snap },
- );
-
- my $src_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $src_snap },
- );
- my $src_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $src_file_blockdev,
- { 'snapshot-name' => $src_snap },
- );
-
- if ($target_was_read_only = $target_fmt_blockdev->{'read-only'}) {
- print "reopening internal read-only block node for '$target_snap' as writable\n";
- $target_fmt_blockdev->{'read-only'} = JSON::false;
- $target_file_blockdev->{'read-only'} = JSON::false;
- mon_cmd($vmid, 'blockdev-reopen', options => [$target_fmt_blockdev]);
- # For the guest, the drive is still read-only, because the top throttle node is.
- }
-
- eval {
- my $job_id = "commit-$deviceid";
- my $jobs = {};
- my $opts = { 'job-id' => $job_id, device => $deviceid };
-
- $opts->{'base-node'} = $target_fmt_blockdev->{'node-name'};
- $opts->{'top-node'} = $src_fmt_blockdev->{'node-name'};
-
- mon_cmd($vmid, "block-commit", %$opts);
- $jobs->{$job_id} = {};
-
- # If the 'current' state is committed to its backing snapshot, the job will not complete
- # automatically, because there is a writer, i.e. the guest. It is necessary to use the
- # 'complete' completion mode, so that the 'current' block node is replaced with the backing
- # node upon completion. Like that, IO after the commit operation will already land in the
- # backing node, which will be renamed since it will be the new top of the chain (done by the
- # caller).
- #
- # For other snapshots in the chain, it can be assumed that they have no writer, so
- # 'block-commit' will complete automatically.
- my $complete = $src_snap && $src_snap ne 'current' ? 'auto' : 'complete';
-
- PVE::QemuServer::BlockJob::monitor($vmid, undef, $jobs, $complete, 0, 'commit');
-
- blockdev_delete(
- $storecfg, $vmid, $drive, $src_file_blockdev, $src_fmt_blockdev, $src_snap,
- );
- };
- my $err = $@;
-
- if ($target_was_read_only) {
- # Even when restoring the read-only flag on the format and file nodes fails, the top
- # throttle node still has it, ensuring it is read-only for the guest.
- print "re-applying read-only flag for internal block node for '$target_snap'\n";
- $target_fmt_blockdev->{'read-only'} = JSON::true;
- $target_file_blockdev->{'read-only'} = JSON::true;
- eval { mon_cmd($vmid, 'blockdev-reopen', options => [$target_fmt_blockdev]); };
- print "failed to re-apply read-only flag - $@\n" if $@;
- }
-
- die $err if $err;
-}
-
-sub blockdev_stream {
- my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $snap, $parent_snap, $target_snap) =
- @_;
-
- my $volid = $drive->{file};
- $target_snap = undef if $target_snap eq 'current';
-
- my $parent_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $parent_snap },
- );
- my $parent_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $parent_file_blockdev,
- { 'snapshot-name' => $parent_snap },
- );
-
- my $target_file_blockdev = generate_file_blockdev(
- $storecfg,
- $drive,
- $machine_version,
- { 'snapshot-name' => $target_snap },
- );
- my $target_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $target_file_blockdev,
- { 'snapshot-name' => $target_snap },
- );
-
- my $snap_file_blockdev =
- generate_file_blockdev($storecfg, $drive, $machine_version, { 'snapshot-name' => $snap });
- my $snap_fmt_blockdev = generate_format_blockdev(
- $storecfg,
- $drive,
- $snap_file_blockdev,
- { 'snapshot-name' => $snap },
- );
-
- my $backing_file = blockdev_relative_backing_file($parent_file_blockdev, $target_file_blockdev);
-
- my $job_id = "stream-$deviceid";
- my $jobs = {};
- my $options = { 'job-id' => $job_id, device => $target_fmt_blockdev->{'node-name'} };
- $options->{'base-node'} = $parent_fmt_blockdev->{'node-name'};
- $options->{'backing-file'} = $backing_file;
-
- mon_cmd($vmid, 'block-stream', %$options);
- $jobs->{$job_id} = {};
-
- PVE::QemuServer::BlockJob::monitor($vmid, undef, $jobs, 'auto', 0, 'stream');
-
- blockdev_delete($storecfg, $vmid, $drive, $snap_file_blockdev, $snap_fmt_blockdev, $snap);
-}
-
1;
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index d599ca91..7e48c388 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -28,7 +28,8 @@ SOURCES=Agent.pm \
RunState.pm \
StateFile.pm \
USB.pm \
- Virtiofs.pm
+ Virtiofs.pm \
+ VolumeChain.pm
.PHONY: install
install: $(SOURCES)
diff --git a/src/PVE/QemuServer/VolumeChain.pm b/src/PVE/QemuServer/VolumeChain.pm
new file mode 100644
index 00000000..e3790683
--- /dev/null
+++ b/src/PVE/QemuServer/VolumeChain.pm
@@ -0,0 +1,337 @@
+package PVE::QemuServer::VolumeChain;
+
+use strict;
+use warnings;
+
+use File::Basename qw(basename dirname);
+use JSON;
+
+use PVE::Storage;
+
+use PVE::QemuServer::Blockdev qw(generate_file_blockdev generate_format_blockdev);
+use PVE::QemuServer::BlockJob;
+use PVE::QemuServer::Drive;
+use PVE::QemuServer::Monitor qw(mon_cmd);
+
+sub blockdev_external_snapshot {
+ my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $snap, $parent_snap) = @_;
+
+ print "Creating a new current volume with $snap as backing snap\n";
+
+ my $volid = $drive->{file};
+
+ #rename current to snap && preallocate add a new current file with reference to snap1 backing-file
+ PVE::Storage::volume_snapshot($storecfg, $volid, $snap);
+
+ #reopen current to snap
+ blockdev_replace(
+ $storecfg,
+ $vmid,
+ $machine_version,
+ $deviceid,
+ $drive,
+ 'current',
+ $snap,
+ $parent_snap,
+ );
+
+ #be sure to add drive in write mode
+ delete($drive->{ro});
+
+ my $new_file_blockdev = generate_file_blockdev($storecfg, $drive);
+ my $new_fmt_blockdev = generate_format_blockdev($storecfg, $drive, $new_file_blockdev);
+
+ my $snap_file_blockdev =
+ generate_file_blockdev($storecfg, $drive, $machine_version, { 'snapshot-name' => $snap });
+ my $snap_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $snap_file_blockdev,
+ { 'snapshot-name' => $snap },
+ );
+
+ #backing need to be forced to undef in blockdev, to avoid reopen of backing-file on blockdev-add
+ $new_fmt_blockdev->{backing} = undef;
+
+ mon_cmd($vmid, 'blockdev-add', %$new_fmt_blockdev);
+
+ print "blockdev-snapshot: reopen current with $snap backing image\n";
+ mon_cmd(
+ $vmid, 'blockdev-snapshot',
+ node => $snap_fmt_blockdev->{'node-name'},
+ overlay => $new_fmt_blockdev->{'node-name'},
+ );
+}
+
+sub blockdev_delete {
+ my ($storecfg, $vmid, $drive, $file_blockdev, $fmt_blockdev, $snap) = @_;
+
+ eval { PVE::QemuServer::Blockdev::detach($vmid, $fmt_blockdev->{'node-name'}); };
+ warn "detaching block node for $file_blockdev->{filename} failed - $@" if $@;
+
+ #delete the file (don't use vdisk_free as we don't want to delete all snapshot chain)
+ print "delete old $file_blockdev->{filename}\n";
+
+ my $storage_name = PVE::Storage::parse_volume_id($drive->{file});
+
+ my $volid = $drive->{file};
+ PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, 1);
+}
+
+my sub blockdev_relative_backing_file {
+ my ($backing, $backed) = @_;
+
+ my $backing_file = $backing->{filename};
+ my $backed_file = $backed->{filename};
+
+ if (dirname($backing_file) eq dirname($backed_file)) {
+ # make backing file relative if in same directory
+ return basename($backing_file);
+ }
+
+ return $backing_file;
+}
+
+sub blockdev_replace {
+ my (
+ $storecfg,
+ $vmid,
+ $machine_version,
+ $deviceid,
+ $drive,
+ $src_snap,
+ $target_snap,
+ $parent_snap,
+ ) = @_;
+
+ print "blockdev replace $src_snap by $target_snap\n";
+
+ my $volid = $drive->{file};
+ my $drive_id = PVE::QemuServer::Drive::get_drive_id($drive);
+
+ my $src_name_options = {};
+ my $src_blockdev_name;
+ if ($src_snap eq 'current') {
+ # there might be other nodes on top like zeroinit, look up the current node below throttle
+ $src_blockdev_name =
+ PVE::QemuServer::Blockdev::get_node_name_below_throttle($vmid, $deviceid, 1);
+ } else {
+ $src_name_options = { 'snapshot-name' => $src_snap };
+ $src_blockdev_name =
+ PVE::QemuServer::Blockdev::get_node_name('fmt', $drive_id, $volid, $src_name_options);
+ }
+
+ my $target_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $target_snap },
+ );
+ my $target_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $target_file_blockdev,
+ { 'snapshot-name' => $target_snap },
+ );
+
+ if ($target_snap eq 'current' || $src_snap eq 'current') {
+ #rename from|to current
+
+ #add backing to target
+ if ($parent_snap) {
+ my $parent_fmt_nodename = PVE::QemuServer::Blockdev::get_node_name(
+ 'fmt',
+ $drive_id,
+ $volid,
+ { 'snapshot-name' => $parent_snap },
+ );
+ $target_fmt_blockdev->{backing} = $parent_fmt_nodename;
+ }
+ mon_cmd($vmid, 'blockdev-add', %$target_fmt_blockdev);
+
+ #reopen the current throttlefilter nodename with the target fmt nodename
+ my $throttle_blockdev = PVE::QemuServer::Blockdev::generate_throttle_blockdev(
+ $drive, $target_fmt_blockdev->{'node-name'}, {},
+ );
+ mon_cmd($vmid, 'blockdev-reopen', options => [$throttle_blockdev]);
+ } else {
+ #intermediate snapshot
+ mon_cmd($vmid, 'blockdev-add', %$target_fmt_blockdev);
+
+ #reopen the parent node with the new target fmt backing node
+ my $parent_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $parent_snap },
+ );
+ my $parent_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $parent_file_blockdev,
+ { 'snapshot-name' => $parent_snap },
+ );
+ $parent_fmt_blockdev->{backing} = $target_fmt_blockdev->{'node-name'};
+ mon_cmd($vmid, 'blockdev-reopen', options => [$parent_fmt_blockdev]);
+
+ my $backing_file =
+ blockdev_relative_backing_file($target_file_blockdev, $parent_file_blockdev);
+
+ #change backing-file in qcow2 metadatas
+ mon_cmd(
+ $vmid, 'change-backing-file',
+ device => $deviceid,
+ 'image-node-name' => $parent_fmt_blockdev->{'node-name'},
+ 'backing-file' => $backing_file,
+ );
+ }
+
+ # delete old file|fmt nodes
+ eval { PVE::QemuServer::Blockdev::detach($vmid, $src_blockdev_name); };
+ warn "detaching block node for $src_snap failed - $@" if $@;
+}
+
+sub blockdev_commit {
+ my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $src_snap, $target_snap) = @_;
+
+ my $volid = $drive->{file};
+ my $target_was_read_only;
+
+ print "block-commit $src_snap to base:$target_snap\n";
+
+ my $target_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $target_snap },
+ );
+ my $target_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $target_file_blockdev,
+ { 'snapshot-name' => $target_snap },
+ );
+
+ my $src_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $src_snap },
+ );
+ my $src_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $src_file_blockdev,
+ { 'snapshot-name' => $src_snap },
+ );
+
+ if ($target_was_read_only = $target_fmt_blockdev->{'read-only'}) {
+ print "reopening internal read-only block node for '$target_snap' as writable\n";
+ $target_fmt_blockdev->{'read-only'} = JSON::false;
+ $target_file_blockdev->{'read-only'} = JSON::false;
+ mon_cmd($vmid, 'blockdev-reopen', options => [$target_fmt_blockdev]);
+ # For the guest, the drive is still read-only, because the top throttle node is.
+ }
+
+ eval {
+ my $job_id = "commit-$deviceid";
+ my $jobs = {};
+ my $opts = { 'job-id' => $job_id, device => $deviceid };
+
+ $opts->{'base-node'} = $target_fmt_blockdev->{'node-name'};
+ $opts->{'top-node'} = $src_fmt_blockdev->{'node-name'};
+
+ mon_cmd($vmid, "block-commit", %$opts);
+ $jobs->{$job_id} = {};
+
+ # If the 'current' state is committed to its backing snapshot, the job will not complete
+ # automatically, because there is a writer, i.e. the guest. It is necessary to use the
+ # 'complete' completion mode, so that the 'current' block node is replaced with the backing
+ # node upon completion. Like that, IO after the commit operation will already land in the
+ # backing node, which will be renamed since it will be the new top of the chain (done by the
+ # caller).
+ #
+ # For other snapshots in the chain, it can be assumed that they have no writer, so
+ # 'block-commit' will complete automatically.
+ my $complete = $src_snap && $src_snap ne 'current' ? 'auto' : 'complete';
+
+ PVE::QemuServer::BlockJob::monitor($vmid, undef, $jobs, $complete, 0, 'commit');
+
+ blockdev_delete(
+ $storecfg, $vmid, $drive, $src_file_blockdev, $src_fmt_blockdev, $src_snap,
+ );
+ };
+ my $err = $@;
+
+ if ($target_was_read_only) {
+ # Even when restoring the read-only flag on the format and file nodes fails, the top
+ # throttle node still has it, ensuring it is read-only for the guest.
+ print "re-applying read-only flag for internal block node for '$target_snap'\n";
+ $target_fmt_blockdev->{'read-only'} = JSON::true;
+ $target_file_blockdev->{'read-only'} = JSON::true;
+ eval { mon_cmd($vmid, 'blockdev-reopen', options => [$target_fmt_blockdev]); };
+ print "failed to re-apply read-only flag - $@\n" if $@;
+ }
+
+ die $err if $err;
+}
+
+sub blockdev_stream {
+ my ($storecfg, $vmid, $machine_version, $deviceid, $drive, $snap, $parent_snap, $target_snap) =
+ @_;
+
+ my $volid = $drive->{file};
+ $target_snap = undef if $target_snap eq 'current';
+
+ my $parent_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $parent_snap },
+ );
+ my $parent_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $parent_file_blockdev,
+ { 'snapshot-name' => $parent_snap },
+ );
+
+ my $target_file_blockdev = generate_file_blockdev(
+ $storecfg,
+ $drive,
+ $machine_version,
+ { 'snapshot-name' => $target_snap },
+ );
+ my $target_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $target_file_blockdev,
+ { 'snapshot-name' => $target_snap },
+ );
+
+ my $snap_file_blockdev =
+ generate_file_blockdev($storecfg, $drive, $machine_version, { 'snapshot-name' => $snap });
+ my $snap_fmt_blockdev = generate_format_blockdev(
+ $storecfg,
+ $drive,
+ $snap_file_blockdev,
+ { 'snapshot-name' => $snap },
+ );
+
+ my $backing_file = blockdev_relative_backing_file($parent_file_blockdev, $target_file_blockdev);
+
+ my $job_id = "stream-$deviceid";
+ my $jobs = {};
+ my $options = { 'job-id' => $job_id, device => $target_fmt_blockdev->{'node-name'} };
+ $options->{'base-node'} = $parent_fmt_blockdev->{'node-name'};
+ $options->{'backing-file'} = $backing_file;
+
+ mon_cmd($vmid, 'block-stream', %$options);
+ $jobs->{$job_id} = {};
+
+ PVE::QemuServer::BlockJob::monitor($vmid, undef, $jobs, 'auto', 0, 'stream');
+
+ blockdev_delete($storecfg, $vmid, $drive, $snap_file_blockdev, $snap_fmt_blockdev, $snap);
+}
+
+1;
--
2.47.3
next prev parent reply other threads:[~2026-02-13 16:53 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-13 16:52 [PATCH-SERIES qemu-server v3 0/4] small block job related improvements Fiona Ebner
2026-02-13 16:52 ` [PATCH qemu-server v3 1/4] blockdev: commit: improve comment about completion mode Fiona Ebner
2026-02-13 16:52 ` [PATCH qemu-server v3 2/4] block job: rename qemu_drive_mirror_monitor() to monitor() Fiona Ebner
2026-02-13 16:52 ` [PATCH qemu-server v3 3/4] blockdev: avoid potentially misleading error messages when block job monitor fails Fiona Ebner
2026-02-13 16:52 ` Fiona Ebner [this message]
2026-02-16 19:13 ` applied-series: [PATCH-SERIES qemu-server v3 0/4] small block job related improvements Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260213165307.177055-5-f.ebner@proxmox.com \
--to=f.ebner@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox