[pve-devel] [PATCH qemu-server 06/15] introduce QemuImage module

Fiona Ebner f.ebner at proxmox.com
Mon Jun 23 17:44:16 CEST 2025


Signed-off-by: Fiona Ebner <f.ebner at proxmox.com>
---
 src/PVE/QemuServer.pm                  | 122 ++----------------------
 src/PVE/QemuServer/ImportDisk.pm       |   6 +-
 src/PVE/QemuServer/Makefile            |   1 +
 src/PVE/QemuServer/QemuImage.pm        | 123 +++++++++++++++++++++++++
 src/test/run_qemu_img_convert_tests.pl |  19 ++--
 5 files changed, 148 insertions(+), 123 deletions(-)
 create mode 100644 src/PVE/QemuServer/QemuImage.pm

diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index b67fe832..63b4d469 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -72,6 +72,7 @@ use PVE::QemuServer::Memory qw(get_current_memory);
 use PVE::QemuServer::MetaInfo;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port parse_hostpci);
+use PVE::QemuServer::QemuImage;
 use PVE::QemuServer::QMPHelpers qw(qemu_deviceadd qemu_devicedel qemu_objectadd qemu_objectdel);
 use PVE::QemuServer::RNG qw(parse_rng print_rng_device_commandline print_rng_object_commandline);
 use PVE::QemuServer::StateFile;
@@ -7684,7 +7685,12 @@ sub restore_external_archive {
                     'is-zero-initialized' => $sparseinit,
                     'source-path-format' => $source_format,
                 };
-                qemu_img_convert($source_path, $d->{volid}, $d->{size}, $convert_opts);
+                PVE::QemuServer::QemuImage::convert(
+                    $source_path,
+                    $d->{volid},
+                    $d->{size},
+                    $convert_opts,
+                );
             };
             my $err = $@;
             eval { $backup_provider->restore_vm_volume_cleanup($volname, $d->{devname}, {}); };
@@ -8336,116 +8342,6 @@ sub template_create : prototype($$;$) {
     );
 }
 
