Compare commits

..

No commits in common. "02acde02b68a306b64bb7d025d870f5e82222e2d" and "43ec7bdfe6c8acbeaafb4bfa3bfd4aefad0a78c6" have entirely different histories.

21 changed files with 2636 additions and 1077 deletions

69
debian/changelog vendored
View File

@ -1,72 +1,3 @@
libpve-storage-perl (9.0.13) trixie; urgency=medium
* deactivate volumes: terminate error message with newline.
-- Proxmox Support Team <support@proxmox.com> Fri, 01 Aug 2025 18:36:51 +0200
libpve-storage-perl (9.0.12) trixie; urgency=medium
* plugin: fix parse_name_dir regression for custom volume names.
* fix #6584: plugin: list_images: only include parseable filenames.
* plugin: extend snapshot name parsing to legacy volnames.
* plugin: parse_name_dir: drop noisy deprecation warning.
* plugin: nfs, cifs: use volume qemu snapshot methods from dir plugin to
ensure a online-snapshot on such storage types with
snapshot-as-volume-chain enabled does not takes a internal qcow2 snapshot.
-- Proxmox Support Team <support@proxmox.com> Thu, 31 Jul 2025 14:22:12 +0200
libpve-storage-perl (9.0.11) trixie; urgency=medium
* lvm volume snapshot info: untaint snapshot filename
-- Proxmox Support Team <support@proxmox.com> Thu, 31 Jul 2025 09:18:56 +0200
libpve-storage-perl (9.0.10) trixie; urgency=medium
* RRD metrics: use new pve-storage-9.0 format RRD file location, if it
exists.
-- Proxmox Support Team <support@proxmox.com> Thu, 31 Jul 2025 04:14:19 +0200
libpve-storage-perl (9.0.9) trixie; urgency=medium
* fix #5181: pbs: store and read passwords as unicode.
* fix #6587: lvm plugin: snapshot info: fix parsing snapshot name.
* config: drop 'maxfiles' parameter, it was replaced with the more flexible
prune options in Proxmox VE 7.0 already.
-- Proxmox Support Team <support@proxmox.com> Wed, 30 Jul 2025 19:51:07 +0200
libpve-storage-perl (9.0.8) trixie; urgency=medium
* snapshot-as-volume-chain: fix offline removal of snapshot on directory
storage via UI/API by untainting/validating a filename correctly.
* snapshot-as-volume-chain: fix typo in log message for rebase operation.
* snapshot-as-volume-chain: ensure backing file references are kept relative
upon snapshot deletion. This ensures the backing chain stays intact should
the volumes be moved to a different path.
* fix #6561: ZFS: ensure refquota for container volumes is correctly applied
after rollback. The quota is tracked via a ZFS user property.
* btrfs plugin: remove unnecessary mkpath call
* drop some left-overs for 'rootdir' sub-directory handling that were
left-over from when Proxmox VE supported OpenVZ.
* path to volume ID conversion: properly quote regexes for hardening.
-- Proxmox Support Team <support@proxmox.com> Tue, 29 Jul 2025 17:17:11 +0200
libpve-storage-perl (9.0.7) trixie; urgency=medium
* fix #6553: lvmthin: implement volume_rollback_is_possible sub

View File

@ -415,10 +415,11 @@ __PACKAGE__->register_method({
code => sub {
my ($param) = @_;
my $path = "pve-storage-9.0/$param->{node}/$param->{storage}";
$path = "pve2-storage/$param->{node}/$param->{storage}"
if !-e "/var/lib/rrdcached/db/${path}";
return PVE::RRD::create_rrd_data($path, $param->{timeframe}, $param->{cf});
return PVE::RRD::create_rrd_data(
"pve2-storage/$param->{node}/$param->{storage}",
$param->{timeframe},
$param->{cf},
);
},
});

View File

