fix: correct iscsi.extent/target/targetextent.delete param shapes (WS transport)

Live-tested the write path against .92 for the first time (alloc_image
-> path -> volume_size_info -> free_image full cycle) and found two
real bugs in the inferred delete mappings:

- iscsi.target.delete and iscsi.targetextent.delete were passing a raw
  regex-captured id as a Perl string; Pydantic rejects "Input should
  be a valid integer" without int().
- iscsi.extent.delete(id, remove=False, force=False) is positional --
  REST's DELETE body {force=>true} was being passed as a single hash
  in position 2, which the API validates as a boolean field named
  "remove", not force. Fixed to unpack positionally: id, remove
  (always false -- REST never asked for this), force (from the REST
  body's force field).

Confirmed clean after the fix: a full alloc_image/free_image cycle
against .92 leaves zero orphaned targets, extents, targetextents, or
datasets. This is the first write-path confirmation for the WebSocket
transport -- narrows ADR-012's "not yet live-tested" warning to the
remaining untested calls (targetextent.create/query filters beyond
what alloc_image exercises, pool.dataset.delete's options shape,
zfs.snapshot.* entirely).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adams
2026-08-31 10:13:34 -04:00
co-authored by Claude Sonnet 5
parent b2187654dc
commit c05de59396
+16 -4
View File
@@ -398,13 +398,13 @@ sub _api_ws {
}
} elsif ($base =~ m{^/iscsi/target/id/(.+)$}) {
my $id = $1;
my $id = int($1);
if ($method eq 'DELETE') {
$rpc_method = 'iscsi.target.delete';
$params = [$id];
} else {
$rpc_method = 'iscsi.target.query';
$params = [[[ 'id', '=', int($id) ]], {}];
$params = [[[ 'id', '=', $id ]], {}];
$single = 1;
}
@@ -422,7 +422,11 @@ sub _api_ws {
} elsif ($base =~ m{^/iscsi/targetextent/id/(.+)$}) {
$rpc_method = 'iscsi.targetextent.delete';
$params = [$1, $data // JSON::false];
# REST call sites pass either a bare boolean or {force=>...} as $data;
# iscsi.targetextent.delete(id, force=False) is positional (id, bool).
my $force = (ref($data) eq 'HASH') ? ($data->{force} ? JSON::true : JSON::false)
: ($data ? JSON::true : JSON::false);
$params = [int($1), $force];
} elsif ($base eq '/iscsi/extent') {
if ($method eq 'POST') {
@@ -435,7 +439,15 @@ sub _api_ws {
} elsif ($base =~ m{^/iscsi/extent/id/(.+)$}) {
$rpc_method = 'iscsi.extent.delete';
$params = [$1, $data // {}];
# CONFIRMED live 2026-08-31: iscsi.extent.delete(id, remove=False,
# force=False) is positional, NOT (id, {force=>bool}) — a {force=>...}
# hash in position 2 fails Pydantic validation on the "remove" field.
# REST's "force" concept maps to WS's 3rd positional "force"; "remove"
# (whether to also delete the underlying zvol/file, which the REST
# path never asked for since free_image deletes the dataset itself
# afterward) stays false.
my $force = (ref($data) eq 'HASH' && $data->{force}) ? JSON::true : JSON::false;
$params = [int($1), JSON::false, $force];
} elsif ($base eq '/service/reload') {
$rpc_method = 'service.reload';