[PATCH pve-storage v1 49/54] fix #2884: support nested subdir scanning for 'vztmpl' volume type

Max R. Carrara
2026-04-22T11:20:22Z

Add support for parsing nested subdirectories for the 'vztmpl' volume
type by adapting its corresponding regex used by parsing helpers.

Add additional test cases wherever applicable to account for nested
subdirectories for 'vztmpl' volumes, as done for the 'iso' volume
type.

Originally-by: Noel Ullreich <n.ullreich@proxmox.com>
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
src/PVE/Storage/Common/Parse.pm | 1 +
src/PVE/Storage/Common/test/parser_tests.pl | 54 ++++++++++++++
src/PVE/Storage/Plugin.pm | 4 +-
src/test/filesystem_path_test.pm | 18 +++++
src/test/list_volumes_test.pm | 81 +++++++++++++++++++++
src/test/parse_volname_test.pm | 57 +++++++++++++++
src/test/path_to_volume_id_test.pm | 8 ++
7 files changed, 221 insertions(+), 2 deletions(-)

diff --git a/src/PVE/Storage/Common/Parse.pm b/src/PVE/Storage/Common/Parse.pm
index f98b702d..92e5b0fd 100644
--- a/src/PVE/Storage/Common/Parse.pm
+++ b/src/PVE/Storage/Common/Parse.pm
@@ -84,6 +84,7 @@ my $RE_ISO_FILE_PATH = qr!

my $RE_VZTMPL_FILE_PATH = qr!
(?<path>
+ (? $RE_DIRECTORY_COMPONENTS )?
(?<file>
[^/]+
\.
diff --git a/src/PVE/Storage/Common/test/parser_tests.pl b/src/PVE/Storage/Common/test/parser_tests.pl
index ad122cd2..c80d0d90 100755
--- a/src/PVE/Storage/Common/test/parser_tests.pl
+++ b/src/PVE/Storage/Common/test/parser_tests.pl
@@ -230,6 +230,36 @@ my $volname_cases_vztmpl_valid = [
volname => 'vztmpl/Alpine 3.10 default_20190626_amd64.Tar.xZ',
},
},
+
+ # subdirectories
+ {
+ path => 'subdir/debian-10.0-standard_10.0-1_amd64.tar.zst',
+ expected => {
+ file => 'debian-10.0-standard_10.0-1_amd64.tar.zst',
+ ext => 'tar.zst',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'zst',
+ dir => 'subdir',
+ 'disk-path' => 'subdir/debian-10.0-standard_10.0-1_amd64.tar.zst',
+ path => 'subdir/debian-10.0-standard_10.0-1_amd64.tar.zst',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/subdir/debian-10.0-standard_10.0-1_amd64.tar.zst',
+ },
+ },
+ {
+ path => 'deeply/nested/dir/debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ expected => {
+ file => 'debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ ext => 'tar.bz2',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'bz2',
+ dir => 'deeply/nested/dir',
+ 'disk-path' => 'deeply/nested/dir/debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ path => 'deeply/nested/dir/debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/deeply/nested/dir/debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ },
+ },
];

my $volname_cases_vztmpl_invalid = [
@@ -241,6 +271,30 @@ my $volname_cases_vztmpl_invalid = [
},
expected => undef,
},
+ {
+ description => "Parent dir reference in path (beginning) (vztmpl)",
+ args => {
+ path => '../archlinux-base_20190924-1_amd64.tar.gz',
+ vtype => 'vztmpl',
+ },
+ expected => undef,
+ },
+ {
+ description => "Parent dir reference in path (middle) (vztmpl)",
+ args => {
+ path => 'subdir/../archlinux-base_20190924-1_amd64.tar.gz',
+ vtype => 'vztmpl',
+ },
+ expected => undef,
+ },
+ {
+ description => "Parent dir reference in path (end) (vztmpl)",
+ args => {
+ path => 'subdir/archlinux-base_20190924-1_amd64.tar.gz/..',
+ vtype => 'vztmpl',
+ },
+ expected => undef,
+ },
];