@ -249,6 +249,27 @@ sub lock_storage_config {
}
}
# FIXME remove maxfiles for PVE 8.0 or PVE 9.0
my $convert_maxfiles_to_prune_backups = sub {
my ($scfg) = @_;
return if !$scfg;
my $maxfiles = delete $scfg->{maxfiles};
if (!defined($scfg->{'prune-backups'}) && defined($maxfiles)) {
my $prune_backups;
if ($maxfiles) {
$prune_backups = { 'keep-last' => $maxfiles };
} else { # maxfiles 0 means no limit
$prune_backups = { 'keep-all' => 1 };
}
$scfg->{'prune-backups'} = PVE::JSONSchema::print_property_string(
$prune_backups, 'prune-backups',
);
}
};
sub storage_config {
my ($cfg, $storeid, $noerr) = @_;
@ -258,6 +279,8 @@ sub storage_config {
die "storage '$storeid' does not exist\n" if (!$noerr && !$scfg);
$convert_maxfiles_to_prune_backups->($scfg);
return $scfg;
}
@ -717,10 +740,11 @@ sub path_to_volume_id {
my $isodir = $plugin->get_subdir($scfg, 'iso');
my $tmpldir = $plugin->get_subdir($scfg, 'vztmpl');
my $backupdir = $plugin->get_subdir($scfg, 'backup');
my $privatedir = $plugin->get_subdir($scfg, 'rootdir');
my $snippetsdir = $plugin->get_subdir($scfg, 'snippets');
my $importdir = $plugin->get_subdir($scfg, 'import');
if ($path =~ m!^\Q$imagedir\E/(\d+)/([^/\s]+)$!) {
if ($path =~ m!^$imagedir/(\d+)/([^/\s]+)$!) {
my $vmid = $1;
my $name = $2;
@ -732,19 +756,22 @@ sub path_to_volume_id {
return ('images', $info->{volid});
}
}
} elsif ($path =~ m!^\Q$isodir\E/([^/]+$ISO_EXT_RE_0)$!) {
} elsif ($path =~ m!^$isodir/([^/]+$ISO_EXT_RE_0)$!) {
my $name = $1;
return ('iso', "$sid:iso/$name");
} elsif ($path =~ m!^\Q$tmpldir\E/([^/]+$VZTMPL_EXT_RE_1)$!) {
} elsif ($path =~ m!^$tmpldir/([^/]+$VZTMPL_EXT_RE_1)$!) {
my $name = $1;
return ('vztmpl', "$sid:vztmpl/$name");
} elsif ($path =~ m!^\Q$backupdir\E/([^/]+$BACKUP_EXT_RE_2)$!) {
} elsif ($path =~ m!^$privatedir/(\d+)$!) {
my $vmid = $1;
return ('rootdir', "$sid:rootdir/$vmid");
} elsif ($path =~ m!^$backupdir/([^/]+$BACKUP_EXT_RE_2)$!) {
my $name = $1;
return ('backup', "$sid:backup/$name");
} elsif ($path =~ m!^\Q$snippetsdir\E/([^/]+)$!) {
} elsif ($path =~ m!^$snippetsdir/([^/]+)$!) {
my $name = $1;
return ('snippets', "$sid:snippets/$name");
} elsif ($path =~ m!^\Q$importdir\E/(${SAFE_CHAR_CLASS_RE}+${IMPORT_EXT_RE_1})$!) {
} elsif ($path =~ m!^$importdir/(${SAFE_CHAR_CLASS_RE}+${IMPORT_EXT_RE_1})$!) {
my $name = $1;
return ('import', "$sid:import/$name");
}
@ -1429,7 +1456,7 @@ sub deactivate_volumes {
}
}
die "volume deactivation failed: " . join(' ', @errlist) . "\n"
die "volume deactivation failed: " . join(' ', @errlist)
if scalar(@errlist);
}

View File