-sub convert_iscsi_path {
-    my ($path) = @_;
-
-    if ($path =~ m|^iscsi://([^/]+)/([^/]+)/(.+)$|) {
-        my $portal = $1;
-        my $target = $2;
-        my $lun = $3;
-
-        my $initiator_name = get_iscsi_initiator_name();
-
-        return "file.driver=iscsi,file.transport=tcp,file.initiator-name=$initiator_name,"
-            . "file.portal=$portal,file.target=$target,file.lun=$lun,driver=raw";
-    }
-
-    die "cannot convert iscsi path '$path', unknown format\n";
-}
-
-# The possible options are:
-# bwlimit - The bandwidth limit in KiB/s.
-# is-zero-initialized - If the destination image is zero-initialized.
-# snapname - Use this snapshot of the source image.
-# source-path-format - Indicate the format of the source when the source is a path. For PVE-managed
-# volumes, the format from the storage layer is always used.
-sub qemu_img_convert {
-    my ($src_volid, $dst_volid, $size, $opts) = @_;
-
-    my ($bwlimit, $snapname) = $opts->@{qw(bwlimit snapname)};
-
-    my $storecfg = PVE::Storage::config();
-    my ($src_storeid) = PVE::Storage::parse_volume_id($src_volid, 1);
-    my ($dst_storeid) = PVE::Storage::parse_volume_id($dst_volid, 1);
-
-    die "destination '$dst_volid' is not a valid volid form qemu-img convert\n" if !$dst_storeid;
-
-    my $cachemode;
-    my $src_path;
-    my $src_is_iscsi = 0;
-    my $src_format;
-
-    if ($src_storeid) {
-        PVE::Storage::activate_volumes($storecfg, [$src_volid], $snapname);
-        my $src_scfg = PVE::Storage::storage_config($storecfg, $src_storeid);
-        $src_format = checked_volume_format($storecfg, $src_volid);
-        $src_path = PVE::Storage::path($storecfg, $src_volid, $snapname);
-        $src_is_iscsi = ($src_path =~ m|^iscsi://|);
-        $cachemode = 'none' if $src_scfg->{type} eq 'zfspool';
-    } elsif (-f $src_volid || -b $src_volid) {
-        $src_path = $src_volid;
-        if ($opts->{'source-path-format'}) {
-            $src_format = $opts->{'source-path-format'};
-        } elsif ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
-            $src_format = $1;
-        }
-    }
-
-    die "source '$src_volid' is not a valid volid nor path for qemu-img convert\n" if !$src_path;
-
-    my $dst_scfg = PVE::Storage::storage_config($storecfg, $dst_storeid);
-    my $dst_format = checked_volume_format($storecfg, $dst_volid);
-    my $dst_path = PVE::Storage::path($storecfg, $dst_volid);
-    my $dst_is_iscsi = ($dst_path =~ m|^iscsi://|);
-
-    my $cmd = [];
-    push @$cmd, '/usr/bin/qemu-img', 'convert', '-p', '-n';
-    push @$cmd, '-l', "snapshot.name=$snapname"
-        if $snapname && $src_format && $src_format eq "qcow2";
-    push @$cmd, '-t', 'none' if $dst_scfg->{type} eq 'zfspool';
-    push @$cmd, '-T', $cachemode if defined($cachemode);
-    push @$cmd, '-r', "${bwlimit}K" if defined($bwlimit);
-
-    if ($src_is_iscsi) {
-        push @$cmd, '--image-opts';
-        $src_path = convert_iscsi_path($src_path);
-    } elsif ($src_format) {
-        push @$cmd, '-f', $src_format;
-    }
-
-    if ($dst_is_iscsi) {
-        push @$cmd, '--target-image-opts';
-        $dst_path = convert_iscsi_path($dst_path);
-    } else {
-        push @$cmd, '-O', $dst_format;
-    }
-
-    push @$cmd, $src_path;
-
-    if (!$dst_is_iscsi && $opts->{'is-zero-initialized'}) {
-        push @$cmd, "zeroinit:$dst_path";
-    } else {
-        push @$cmd, $dst_path;
-    }
-
-    my $parser = sub {
-        my $line = shift;
-        if ($line =~ m/\((\S+)\/100\%\)/) {
-            my $percent = $1;
-            my $transferred = int($size * $percent / 100);
-            my $total_h = render_bytes($size, 1);
-            my $transferred_h = render_bytes($transferred, 1);
-
-            print "transferred $transferred_h of $total_h ($percent%)\n";
-        }
-
-    };
-
-    eval { run_command($cmd, timeout => undef, outfunc => $parser); };
-    my $err = $@;
-    die "copy failed: $err" if $err;
-}
-
 sub qemu_drive_mirror {
     my (
         $vmid,
@@ -8913,7 +8809,7 @@ sub clone_disk {
                     'is-zero-initialized' => $sparseinit,
                     snapname => $snapname,
                 };
-                qemu_img_convert($drive->{file}, $newvolid, $size, $opts);
+                PVE::QemuServer::QemuImage::convert($drive->{file}, $newvolid, $size, $opts);
             }
         }
     }
@@ -8998,7 +8894,7 @@ sub create_efidisk($$$$$$$$) {
     my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $vars_size);
     PVE::Storage::activate_volumes($storecfg, [$volid]);
 
-    qemu_img_convert($ovmf_vars, $volid, $vars_size_b);
+    PVE::QemuServer::QemuImage::convert($ovmf_vars, $volid, $vars_size_b);
     my $size = PVE::Storage::volume_size_info($storecfg, $volid, 3);
 
     return ($volid, $size / 1024);
diff --git a/src/PVE/QemuServer/ImportDisk.pm b/src/PVE/QemuServer/ImportDisk.pm
index 8ecd5521..01289fc5 100755
--- a/src/PVE/QemuServer/ImportDisk.pm
+++ b/src/PVE/QemuServer/ImportDisk.pm
@@ -4,9 +4,11 @@ use strict;
 use warnings;
 
 use PVE::Storage;
