From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id EF4F21FF13B for ; Wed, 22 Apr 2026 13:16:18 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4E991180A4; Wed, 22 Apr 2026 13:14:33 +0200 (CEST) From: "Max R. Carrara" To: pve-devel@lists.proxmox.com Subject: [PATCH pve-storage v1 21/54] test: guest import: add tests for PVE::GuestImport Date: Wed, 22 Apr 2026 13:12:47 +0200 Message-ID: <20260422111322.257380-22-m.carrara@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260422111322.257380-1-m.carrara@proxmox.com> References: <20260422111322.257380-1-m.carrara@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1776856337732 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.085 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: DDQUZI6SECJ2MWNJC4CBGBZ3DGTZ5TLV X-Message-ID-Hash: DDQUZI6SECJ2MWNJC4CBGBZ3DGTZ5TLV X-MailFrom: m.carrara@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 command, +optionally with a certain C<$format>. + +The known formats are currently C, C, and C. + +=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 C<$file_path> and put a file named +C<$content_file_name> inside using the C 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