From f521089e575d92b8e28f3bbfc70f6abba4baad85 Mon Sep 17 00:00:00 2001 From: Kevin Adams Date: Sun, 24 May 2026 08:12:41 -0400 Subject: [PATCH] fix: parse_volname + free_image session/LUN teardown for CORE 13 parse_volname: implement for vm-/base- volume names so PVE can resolve device paths and hotplug disks. Without this the base class falls through to directory-volume parsing and fails with a 400 hotplug error. free_image: TrueNAS CORE 13 holds iSCSI sessions in recovery state after TCP disconnect, blocking targetextent deletion with 422 even seconds after logout. Fix: log out the initiator session by SID, then restart the TrueNAS iSCSI service to immediately clear server-side session state. Delete the targetextent and extent, then restore the initiator session so other LUNs on the same target remain accessible. Co-Authored-By: Claude Sonnet 4.6 --- perl5/PVE/Storage/Custom/TrueNAS.pm | 50 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/perl5/PVE/Storage/Custom/TrueNAS.pm b/perl5/PVE/Storage/Custom/TrueNAS.pm index 3e9f1c4..a2754b2 100644 --- a/perl5/PVE/Storage/Custom/TrueNAS.pm +++ b/perl5/PVE/Storage/Custom/TrueNAS.pm @@ -352,6 +352,18 @@ sub _iscsi_ensure_session { # ── PVE::Storage::Plugin interface ─────────────────────────────────────────── +sub parse_volname { + my ($class, $volname) = @_; + + if ($volname =~ /^(vm|base)-(\d+)-disk-\d+$/) { + my ($prefix, $vmid) = ($1, $2); + my $isBase = $prefix eq 'base' ? 1 : 0; + return ('images', $volname, $vmid, undef, undef, $isBase, 'raw'); + } + + die "unable to parse TrueNAS volume name '$volname'\n"; +} + sub status { my ($class, $storeid, $scfg, $cache) = @_; @@ -425,10 +437,44 @@ sub free_image { if ($ext) { if (defined $ext->{targetextent_id}) { + # TrueNAS CORE 13 keeps iSCSI sessions in recovery state after TCP + # disconnect — a reload is not sufficient to clear them. A service + # restart immediately purges all server-side session state, which is + # the only reliable way to allow targetextent deletion. + my $target = _resolve_target($scfg); + my $portal = _portal($scfg); + my $iqn = $target->{iqn}; + + # Logout from the initiator side + my $sessions = ''; + eval { run_command(['iscsiadm', '-m', 'session'], + outfunc => sub { $sessions .= shift . "\n" }, + noerr => 1) }; + if ($sessions =~ /\[(\d+)\][^\n]*\Q$iqn\E/) { + my $sid = $1; + _log('info', "free_image: logging out iSCSI session $sid"); + eval { run_command(['iscsiadm', '-m', 'session', '-r', $sid, '--logout'], + noerr => 1) }; + } + + # Restart TrueNAS iSCSI service to immediately clear server-side sessions + _log('info', "free_image: restarting TrueNAS iSCSI service to clear session state"); + _api($scfg, 'POST', '/service/restart', { service => 'iscsitarget' }); + _api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}"); + _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}"); + + # Restore the session so remaining LUNs stay accessible + _log('info', "free_image: restoring iSCSI session"); + eval { run_command(['iscsiadm', '-m', 'discovery', '-t', 'sendtargets', + '-p', "$portal:3260"], noerr => 1) }; + eval { run_command(['iscsiadm', '-m', 'node', '-T', $iqn, + '-p', "$portal:3260", '--login'], noerr => 1) }; + eval { run_command(['iscsiadm', '-m', 'session', '--rescan'], noerr => 1) }; + } else { + _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}"); + _reload_iscsi($scfg); } - _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}"); - _reload_iscsi($scfg); } else { _log('warning', "free_image: no iSCSI extent found for $volname — skipping extent removal"); }