From 0dea6bc0784e35141b1f2e61c0144810e5de7abc Mon Sep 17 00:00:00 2001 From: Kevin Adams Date: Wed, 27 May 2026 17:31:02 -0400 Subject: [PATCH] fix: delete all targetextent associations in free_image (#265) _find_extent returned only the first targetextent row for an extent. If duplicate rows exist (from a prior failed alloc_image leaving an orphan), free_image deleted one association and then hit a 422 on the extent DELETE because TrueNAS still saw the second association as keeping the target in use. Return all rows in a new targetextents field and loop over them in free_image so every association is removed before the extent DELETE. Fixes: 422 Unprocessable Entity on free_image (SCALE 24.10, vm-103-disk-3) Co-Authored-By: Claude Sonnet 4.6 --- perl5/PVE/Storage/Custom/TrueNAS.pm | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/perl5/PVE/Storage/Custom/TrueNAS.pm b/perl5/PVE/Storage/Custom/TrueNAS.pm index d741f6a..aa8b29b 100644 --- a/perl5/PVE/Storage/Custom/TrueNAS.pm +++ b/perl5/PVE/Storage/Custom/TrueNAS.pm @@ -365,8 +365,9 @@ sub _next_lun_id { return int($lun); } -# Returns { extent_id, targetextent_id, lun_id, target_id } for a volname, -# or undef if no extent exists. +# Returns { extent_id, targetextents, targetextent_id, lun_id, target_id } for a volname, +# or undef if no extent exists. targetextents is the full array of all associations +# (normally one, but may be >1 if duplicate rows exist from a prior failed alloc_image). sub _find_extent { my ($scfg, $volname) = @_; @@ -379,6 +380,7 @@ sub _find_extent { return { extent_id => $ext->{id}, + targetextents => $tes, targetextent_id => defined $te ? $te->{id} : undef, lun_id => defined $te ? $te->{lunid} : 0, target_id => defined $te ? $te->{target} : undef, @@ -508,14 +510,13 @@ sub free_image { my $ext = _find_extent($scfg, $volname); if ($ext) { - # Step 1: unmap the LUN from the target. This must happen before the - # extent delete when the per-VM target still has other LUNs with active - # sessions (e.g. migrating one disk while the VM is running with others). - # Removing the targetextent severs just this LUN's association without - # touching the target session or other LUNs. - if (defined $ext->{targetextent_id}) { - eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}") }; - _log('warning', "free_image: could not remove targetextent $ext->{targetextent_id}: $@") if $@; + # Step 1: unmap ALL LUN associations for this extent. Normally there is + # exactly one targetextent row, but duplicate rows can exist if a prior + # alloc_image failed partway through and left an orphan. Deleting all of + # them before the extent DELETE prevents a 422 "target in use" rejection. + for my $te (@{ $ext->{targetextents} // [] }) { + eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$te->{id}") }; + _log('warning', "free_image: could not remove targetextent $te->{id}: $@") if $@; } # Step 2: delete the extent. force=true handles any residual session