From 377503f89d35877e85346a86c7c38ff86f63916e Mon Sep 17 00:00:00 2001 From: Kevin Adams Date: Sun, 24 May 2026 09:03:38 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20follow=20original=20FreeNAS.pm=20delete?= =?UTF-8?q?=20order=20=E2=80=94=20extent=20with=20force=20cascades=20targe?= =?UTF-8?q?textent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original LunCmd/FreeNAS.pm (v2.0 path) deleted the extent with { force: true } and skipped the explicit targetextent DELETE entirely, relying on TrueNAS to cascade it. Our v3.0 had the order backwards: deleting the targetextent first always hit 422 "target in use" when any iSCSI session was active. New approach mirrors the original: 1. Log out initiator session 2. DELETE extent with { force: true } — TrueNAS cascades targetextent 3. If that fails (CORE 13 strict session enforcement), restart iSCSI service and retry the extent DELETE (up to 5 times) 4. Restore session Co-Authored-By: Claude Sonnet 4.6 --- perl5/PVE/Storage/Custom/TrueNAS.pm | 98 +++++++++++++---------------- 1 file changed, 44 insertions(+), 54 deletions(-) diff --git a/perl5/PVE/Storage/Custom/TrueNAS.pm b/perl5/PVE/Storage/Custom/TrueNAS.pm index 347377a..a188728 100644 --- a/perl5/PVE/Storage/Custom/TrueNAS.pm +++ b/perl5/PVE/Storage/Custom/TrueNAS.pm @@ -436,62 +436,52 @@ sub free_image { my $ext = _find_extent($scfg, $volname); 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}; + my $target = _resolve_target($scfg); + my $portal = _portal($scfg); + my $iqn = $target->{iqn}; - # TrueNAS CORE 13 refuses targetextent deletion while any iSCSI session - # exists. We log out, restart the service to purge server-side state, - # then immediately attempt the DELETE. pvedaemon workers can race us - # and reconnect the session, so we retry up to 5 times with a fresh - # logout+restart cycle each time. - my $deleted = 0; - for my $attempt (1..5) { - # Log out any active session for this target - my $sessions = ''; - eval { run_command(['iscsiadm', '-m', 'session'], - outfunc => sub { $sessions .= shift . "\n" }, - noerr => 1) }; - for my $sid ($sessions =~ /\[(\d+)\][^\n]*\Q$iqn\E/g) { - _log('info', "free_image: logging out iSCSI session $sid (attempt $attempt)"); - eval { run_command(['iscsiadm', '-m', 'session', '-r', $sid, '--logout'], - noerr => 1) }; - } - - # Restart TrueNAS iSCSI service to clear all server-side session state - _log('info', "free_image: restarting TrueNAS iSCSI service (attempt $attempt)"); - eval { _api($scfg, 'POST', '/service/restart', { service => 'iscsitarget' }) }; - - eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}") }; - if ($@) { - _log('warning', "free_image: targetextent delete attempt $attempt failed: $@"); - sleep $attempt; # back off before retry - next; - } - $deleted = 1; - last; - } - die "free_image: could not delete targetextent $ext->{targetextent_id} after 5 attempts\n" - unless $deleted; - - _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); + # Log out any active initiator session for this target before we touch TrueNAS. + my $sessions = ''; + eval { run_command(['iscsiadm', '-m', 'session'], + outfunc => sub { $sessions .= shift . "\n" }, + noerr => 1) }; + for my $sid ($sessions =~ /\[(\d+)\][^\n]*\Q$iqn\E/g) { + _log('info', "free_image: logging out iSCSI session $sid"); + eval { run_command(['iscsiadm', '-m', 'session', '-r', $sid, '--logout'], + noerr => 1) }; } + + # Delete the extent with force=true. The v2.0 API cascades targetextent + # deletion automatically — explicit targetextent DELETE is not needed and + # would fail with 422 "target in use" if any session is still active. + # If force is not enough (CORE 13 is strict about active sessions), restart + # the TrueNAS iSCSI service to purge server-side state and retry. + my $deleted = 0; + eval { _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}", + { force => JSON::true }) }; + if (!$@) { + $deleted = 1; + } else { + _log('warning', "free_image: extent delete with force failed ($@) — trying service restart"); + for my $attempt (1..5) { + eval { _api($scfg, 'POST', '/service/restart', { service => 'iscsitarget' }) }; + eval { _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}", + { force => JSON::true }) }; + if (!$@) { $deleted = 1; last; } + _log('warning', "free_image: extent delete attempt $attempt failed: $@"); + sleep $attempt; + } + } + die "free_image: could not delete extent $ext->{extent_id} after retries\n" + unless $deleted; + + # 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 { _log('warning', "free_image: no iSCSI extent found for $volname — skipping extent removal"); }