all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Elias Huhsovitz <e.huhsovitz@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Elias Huhsovitz <e.huhsovitz@proxmox.com>
Subject: [PATCH storage 2/3] plugin: generalize volname_for_format across storage plugins
Date: Wed, 12 Aug 2026 17:32:51 +0200	[thread overview]
Message-ID: <20260812153252.222298-3-e.huhsovitz@proxmox.com> (raw)
In-Reply-To: <20260812153252.222298-1-e.huhsovitz@proxmox.com>

Move `volname_for_format()` from `LVMPlugin` into `Plugin` to make it
reusable for all storage backends. This function reconciles the
requested format with the volume name and ensures they match.

Introduce `get_parsed_format()` as an extension point. Subclasses whose
volume names encode the format differently (e.g., LVM, ZFS) override
this method to derive the format via `parse_volname()`.

Add `FORMAT_EXTENSION` constants to affected plugins, enabling the
generation of useful format mismatch suggestions.

Call `volname_for_format()` in `alloc_image()` and `rename_volume()`
for the `Plugin`, `BTRFS`, `LVM`, `LvmThin`, `RBD`, `ZFS`, and `ZFSPool`
backends.

In `ZFSPoolPlugin`, override `volname_for_format()` and
`volname_with_format()` to handle its unique prefix-based naming scheme
(e.g., `vm-` vs `subvol-`).

Signed-off-by: Elias Huhsovitz <e.huhsovitz@proxmox.com>
---
 src/PVE/Storage/BTRFSPlugin.pm   |  6 +-
 src/PVE/Storage/LVMPlugin.pm     | 37 ++++++------
 src/PVE/Storage/LvmThinPlugin.pm | 12 ++++
 src/PVE/Storage/Plugin.pm        | 97 ++++++++++++++++++++++++++++++--
 src/PVE/Storage/RBDPlugin.pm     | 14 +++++
 src/PVE/Storage/ZFSPlugin.pm     |  2 +
 src/PVE/Storage/ZFSPoolPlugin.pm | 37 ++++++++++++
 7 files changed, 176 insertions(+), 29 deletions(-)

diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fb47aa0..fa28592 100644
--- a/src/PVE/Storage/BTRFSPlugin.pm
+++ b/src/PVE/Storage/BTRFSPlugin.pm
@@ -333,10 +333,7 @@ sub alloc_image {
 
     $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
 
-    my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name);
-
-    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
-        if $tmpfmt ne $fmt;
+    $name = $class->volname_for_format($name, $fmt, 0);
 
     # End copy from Plugin.pm
 