my $volname_cases_backup_valid = [
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 0fbcbd4b..2df56e76 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -235,7 +235,7 @@ my $defaultData = {
},
'max-scan-depth' => {
description => "Maximum depth of subdirectories to traverse when searching for"
- . " ISOs in directories.",
+ . " ISOs and container templates in directories.",
type => 'integer',
default => 0,
minimum => 0,
@@ -1823,7 +1823,7 @@ sub list_volumes {
}

if ($type eq 'vztmpl' && !defined($vmid)) {
- return get_subdir_files($storeid, $scfg, 'vztmpl', undef);
+ return get_subdir_files($storeid, $scfg, 'vztmpl', undef, $depth);
}

if ($type eq 'backup') {
diff --git a/src/test/filesystem_path_test.pm b/src/test/filesystem_path_test.pm
index 26a74a0d..b5c1ab33 100644
--- a/src/test/filesystem_path_test.pm
+++ b/src/test/filesystem_path_test.pm
@@ -56,6 +56,24 @@ my $tests = [
'iso',
],
},
+ {
+ volname => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
+ snapname => undef,
+ expected => [
+ "$DEFAULT_STORAGE_DIR/template/cache/debian-10.0-standard_10.0-1_amd64.tar.gz",
+ undef,
+ 'vztmpl',
+ ],
+ },
+ {
+ volname => 'vztmpl/foo/bar/baz/debian-10.0-standard_10.0-1_amd64.tar.gz',
+ snapname => undef,
+ expected => [
+ "$DEFAULT_STORAGE_DIR/template/cache/foo/bar/baz/debian-10.0-standard_10.0-1_amd64.tar.gz",
+ undef,
+ 'vztmpl',
+ ],
+ },
{
volname => "backup/vzdump-qemu-1234-2020_03_30-21_12_40.vma",
snapname => undef,
diff --git a/src/test/list_volumes_test.pm b/src/test/list_volumes_test.pm
index 0daaba94..6bdfa37c 100644
--- a/src/test/list_volumes_test.pm
+++ b/src/test/list_volumes_test.pm
@@ -1321,6 +1321,25 @@ my $test_param_list = [
file => "$DEFAULT_STORAGE_PATH/template/iso/1/2/3/4/5/some-installer.iso",
expected => undef,
},
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/1/some-lxc-template.tar.gz",
+ expected => undef,
+ },
+ {
+ file =>
+ "$DEFAULT_STORAGE_PATH/template/cache/1/2/3/4/5/some-lxc-template.tar.gz",
+ expected => undef,
+ },
],
},
{
@@ -1366,6 +1385,31 @@ my $test_param_list = [
file => "$DEFAULT_STORAGE_PATH/template/iso/1/2/some-installer.iso",
expected => undef,
},
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/1/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/1/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ # Exceeds max-scan-depth
+ file => "$DEFAULT_STORAGE_PATH/template/cache/1/2/some-lxc-template.tar.gz",
+ expected => undef,
+ },
],
},
{
@@ -1421,6 +1465,43 @@ my $test_param_list = [
file => "$DEFAULT_STORAGE_PATH/template/iso/1/2/3/4/5/6/some-installer.iso",
expected => undef,
},
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ file => "$DEFAULT_STORAGE_PATH/template/cache/1/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/1/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ file =>
+ "$DEFAULT_STORAGE_PATH/template/cache/1/2/3/4/5/some-lxc-template.tar.gz",
+ expected => {
+ content => 'vztmpl',
+ ctime => $DEFAULT_CTIME,
+ format => 'tgz',
+ size => $DEFAULT_SIZE,
+ volid => 'local:vztmpl/1/2/3/4/5/some-lxc-template.tar.gz',
+ },
+ },
+ {
+ # Exceeds max-scan-depth
+ file =>
+ "$DEFAULT_STORAGE_PATH/template/cache/1/2/3/4/5/6/some-lxc-template.tar.gz",
+ expected => undef,
+ },
],
},
];
diff --git a/src/test/parse_volname_test.pm b/src/test/parse_volname_test.pm
index 24d1ed0e..b90815c2 100644
--- a/src/test/parse_volname_test.pm
+++ b/src/test/parse_volname_test.pm
@@ -245,10 +245,67 @@ my $tests = [
'vztmpl', "$file_name", undef, undef, undef, undef, 'raw',
],
},
+ {
+ description => "Container template, $suffix, subdirectory",
+ volname => "vztmpl/foo/$file_name",
+ expected => [
+ 'vztmpl', "foo/$file_name", undef, undef, undef, undef, 'raw',
+ ],
+ },
+ {
+ description => "Container template, $suffix, nested subdirectories",
+ volname => "vztmpl/foo/bar/baz/$file_name",
+ expected => [
+ 'vztmpl', "foo/bar/baz/$file_name", undef, undef, undef, undef, 'raw',
+ ],
+ },
+ {
+ description =>
+ "Container template, $suffix, subdirectory with same name as file",
+ volname => "vztmpl/$file_name/$file_name",
+ expected => [
+ 'vztmpl', "$file_name/$file_name", undef, undef, undef, undef, 'raw',
+ ],
+ },
);

push($tests->@*, @extra_tests);
}
+
+ # Failed tests
+ {
+ my $file_name = "$prefix.tar.gz";
+
+ my @extra_failed_tests = (
+ {
+ description =>
+ "Container template, tar.gz, parent directory reference before volume type prefix",
+ volname => "../vztmpl/$file_name",
+ expected => "unable to parse directory volume name '../vztmpl/$file_name'\n",
+ },
+ {
+ description =>
+ "Container template, tar.gz, parent directory reference at beginning of volume path",
+ volname => "vztmpl/../$file_name",
+ expected => "unable to parse directory volume name 'vztmpl/../$file_name'\n",
+ },
+ {
+ description =>
+ "Container template, tar.gz, parent directory reference at end of volume path",
+ volname => "vztmpl/$file_name/..",
+ expected => "unable to parse directory volume name 'vztmpl/$file_name/..'\n",
+ },
+ {
+ description =>
+ "Container template, tar.gz, parent directory reference between dir components of volume path",
+ volname => "vztmpl/foo/../bar/$file_name",
+ expected =>
+ "unable to parse directory volume name 'vztmpl/foo/../bar/$file_name'\n",
+ },
+ );
+
+ push($tests->@*, @extra_failed_tests);
+ }
}

# Additional tests for backup files
diff --git a/src/test/path_to_volume_id_test.pm b/src/test/path_to_volume_id_test.pm
index bc87d289..4dfc68e1 100644
--- a/src/test/path_to_volume_id_test.pm
+++ b/src/test/path_to_volume_id_test.pm
@@ -137,6 +137,14 @@ my $tests = [
'vztmpl', 'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
],
},
+ {
+ description => 'CT template, tar.gz, nested subdirectories',
+ file =>
+ "$DEFAULT_STORAGE_DIR/template/cache/foo/bar/debian-10.0-standard_10.0-1_amd64.tar.gz",
+ expected => [
+ 'vztmpl', 'local:vztmpl/foo/bar/debian-10.0-standard_10.0-1_amd64.tar.gz',
+ ],
+ },
{
description => 'CT template, wrong ending, tar bz2',
file => "$DEFAULT_STORAGE_DIR/template/cache/debian-10.0-standard_10.0-1_amd64.tar.bz2",
--
2.47.3