public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Max R. Carrara" <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-storage v2 21/50] test: guest import: add tests for PVE::GuestImport
Date: Wed, 23 Sep 2026 17:05:35 +0200	[thread overview]
Message-ID: <20260923150606.531239-22-m.carrara@proxmox.com> (raw)
In-Reply-To: <20260923150606.531239-1-m.carrara@proxmox.com>

Set up a new test script for the `extract_disk_from_import_file()` sub
of `PVE::GuestImport` and run it as part of
`src/test/run_plugin_tests.pl`.

Make this possible by mocking cluster-related functionality that would
otherwise require to be run as root, namely reading, updating and
locking the storage config, as well as retrieving the list of
available VMs.

Then, make the test cases declarative in such a way that they are
mostly self-documenting regarding what they test. Document the keys
that the test declarations use.

Run each test in its clean environment, creating and removing the
entire directory structure before and after each test. Provide small
helpers specific to the testing code for creating arbitrary QEMU
images / .ova files and document them briefly.

Test for successful as well as failed imports.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/test/guest_import_test.pl | 948 ++++++++++++++++++++++++++++++++++
 src/test/run_plugin_tests.pl  |   1 +
 2 files changed, 949 insertions(+)
 create mode 100755 src/test/guest_import_test.pl

diff --git a/src/test/guest_import_test.pl b/src/test/guest_import_test.pl
new file mode 100755
index 00000000..04eec24d
--- /dev/null
+++ b/src/test/guest_import_test.pl
@@ -0,0 +1,948 @@
+#!/usr/bin/env perl
+
+use v5.36;
+
+use lib qw(..);
+
+use re qw(is_regexp);
+
+use Carp qw(confess);
+use File::Basename qw(dirname basename);
+use File::Path qw(make_path remove_tree);
+use File::Spec;
+use File::Temp;
+
+use PVE::Cluster;
+use PVE::GuestImport;
+use PVE::Storage;
+use PVE::Storage::Common qw(plugin_get_vtype_subdir);
+use PVE::Tools qw(run_command);
+
+use Test::More;
+use Test::MockModule;
+
+$ENV{TZ} = 'UTC';
+
+my $DEFAULT_SIZE = 4 * 1024 * 1024; # 4 MiB
+
+my $SOURCE_STOREID = 'source-store';
+my $SOURCE_STORAGE_PATH = File::Temp->newdir();
+
+my $TARGET_STOREID = 'target-store';
+my $TARGET_STORAGE_PATH = File::Temp->newdir();
+
+# Not actually set up on disk or elsewhere
+my $DUMMY_LVMTHIN_STOREID = 'dummy-lvmthin';
+
+my $STORAGE_CFG = <<~EOF;
+dir: $SOURCE_STOREID
+    shared 0
+    path $SOURCE_STORAGE_PATH
+    content images,iso,import
+
+dir: $TARGET_STOREID
+    shared 0
+    path $TARGET_STORAGE_PATH
+    content images,iso,import
+
+lvmthin: $DUMMY_LVMTHIN_STOREID
+    thinpool data
+    vgname pve
+    content images,rootdir
+EOF
+
+my $MOCKED_VMLIST = {
+    version => 1,
+    ids => {
+        1337 => {
+            node => 'node-0',
+            type => 'qemu',
+            version => 4,
+        },
+        1338 => {
+            node => 'node-0',
+            type => 'qemu',
+            version => 7,
+        },
+        1339 => {
+            node => 'node-0',
+            type => 'qemu',
+            version => 7,
+        },
+    },
+};
+
+my $mock_pve_cluster = Test::MockModule->new('PVE::Cluster')->redefine(
+    cfs_update => sub { },
+    get_config => sub($file) {
+        if ($file eq 'storage.cfg') {
+            return $STORAGE_CFG;
+        }
+
+        confess "mocked get_config() - file '$file' not handled";
+    },
+    get_vmlist => sub() { return $MOCKED_VMLIST; },
+);
+
+my $_LOCKDIR = File::Temp->newdir();
+my $mock_pve_storage_plugin = Test::MockModule->new('PVE::Storage::Plugin')->redefine(
+    cluster_lock_storage => sub($class, $storeid, $shared, $timeout, $func, @param) {
+        confess "\$shared = 1 is not handled" if $shared;
+
+        my $res = PVE::Tools::lock_file("$_LOCKDIR/pve-storage-test", $timeout, $func, @param);
+        die $@ if $@;
+        return $res;
+    },
+);
+
+=head3 create_test_image_file
+
+    my $file_path = "/foo/bar/disk.qcow2";
+    create_test_image_file($file_path, 'qcow2');
+
+Create a new QEMU image at C<$file_path> using the C<qemu-img> command,
+optionally with a certain C<$format>.
+
+The known formats are currently C<qcow2>, C<raw>, and C<vmdk>.
+
+=cut
+
+my sub create_test_image_file : prototype($;$) ($file_path, $format = undef) {
+    my $KNOWN_FORMATS = {
+        qcow2 => 1,
+        raw => 1,
+        vmdk => 1,
+    };
+
+    my $command = ['/usr/bin/qemu-img', 'create', $file_path, $DEFAULT_SIZE];
+
+    if (defined($format) && defined($KNOWN_FORMATS->{$format})) {
+        push($command->@*, '-f', $format);
+    }
+
+    eval { run_command($command, timeout => 10); };
+    confess $@ if $@;
+
+    return;
+}
+
+=head3 create_test_ova_file
+
+    my $file_path = "/srv/imports/some-import.ova";
+    my $content_file_name = "some-disk.qcow2";
+
+    create_test_ova_file($file_path, $content_file_name)
+
+Create a new C<.ova> file at the I<absolute> C<$file_path> and put a file named
+C<$content_file_name> inside using the C<tar> command. C<$content_file_name>
+must not have any directory components and is assumed to be in the same
+directory as C<$file_path>.
+
+=cut
+
+my sub create_test_ova_file : prototype($$) ($file_path, $content_file_name) {
+    my $file_path_dir = dirname($file_path);
+
+    confess "\$content_file_name ne basename(\$content_file_name)"
+        if $content_file_name ne basename($content_file_name);
+
+    my $tar_command = [
+        'tar',
+        '-c',
+        '--force-local',
+        '--no-same-owner',
+        '-f',
+        $file_path,
+        '-C',
+        $file_path_dir,
+        $content_file_name,
+    ];
+
+    eval { run_command($tar_command, timeout => 10); };
+    confess $@ if $@;
+
+    return;
+}
+
+=head2 TEST CASE FORMAT
+
+The parameters for individual tests are hashes with the following keys:
+
+    {
+        # Name of the test, also displayed on error.
+        description => "Importing a single disk, no existing disks (qcow2)",
+
+        # Definition of the source storage, i.e. the storage from which to import.
+        source => {
+            # The ID of the source storage.
+            storeid => $SOURCE_STOREID,
+
+            # A setup function used to set up the necessary volumes before the test cases are run.
+            # This is used to actually create the file(s) to be imported, for example.
+            'fn-setup-volumes' => sub($storeid) {
+                # [...]
+                return;
+            },
+        },
+
+        # Definition of the target storage, i.e. the storage which disk images are imported to.
+        target => {
+
+            # The ID of the target storage.
+            storeid => $TARGET_STOREID,
+
+            # Like for the source storage, this function can be used to set up any pre-existing
+            # volumes for the storage.
+            'fn-setup-volumes' => sub($storeid) {
+                # [...]
+                return;
+            },
+        },
+
+        # The cases being tested.
+        cases => [
+            {
+                # The name of the volume to import.
+                volname => 'import/some-import.ova/disk.qcow2',
+
+                # The ID of the guest to import the volume for.
+                vmid => 1337,
+
+                # The expected outcome when the invocation succeeds.
+                # May be a string or a regular expression.
+                # If this is a string, then the output has to be an exact match.
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-0.qcow2",
+            },
+
+            {
+                volname => 'import/some-import.ova/disk-link.qcow2',
+                vmid => 1337,
+
+                # The expected outcome when the invocation fails.
+                # May be a string or a regular expression.
+                # If this is a string, then the output has to be an exact match.
+                'expect-fail' => qr/extracted file .* not a regular file/,
+            },
+        ],
+    },
+
+=cut
+
+my $tests = [
+    {
+        description => "Importing a single disk, no existing disks (qcow2)",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'qcow2');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-0.qcow2",
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, no existing disks (vmdk)",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.vmdk';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'vmdk');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.vmdk',
+                vmid => 1338,
+                'expect-match' => "$TARGET_STOREID:1338/vm-1338-disk-0.vmdk",
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, no existing disks (raw)",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.raw';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'raw');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.raw',
+                vmid => 1339,
+                'expect-match' => "$TARGET_STOREID:1339/vm-1339-disk-0.raw",
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, invalid extension",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.txt';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'qcow2');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.txt',
+                vmid => 1337,
+                'expect-fail' => qr/unable to parse directory volume name/,
+            },
+        ],
+    },
+    {
+        description => "Importing multiple disks, no existing disks (qcow2, vmdk, raw)",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                {
+                    my $volume_file_name = 'some-qcow2-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.qcow2';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'qcow2');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                {
+                    my $volume_file_name = 'some-vmdk-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.vmdk';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'vmdk');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                {
+                    my $volume_file_name = 'some-raw-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.raw';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'raw');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-qcow2-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-0.qcow2",
+            },
+            {
+                volname => 'import/some-vmdk-import.ova/disk.vmdk',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-1.vmdk",
+            },
+            {
+                volname => 'import/some-raw-import.ova/disk.raw',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-2.raw",
+            },
+        ],
+    },
+    {
+        description => "Importing multiple disks, existing disks (qcow2, vmdk, raw)",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                {
+                    my $volume_file_name = 'some-qcow2-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.qcow2';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'qcow2');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                {
+                    my $volume_file_name = 'some-vmdk-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.vmdk';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'vmdk');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                {
+                    my $volume_file_name = 'some-raw-import.ova';
+                    my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                    my $content_file_name = 'disk.raw';
+                    my $content_file_path =
+                        File::Spec->catfile($import_dir, $content_file_name);
+
+                    create_test_image_file($content_file_path, 'raw');
+
+                    create_test_ova_file($volume_file_path, $content_file_name);
+                }
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $vmid = 1337;
+
+                PVE::Storage::vdisk_alloc(
+                    $cfg, $storeid, $vmid, 'qcow2', undef, $DEFAULT_SIZE,
+                );
+
+                PVE::Storage::vdisk_alloc(
+                    $cfg, $storeid, $vmid, 'qcow2', undef, $DEFAULT_SIZE,
+                );
+
+                return;
+            },
+        },
+        cases => [
+            {
+                volname => 'import/some-qcow2-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-2.qcow2",
+            },
+            {
+                volname => 'import/some-vmdk-import.ova/disk.vmdk',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-3.vmdk",
+            },
+            {
+                volname => 'import/some-raw-import.ova/disk.raw',
+                vmid => 1337,
+                'expect-match' => "$TARGET_STOREID:1337/vm-1337-disk-4.raw",
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, invalid volume name",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-🐧-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-fail' => qr/unable to parse directory volume name/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, invalid volume type",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $iso_dir = PVE::Storage::get_iso_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-linux-distro.iso';
+                my $volume_file_path = File::Spec->catfile($iso_dir, $volume_file_name);
+
+                create_test_image_file($volume_file_path, undef);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'iso/some-linux-distro.iso',
+                vmid => 1337,
+                'expect-fail' => qr/only files with content type 'import' can be extracted/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, not .ova file",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-qcow2-import.ovf';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                create_test_image_file($volume_file_path, undef);
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-qcow2-import.ovf',
+                vmid => 1337,
+                'expect-fail' => qr/only files from 'ova' format can be extracted/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, no content referenced",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'qcow2');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova',
+                vmid => 1337,
+                'expect-fail' => qr/only files from 'ova' format can be extracted/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, invalid archive format for .ova",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                create_test_image_file($volume_file_path, 'qcow2');
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-fail' => qr/error during extraction/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, symlink instead of file",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                my $symlink_file_name = 'disk-link.qcow2';
+                my $symlink_file_path = File::Spec->catfile($import_dir, $symlink_file_name);
+
+                create_test_image_file($content_file_path, 'qcow2');
+
+                symlink($content_file_path, $symlink_file_path)
+                    or confess "creating symlink failed";
+
+                create_test_ova_file($volume_file_path, $symlink_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk-link.qcow2',
+                vmid => 1337,
+                'expect-fail' => qr/extracted file .* not a regular file/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, directory instead of file",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                make_path($content_file_path, { verbose => 1, mode => 0750 });
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-fail' => qr/extracted file .* not a regular file/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, file is not an image",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                eval { `touch $content_file_path` };
+                confess $@ if $@;
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $TARGET_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.qcow2',
+                vmid => 1337,
+                'expect-fail' => qr/Could not open .* Image is not in qcow2 format/,
+            },
+        ],
+    },
+    {
+        description => "Importing a single disk, incompatible target storage type",
+        source => {
+            storeid => $SOURCE_STOREID,
+            'fn-setup-volumes' => sub($storeid) {
+                my $cfg = PVE::Storage::config();
+                my $import_dir = PVE::Storage::get_import_dir($cfg, $storeid);
+
+                my $volume_file_name = 'some-import.ova';
+                my $volume_file_path = File::Spec->catfile($import_dir, $volume_file_name);
+
+                my $content_file_name = 'disk.qcow2';
+                my $content_file_path = File::Spec->catfile($import_dir, $content_file_name);
+
+                create_test_image_file($content_file_path, 'qcow2');
+
+                create_test_ova_file($volume_file_path, $content_file_name);
+
+                return;
+            },
+        },
+        target => {
+            storeid => $DUMMY_LVMTHIN_STOREID,
+            'fn-setup-volumes' => sub($storeid) { return; },
+        },
+        cases => [
+            {
+                volname => 'import/some-import.ova/disk.qcow2',
+                vmid => 1339,
+                'expect-fail' => qr/has no path/,
+            },
+        ],
+    },
+];
+
+my sub setup_scfg_dir($scfg) {
+    confess "\$scfg has no 'path' key" if !$scfg->{path};
+
+    make_path($scfg->{path}, { verbose => 1, mode => 0700 });
+
+    my @vtype_subdirs = map { plugin_get_vtype_subdir($scfg, $_) } keys $scfg->{content}->%*;
+
+    make_path(@vtype_subdirs, { verbose => 1, mode => 0755 });
+
+    return;
+}
+
+my sub setup_test_env($test_def) {
+    my ($source, $target) = $test_def->@{qw(source target)};
+
+    my $setup_storage = sub($storage_def) {
+        my $cfg = PVE::Storage::config();
+
+        my $storeid = $storage_def->{storeid};
+        die "\$storeid = undef" if !defined($storeid);
+
+        my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+        if ($scfg->{path}) {
+            setup_scfg_dir($scfg);
+        }
+
+        $storage_def->{'fn-setup-volumes'}->($storeid);
+    };
+
+    eval { $setup_storage->($source); };
+    if (my $err = $@) {
+        confess "failed to set up source storage: $err";
+    }
+
+    eval { $setup_storage->($target); };
+    if (my $err = $@) {
+        confess "failed to set up target storage: $err";
+    }
+
+    return;
+}
+
+my sub teardown_scfg_dir($scfg) {
+    confess "\$scfg has no 'path' key" if !$scfg->{path};
+
+    remove_tree($scfg->{path}, { verbose => 0 });
+
+    return;
+}
+
+my sub teardown_test_env($test_def) {
+    my ($source, $target) = $test_def->@{qw(source target)};
+
+    my $teardown_storage = sub($storage_def) {
+        my $cfg = PVE::Storage::config();
+
+        my $storeid = $storage_def->{storeid};
+        die "\$storeid = undef" if !defined($storeid);
+
+        my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+        if ($scfg->{path}) {
+            teardown_scfg_dir($scfg);
+        }
+    };
+
+    eval { $teardown_storage->($target); };
+    if (my $err = $@) {
+        confess "failed to tear down target storage: $err";
+    }
+
+    eval { $teardown_storage->($source); };
+    if (my $err = $@) {
+        confess "failed to tear down source storage: $err";
+    }
+
+    return;
+}
+
+my sub run_test($test_def) {
+    my ($desc, $cases) = $test_def->@{qw(description cases)};
+
+    eval { setup_test_env($test_def); };
+    if (my $err = $@) {
+        teardown_test_env($test_def);
+
+        fail($desc);
+        diag("Failed to set up test environment: $err");
+    }
+
+    my $source_storeid = $test_def->{source}->{storeid};
+    my $target_storeid = $test_def->{target}->{storeid};
+
+    for my $case ($cases->@*) {
+        my ($volname, $vmid) = $case->@{qw(volname vmid)};
+
+        confess "($desc) case cannot have both 'expect-match' and 'expect-fail'"
+            if defined($case->{'expect-match'}) && defined($case->{'expect-fail'});
+
+        my $source_volid = $source_storeid . ':' . $volname;
+
+        my $case_desc = "$desc | $vmid | $volname";
+
+        my $result = eval {
+            return PVE::GuestImport::extract_disk_from_import_file(
+                $source_volid, $vmid, $target_storeid,
+            );
+        };
+        if (my $err = $@) {
+            my $re_expect_fail = $case->{'expect-fail'};
+            $re_expect_fail = qr/^\Q$re_expect_fail\E$/ if !is_regexp($re_expect_fail);
+
+            if (!ok($err =~ $re_expect_fail, $case_desc)) {
+                diag(explain($err));
+                diag("does not match");
+                diag(explain($case->{'expect-fail'}));
+            }
+
+            next;
+        }
+
+        my $re_expect_match = $case->{'expect-match'};
+        $re_expect_match = qr/^\Q$re_expect_match\E$/ if !is_regexp($re_expect_match);
+
+        if (!ok($result =~ $re_expect_match, $case_desc)) {
+            diag(explain($result));
+            diag("does not match");
+            diag(explain($case->{'expect-match'}));
+        }
+    }
+
+    teardown_test_env($test_def);
+
+    return;
+}
+
+my sub main() {
+    my $sum_cases = 0;
+    for my $test_def ($tests->@*) {
+        $sum_cases += scalar($test_def->{cases}->@*);
+    }
+
+    plan tests => $sum_cases;
+
+    for my $test_def ($tests->@*) {
+        run_test($test_def);
+    }
+
+    done_testing();
+
+    return;
+}
+
+main();
diff --git a/src/test/run_plugin_tests.pl b/src/test/run_plugin_tests.pl
index 27c45b8c..7d3e2ce0 100755
--- a/src/test/run_plugin_tests.pl
+++ b/src/test/run_plugin_tests.pl
@@ -20,6 +20,7 @@ my $res = $harness->runtests(
     "archive_info_test.pm",
     "filesystem_path_test.pm",
     "get_subdir_test.pm",
+    "guest_import_test.pl",
     "list_volumes_test.pm",
     "parse_volname_test.pm",
     "path_to_volume_id_test.pm",
-- 
2.47.3





  parent reply	other threads:[~2026-09-23 15:09 UTC|newest]

Thread overview: 51+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-23 15:05 [PATCH manager/storage v2 00/50] Fix #2884: Implement Subdirectory Scanning for Dir-Based Storage Types Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 01/50] test: plugin tests: run tests with at most 4 jobs Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 02/50] plugin, common: remove superfluous use of =pod command paragraph Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 03/50] common: add POD headings for groups of helpers Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 04/50] common: use Exporter module for PVE::Storage::Common Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 05/50] plugin: make get_subdir_files a proper subroutine and update style Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 06/50] plugin api: replace helpers w/ standalone subs, bump API version & age Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 07/50] common: prevent autovivification in plugin_get_vtype_subdir helper Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 08/50] plugin: break up if-elsif chain into separate if-blocks Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 09/50] plugin: adapt get_subdir_files helper of list_volumes API method Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 10/50] plugin: update code style of list_volumes plugin " Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 11/50] plugin: use closure for obtaining raw volume data in list_volumes Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 12/50] plugin: use closure for inner loop logic " Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 13/50] storage: update code style in function path_to_volume_id Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 14/50] storage: break up if-elsif chain in path_to_volume_id Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 15/50] storage: heave vtype file path parsing logic inside loop into helper Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 16/50] storage: clean up code that was moved into helper in path_to_volume_id Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 17/50] api: status: move content type assert for up-/downloads into helper Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 18/50] api: status: use helper from common module to get content directory Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 19/50] api: status: move up-/download file path parsing code into helper Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 20/50] api: status: simplify file content assertion logic for up-/download Max R. Carrara
2026-09-23 15:05 ` Max R. Carrara [this message]
2026-09-23 15:05 ` [PATCH pve-storage v2 22/50] tree-wide: introduce parsing module and replace usages of ISO_EXT_RE_0 Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 23/50] common: test: set up parser testing code, add tests for 'iso' vtype Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 24/50] tree-wide: replace usages of VZTMPL_EXT_RE_1 with parsing functions Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 25/50] tree-wide: replace usages of BACKUP_EXT_RE_2 " Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 26/50] tree-wide: replace usages of inline regexes for snippets with parsers Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 27/50] tree-wide: partially replace usages of regexes for 'import' vtype Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 28/50] tree-wide: replace remaining " Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 29/50] tree-wide: simplify recently refactored parsing logic Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 30/50] test: list volumes: reorganize and modernize test running code Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 31/50] test: list volumes: fix broken test checking for vmlist modifications Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 32/50] test: list volumes: introduce new format for test cases Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 33/50] test: list volumes: remove legacy code and migrate cases to new format Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 34/50] plugin: do not return volumes of undeclared content types anymore Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 35/50] plugin: correct comment in get_subdir_files helper Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 36/50] test: parse volname: modernize code Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 37/50] test: parse volname: adapt tests regarding 'import' volume type Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 38/50] test: parse volname: move VM disk test creation into separate block Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 39/50] test: parse volname: move backup file test creation into sep. block Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 40/50] test: parse volname: parameterize test case creation for some vtypes Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 41/50] test: volume id: modernize code Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 42/50] test: volume id: rename 'volname' test case parameter to 'file' Max R. Carrara
2026-09-23 15:05 ` [PATCH pve-storage v2 43/50] test: filesystem path: modernize code Max R. Carrara
2026-09-23 15:16 ` [PATCH pve-storage v2 44/50] fix #2884: implement nested subdir scanning and support 'iso' vtype Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-storage v2 45/50] fix #2884: support nested subdir scanning for 'vztmpl' volume type Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-storage v2 46/50] fix #2884: support nested subdir scanning for 'snippets' vtype Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-storage v2 47/50] test: add more tests for 'import' vtype & guard against nested subdirs Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-storage v2 48/50] test: add tests guarding against subdir scanning for vtypes Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-manager v2 49/50] fix #2884: ui: storage: add field for 'max-scan-depth' property Max R. Carrara
2026-09-23 15:16   ` [PATCH pve-manager v2 50/50] pve8to9: use helper from common storage module to get vtype subdir Max R. Carrara

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=20260923150606.531239-22-m.carrara@proxmox.com \
    --to=m.carrara@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal