fix: hybrid free_image fallback — service restart + explicit targetextent delete (#251)

force=true on extent DELETE is not sufficient when CORE 13 has an active
or recovering session holding the targetextent (422 persists through retries).

Fallback path (triggered only when force fails and a targetextent exists):
1. POST /service/restart — purges all server-side session state
2. DELETE /iscsi/targetextent/id/{id} — now succeeds with service down
3. DELETE /iscsi/extent/id/{id} — clean delete with no association

Fast path (force=true) is tried first and handles the normal case without
any service disruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kevin Adams 2026-05-24 09:24:36 -04:00
parent 9cd3cfa234
commit c1f021c5a6
1 changed files with 15 additions and 10 deletions

View File

@ -436,23 +436,28 @@ sub free_image {
my $ext = _find_extent($scfg, $volname);
if ($ext) {
# Delete the extent with force=true. The v2.0 API cascades targetextent
# deletion automatically — explicit targetextent DELETE is not needed and
# would 422 "target in use" if attempted while a session is active.
# No initiator logout required: force=true is handled server-side by TrueNAS.
# If force alone fails (CORE 13 strict enforcement), restart the iSCSI
# service to purge server-side session state and retry.
# Fast path: DELETE extent with force=true — TrueNAS v2.0 API cascades
# the targetextent association automatically on success.
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");
} elsif (defined $ext->{targetextent_id}) {
# force=true was not enough (CORE 13 strict session enforcement).
# Restart the TrueNAS iSCSI service to purge all server-side session
# state, explicitly delete the targetextent (now possible with the
# service down), then delete the extent cleanly.
_log('warning', "free_image: force delete failed ($@) — falling back to service restart + explicit targetextent delete");
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 }) };
eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}") };
if ($@) {
_log('warning', "free_image: targetextent delete attempt $attempt failed: $@");
sleep $attempt;
next;
}
eval { _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}") };
if (!$@) { $deleted = 1; last; }
_log('warning', "free_image: extent delete attempt $attempt failed: $@");
sleep $attempt;