@@ -974,6 +971,7 @@ sub rename_volume {
 
     $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
         if !$target_volname;
+    $target_volname = $class->volname_for_format($target_volname, $format, 0);
     $target_volname = "$target_vmid/$target_volname";
 
     my $basedir = $class->get_subdir($scfg, 'images');
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
index 0f2ef66..6ed613d 100644
--- a/src/PVE/Storage/LVMPlugin.pm
+++ b/src/PVE/Storage/LVMPlugin.pm
@@ -16,6 +16,16 @@ use PVE::Storage::Plugin;
 
 use base qw(PVE::Storage::Plugin);
 
+use constant FORMAT_EXTENSION => {
+    raw => '',
+    qcow2 => 'qcow2',
+};
+
+use constant EXTENSION_FORMAT => {
+    '' => 'raw',
+    'qcow2' => 'qcow2',
+};
+
 # lvm helper functions
 
 my $ignore_no_medium_warnings = sub {
@@ -738,27 +748,10 @@ my sub alloc_lvm_image {
 
 }
 
-# Only 'qcow2' names carry an extension, like the ones find_free_diskname() generates, so add it
-# for callers passing a fixed name, like cloud-init drives. A name that already spells out another
-# format stays an error, it states an intent that the requested format contradicts.
-my sub volname_for_format {
-    my ($class, $name, $fmt) = @_;
-
-    return $name if $fmt ne 'raw' && $fmt ne 'qcow2'; # alloc_lvm_image() reports unsupported ones
+sub get_parsed_format {
+    my ($class, $name) = @_;
 
-    my $name_fmt = ($class->parse_volname($name))[6];
-    return $name if $name_fmt eq $fmt;
-
-    if ($fmt eq 'raw') {
-        my $suggested_name = $name =~ s/\.\Q$name_fmt\E$//r;
-        die "volume name '$name' does not match requested format '$fmt'"
-            . " (did you mean '$suggested_name'?)\n";
-    }
-
-    my $adapted_name = "$name.$fmt";
-    warn "volume name '$name' is missing the '.$fmt' extension - allocating '$adapted_name'\n";
-
-    return $adapted_name;
+    return ($class->parse_volname($name))[6];
 }
 
 sub alloc_image {
@@ -767,7 +760,7 @@ sub alloc_image {
     $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
         if !$name;
 
-    $name = volname_for_format($class, $name, $fmt);
+    $name = $class->volname_for_format($name, $fmt, 0);
 
     alloc_lvm_image($class, $storeid, $scfg, $vmid, $fmt, $name, $size);
 
@@ -1542,6 +1535,8 @@ sub rename_volume {
     $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
         if !$target_volname;
 
+    $target_volname = $class->volname_for_format($target_volname, $format, 0);
+
     my $vg = $scfg->{vgname};
     my $lvs = lvm_list_volumes($vg);
     die "target volume '${target_volname}' already exists\n"
diff --git a/src/PVE/Storage/LvmThinPlugin.pm b/src/PVE/Storage/LvmThinPlugin.pm
index d44e04d..0c25973 100644
--- a/src/PVE/Storage/LvmThinPlugin.pm
+++ b/src/PVE/Storage/LvmThinPlugin.pm
@@ -25,6 +25,10 @@ use PVE::Storage::LVMPlugin;
 
 use base qw(PVE::Storage::LVMPlugin);
 
+use constant FORMAT_EXTENSION => {
+    raw => '',
+};
+
 sub type {
     return 'lvmthin';
 }
@@ -100,6 +104,12 @@ my $set_lv_autoactivation = sub {
     warn "could not set autoactivation: $@" if $@;
 };
 
+sub get_parsed_format {
+    my ($class, $name) = @_;
+
+    return ($class->parse_volname($name))[6];
+}
+
 sub alloc_image {
     my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
 
@@ -117,6 +127,8 @@ sub alloc_image {
     $name = $class->find_free_diskname($storeid, $scfg, $vmid)
         if !$name;
 
+    $name = $class->volname_for_format($name, $fmt, 0);
+
     my $cmd = [
         '/sbin/lvcreate',
         '-aly',
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index c5046a4..d596397 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -27,6 +27,13 @@ use constant COMPRESSOR_RE => join('|', KNOWN_COMPRESSION_FORMATS);
 use constant LOG_EXT => ".log";
 use constant NOTES_EXT => ".notes";
 
+use constant FORMAT_EXTENSION => {
+    raw => 'raw',
+    qcow2 => 'qcow2',
+    vmdk => 'vmdk',
+    subvol => 'subvol',
+};
+
 our @COMMON_TAR_FLAGS = qw(
     --one-file-system
     -p --sparse --numeric-owner --acls
@@ -838,6 +845,89 @@ sub parse_volname {
     die "unable to parse directory volume name '$volname'\n";
 }
 
+=item get_parsed_format($class, $name)
+
+Return the disk format encoded in the given volume name.
+
+This is an extension point. Subclasses whose volume names encode the format
+differently override this method. LVM and ZFS, for example, derive the format
+via C<parse_volname> .
+
+returns: the format indicated by the name.
+
+=cut
+
+sub get_parsed_format {
+    my ($class, $name) = @_;
+
+    return (parse_name_dir($name))[1];
+}
+
+sub is_valid_format {
+    my ($class, $fmt) = @_;
+
+    return defined($class->FORMAT_EXTENSION->{$fmt});
+}
+
+=item volname_for_format($class, $name, $fmt, $strict)
+
+Reconcile the requested format with the volume name and return the name to
+allocate.
+
+Dies immediately if C<$fmt> is not a valid format for this plugin.
+
+If the name already encodes the requested format, returns the name unchanged.
+
+If C<$strict> is true, any format mismatch dies with a suggestion for the
+corrected name.
+
+If C<$strict> is false (lax mode), a name without a file extension is adapted
+to the requested format and a warning is emitted. A name that already carries
+a file extension dies with a suggestion, because the extension indicates an
+explicit format choice that contradicts the request.
+
+returns: the name to allocate
+
+=cut
+
+sub volname_for_format {
+    my ($class, $name, $fmt, $strict) = @_;
+
+    die "unsupported format '$fmt'\n" if !($class->is_valid_format($fmt));
+
+    my $parsed_volname_fmt = $class->get_parsed_format($name);
+
+    return $name if $parsed_volname_fmt eq $fmt;
+
+    my $suggestion = $class->volname_with_format($name, $fmt);
+
+    if ($strict || $name =~ /\.[^.]+$/) {
+        die "illegal name $name - volume name does not match requested format "
+            . "'$fmt' (did you mean '$suggestion'?)\n";
+    }
+
+    warn "volume name '$name' is missing the '.$fmt' extension - allocating '$suggestion'\n";
+
+    return $suggestion;
+}
+
+sub get_format_extension {
+    my ($class, $fmt) = @_;
+
+    return $class->FORMAT_EXTENSION->{$fmt};
+}
+
+sub volname_with_format {
+    my ($class, $name, $fmt) = @_;
+
+    my $correct_fmt_ext = $class->get_format_extension($fmt);
+
+    $name =~ s/\.[^.]+$//;
+
+    return $name if !$correct_fmt_ext;
+    return "$name.$correct_fmt_ext";
+}
+
 my $vtype_subdirs = {
     images => 'images',
     rootdir => 'private',
@@ -1058,10 +1148,7 @@ sub alloc_image {
 
     $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
 
-    my (undef, $tmpfmt) = parse_name_dir($name);
-
-    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
-        if $tmpfmt ne $fmt;
+    $name = $class->volname_for_format($name, $fmt, 0);
 
     my $path = "$imagedir/$name";
 
@@ -2391,6 +2478,8 @@ sub rename_volume {
     $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
         if !$target_volname;
 
+    $target_volname = $class->volname_for_format($target_volname, $format, 0);
+
     my $basedir = $class->get_subdir($scfg, 'images');
 
     mkpath "${basedir}/${target_vmid}";
diff --git a/src/PVE/Storage/RBDPlugin.pm b/src/PVE/Storage/RBDPlugin.pm
index 8ebb999..95b65d6 100644
--- a/src/PVE/Storage/RBDPlugin.pm
+++ b/src/PVE/Storage/RBDPlugin.pm
@@ -22,6 +22,10 @@ use PVE::Storage::Common;
 
 use base qw(PVE::Storage::Plugin);
 
+use constant FORMAT_EXTENSION => {
+    raw => '',
+};
+
 my $get_parent_image_name = sub {
     my ($parent) = @_;
     return undef if !$parent;
@@ -522,6 +526,12 @@ sub parse_volname {
     die "unable to parse rbd volume name '$volname'\n";
 }
 
+sub get_parsed_format {
+    my ($class, $name) = @_;
+
+    return ($class->parse_volname($name))[6];
+}
+
 sub path {
     my ($class, $scfg, $volname, $storeid, $snapname) = @_;
 
@@ -715,6 +725,8 @@ sub alloc_image {
 
     $name = $class->find_free_diskname($storeid, $scfg, $vmid) if !$name;
 
+    $name = $class->volname_for_format($name, $fmt, 0);
+
     my @options = (
         '--image-format', 2, '--size', int(($size + 1023) / 1024),
     );
@@ -1087,6 +1099,8 @@ sub rename_volume {
     $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
         if !$target_volname;
 
+    $target_volname = $class->volname_for_format($target_volname, $format, 0);
+
     die "target volume '${target_volname}' already exists\n"
         if rbd_volume_exists($scfg, $storeid, $target_volname);
 
diff --git a/src/PVE/Storage/ZFSPlugin.pm b/src/PVE/Storage/ZFSPlugin.pm
index 74e0a08..bfae513 100644
--- a/src/PVE/Storage/ZFSPlugin.pm
+++ b/src/PVE/Storage/ZFSPlugin.pm
@@ -366,6 +366,8 @@ sub alloc_image {
 
     $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname;
 
+    $volname = $class->volname_for_format($volname, $fmt, 0);
+
     $class->zfs_create_zvol($scfg, $volname, $size);
 
     my $guid = $class->zfs_create_lu($scfg, $volname);
diff --git a/src/PVE/Storage/ZFSPoolPlugin.pm b/src/PVE/Storage/ZFSPoolPlugin.pm
index 55f0bbc..4fecebb 100644
--- a/src/PVE/Storage/ZFSPoolPlugin.pm
+++ b/src/PVE/Storage/ZFSPoolPlugin.pm
@@ -150,6 +150,37 @@ sub parse_volname {
     die "unable to parse zfs volume name '$volname'\n";
 }
 
+sub get_parsed_format {
+    my ($class, $name) = @_;
+
+    return ($class->parse_volname($name))[6];
+}
+
+# ZFS volume names always encode their format in the name prefix (vm- for raw
+# zvols, subvol- for subvolumes). Any mismatch therefore spells out a
+# contradicting format and is an error, regardless of $strict.
+sub volname_for_format {
+    my ($class, $name, $fmt, $strict) = @_;
+
+    my $name_fmt = $class->get_parsed_format($name);
+    return $name if $name_fmt eq $fmt;
+
+    my $suggestion = $class->volname_with_format($name, $name_fmt, $fmt);
+
+    die "illegal name $name - volume name does not match requested format "
+        . "'$fmt' (did you mean '$suggestion'?)\n";
+}
+
+sub volname_with_format {
+    my ($class, $name, $name_fmt, $fmt) = @_;
+
+    if ($fmt eq 'subvol') {
+        return $name =~ s/^(?:vm|base|basevol)-/subvol-/r;
+    }
+
+    return $name =~ s/^(?:subvol|basevol|base)-/vm-/r;
+}
+
 # virtual zfs methods (subclass can overwrite them)
 
 sub on_add_hook {
@@ -273,6 +304,8 @@ sub alloc_image {
         $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
             if !$volname;
 
+        $volname = $class->volname_for_format($volname, $fmt, 0);
+
         $class->zfs_create_zvol($scfg, $volname, $size);
         $class->zfs_wait_for_zvol_link($scfg, $volname);
 
@@ -283,6 +316,8 @@ sub alloc_image {
         $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
             if !$volname;
 
+        $volname = $class->volname_for_format($volname, $fmt, 0);
+
         die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
             if $volname !~ m/^subvol-$vmid-/;
 
@@ -960,6 +995,8 @@ sub rename_volume {
     $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
         if !$target_volname;
 
+    $target_volname = $class->volname_for_format($target_volname, $format, 0);
+
     my $pool = $scfg->{pool};
     my $source_zfspath = "${pool}/${source_image}";
     my $target_zfspath = "${pool}/${target_volname}";
-- 
2.47.3





  parent reply	other threads:[~2026-08-12 15:33 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-12 15:32 [PATCH storage 0/3] generalize volname_for_format across storage plugins Elias Huhsovitz
2026-08-12 15:32 ` [PATCH storage 1/3] plugin: refactor format parsing in parse_volname and parse_name_dir Elias Huhsovitz
2026-08-12 15:32 ` Elias Huhsovitz [this message]
2026-08-12 15:32 ` [PATCH storage 3/3] test: plugin: add unit tests for volname_for_format Elias Huhsovitz

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=20260812153252.222298-3-e.huhsovitz@proxmox.com \
    --to=e.huhsovitz@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal