fix: explicitly delete zvol via pool/dataset API on extent removal (#239)

remove:true in the iSCSI extent DELETE body is unreliable on TrueNAS
SCALE 24.10+ — the extent config is removed but the underlying zvol is
left behind as an orphaned dataset. Add freenas_delete_zvol() which calls
DELETE /api/v2.0/pool/dataset/id/{path}/ explicitly after every extent
removal, covering both the rollback path in run_create_lu and normal
disk deletion in run_delete_lu. Guarded to v2.0 API only using the
per-host cache to avoid stale package-global state when two storage
hosts with different API versions are active simultaneously.

Verified on TrueNAS SCALE 24.10.2.1: zvol is fully removed from
Datasets after a forced rollback test (FREENAS_TEST_ROLLBACK=1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kevin Adams 2026-05-20 08:56:02 -04:00
parent ec4085b244
commit 267e14ebfa
1 changed files with 39 additions and 0 deletions

View File

@ -279,6 +279,7 @@ sub run_create_lu {
syslog("err", (caller(0))[3] . " : rolling back after error for $lun_path: $err");
freenas_iscsi_remove_target_to_extent($scfg, $link_id) if defined $link_id;
freenas_iscsi_remove_extent($scfg, $extent_id) if defined $extent_id;
freenas_delete_zvol($scfg, $lun_path) if defined $extent_id;
die "Unable to create lun $lun_path (rolled back): $err";
}
@ -331,6 +332,10 @@ sub run_delete_lu {
# Remove the link
my $remove_link = freenas_iscsi_remove_target_to_extent($scfg, $link->{'id'});
# Explicitly delete the underlying zvol — remove:true on extent DELETE is
# unreliable on TrueNAS SCALE 24.10+ (#239)
freenas_delete_zvol($scfg, $params[0]);
if($remove_link == 1 && $remove_extent == 1) {
syslog("info", (caller(0))[3] . "(lun_path=$lun_path) : successful");
} else {
@ -597,6 +602,40 @@ sub freenas_iscsi_create_extent {
}
}
#
# Explicitly delete the underlying ZFS zvol via the pool/dataset API.
# The extent DELETE with remove:true is unreliable on TrueNAS SCALE 24.10+
# (extent config is removed but the zvol is left behind). This function
# is a belt-and-suspenders call after every extent removal.
# Only runs on v2.0; v1.0 hosts are expected to handle it via remove:true.
# Parameters:
# - scfg
# - lun_path (either /dev/zvol/tank/... or zvol/tank/... form)
#
sub freenas_delete_zvol {
my ($scfg, $lun_path) = @_;
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
my $api_version = ($freenas_server_list->{$apihost}{api_version} // $freenas_api_version) // '';
return 1 unless $api_version eq 'v2.0';
my $dataset = $lun_path;
$dataset =~ s{^/dev/zvol/}{}; # strip /dev/zvol/ prefix (run_create_lu form)
$dataset =~ s{^zvol/}{}; # strip zvol/ prefix (run_delete_lu form after $dev_prefix strip)
$dataset =~ s{/}{%2F}g; # URL-encode path separators for REST endpoint
syslog("info", (caller(0))[3] . " : explicitly deleting zvol dataset '$dataset'");
freenas_api_call($scfg, 'DELETE', "/api/v2.0/pool/dataset/id/$dataset/", {"recursive" => \0});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 204) {
syslog("info", (caller(0))[3] . " : zvol '$dataset' deleted successfully");
return 1;
} else {
syslog("err", (caller(0))[3] . " : WARNING: could not delete zvol '$dataset' (HTTP $code) — manual cleanup may be needed");
return 0;
}
}
#
# Remove an extent by it's id
# http://api.freenas.org/resources/iscsi/index.html#delete-resource