fix: service-stop fallback for free_image on CORE 13 (#251)

force=true on extent/targetextent DELETE is unconditionally ignored by
CORE 13.0 — it always checks for active sessions. POST /service/restart
is async and iscsid reconnects in milliseconds, so the restart window
was never clear enough for the DELETE to succeed.

New fallback sequence (only triggered when force=true fails):
1. Set iscsiadm node to manual startup — prevents auto-reconnect
2. iscsiadm --logout — drops initiator session
3. POST /service/stop + poll until service is stopped (up to 15s)
4. DELETE targetextent — succeeds with service fully stopped
5. DELETE extent
6. POST /service/start — restore service
7. Restore automatic startup + discovery + login + rescan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kevin Adams 2026-05-24 11:20:11 -04:00
parent c1f021c5a6
commit 178088197f
1 changed files with 44 additions and 17 deletions

View File

@ -444,24 +444,51 @@ sub free_image {
if (!$@) {
$deleted = 1;
} 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/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;
# force=true is ignored by TrueNAS CORE 13 — unconditional session check.
# POST /service/restart is async and iscsid reconnects in milliseconds,
# so we must: disable auto-reconnect → logout → wait for service to stop
# → delete → re-enable auto-reconnect → login.
_log('warning', "free_image: force delete failed — using service-stop fallback");
my $target = _resolve_target($scfg);
my $portal = _portal($scfg);
my $iqn = $target->{iqn};
# Prevent iscsid from auto-reconnecting during the delete window
eval { run_command(['iscsiadm', '-m', 'node', '-T', $iqn,
'-p', "$portal:3260", '--op', 'update',
'-n', 'node.startup', '-v', 'manual'], noerr => 1) };
eval { run_command(['iscsiadm', '-m', 'node', '-T', $iqn,
'-p', "$portal:3260", '--logout'], noerr => 1) };
# Stop the service and wait up to 15s for it to actually stop
eval { _api($scfg, 'POST', '/service/stop', { service => 'iscsitarget' }) };
for my $i (1..15) {
my $svc = _api($scfg, 'GET', '/service?service=iscsitarget') // [];
my ($s) = grep { ($_->{service} // '') eq 'iscsitarget' } @$svc;
last if $s && !$s->{state};
sleep 1;
}
my $deleted_te = 0;
eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}") };
$deleted_te = 1 unless $@;
_log('warning', "free_image: targetextent delete failed: $@") if $@;
eval { _api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}") };
$deleted = 1 unless $@;
_log('warning', "free_image: extent delete failed: $@") if $@;
# Restart the service and restore the session
eval { _api($scfg, 'POST', '/service/start', { service => 'iscsitarget' }) };
eval { run_command(['iscsiadm', '-m', 'node', '-T', $iqn,
'-p', "$portal:3260", '--op', 'update',
'-n', 'node.startup', '-v', 'automatic'], noerr => 1) };
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) };
}
die "free_image: could not delete extent $ext->{extent_id} after retries\n"
unless $deleted;