feat: auto-detect and correct ZFS blocksize for TrueNAS SCALE/CORE (#241)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a0d0285194
commit
c166501873
|
|
@ -69,6 +69,13 @@ jobs:
|
||||||
< stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \
|
< stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \
|
||||||
&& echo "ZFSPlugin PVE-8 patch: OK"
|
&& 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)"
|
- name: "Dry-run patch: apidoc.js (PVE 8)"
|
||||||
run: |
|
run: |
|
||||||
patch --dry-run --ignore-whitespace \
|
patch --dry-run --ignore-whitespace \
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package PVE::Storage::LunCmd::FreeNAS;
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
our $VERSION = '2.3.0';
|
our $VERSION = '2.4.0';
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
use PVE::SafeSyslog;
|
use PVE::SafeSyslog;
|
||||||
use IO::Socket::SSL;
|
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
|
# Remove an extent by it's id
|
||||||
# http://api.freenas.org/resources/iscsi/index.html#delete-resource
|
# http://api.freenas.org/resources/iscsi/index.html#delete-resource
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
--- /tmp/ZFSPlugin-8.4.19-stock.pm 2026-05-19 23:09:15.498561777 -0400
|
--- /tmp/ZFSPlugin-8.4.orig 2026-05-22 08:54:24.992472212 -0400
|
||||||
+++ /tmp/ZFSPlugin-8.4.19-patched.pm 2026-05-19 23:13:40.830560931 -0400
|
+++ /tmp/ZFSPlugin-8.4-final.pm 2026-05-22 08:51:00.460468461 -0400
|
||||||
@@ -11,6 +11,7 @@
|
@@ -11,6 +11,7 @@
|
||||||
|
|
||||||
use base qw(PVE::Storage::ZFSPoolPlugin);
|
use base qw(PVE::Storage::ZFSPoolPlugin);
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
use PVE::Storage::LunCmd::Istgt;
|
use PVE::Storage::LunCmd::Istgt;
|
||||||
use PVE::Storage::LunCmd::Iet;
|
use PVE::Storage::LunCmd::Iet;
|
||||||
use PVE::Storage::LunCmd::LIO;
|
use PVE::Storage::LunCmd::LIO;
|
||||||
@@ -26,6 +27,7 @@
|
@@ -26,13 +27,14 @@
|
||||||
modify_lu => 1,
|
modify_lu => 1,
|
||||||
add_view => 1,
|
add_view => 1,
|
||||||
list_view => 1,
|
list_view => 1,
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
list_lu => 1,
|
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 @@
|
@@ -44,6 +46,8 @@
|
||||||
return PVE::Storage::LunCmd::Istgt::get_base($scfg);
|
return PVE::Storage::LunCmd::Istgt::get_base($scfg);
|
||||||
} elsif ($scfg->{iscsiprovider} eq 'iet') {
|
} elsif ($scfg->{iscsiprovider} eq 'iet') {
|
||||||
|
|
@ -104,3 +112,58 @@
|
||||||
sparse => { optional => 1 },
|
sparse => { optional => 1 },
|
||||||
comstar_hg => { optional => 1 },
|
comstar_hg => { optional => 1 },
|
||||||
comstar_tg => { 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);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -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
|
||||||
+++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500
|
+++ /tmp/ZFSPlugin-8x-work.pm 2026-05-22 08:57:21.728470343 -0400
|
||||||
@@ -10,6 +10,7 @@
|
@@ -10,6 +10,7 @@
|
||||||
|
|
||||||
use base qw(PVE::Storage::ZFSPoolPlugin);
|
use base qw(PVE::Storage::ZFSPoolPlugin);
|
||||||
|
|
@ -45,16 +45,16 @@
|
||||||
@@ -157,7 +163,7 @@
|
@@ -157,7 +163,7 @@
|
||||||
sub zfs_get_lun_number {
|
sub zfs_get_lun_number {
|
||||||
my ($class, $scfg, $guid) = @_;
|
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 !$guid;
|
||||||
+ die "could not find lun_number for guid $guid" if !defined $guid;
|
+ die "could not find lun_number for guid $guid" if !defined $guid;
|
||||||
|
|
||||||
if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) {
|
if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) {
|
||||||
return $1;
|
return $1;
|
||||||
@@ -166,6 +172,15 @@
|
@@ -166,6 +172,15 @@
|
||||||
die "lun_number for guid $guid is not a number";
|
die "lun_number for guid $guid is not a number";
|
||||||
}
|
}
|
||||||
|
|
||||||
+# Part of the multipath enhancement
|
+# Part of the multipath enhancement
|
||||||
+sub zfs_get_wwid_number {
|
+sub zfs_get_wwid_number {
|
||||||
+ my ($class, $scfg, $guid) = @_;
|
+ my ($class, $scfg, $guid) = @_;
|
||||||
|
|
@ -164,3 +164,38 @@
|
||||||
return ($path, $vmid, $vtype);
|
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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue