From c166501873fa60e7e514f7d9b0dbed1d26e80ede Mon Sep 17 00:00:00 2001 From: Kevin Scott Adams Date: Fri, 22 May 2026 11:49:05 -0400 Subject: [PATCH] feat: auto-detect and correct ZFS blocksize for TrueNAS SCALE/CORE (#241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: auto-detect and correct ZFS blocksize for TrueNAS SCALE/CORE (#241) SCALE requires >= 16k volblocksize; CORE works with 8k. Without correction, creating a disk on SCALE with blocksize=8k triggers a ZFS warning and wastes space on SCALE's minimum-4k-block pool layout. Three changes: - freenas_get_recommended_blocksize: fixes freenas_api_connect → freenas_api_check so product_name is actually populated before the SCALE check (was always returning 8192 before this fix) - freenas_parse_blocksize: new helper to compare "8k"/"16k"/integer blocksize strings - alloc_image (both 8.x and 8.4.x patches): always detect recommended blocksize for freenas provider; if configured < recommended, override for this call AND persist the correction back to storage.cfg via lock_storage_config - on_add_hook (8.4.x patch only): detect at storage creation time and correct $scfg before write_config saves it — no extra write needed Tested on pve01-hq (PVE 8.4.19) against Tank02 (SCALE 24.10.2.1): - Disk created with blocksize=8k → task log shows correction message, storage.cfg updated to 16384, no volblocksize warning from TrueNAS - Disk created with blocksize=16384 → no-op, clean TASK OK Co-Authored-By: Claude Sonnet 4.6 * ci: add validate-patches step for ZFSPlugin PVE 8.4.x patch ZFSPlugin-8.4.14_1.pm.patch has been in the build since v2.3.0 but was never dry-run validated in CI. Now that we have the 8.4 orig committed (ZFSPlugin-8.4.14_1.pm.orig from libpve-storage-perl 8.3.8), wire it up. Co-Authored-By: Claude Sonnet 4.6 * fix: replace explicit return undef with bare return (perlcritic) Subroutines::ProhibitExplicitReturnUndef violation in freenas_get_recommended_blocksize. Bare return in list context returns an empty list rather than a list containing undef, which is the correct Perl idiom. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/build.yml | 7 + perl5/PVE/Storage/LunCmd/FreeNAS.pm | 39 +- .../PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig | 470 ++++++++++++++++++ .../PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch | 69 ++- stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch | 45 +- 5 files changed, 621 insertions(+), 9 deletions(-) create mode 100644 stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c424930..5647e3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,13 @@ jobs: < stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \ && echo "ZFSPlugin PVE-8 patch: OK" + - name: "Dry-run patch: ZFSPlugin (PVE 8.4.x)" + run: | + patch --dry-run --ignore-whitespace \ + stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig \ + < stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch \ + && echo "ZFSPlugin PVE-8.4 patch: OK" + - name: "Dry-run patch: apidoc.js (PVE 8)" run: | patch --dry-run --ignore-whitespace \ diff --git a/perl5/PVE/Storage/LunCmd/FreeNAS.pm b/perl5/PVE/Storage/LunCmd/FreeNAS.pm index 6870b38..50285f2 100644 --- a/perl5/PVE/Storage/LunCmd/FreeNAS.pm +++ b/perl5/PVE/Storage/LunCmd/FreeNAS.pm @@ -3,7 +3,7 @@ package PVE::Storage::LunCmd::FreeNAS; use strict; use warnings; -our $VERSION = '2.3.0'; +our $VERSION = '2.4.0'; use Data::Dumper; use PVE::SafeSyslog; use IO::Socket::SSL; @@ -638,6 +638,43 @@ sub freenas_delete_zvol { } } +# Parse a blocksize string (e.g. "8k", "16k", "8192") to bytes. +sub freenas_parse_blocksize { + my ($val) = @_; + return 0 unless defined $val && $val ne ''; + return $1 * 1024 if $val =~ /^(\d+)\s*[kK]$/; + return $1 * 1024 * 1024 if $val =~ /^(\d+)\s*[mM]$/; + return $val + 0; +} + +# Returns the recommended ZFS volblocksize for this TrueNAS host based on its +# product type. TrueNAS SCALE ships a newer ZFS that requires >= 16k; CORE is +# fine with 8k. Returns undef if the API is unreachable so callers can fall +# back to the user-configured value or the ZFS default. +sub freenas_get_recommended_blocksize { + my ($scfg) = @_; + + my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal}; + + # freenas_api_check (not freenas_api_connect) must be used here because + # product_name is only parsed and cached by freenas_api_check. + # freenas_api_connect only sets up the HTTP client. + eval { freenas_api_check($scfg) }; + if ($@) { + syslog("warn", (caller(0))[3] . " : unable to connect to TrueNAS API for blocksize detection: $@"); + return; + } + + my $product = $freenas_server_list->{$apihost}{product_name} // ''; + if ($product =~ /SCALE/i) { + syslog("info", (caller(0))[3] . " : detected TrueNAS SCALE — recommending blocksize 16384"); + return 16384; + } + + syslog("info", (caller(0))[3] . " : detected '$product' — recommending blocksize 8192"); + return 8192; +} + # # Remove an extent by it's id # http://api.freenas.org/resources/iscsi/index.html#delete-resource diff --git a/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig new file mode 100644 index 0000000..3dc3944 --- /dev/null +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig @@ -0,0 +1,470 @@ +package PVE::Storage::ZFSPlugin; + +use strict; +use warnings; +use IO::File; +use POSIX qw(ENOENT); +use PVE::Tools qw(run_command); +use PVE::Storage::ZFSPoolPlugin; +use PVE::RESTEnvironment qw(log_warn); +use PVE::RPCEnvironment; + +use base qw(PVE::Storage::ZFSPoolPlugin); +use PVE::Storage::LunCmd::Comstar; +use PVE::Storage::LunCmd::Istgt; +use PVE::Storage::LunCmd::Iet; +use PVE::Storage::LunCmd::LIO; + +my @ssh_opts = ('-o', 'BatchMode=yes'); +my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts); +my $id_rsa_path = '/etc/pve/priv/zfs'; + +my $lun_cmds = { + create_lu => 1, + delete_lu => 1, + import_lu => 1, + modify_lu => 1, + add_view => 1, + list_view => 1, + list_lu => 1, +}; + +my $zfs_unknown_scsi_provider = sub { + my ($provider) = @_; + + die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]"; +}; + +my $zfs_get_base = sub { + my ($scfg) = @_; + + if ($scfg->{iscsiprovider} eq 'comstar') { + return PVE::Storage::LunCmd::Comstar::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + return PVE::Storage::LunCmd::Istgt::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'iet') { + return PVE::Storage::LunCmd::Iet::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'LIO') { + return PVE::Storage::LunCmd::LIO::get_base($scfg); + } else { + $zfs_unknown_scsi_provider->($scfg->{iscsiprovider}); + } +}; + +sub zfs_request { + my ($class, $scfg, $timeout, $method, @params) = @_; + + $timeout = PVE::RPCEnvironment->is_worker() ? 60 * 60 : 10 + if !$timeout; + + my $msg = ''; + + if ($lun_cmds->{$method}) { + if ($scfg->{iscsiprovider} eq 'comstar') { + $msg = + PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'iet') { + $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'LIO') { + $msg = PVE::Storage::LunCmd::LIO::run_lun_command($scfg, $timeout, $method, @params); + } else { + $zfs_unknown_scsi_provider->($scfg->{iscsiprovider}); + } + } else { + + my $target = 'root@' . $scfg->{portal}; + + my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target]; + + if ($method eq 'zpool_list') { + push @$cmd, 'zpool', 'list'; + } else { + push @$cmd, 'zfs', $method; + } + + push @$cmd, @params; + + my $output = sub { + my $line = shift; + $msg .= "$line\n"; + }; + + run_command($cmd, outfunc => $output, timeout => $timeout); + } + + return $msg; +} + +sub zfs_get_lu_name { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + + $zvol = ($class->parse_volname($zvol))[1]; + + my $object = ($zvol =~ /^.+\/.+/) ? "$base/$zvol" : "$base/$scfg->{pool}/$zvol"; + + my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object); + + return $lu_name if $lu_name; + + die "Could not find lu_name for zvol $zvol"; +} + +sub zfs_add_lun_mapping_entry { + my ($class, $scfg, $zvol, $guid) = @_; + + if (!defined($guid)) { + $guid = $class->zfs_get_lu_name($scfg, $zvol); + } + + $class->zfs_request($scfg, undef, 'add_view', $guid); +} + +sub zfs_delete_lu { + my ($class, $scfg, $zvol) = @_; + + my $guid = $class->zfs_get_lu_name($scfg, $zvol); + + $class->zfs_request($scfg, undef, 'delete_lu', $guid); +} + +sub zfs_create_lu { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol"); + + return $guid; +} + +sub zfs_import_lu { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + $class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol"); +} + +sub zfs_resize_lu { + my ($class, $scfg, $zvol, $size) = @_; + + my $guid = $class->zfs_get_lu_name($scfg, $zvol); + + $class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid); +} + +sub zfs_get_lun_number { + my ($class, $scfg, $guid) = @_; + + die "could not find lun_number for guid $guid" if !$guid; + + if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) { + return $1; + } + + die "lun_number for guid $guid is not a number"; +} + +# Configuration + +sub type { + return 'zfs'; +} + +sub plugindata { + return { + content => [{ images => 1 }, { images => 1 }], + 'sensitive-properties' => {}, + }; +} + +sub properties { + return { + iscsiprovider => { + description => "iscsi provider", + type => 'string', + }, + # this will disable write caching on comstar and istgt. + # it is not implemented for iet. iet blockio always operates with + # writethrough caching when not in readonly mode + nowritecache => { + description => "disable write caching on the target", + type => 'boolean', + }, + comstar_tg => { + description => "target group for comstar views", + type => 'string', + }, + comstar_hg => { + description => "host group for comstar views", + type => 'string', + }, + lio_tpg => { + description => "target portal group for Linux LIO targets", + type => 'string', + }, + 'zfs-base-path' => { + description => "Base path where to look for the created ZFS block devices. Set" + . " automatically during creation if not specified. Usually '/dev/zvol'.", + type => 'string', + format => 'pve-storage-path', + }, + }; +} + +sub options { + return { + nodes => { optional => 1 }, + disable => { optional => 1 }, + portal => { fixed => 1 }, + target => { fixed => 1 }, + pool => { fixed => 1 }, + blocksize => { fixed => 1 }, + iscsiprovider => { fixed => 1 }, + nowritecache => { optional => 1 }, + sparse => { optional => 1 }, + comstar_hg => { optional => 1 }, + comstar_tg => { optional => 1 }, + lio_tpg => { optional => 1 }, + content => { optional => 1 }, + bwlimit => { optional => 1 }, + 'zfs-base-path' => { optional => 1 }, + }; +} + +# Storage implementation + +sub on_add_hook { + my ($class, $storeid, $scfg, %param) = @_; + + if (!$scfg->{'zfs-base-path'}) { + my $base_path; + if ($scfg->{iscsiprovider} eq 'comstar') { + $base_path = PVE::Storage::LunCmd::Comstar::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $base_path = PVE::Storage::LunCmd::Istgt::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'iet' || $scfg->{iscsiprovider} eq 'LIO') { + # Provider implementations hard-code '/dev/', which does not work for distributions like + # Debian 12. Keep that implementation as-is for backwards compatibility, but use custom + # logic here. + my $target = 'root@' . $scfg->{portal}; + my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target]; + push $cmd->@*, 'ls', '/dev/zvol'; + + my $rc = eval { run_command($cmd, timeout => 10, noerr => 1, quiet => 1) }; + my $err = $@; + if (defined($rc) && $rc == 0) { + $base_path = '/dev/zvol'; + } elsif (defined($rc) && $rc == ENOENT) { + $base_path = '/dev'; + } else { + my $message = $err ? $err : "remote command failed"; + chomp($message); + $message .= " ($rc)" if defined($rc); + $message .= " - check 'zfs-base-path' setting manually!"; + log_warn($message); + $base_path = '/dev/zvol'; + } + } else { + $zfs_unknown_scsi_provider->($scfg->{iscsiprovider}); + } + + $scfg->{'zfs-base-path'} = $base_path; + } + + return; +} + +sub path { + my ($class, $scfg, $volname, $storeid, $snapname) = @_; + + die "direct access to snapshots not implemented" + if defined($snapname); + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + my $target = $scfg->{target}; + my $portal = $scfg->{portal}; + + my $guid = $class->zfs_get_lu_name($scfg, $name); + my $lun = $class->zfs_get_lun_number($scfg, $guid); + + my $path = "iscsi://$portal/$target/$lun"; + + return ($path, $vmid, $vtype); +} + +sub create_base { + my ($class, $storeid, $scfg, $volname) = @_; + + my $snap = '__base__'; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = $class->parse_volname($volname); + + die "create_base not possible with base image\n" if $isBase; + + my $newname = $name; + $newname =~ s/^vm-/base-/; + + my $newvolname = $basename ? "$basename/$newname" : "$newname"; + + $class->zfs_delete_lu($scfg, $name); + $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname"); + + my $guid = $class->zfs_create_lu($scfg, $newname); + $class->zfs_add_lun_mapping_entry($scfg, $newname, $guid); + + my $running = undef; #fixme : is create_base always offline ? + + $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running); + + return $newvolname; +} + +sub clone_image { + my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; + + my $name = $class->SUPER::clone_image($scfg, $storeid, $volname, $vmid, $snap); + + # get ZFS dataset name from PVE volname + my (undef, $clonedname) = $class->parse_volname($name); + + my $guid = $class->zfs_create_lu($scfg, $clonedname); + $class->zfs_add_lun_mapping_entry($scfg, $clonedname, $guid); + + return $name; +} + +sub alloc_image { + my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_; + + die "unsupported format '$fmt'" if $fmt ne 'raw'; + + die "illegal name '$name' - should be 'vm-$vmid-*'\n" + if $name && $name !~ m/^vm-$vmid-/; + + my $volname = $name; + + $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname; + + $class->zfs_create_zvol($scfg, $volname, $size); + + my $guid = $class->zfs_create_lu($scfg, $volname); + $class->zfs_add_lun_mapping_entry($scfg, $volname, $guid); + + return $volname; +} + +sub free_image { + my ($class, $storeid, $scfg, $volname, $isBase) = @_; + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + $class->zfs_delete_lu($scfg, $name); + + eval { $class->zfs_delete_zvol($scfg, $name); }; + if (my $err = $@) { + my $guid = $class->zfs_create_lu($scfg, $name); + $class->zfs_add_lun_mapping_entry($scfg, $name, $guid); + die $err; + } + + return undef; +} + +sub volume_resize { + my ($class, $scfg, $storeid, $volname, $size, $running) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + my $new_size = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running); + + $class->zfs_resize_lu($scfg, $volname, $new_size); + + return $new_size; +} + +sub volume_snapshot_delete { + my ($class, $scfg, $storeid, $volname, $snap, $running) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap"); +} + +sub volume_snapshot_rollback { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + $class->zfs_delete_lu($scfg, $volname); + + $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap"); + + $class->zfs_import_lu($scfg, $volname); + + $class->zfs_add_lun_mapping_entry($scfg, $volname); +} + +sub storage_can_replicate { + my ($class, $scfg, $storeid, $format) = @_; + + return 0; +} + +sub volume_has_feature { + my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; + + my $features = { + snapshot => { current => 1, snap => 1 }, + clone => { base => 1 }, + template => { current => 1 }, + copy => { base => 1, current => 1 }, + }; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = $class->parse_volname($volname); + + my $key = undef; + + if ($snapname) { + $key = 'snap'; + } else { + $key = $isBase ? 'base' : 'current'; + } + + return 1 if $features->{$feature}->{$key}; + + return undef; +} + +sub activate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + + return 1; +} + +sub deactivate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + + return 1; +} + +sub activate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "unable to activate snapshot from remote zfs storage" if $snapname; + + return 1; +} + +sub deactivate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "unable to deactivate snapshot from remote zfs storage" if $snapname; + + return 1; +} + +1; diff --git a/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch index d7dc610..ebb0dc4 100644 --- a/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch @@ -1,5 +1,5 @@ ---- /tmp/ZFSPlugin-8.4.19-stock.pm 2026-05-19 23:09:15.498561777 -0400 -+++ /tmp/ZFSPlugin-8.4.19-patched.pm 2026-05-19 23:13:40.830560931 -0400 +--- /tmp/ZFSPlugin-8.4.orig 2026-05-22 08:54:24.992472212 -0400 ++++ /tmp/ZFSPlugin-8.4-final.pm 2026-05-22 08:51:00.460468461 -0400 @@ -11,6 +11,7 @@ use base qw(PVE::Storage::ZFSPoolPlugin); @@ -8,7 +8,7 @@ use PVE::Storage::LunCmd::Istgt; use PVE::Storage::LunCmd::Iet; use PVE::Storage::LunCmd::LIO; -@@ -26,6 +27,7 @@ +@@ -26,13 +27,14 @@ modify_lu => 1, add_view => 1, list_view => 1, @@ -16,6 +16,14 @@ list_lu => 1, }; + my $zfs_unknown_scsi_provider = sub { + my ($provider) = @_; + +- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]"; ++ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]"; + }; + + my $zfs_get_base = sub { @@ -44,6 +46,8 @@ return PVE::Storage::LunCmd::Istgt::get_base($scfg); } elsif ($scfg->{iscsiprovider} eq 'iet') { @@ -104,3 +112,58 @@ sparse => { optional => 1 }, comstar_hg => { optional => 1 }, comstar_tg => { optional => 1 }, +@@ -243,6 +289,21 @@ + my $base_path; + if ($scfg->{iscsiprovider} eq 'comstar') { + $base_path = PVE::Storage::LunCmd::Comstar::get_base($scfg); ++ } elsif ($scfg->{iscsiprovider} eq 'freenas') { ++ $base_path = PVE::Storage::LunCmd::FreeNAS::get_base($scfg); ++ # Auto-detect and persist correct blocksize at storage creation time. ++ # $scfg is the live config ref -- write_config() saves it after this hook returns. ++ eval { ++ my $recommended = PVE::Storage::LunCmd::FreeNAS::freenas_get_recommended_blocksize($scfg); ++ if (defined $recommended) { ++ my $configured = PVE::Storage::LunCmd::FreeNAS::freenas_parse_blocksize($scfg->{blocksize}); ++ if (!$configured || $configured < $recommended) { ++ warn "freenas-proxmox: auto-correcting blocksize from " . ($configured ? $configured : 'unset') . " to $recommended for '$storeid'\n"; ++ $scfg->{blocksize} = $recommended; ++ } ++ } ++ }; ++ warn "freenas-proxmox: blocksize detection in on_add_hook failed: $@\n" if $@; + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $base_path = PVE::Storage::LunCmd::Istgt::get_base($scfg); + } elsif ($scfg->{iscsiprovider} eq 'iet' || $scfg->{iscsiprovider} eq 'LIO') { +@@ -349,6 +410,32 @@ + + $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname; + ++ # Auto-detect ZFS blocksize from TrueNAS API and override if wrong. ++ # SCALE requires >= 16k; CORE works with 8k. If the configured blocksize is ++ # less than recommended (or unset), correct it now and persist to storage.cfg. ++ if ($scfg->{iscsiprovider} eq 'freenas') { ++ eval { ++ my $recommended = PVE::Storage::LunCmd::FreeNAS::freenas_get_recommended_blocksize($scfg); ++ if (defined $recommended) { ++ my $configured = PVE::Storage::LunCmd::FreeNAS::freenas_parse_blocksize($scfg->{blocksize}); ++ if (!$configured || $configured < $recommended) { ++ warn "freenas-proxmox: blocksize " . ($configured ? $configured : 'unset') . " < recommended $recommended -- correcting storage '$storeid'\n"; ++ $scfg->{blocksize} = $recommended; ++ eval { ++ require PVE::Storage; ++ PVE::Storage::lock_storage_config(sub { ++ my $cfg = PVE::Storage::config(); ++ $cfg->{ids}{$storeid}{blocksize} = $recommended if $cfg->{ids}{$storeid}; ++ PVE::Storage::write_config($cfg); ++ }); ++ }; ++ warn "freenas-proxmox: could not persist blocksize to storage config: $@\n" if $@; ++ } ++ } ++ }; ++ warn "TrueNAS blocksize detection failed: $@" if $@; ++ } ++ + $class->zfs_create_zvol($scfg, $volname, $size); + + my $guid = $class->zfs_create_lu($scfg, $volname); diff --git a/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch index 8124131..07d7566 100644 --- a/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch @@ -1,5 +1,5 @@ ---- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500 -+++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500 +--- /mnt/c/Users/Kevin/eclipse-workspace/freenas-proxmox/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig 2026-05-20 12:28:37.312211300 -0400 ++++ /tmp/ZFSPlugin-8x-work.pm 2026-05-22 08:57:21.728470343 -0400 @@ -10,6 +10,7 @@ use base qw(PVE::Storage::ZFSPoolPlugin); @@ -45,16 +45,16 @@ @@ -157,7 +163,7 @@ sub zfs_get_lun_number { my ($class, $scfg, $guid) = @_; - + - die "could not find lun_number for guid $guid" if !$guid; + die "could not find lun_number for guid $guid" if !defined $guid; - + if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) { return $1; @@ -166,6 +172,15 @@ die "lun_number for guid $guid is not a number"; } - + +# Part of the multipath enhancement +sub zfs_get_wwid_number { + my ($class, $scfg, $guid) = @_; @@ -164,3 +164,38 @@ return ($path, $vmid, $vtype); } +@@ -299,7 +380,33 @@ + my $volname = $name; + + $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname; +- ++ ++ # Auto-detect ZFS blocksize from TrueNAS API and override if wrong. ++ # SCALE requires >= 16k; CORE works with 8k. If the configured blocksize is ++ # less than recommended (or unset), correct it now and persist to storage.cfg. ++ if ( eq 'freenas') { ++ eval { ++ my = PVE::Storage::LunCmd::FreeNAS::freenas_get_recommended_blocksize(HASH(0x6158b91f5240)); ++ if (defined ) { ++ my = PVE::Storage::LunCmd::FreeNAS::freenas_parse_blocksize(); ++ if (! || < ) { ++ warn "freenas-proxmox: blocksize " . ( ? : 'unset') . " < recommended -- correcting storage ''\n"; ++ = ; ++ eval { ++ require PVE::Storage; ++ PVE::Storage::lock_storage_config(sub { ++ my HASH(0x6158b91f52a0) = PVE::Storage::config(); ++ = if HASH(0x6158b91f5420); ++ PVE::Storage::write_config(HASH(0x6158b91f52a0)); ++ }); ++ }; ++ warn "freenas-proxmox: could not persist blocksize to storage config: \n" if ; ++ } ++ } ++ }; ++ warn "TrueNAS blocksize detection failed: " if ; ++ } ++ + $class->zfs_create_zvol($scfg, $volname, $size); + + my $guid = $class->zfs_create_lu($scfg, $volname);