-use PVE::QemuServer;
 use PVE::Tools qw(run_command extract_param);
 
+use PVE::QemuServer;
+use PVE::QemuServer::QemuImage;
+
 # imports an external disk image to an existing VM
 # and creates by default a drive entry unused[n] pointing to the created volume
 # $params->{drive_name} may be used to specify ide0, scsi1, etc ...
@@ -82,7 +84,7 @@ sub do_import {
             local $SIG{PIPE} = sub { die "interrupted by signal $!\n"; };
 
         PVE::Storage::activate_volumes($storecfg, [$dst_volid]);
-        PVE::QemuServer::qemu_img_convert(
+        PVE::QemuImage::convert(
             $src_path,
             $dst_volid,
             $src_size,
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index 7d3830de..a34ec83b 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -15,6 +15,7 @@ SOURCES=Agent.pm	\
 	MetaInfo.pm	\
 	Monitor.pm	\
 	PCI.pm		\
+	QemuImage.pm	\
 	QMPHelpers.pm	\
 	RNG.pm		\
 	StateFile.pm	\
diff --git a/src/PVE/QemuServer/QemuImage.pm b/src/PVE/QemuServer/QemuImage.pm
new file mode 100644
index 00000000..38f7d52b
--- /dev/null
+++ b/src/PVE/QemuServer/QemuImage.pm
@@ -0,0 +1,123 @@
+package PVE::QemuServer::QemuImage;
+
+use strict;
+use warnings;
+
+use PVE::Format qw(render_bytes);
+use PVE::Storage;
+use PVE::Tools;
+
+use PVE::QemuServer::Drive qw(checked_volume_format);
+use PVE::QemuServer::Helpers;
+
+sub convert_iscsi_path {
+    my ($path) = @_;
+
+    if ($path =~ m|^iscsi://([^/]+)/([^/]+)/(.+)$|) {
+        my $portal = $1;
+        my $target = $2;
+        my $lun = $3;
+
+        my $initiator_name = PVE::QemuServer::Helpers::get_iscsi_initiator_name();
+
+        return "file.driver=iscsi,file.transport=tcp,file.initiator-name=$initiator_name,"
+            . "file.portal=$portal,file.target=$target,file.lun=$lun,driver=raw";
+    }
+
+    die "cannot convert iscsi path '$path', unknown format\n";
+}
+
+# The possible options are:
+# bwlimit - The bandwidth limit in KiB/s.
+# is-zero-initialized - If the destination image is zero-initialized.
+# snapname - Use this snapshot of the source image.
+# source-path-format - Indicate the format of the source when the source is a path. For PVE-managed
+# volumes, the format from the storage layer is always used.
+sub convert {
+    my ($src_volid, $dst_volid, $size, $opts) = @_;
+
+    my ($bwlimit, $snapname) = $opts->@{qw(bwlimit snapname)};
+
+    my $storecfg = PVE::Storage::config();
+    my ($src_storeid) = PVE::Storage::parse_volume_id($src_volid, 1);
+    my ($dst_storeid) = PVE::Storage::parse_volume_id($dst_volid, 1);
+
+    die "destination '$dst_volid' is not a valid volid form qemu-img convert\n" if !$dst_storeid;
+
+    my $cachemode;
+    my $src_path;
+    my $src_is_iscsi = 0;
+    my $src_format;
+
+    if ($src_storeid) {
+        PVE::Storage::activate_volumes($storecfg, [$src_volid], $snapname);
+        my $src_scfg = PVE::Storage::storage_config($storecfg, $src_storeid);
+        $src_format = checked_volume_format($storecfg, $src_volid);
+        $src_path = PVE::Storage::path($storecfg, $src_volid, $snapname);
+        $src_is_iscsi = ($src_path =~ m|^iscsi://|);
+        $cachemode = 'none' if $src_scfg->{type} eq 'zfspool';
+    } elsif (-f $src_volid || -b $src_volid) {
+        $src_path = $src_volid;
+        if ($opts->{'source-path-format'}) {
+            $src_format = $opts->{'source-path-format'};
+        } elsif ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
+            $src_format = $1;
+        }
+    }
+
+    die "source '$src_volid' is not a valid volid nor path for qemu-img convert\n" if !$src_path;
+
+    my $dst_scfg = PVE::Storage::storage_config($storecfg, $dst_storeid);
+    my $dst_format = checked_volume_format($storecfg, $dst_volid);
+    my $dst_path = PVE::Storage::path($storecfg, $dst_volid);
+    my $dst_is_iscsi = ($dst_path =~ m|^iscsi://|);
+
+    my $cmd = [];
+    push @$cmd, '/usr/bin/qemu-img', 'convert', '-p', '-n';
+    push @$cmd, '-l', "snapshot.name=$snapname"
+        if $snapname && $src_format && $src_format eq "qcow2";
+    push @$cmd, '-t', 'none' if $dst_scfg->{type} eq 'zfspool';
+    push @$cmd, '-T', $cachemode if defined($cachemode);
+    push @$cmd, '-r', "${bwlimit}K" if defined($bwlimit);
+
+    if ($src_is_iscsi) {
+        push @$cmd, '--image-opts';
+        $src_path = convert_iscsi_path($src_path);
+    } elsif ($src_format) {
+        push @$cmd, '-f', $src_format;
+    }
+
+    if ($dst_is_iscsi) {
+        push @$cmd, '--target-image-opts';
+        $dst_path = convert_iscsi_path($dst_path);
+    } else {
+        push @$cmd, '-O', $dst_format;
+    }
+
+    push @$cmd, $src_path;
+
+    if (!$dst_is_iscsi && $opts->{'is-zero-initialized'}) {
+        push @$cmd, "zeroinit:$dst_path";
+    } else {
+        push @$cmd, $dst_path;
+    }
+
+    my $parser = sub {
+        my $line = shift;
+        if ($line =~ m/\((\S+)\/100\%\)/) {
+            my $percent = $1;
+            my $transferred = int($size * $percent / 100);
+            my $total_h = render_bytes($size, 1);
+            my $transferred_h = render_bytes($transferred, 1);
+
+            print "transferred $transferred_h of $total_h ($percent%)\n";
+        }
+
+    };
+
+    eval { PVE::Tools::run_command($cmd, timeout => undef, outfunc => $parser); };
+    my $err = $@;
+    die "copy failed: $err" if $err;
+}
+
+1;
diff --git a/src/test/run_qemu_img_convert_tests.pl b/src/test/run_qemu_img_convert_tests.pl
index 86eb53be..b5a457c3 100755
--- a/src/test/run_qemu_img_convert_tests.pl
+++ b/src/test/run_qemu_img_convert_tests.pl
@@ -8,7 +8,7 @@ use lib qw(..);
 use Test::More;
 use Test::MockModule;
 
-use PVE::QemuServer;
+use PVE::QemuServer::QemuImage;
 
 my $vmid = 8006;
 my $storage_config = {
@@ -498,21 +498,24 @@ $zfsplugin_module->mock(
     },
 );
 
-# we use the exported run_command so we have to mock it there
-my $qemu_server_module = Test::MockModule->new("PVE::QemuServer");
-$qemu_server_module->mock(
-    run_command => sub {
-        $command = shift;
-    },
+my $qemu_server_helpers_module = Test::MockModule->new("PVE::QemuServer::Helpers");
+$qemu_server_helpers_module->mock(
     get_iscsi_initiator_name => sub {
         return "foobar";
     },
 );
 
+my $tools_module = Test::MockModule->new("PVE::Tools");
+$tools_module->mock(
+    run_command => sub {
+        $command = shift;
+    },
+);
+
 foreach my $test (@$tests) {
     my $name = $test->{name};
     my $expected = $test->{expected};
-    eval { PVE::QemuServer::qemu_img_convert(@{ $test->{parameters} }) };
+    eval { PVE::QemuServer::QemuImage::convert(@{ $test->{parameters} }) };
     if (my $err = $@) {
         is($err, $expected, $name);
     } elsif (defined($command)) {
-- 
2.47.2





More information about the pve-devel mailing list