From 96957c037f8ff5a64633177fc0d9047355cf1db1 Mon Sep 17 00:00:00 2001 From: Kevin Adams Date: Tue, 26 May 2026 23:01:25 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20SCALE=2025.04=20compatibility=20?= =?UTF-8?q?=E2=80=94=20integer=20types=20and=20alias=20uniqueness=20(#264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrueNAS SCALE 25.04 introduced two breaking changes in its iSCSI API: 1. Unique alias constraint: POST /iscsi/target with alias collides on retry. Remove alias field entirely — names are already unique per-VM. 2. Pydantic v2 strict integer validation: rejects JSON strings ("7") where integers are expected. Perl scalars become dual-vars (string+int flags) after log interpolation or hash-key lookups, causing encode_json to emit quoted values. Fix: int() at all integer-ID pass sites (portal, target, extent, lun) and omit null initiator/auth instead of sending explicit null. Confirmed: alloc_image works on SCALE 25.04, SCALE 24.10, and CORE 13.0-U6. Co-Authored-By: Claude Sonnet 4.6 --- perl5/PVE/Storage/Custom/TrueNAS.pm | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/perl5/PVE/Storage/Custom/TrueNAS.pm b/perl5/PVE/Storage/Custom/TrueNAS.pm index a948bf7..1119bae 100644 --- a/perl5/PVE/Storage/Custom/TrueNAS.pm +++ b/perl5/PVE/Storage/Custom/TrueNAS.pm @@ -277,19 +277,25 @@ sub _portal_groups_for_new_target { return _clean_groups(\@gs) if @gs; } - # No existing targets — use first matching portal, no initiator restriction + # No existing targets — use first matching portal, no initiator restriction. + # Force int(): hash keys are always Perl strings; encode_json must see an IV + # to emit "portal":1 rather than "portal":"1" (rejected by Pydantic v2). my ($pid) = keys %our_portal_ids; - return [ { portal => $pid, initiator => undef, authmethod => 'NONE', auth => undef } ]; + return [ { portal => int($pid), authmethod => 'NONE' } ]; } sub _clean_groups { my ($gs) = @_; - return [ map { { - portal => $_->{portal}, - initiator => $_->{initiator}, - authmethod => $_->{authmethod} // 'NONE', - auth => $_->{auth}, - } } @$gs ]; + return [ map { + my %g = ( + portal => int($_->{portal}), + authmethod => $_->{authmethod} // 'NONE', + ); + # Omit null integer fields — Pydantic v2 (SCALE 25.04+) is strict about types + $g{initiator} = int($_->{initiator}) if defined $_->{initiator}; + $g{auth} = int($_->{auth}) if defined $_->{auth}; + \%g + } @$gs ]; } # Finds or creates the per-VM iSCSI target for $vmid. @@ -314,7 +320,6 @@ sub _resolve_vm_target { my $groups = _portal_groups_for_new_target($scfg); my $new = _api($scfg, 'POST', '/iscsi/target', { name => $target_name, - alias => "Proxmox VM $vmid", mode => 'ISCSI', groups => $groups, }); @@ -323,7 +328,9 @@ sub _resolve_vm_target { _log('info', "Created per-VM target: $iqn (id=$t_id)"); } - $state->{$host}{vm_targets}{$vmid} = { id => $t_id, iqn => $iqn }; + # int() after _log interpolation: string-interpolating $t_id sets its PV flag, + # turning it into a dual-var that encode_json encodes as a string. + $state->{$host}{vm_targets}{$vmid} = { id => int($t_id), iqn => $iqn }; return $state->{$host}{vm_targets}{$vmid}; } @@ -353,8 +360,8 @@ sub _next_lun_id { my $tes = _api($scfg, 'GET', "/iscsi/targetextent?target=$target_id") // []; my %used = map { $_->{lunid} => 1 } @$tes; my $lun = 0; - $lun++ while $used{$lun}; - return $lun; + $lun++ while $used{$lun}; # hash-key lookup stringifies $lun; int() restores IV type + return int($lun); } # Returns { extent_id, targetextent_id, lun_id, target_id } for a volname, @@ -449,10 +456,13 @@ sub alloc_image { my $target = _resolve_vm_target($scfg, $vmid); my $lun_id = _next_lun_id($scfg, $target->{id}); + # int() forces fresh IV scalars — decode_json IDs can become dual-vars + # (string+int) after log interpolation or hash-key lookup, which causes + # encode_json to emit "7" instead of 7, rejected by Pydantic v2. _api($scfg, 'POST', '/iscsi/targetextent', { - target => $target->{id}, - extent => $extent->{id}, - lunid => $lun_id, + target => int($target->{id}), + extent => int($extent->{id}), + lunid => int($lun_id), }); _reload_iscsi($scfg);