@ -68,6 +68,7 @@ sub options {
nodes => { optional => 1 },
shared => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
@ -528,6 +529,9 @@ sub volume_snapshot {
$snap_path = raw_file_to_subvol($snap_path);
}
my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
mkpath $snapshot_dir;
$class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
return undef;
}

View File

@ -153,6 +153,7 @@ sub options {
subdir => { optional => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
@ -331,8 +332,4 @@ sub get_import_metadata {
return PVE::Storage::DirPlugin::get_import_metadata(@_);
}
sub volume_qemu_snapshot_method {
return PVE::Storage::DirPlugin::volume_qemu_snapshot_method(@_);
}
1;

View File

@ -153,6 +153,7 @@ sub options {
'create-subdirs' => { optional => 1 },
fuse => { optional => 1 },
bwlimit => { optional => 1 },
maxfiles => { optional => 1 },
keyring => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },

View File

@ -1,6 +1,7 @@
package PVE::Storage::Common;
use v5.36;
use strict;
use warnings;
use PVE::JSONSchema;
use PVE::Syscall;

View File

@ -84,6 +84,7 @@ sub options {
nodes => { optional => 1 },
shared => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },

View File

@ -211,17 +211,7 @@ sub esxi_mount : prototype($$$;$) {
if (!$pid) {
eval {
undef $rd;
# Double fork to properly daemonize
POSIX::setsid() or die "failed to create new session: $!\n";
my $pid2 = fork();
die "second fork failed: $!\n" if !defined($pid2);
if ($pid2) {
# First child exits immediately
POSIX::_exit(0);
}
# Second child (grandchild) enters systemd scope
POSIX::setsid();
PVE::Systemd::enter_systemd_scope(
$scope_name_base,
"Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
@ -253,8 +243,6 @@ sub esxi_mount : prototype($$$;$) {
}
POSIX::_exit(1);
}
# Parent wait for first child to exit
waitpid($pid, 0);
undef $wr;
my $result = do { local $/ = undef; <$rd> };

View File

@ -33,7 +33,7 @@ my sub assert_iscsi_support {
}
# Example: 192.168.122.252:3260,1 iqn.2003-01.org.linux-iscsi.proxmox-nfs.x8664:sn.00567885ba8f
my $ISCSI_TARGET_RE = qr/^(\S+:\d+)\,\S+\s+(\S+)\s*$/;
my $ISCSI_TARGET_RE = qr/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/;
sub iscsi_session_list {
assert_iscsi_support();
@ -48,7 +48,9 @@ sub iscsi_session_list {
outfunc => sub {
my $line = shift;
# example: tcp: [1] 192.168.122.252:3260,1 iqn.2003-01.org.linux-iscsi.proxmox-nfs.x8664:sn.00567885ba8f (non-flash)
if ($line =~ m/^tcp:\s+\[(\S+)\]\s+(\S+:\d+)\,\S+\s+(\S+)\s+\S+?\s*$/) {
if ($line =~
m/^tcp:\s+\[(\S+)\]\s+((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s+\S+?\s*$/
) {
my ($session_id, $portal, $target) = ($1, $2, $3);
# there can be several sessions per target (multipath)
push @{ $res->{$target} }, { session_id => $session_id, portal => $portal };

View File

@ -470,11 +470,9 @@ my sub get_snap_name {
}
my sub parse_snap_name {
my ($name, $short_volname) = @_;
my ($name) = @_;
$short_volname =~ s/\.(qcow2)$//;
if ($name =~ m/^snap_\Q$short_volname\E_(.*)\.qcow2$/) {
if ($name =~ m/^snap_\S+_(.*)\.qcow2$/) {
return $1;
}
}
@ -801,13 +799,11 @@ sub status {
sub volume_snapshot_info {
my ($class, $scfg, $storeid, $volname) = @_;
my $short_volname = ($class->parse_volname($volname))[1];
my $get_snapname_from_path = sub {
my ($path) = @_;
my ($volname, $path) = @_;
my $name = basename($path);
if (my $snapname = parse_snap_name($name, $short_volname)) {
if (my $snapname = parse_snap_name($name)) {
return $snapname;
} elsif ($name eq $volname) {
return 'current';
@ -816,6 +812,8 @@ sub volume_snapshot_info {
};
my $path = $class->filesystem_path($scfg, $volname);
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
$class->parse_volname($volname);
my $json = PVE::Storage::Common::qemu_img_info($path, undef, 10, 1);
die "failed to query file information with qemu-img\n" if !$json;
@ -831,8 +829,7 @@ sub volume_snapshot_info {
my $snapshots = $json_decode;
for my $snap (@$snapshots) {
my $snapfile = $snap->{filename};
($snapfile) = $snapfile =~ m|^(/.*)|; # untaint
my $snapname = $get_snapname_from_path->($snapfile);
my $snapname = $get_snapname_from_path->($volname, $snapfile);
#not a proxmox snapshot
next if !$snapname;
@ -845,7 +842,7 @@ sub volume_snapshot_info {
my $parentfile = $snap->{'backing-filename'};
if ($parentfile) {
my $parentname = $get_snapname_from_path->($parentfile);
my $parentname = $get_snapname_from_path->($volname, $parentfile);
$info->{$snapname}->{parent} = $parentname;
$info->{$parentname}->{child} = $snapname;
}
@ -992,7 +989,7 @@ sub volume_snapshot {
#rename current volume to snap volume
eval { $class->rename_snapshot($scfg, $storeid, $volname, 'current', $snap) };
die "error rename $volname to $snap - $@\n" if $@;
die "error rename $volname to $snap\n" if $@;
eval { alloc_snap_image($class, $storeid, $scfg, $volname, $snap) };
if ($@) {
@ -1120,21 +1117,21 @@ sub volume_snapshot_delete {
} else {
#we rebase the child image on the parent as new backing image
my $parentpath = $snapshots->{$parentsnap}->{file};
print
"$volname: deleting snapshot '$snap' by rebasing '$childsnap' on top of '$parentsnap'\n";
my $rel_parent_path = get_snap_name($class, $volname, $parentsnap);
print "running 'qemu-img rebase -b $parentpath -F qcow -f qcow2 $childpath'\n";
$cmd = [
'/usr/bin/qemu-img',
'rebase',
'-b',
$rel_parent_path,
$parentpath,
'-F',
'qcow2',
'-f',
'qcow2',
$childpath,
];
print "running '" . join(' ', $cmd->@*) . "'\n";
eval { run_command($cmd) };
if ($@) {
#in case of abort, the state of the snap is still clean, just a little bit bigger

View File

@ -93,6 +93,7 @@ sub options {
export => { fixed => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
options => { optional => 1 },
@ -241,8 +242,4 @@ sub get_import_metadata {
return PVE::Storage::DirPlugin::get_import_metadata(@_);
}
sub volume_qemu_snapshot_method {
return PVE::Storage::DirPlugin::volume_qemu_snapshot_method(@_);
}
1;

View File

@ -5,7 +5,6 @@ package PVE::Storage::PBSPlugin;
use strict;
use warnings;
use Encode qw(decode);
use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
use IO::File;
use JSON;
@ -73,6 +72,7 @@ sub options {
password => { optional => 1 },
'encryption-key' => { optional => 1 },
'master-pubkey' => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
fingerprint => { optional => 1 },
@ -93,7 +93,7 @@ sub pbs_set_password {
my $pwfile = pbs_password_file_name($scfg, $storeid);
mkdir "/etc/pve/priv/storage";
PVE::Tools::file_set_contents($pwfile, "$password\n", 0600, 1);
PVE::Tools::file_set_contents($pwfile, "$password\n");
}
sub pbs_delete_password {
@ -109,9 +109,7 @@ sub pbs_get_password {
my $pwfile = pbs_password_file_name($scfg, $storeid);
my $contents = PVE::Tools::file_read_firstline($pwfile);
return eval { decode('UTF-8', $contents, 1) } // $contents;
return PVE::Tools::file_read_firstline($pwfile);
}
sub pbs_encryption_key_file_name {

View File

@ -159,6 +159,13 @@ my $defaultData = {
type => 'boolean',
optional => 1,
},
maxfiles => {
description => "Deprecated: use 'prune-backups' instead. "
. "Maximal number of backup files per VM. Use '0' for unlimited.",
type => 'integer',
minimum => 0,
optional => 1,
},
'prune-backups' => get_standard_option('prune-backups'),
'max-protected-backups' => {
description =>
@ -702,9 +709,9 @@ sub cluster_lock_storage {
}
my sub parse_snap_name {
my ($filename, $volname) = @_;
my ($name) = @_;
if ($filename =~ m/^snap-(.*)-\Q$volname\E$/) {
if ($name =~ m/^snap-(.*)-vm(.*)$/) {
return $1;
}
}
@ -715,10 +722,8 @@ sub parse_name_dir {
if ($name =~ m!^((vm-|base-|subvol-)(\d+)-[^/\s]+\.(raw|qcow2|vmdk|subvol))$!) {
my $isbase = $2 eq 'base-' ? $2 : undef;
return ($1, $4, $isbase); # (name, format, isBase)
} elsif ($name =~ m!^snap-.*\.qcow2$!) {
die "'$name' is a snapshot filename, not a volume!\n";
} elsif ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk|subvol))$!) {
return ($1, $3, $2); # (name ,format, isBase)
warn "this volume name `$name` is not supported anymore\n" if !parse_snap_name($name);
}
die "unable to parse volume filename '$name'\n";
@ -741,6 +746,8 @@ sub parse_volname {
return ('iso', $1, undef, undef, undef, undef, 'raw');
} elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
return ('vztmpl', $1, undef, undef, undef, undef, 'raw');
} elsif ($volname =~ m!^rootdir/(\d+)$!) {
return ('rootdir', $1, $1);
} elsif ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
my $fn = $1;
if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) {
@ -1427,21 +1434,21 @@ sub volume_snapshot_delete {
} else {
#we rebase the child image on the parent as new backing image
my $parentpath = $snapshots->{$parentsnap}->{file};
print
"$volname: deleting snapshot '$snap' by rebasing '$childsnap' on top of '$parentsnap'\n";
my $rel_parent_path = get_snap_name($class, $volname, $parentsnap);
print "running 'qemu-img rebase -b $parentpath -F qcow -f qcow2 $childpath'\n";
$cmd = [
'/usr/bin/qemu-img',
'rebase',
'-b',
$rel_parent_path,
$parentpath,
'-F',
'qcow2',
'-f',
'qcow2',
$childpath,
];
print "running '" . join(' ', $cmd->@*) . "'\n";
eval { run_command($cmd) };
if ($@) {
#in case of abort, the state of the snap is still clean, just a little bit bigger
@ -1554,10 +1561,6 @@ sub list_images {
next if !$vollist && defined($vmid) && ($owner ne $vmid);
# skip files that are snapshots or have invalid names
my ($parsed_name) = eval { parse_name_dir(basename($fn)) };
next if !defined($parsed_name);
my ($size, undef, $used, $parent, $ctime) = eval { file_size_info($fn, undef, $format); };
if (my $err = $@) {
die $err if $err !~ m/Image is not in \S+ format$/;
@ -1752,7 +1755,7 @@ sub volume_snapshot_info {
my $name = basename($path);
if (my $snapname = parse_snap_name($name, basename($volname))) {
if (my $snapname = parse_snap_name($name)) {
return $snapname;
} elsif ($name eq basename($volname)) {
return 'current';
@ -1786,7 +1789,6 @@ sub volume_snapshot_info {
my $snapshots = $json_decode;
for my $snap (@$snapshots) {
my $snapfile = $snap->{filename};
($snapfile) = $snapfile =~ m|^(/.*)|; # untaint
my $snapname = $get_snapname_from_path->($volname, $snapfile);
#not a proxmox snapshot
next if !$snapname;

View File

@ -482,25 +482,9 @@ sub volume_size_info {
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my (undef, $vname, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
my $snapshot_name = "$scfg->{pool}/$vname\@$snap";
my $vname = ($class->parse_volname($volname))[1];
$class->zfs_request($scfg, undef, 'snapshot', $snapshot_name);
# if this is a subvol, track refquota information via user properties. zfs
# does not track this property for snapshosts and consequently does not roll
# it back. so track this information manually.
if ($format eq 'subvol') {
my $refquota = $class->zfs_get_properties($scfg, 'refquota', "$scfg->{pool}/$vname");
$class->zfs_request(
$scfg,
undef,
'set',
"pve-storage:refquota=${refquota}",
$snapshot_name,
);
}
$class->zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$vname\@$snap");
}
sub volume_snapshot_delete {
@ -516,24 +500,8 @@ sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my (undef, $vname, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
my $snapshot_name = "$scfg->{pool}/$vname\@$snap";
my $msg = $class->zfs_request($scfg, undef, 'rollback', $snapshot_name);
# if this is a subvol, check if we tracked the refquota manually via user
# properties and if so, set it appropriatelly again.
if ($format eq 'subvol') {
my $refquota = $class->zfs_get_properties($scfg, 'pve-storage:refquota', $snapshot_name);
if ($refquota =~ m/^\d+$/) {
$class->zfs_request(
$scfg, undef, 'set', "refquota=${refquota}", "$scfg->{pool}/$vname",
);
} elsif ($refquota ne "-") {
# refquota user property was set, but not a number -> warn
warn "property for refquota tracking contained unknown value '$refquota'\n";
}
}
my $msg = $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$vname\@$snap");
# we have to unmount rollbacked subvols, to invalidate wrong kernel
# caches, they get mounted in activate volume again

View File

@ -1,6 +1,6 @@
all: test
test: test_zfspoolplugin test_lvmplugin test_disklist test_bwlimit test_plugin test_ovf test_volume_access
test: test_zfspoolplugin test_lvmplugin test_disklist test_bwlimit test_plugin test_ovf
test_zfspoolplugin: run_test_zfspoolplugin.pl
./run_test_zfspoolplugin.pl
@ -19,6 +19,3 @@ test_plugin: run_plugin_tests.pl
test_ovf: run_ovf_tests.pl
./run_ovf_tests.pl
test_volume_access: run_volume_access_tests.pl
./run_volume_access_tests.pl

View File

@ -63,6 +63,7 @@ my $mocked_vmlist = {
my $storage_dir = File::Temp->newdir();
my $scfg = {
'type' => 'dir',
'maxfiles' => 0,
'path' => $storage_dir,
'shared' => 0,
'content' => {

View File

@ -90,6 +90,11 @@ my $tests = [
#
# container rootdir
#
{
description => 'Container rootdir, sub directory',
volname => "rootdir/$vmid",
expected => ['rootdir', "$vmid", "$vmid"],
},
{
description => 'Container rootdir, subvol',
volname => "$vmid/subvol-$vmid-disk-0.subvol",
@ -177,6 +182,11 @@ my $tests = [
expected =>
"unable to parse directory volume name 'vztmpl/debian-10.0-standard_10.0-1_amd64.zip.gz'\n",
},
{
description => 'Failed match: Container rootdir, subvol',
volname => "rootdir/subvol-$vmid-disk-0",
expected => "unable to parse directory volume name 'rootdir/subvol-$vmid-disk-0'\n",
},
{
description => 'Failed match: VM disk image, linked, vhdx',
volname => "$vmid/base-$vmid-disk-0.vhdx/$vmid/vm-$vmid-disk-0.vhdx",
@ -312,9 +322,7 @@ foreach my $t (@$tests) {
# to check if all $vtype_subdirs are defined in path_to_volume_id
# or have a test
# FIXME re-enable after vtype split changes
#is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
is_deeply({}, {}, "vtype_subdir check");
is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
done_testing();

View File

@ -22,6 +22,7 @@ my $scfg = {
'shared' => 0,
'path' => "$storage_dir",
'type' => 'dir',
'maxfiles' => 0,
'content' => {
'snippets' => 1,
'rootdir' => 1,
@ -137,10 +138,10 @@ my @tests = (
},
{
description => 'Rootdir, folder subvol, legacy naming',
volname => "$storage_dir/images/1234/subvol-1234-disk-0.subvol/", # fileparse needs / at the end
description => 'Rootdir',
volname => "$storage_dir/private/1234/", # fileparse needs / at the end
expected => [
'images', 'local:1234/subvol-1234-disk-0.subvol',
'rootdir', 'local:rootdir/1234',
],
},
{
@ -202,6 +203,11 @@ my @tests = (
volname => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.zip.gz",
expected => [''],
},
{
description => 'Rootdir as subvol, wrong path',
volname => "$storage_dir/private/subvol-19254-disk-0/",
expected => [''],
},
{
description => 'Backup, wrong format, openvz, zip.gz',
volname => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.zip.gz",
@ -266,9 +272,7 @@ foreach my $tt (@tests) {
# to check if all $vtype_subdirs are defined in path_to_volume_id
# or have a test
# FIXME re-enable after vtype split changes
#is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
is_deeply({}, {}, "vtype_subdir check");
is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
#cleanup
# File::Temp unlinks tempdir on exit

File diff suppressed because it is too large Load Diff

View File

@ -1,254 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use Test::MockModule;
use Test::More;
use lib ('.', '..');
use PVE::RPCEnvironment;
use PVE::Storage;
use PVE::Storage::Plugin;
my $storage_cfg = <<'EOF';
dir: dir
path /mnt/pve/dir
content vztmpl,snippets,iso,backup,rootdir,images
EOF
my $user_cfg = <<'EOF';
user:root@pam:1:0::::::
user:noperm@pve:1:0::::::
user:otherstorage@pve:1:0::::::
user:dsallocate@pve:1:0::::::
user:dsaudit@pve:1:0::::::
user:backup@pve:1:0::::::
user:vmuser@pve:1:0::::::
role:dsallocate:Datastore.Allocate:
role:dsaudit:Datastore.Audit:
role:vmuser:VM.Config.Disk,Datastore.Audit:
role:backup:VM.Backup,Datastore.AllocateSpace:
acl:1:/storage/foo:otherstorage@pve:dsallocate:
acl:1:/storage/dir:dsallocate@pve:dsallocate:
acl:1:/storage/dir:dsaudit@pve:dsaudit:
acl:1:/vms/100:backup@pve:backup:
acl:1:/storage/dir:backup@pve:backup:
acl:1:/vms/100:vmuser@pve:vmuser:
acl:1:/vms/111:vmuser@pve:vmuser:
acl:1:/storage/dir:vmuser@pve:vmuser:
EOF
my @users =
qw(root@pam noperm@pve otherstorage@pve dsallocate@pve dsaudit@pve backup@pve vmuser@pve);
my $pve_cluster_module;
$pve_cluster_module = Test::MockModule->new('PVE::Cluster');
$pve_cluster_module->mock(
cfs_update => sub { },
get_config => sub {
my ($file) = @_;
if ($file eq 'storage.cfg') {
return $storage_cfg;
} elsif ($file eq 'user.cfg') {
return $user_cfg;
}
die "TODO: mock get_config($file)\n";
},
);
my $rpcenv = PVE::RPCEnvironment->init('pub');
$rpcenv->init_request();
my @types = sort keys PVE::Storage::Plugin::get_vtype_subdirs()->%*;
my $all_types = { map { $_ => 1 } @types };
my @tests = (
{
volid => 'dir:backup/vzdump-qemu-100-2025_07_29-13_00_55.vma',
denied_users => {
'dsaudit@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => {
'backup' => 1,
},
},
{
volid => 'dir:100/vm-100-disk-0.qcow2',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
},
allowed_types => {
'images' => 1,
'rootdir' => 1,
},
},
{
volid => 'dir:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz',
denied_users => {},
allowed_types => {
'vztmpl' => 1,
},
},
{
volid => 'dir:iso/virtio-win-0.1.271.iso',
denied_users => {},
allowed_types => {
'iso' => 1,
},
},
{
volid => 'dir:111/subvol-111-disk-0.subvol',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
},
allowed_types => {
'images' => 1,
'rootdir' => 1,
},
},
# test different VM IDs
{
volid => 'dir:backup/vzdump-qemu-200-2025_07_29-13_00_55.vma',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => {
'backup' => 1,
},
},
{
volid => 'dir:200/vm-200-disk-0.qcow2',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => {
'images' => 1,
'rootdir' => 1,
},
},
{
volid => 'dir:backup/vzdump-qemu-200-2025_07_29-13_00_55.vma',
vmid => 200,
denied_users => {},
allowed_types => {
'backup' => 1,
},
},
{
volid => 'dir:200/vm-200-disk-0.qcow2',
vmid => 200,
denied_users => {},
allowed_types => {
'images' => 1,
'rootdir' => 1,
},
},
{
volid => 'dir:backup/vzdump-qemu-200-2025_07_29-13_00_55.vma',
vmid => 300,
denied_users => {
'noperm@pve' => 1,
'otherstorage@pve' => 1,
'backup@pve' => 1,
'dsaudit@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => {
'backup' => 1,
},
},
{
volid => 'dir:200/vm-200-disk-0.qcow2',
vmid => 300,
denied_users => {
'noperm@pve' => 1,
'otherstorage@pve' => 1,
'backup@pve' => 1,
'dsaudit@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => {
'images' => 1,
'rootdir' => 1,
},
},
# test paths
{
volid => 'relative_path',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
'dsallocate@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => $all_types,
},
{
volid => '/absolute_path',
denied_users => {
'backup@pve' => 1,
'dsaudit@pve' => 1,
'dsallocate@pve' => 1,
'vmuser@pve' => 1,
},
allowed_types => $all_types,
},
);
my $cfg = PVE::Storage::config();
is(scalar(@users), 7, 'number of users');
for my $t (@tests) {
my ($volid, $vmid, $expected_denied_users, $expected_allowed_types) =
$t->@{qw(volid vmid denied_users allowed_types)};
# certain users are always expected to be denied, except in the special case where VM ID is set
$expected_denied_users->{'noperm@pve'} = 1 if !$vmid;
$expected_denied_users->{'otherstorage@pve'} = 1 if !$vmid;
for my $user (@users) {
my $description = "user: $user, volid: $volid";
$rpcenv->set_user($user);
my $actual_denied;
eval { PVE::Storage::check_volume_access($rpcenv, $user, $cfg, $vmid, $volid, undef); };
if (my $err = $@) {
$actual_denied = 1;
note($@) if !$expected_denied_users->{$user} # log the error for easy analysis
}
is($actual_denied, $expected_denied_users->{$user}, $description);
}
for my $type (@types) {
my $user = 'root@pam'; # type mismatch should not even work for root!
my $description = "type $type, volid: $volid";
$rpcenv->set_user($user);
my $actual_allowed = 1;
eval { PVE::Storage::check_volume_access($rpcenv, $user, $cfg, $vmid, $volid, $type); };
if (my $err = $@) {
$actual_allowed = undef;
note($@) if $expected_allowed_types->{$type} # log the error for easy analysis
}
is($actual_allowed, $expected_allowed_types->{$type}, $description);
}
}
done_testing();