Commit Graph

115 Commits

Author SHA1 Message Date
Kevin Adams 6c313dfa9a feat: bump version to 3.2.0 for multipath stable release
Multipath tested and confirmed working on TrueNAS CORE 13.x (FreeBSD/ALUA)
and SCALE 24.10 + 25.04 (Linux LIO). Failover and path recovery validated
on both platforms. Closes #256.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:00:05 -04:00
Kevin Adams f0150c225e fix: delegate qemu_blockdev_options to Plugin.pm for host_device path
TrueNAS.pm::qemu_blockdev_options returns an iscsi:// blockdev — wrong for
multipath where path() returns /dev/mapper/<wwid>. Simply removing the override
caused the parent's iscsi driver to be inherited instead.

Fix: explicitly call PVE::Storage::Plugin::qemu_blockdev_options (the
grandparent). It calls path(), sees /dev/mapper/<wwid> starts with '/',
stats it as S_ISBLK, and returns { driver => 'host_device', filename => ... }.
QEMU receives: "driver":"host_device","filename":"/dev/mapper/<wwid>"

Tested: VM 103 started with multipath disk (vm-103-disk-0 on TrueNAS-Scale01-MP).
QEMU confirmed using host_device driver on /dev/mapper/36589cfc000000ad...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:48:39 -04:00
Kevin Adams 3bd9f551c7 fix: multipath activate_volume and naa field in _find_extent
TrueNAS.pm: expose naa field from _find_extent return hash — needed by
TrueNASMultipath.pm to build the /dev/mapper/<wwid> path.

TrueNASMultipath.pm: use multipathd reconfigure + fallback multipath -v0
instead of bare multipath(8) which conflicts with a running multipathd
daemon. Tested end-to-end on pve01-hq against TrueNAS SCALE:
  - Two sessions (172.31.69.91 + 192.168.69.91) both established
  - /dev/mapper/36589cfc000000eb2adcd24154021d376 appears as block device
  - deactivate_volume cleanly flushes and logs out

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:31:39 -04:00
Kevin Adams db730e39f5 fix: only declare new properties in TrueNASMultipath::properties()
PVE::SectionConfig registers properties globally across all plugin types.
Re-declaring the inherited TrueNAS.pm properties (truenas_host, truenas_pool,
etc.) caused a "duplicate property" error at pvedaemon startup when both
plugins are loaded. TrueNASMultipath::properties() now returns only the
new truenas_portals property; all inherited properties remain registered
by TrueNAS.pm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:09:48 -04:00
Kevin Adams 15d84637c9 fix: bare return in qemu_blockdev_options (perlcritic) 2026-06-08 11:54:40 -04:00
Kevin Adams 0d195a9074 feat: truenas-proxmox-multipath plugin and packaging (#256)
Adds TrueNASMultipath.pm — a separate PVE storage type 'truenas-multipath'
that inherits all REST API and iSCSI resource management from TrueNAS.pm but
uses iscsiadm + dm-multipath for block device access:

  path()             → /dev/mapper/<wwid>  (NAA from TrueNAS extent)
  activate_volume()  → iscsiadm login on each portal in truenas_portals;
                       waits for /dev/mapper device to appear
  deactivate_volume()→ multipath flush + iscsiadm logout
  qemu_blockdev_options → returns undef (path() used instead)

Packaging:
  packaging/DEBIAN-multipath/ — control.j2, postinst, postrm,
  changelog.Debian, multipath.conf.example
  Distributed via the 'multipath' apt component (ADR-011)
  Depends: truenas-proxmox >= 3.1.0, open-iscsi, multipath-tools

CI (build.yml):
  - Third staging dir (dist-multipath) and .deb output
  - multipath component pool under pool/multipath/v<major>
  - Both stable and testing publish steps updated to generate
    main + multipath components; Release lists both
  - Lint checks TrueNASMultipath.pm and multipath packaging scripts

PoC confirmed 2026-06-07 on pve01-hq against TrueNAS SCALE 24.10:
  - Two sessions (172.31.69.91 + 192.168.69.91), both active
  - dm-multipath aggregated 3 disks × 2 paths each
  - /dev/mapper/mpath* devices appeared correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:52:47 -04:00
Kevin Adams 80a78896bf fix: add state volume pattern to parse_volname (#234)
PVE allocates a vm-<vmid>-state-<snapname> volume when a snapshot is taken
with "Include RAM" selected. parse_volname didn't recognise this pattern,
causing a crash during both setup and cleanup of the state volume.

Added state volume pattern — treated as raw images, same vmid extraction
as disk volumes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:19:14 -04:00
Kevin Adams a47346161b fix: use bare return instead of return undef (perlcritic)
Subroutines::ProhibitExplicitReturnUndef — volume_snapshot,
volume_snapshot_delete, volume_snapshot_rollback all used return undef
where bare return is the correct Perl idiom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:14:53 -04:00
Kevin Adams 59d8a8100f feat: implement ZFS snapshot interface via TrueNAS REST API (#234)
Implements the full PVE storage plugin snapshot interface backed by native
ZFS snapshots on TrueNAS, via the v2.0 REST API:

  volume_has_feature       — advertises snapshot support for raw volumes
  volume_snapshot_needs_fsfreeze — returns 1 (request guest IO freeze)
  volume_snapshot          — POST /zfs/snapshot {dataset, name}
  volume_snapshot_delete   — DELETE /zfs/snapshot/id/{uri_escaped_id}
  volume_snapshot_rollback — POST /zfs/snapshot/rollback {id, options}
  volume_rollback_is_possible — enforces ZFS "most recent snap only" rule
  volume_snapshot_info     — GET /zfs/snapshot?dataset=... → {name: {id, ctime}}

Snapshot ID format: pool/volname@snapname
DELETE path requires full URI encoding (uri_escape) — unlike the dataset
endpoint, the snapshot API does not accept bare slashes in the path segment.

Tested on pve01-hq (PVE 9.2.3) against TrueNAS CORE 13.0-U6:
  - create / list / delete: OK
  - rollback_is_possible ordering: correctly rejects non-latest snapshots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:09:45 -04:00
Kevin Adams fc74a42cf2 feat: bump VERSION to 3.1.0 and api() to 14 for PVE 9 compatibility
Closes #270. PVE 9 expects APIVER=14; returning 11 caused a "older storage
API, upgrade recommended" warning on every pvedaemon start and qm command.
No new method implementations required — Plugin.pm has no gates between
api() 11 and 14. Bumping VERSION starts the v3.1.0 development cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:34:25 -04:00
Kevin Adams 84d459da02 docs: add removal instructions for qemu_blockdev_options override (#266)
When Proxmox fixes PVE::Storage::Plugin to use int($3) for the iscsi lun
field, this override becomes redundant. The comment now includes the exact
grep command to verify the upstream fix and confirms when removal is safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:05:17 -04:00
Kevin Adams b1423a9d72 fix: override qemu_blockdev_options to emit lun as integer on PVE 9 (#266)
PVE::Storage::Plugin::qemu_blockdev_options builds the iscsi blockdev JSON
with lun => "$3" — a string capture from the URL regex. QEMU 9's strict
blockdev schema rejects a string where an integer is required, causing every
VM with TrueNAS storage to fail to start on PVE 9.

Override the method in TrueNAS.pm to return the blockdev hash directly from
_find_extent, with lun => int($ext->{lun_id}). This bypasses the base class
entirely and is correct on both PVE 8 and PVE 9.

Verified on pve01-hq (PVE 9.2.3 / QEMU 9 / kernel 7.0.6-2-pve):
VM 103 (TrueNAS-Scale01-Tank01, SCALE 24.10) starts successfully.

The upstream bug in PVE::Storage::Plugin is tracked separately — when
Proxmox fixes it, the override can be removed. See issue #266 for details.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:04:30 -04:00
Kevin Adams c7ce39d8ae fix: SCALE 25.10 strict Pydantic rejects volsize encoded as string (#269)
SCALE 25.10 (Goldeye) enforces strict integer validation. The log call
preceding the API call stringifies $size_b (sets Perl's POK flag), causing
JSON::XS to encode it as "8589934592" (quoted) instead of 8589934592.
int() at the encode site produces a fresh IV without the POK flag.

Also adds error parser support for SCALE 25.10's field-keyed validation
error format so the full Pydantic detail surfaces in PVE task logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 08:58:00 -04:00
Kevin Adams 359c2af88e fix: force-delete targetextent when VM is running on free_image
SCALE 24.10 rejects DELETE /iscsi/targetextent/id/<id> with 422 if
the associated target has an active iSCSI session — which happens when
removing an unused disk from a running VM (the VM still holds the target
session for its other disks).

Pass bare boolean true as the request body to force the targetextent
delete.  Confirmed working on SCALE 24.10 and SCALE 25.04.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 23:08:40 -04:00
Kevin Adams 0dea6bc078 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 <noreply@anthropic.com>
2026-05-27 17:31:02 -04:00
Kevin Adams eab964c5a2 feat: rollback orphaned TrueNAS resources on alloc_image failure (#250)
Steps 2-4 of alloc_image (extent create, target find/create, targetextent
create) are now wrapped in an eval. On any failure the rollback runs in
reverse order, best-effort: target removed if it has no remaining extents
(i.e. it was just created for this disk), extent deleted with force=true,
zvol deleted recursively. This prevents the orphaned-resources-on-failure
state that previously required manual cleanup.

Also restore alias ("Proxmox VM <vmid>") on target creation. The alias was
removed during SCALE 25.04 debugging as a red herring — the actual bug was
int() type coercion (#264). Alias is unique per-VM and _resolve_vm_target
always checks for an existing target by name before attempting a POST, so
there is no alias-collision risk in normal operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:47:49 -04:00
Kevin Adams 96957c037f fix: SCALE 25.04 compatibility — integer types and alias uniqueness (#264)
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 <noreply@anthropic.com>
2026-05-26 23:01:25 -04:00
Kevin Adams 7a66e59dc7 fix: surface Pydantic array validation errors from SCALE 25.04+ API
SCALE 25.04 returns validation errors as a JSON array rather than a
{message: "..."} hash. The error detail was silently lost, making 4xx
failures appear as bare status lines in logs. Now handles both formats.

Prompted by a transient 422 on /iscsi/target during first-run testing
on SCALE 25.04 — root cause was iSCSI service mid-reload, not a format
incompatibility. Plugin is compatible with SCALE 25.04 as-is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:12:44 -04:00
Kevin Adams f30862f3c0 feat: read API token from /etc/pve/priv/truenas-<storeid>.key (#261)
The API token was previously stored only in storage.cfg, which is
replicated in plaintext across all cluster nodes via pmxcfs. This adds
a keyfile mechanism: at startup each public entry point calls
_resolve_token(), which checks /etc/pve/priv/truenas-<storeid>.key
before falling back to truenas_api_key in storage.cfg. The resolved
token is cached in the per-host $state so the file is read at most once
per daemon session. truenas_api_key is now optional in storage.cfg;
either the keyfile or the config key must be present.

Docs updated in getting-started.md (section 4.1) and the in-app help
HTML with setup commands and the cluster-copy reminder.

Closes #261

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:36:21 -04:00
Kevin Adams 3a8a5dfb3e fix: correct truenas_target IQN match in _portal_groups_for_new_target (#263)
The regex was testing whether the TrueNAS target's short name ends with
the user-supplied value — the opposite of what we want. When truenas_target
holds a full IQN (iqn...ctl:proxmox) the check must test whether that IQN
ends with the short name stored in TrueNAS. Silently fell through to
auto-discovery for every user who supplied a full IQN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:16:13 -04:00
Kevin Adams f71a1171ec fix: resolve perlcritic violations blocking CI
- Replace `return undef` with bare `return` (ProhibitExplicitReturnUndef)
- Add `return;` to void-context subs _log, _maybe_cleanup_vm_target,
  _reload_iscsi (RequireFinalReturn)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:14:05 -04:00
Kevin Adams 415c2a250b fix: unmap targetextent before extent delete in free_image (#259)
When a VM has multiple disks on the same per-VM target, migrating one
disk while the VM is running leaves the target in use for the other
disks. TrueNAS refuses to delete an extent associated with an in-use
target even with force=true.

Fix: delete the targetextent (LUN mapping) first, then delete the
extent. Removing the mapping severs this disk's association without
touching the active session or other LUNs on the same target.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:37:33 -04:00
Kevin Adams 5e2e5a8cbe fix: allow free_image on detached disks while VM is running (#258)
Removed _vm_is_running() check from free_image. When a disk is detached
from a running VM (unused0), QEMU immediately closes its libiscsi
connection — there is no active session to block the delete. The
force=true flag on the TrueNAS extent DELETE is the correct guard for
any residual session. The VM-running check was overly broad and blocked
legitimate deletes of already-detached disks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:28:32 -04:00
Kevin Adams d7b0735902 refactor: per-VM iSCSI targets with iscsi:// paths (#255)
Each VM now gets its own TrueNAS iSCSI target (proxmox-vm-<vmid>)
instead of sharing a single storage-wide target.  path() returns
iscsi://portal/iqn:proxmox-vm-<vmid>/lun — QEMU connects via libiscsi,
the same pattern ZFSPlugin.pm uses.

When a VM stops, QEMU closes its libiscsi connection.  TrueNAS sees
no active session on that VM's target, so extent DELETE with force=true
succeeds without stopping the iSCSI service.  Deleting one VM's disk
while other VMs are running on the same storage now works correctly.

Changes:
- Remove all iscsiadm session management (_iscsi_ensure_session,
  _iscsi_session_exists, _wait_for_device, _dev_path,
  _running_vms_on_storage, _resolve_target)
- Add _resolve_vm_target: find-or-create proxmox-vm-<vmid> target,
  inherit portal/initiator groups from existing targets
- Add _maybe_cleanup_vm_target: delete empty VM target after last disk
- Add _vm_is_running: check owning VM before deletion (uses PID file)
- Add _api_global: cached iSCSI global config (basename)
- path(): returns iscsi://portal/per-vm-iqn/lun (target looked up by
  target_id from extent, so legacy shared-target disks still work)
- activate_storage: API reachability check only
- activate_volume: verify extent exists, QEMU handles the connection
- deactivate_storage/volume: clear cache, return 1
- free_image: check owning VM stopped, force-DELETE extent, zvol,
  cleanup empty target — no service-stop fallback needed

Closes #255

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:04:01 -04:00
Kevin Adams 01318919fe fix: untaint PID in _running_vms_on_storage for PVE taint-mode Perl
PVE runs pvedaemon with -T (taint mode). Data read from files is tainted
and cannot be passed to kill() without explicit untainting. Extract PID
via regex capture which Perl treats as safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:35:46 -04:00
Kevin Adams 5684b335a1 fix: pre-flight check before service-stop fallback in free_image (#253)
Stopping the TrueNAS iSCSI service to clear session state (required on
CORE 13 when force=true is ignored) disrupts all LUNs on the target,
causing io-error on any other running VM using this storage.

Before triggering the service-stop path, scan /etc/pve/qemu-server/*.conf
for running VMs (PID file exists + process alive) that reference this
storage ID. If any are found, fail with a clear message naming the
blocking VMs rather than silently disrupting them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:29:14 -04:00
Kevin Adams 178088197f 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>
2026-05-24 11:20:11 -04:00
Kevin Adams c1f021c5a6 fix: hybrid free_image fallback — service restart + explicit targetextent delete (#251)
force=true on extent DELETE is not sufficient when CORE 13 has an active
or recovering session holding the targetextent (422 persists through retries).

Fallback path (triggered only when force fails and a targetextent exists):
1. POST /service/restart — purges all server-side session state
2. DELETE /iscsi/targetextent/id/{id} — now succeeds with service down
3. DELETE /iscsi/extent/id/{id} — clean delete with no association

Fast path (force=true) is tried first and handles the normal case without
any service disruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 09:24:36 -04:00
Kevin Adams 9cd3cfa234 refactor: remove initiator logout/login from free_image
Session management was only needed to prevent 422 when explicitly
deleting the targetextent.  Now that we delete the extent first with
force=true (which cascades targetextent removal server-side), the
initiator session can stay up throughout — same as the original
LunCmd/FreeNAS.pm delete path which never touched iscsiadm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 09:09:55 -04:00
Kevin Adams 377503f89d fix: follow original FreeNAS.pm delete order — extent with force cascades targetextent
The original LunCmd/FreeNAS.pm (v2.0 path) deleted the extent with
{ force: true } and skipped the explicit targetextent DELETE entirely,
relying on TrueNAS to cascade it.  Our v3.0 had the order backwards:
deleting the targetextent first always hit 422 "target in use" when
any iSCSI session was active.

New approach mirrors the original:
1. Log out initiator session
2. DELETE extent with { force: true } — TrueNAS cascades targetextent
3. If that fails (CORE 13 strict session enforcement), restart iSCSI
   service and retry the extent DELETE (up to 5 times)
4. Restore session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 09:03:38 -04:00
Kevin Adams 594b533a06 fix: volume_size_info override + free_image retry loop + remove rootdir content type
volume_size_info: base class routes through filesystem_path() → get_subdir()
which dies on block storage. Override to query TrueNAS API directly for
volsize.parsed so create_efidisk and other callers get the correct zvol size.

free_image retry loop: TrueNAS CORE 13 refuses targetextent deletion while any
iSCSI session is active. Single logout+restart was racy — pvedaemon workers
reconnect between the restart and DELETE when multiple disks are deleted
concurrently (e.g. VM destroy). Retry up to 5 times with fresh logout+service
restart each cycle and exponential backoff.

Remove rootdir from plugindata content types: rootdir signals LXC
container/directory storage and caused PVE to route TPM state allocation to
TrueNAS, which immediately failed since TrueNAS has no filesystem path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 08:57:25 -04:00
Kevin Adams f521089e57 fix: parse_volname + free_image session/LUN teardown for CORE 13
parse_volname: implement for vm-/base- volume names so PVE can resolve
device paths and hotplug disks. Without this the base class falls through
to directory-volume parsing and fails with a 400 hotplug error.

free_image: TrueNAS CORE 13 holds iSCSI sessions in recovery state after
TCP disconnect, blocking targetextent deletion with 422 even seconds after
logout. Fix: log out the initiator session by SID, then restart the TrueNAS
iSCSI service to immediately clear server-side session state. Delete the
targetextent and extent, then restore the initiator session so other LUNs
on the same target remain accessible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 08:12:41 -04:00
Kevin Adams c6f662b88c fix: rescan iSCSI session in _wait_for_device to discover new LUNs
After TrueNAS reloads its iSCSI service, the PVE host's existing session
does not automatically pick up new LUNs. Adding iscsiadm --rescan every 5s
during the device wait loop lets the kernel discover newly exported LUNs
without requiring a full session logout/login.

Fixes the 30s hotplug timeout seen when adding a disk to a running VM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:28:26 -04:00
Kevin Adams 835648fd69 fix: use /pool/dataset for space stats; default shared=1 for iSCSI
TrueNAS CORE 13.0 /pool API does not expose top-level size/free/allocated
fields (they are nested in topology.data[].stats). Switch status() to query
/pool/dataset?id=<pool> which has available.parsed + used.parsed on both
CORE and SCALE.

Add shared=1 as the default in the UI panel — iSCSI is a network block
device accessible from all cluster nodes, so it should be shared storage
by default.

Verified on pve01-hq against Tank01 (CORE 13.0-U6.7):
  TrueNAS01-Tank01  active  1804599296  165936464  1638662832  9.20%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:17:18 -04:00
Kevin Adams 9c85fdb6e9 fix: extract pool name from path in status(); improve Pool/Dataset UX
status() was comparing the full dataset path (e.g. tank/proxmox/vdisks)
against TrueNAS /pool names which are top-level only (e.g. tank).
Extract the first path component so pool stats resolve correctly.

Rename Pool field label to 'Pool / Dataset Path' and update its hint
to make clear it accepts the full ZFS path (matching v2.x 'pool' field).
Rename Dataset to 'Sub-dataset' with a hint that discourages filling it
unless you genuinely need an extra sub-level beyond what's in Pool.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:52:41 -04:00
Kevin Adams 132875a5c5 fix: replace /iscsi/targetgroup with target.groups[] for CORE compatibility
TrueNAS CORE 13.0 does not expose /iscsi/targetgroup as a REST endpoint
(returns 404). Both CORE and SCALE include the portal group associations
inline in each target's 'groups' array from GET /iscsi/target, which is
all we need to filter targets by reachable portal IP.

Remove the /iscsi/targetgroup call entirely — use target.groups[].portal
cross-referenced against /iscsi/portal results instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:42:19 -04:00
Kevin Adams c5868dd4f6 fix: remove sensitive-properties — store API key in storage.cfg
PVE's sensitive-properties mechanism extracts listed keys from $param
before check_config and passes them only to on_add_hook/on_update_hook.
activate_storage reads from $cfg which never receives those values, so
the API key was always missing at runtime.

The API key now lives in storage.cfg (root-readable, mode 0640, same as
the v2.x truenas_secret field). Proper on_add_hook private-file storage
is tracked in issue #247.

Restore truenas_api_key => {} (required on create). PVE's update flow
passes $create=0 to check_config which skips absent keys, so
edit-without-changing-key still works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:34:36 -04:00
Kevin Adams efcad405e5 fix: sensitive-property handling + UI autofill + API key reveal button
truenas_api_key must be optional in options() because PVE extracts
sensitive-properties from the POST body before calling check_config,
causing a spurious 'missing required option' 500 on storage create.
Add a guard in _api() so a missing key produces a clear error.

UI: add autocomplete="url" on host and "new-password" on API key to
prevent Firefox/Chrome from filling in PVE login credentials.
Add a reveal trigger button to show/hide the API key field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:17:58 -04:00
Kevin Adams 8293a718a3 ci: rewrite build pipeline for v3.0 — no patches, TrueNAS.pm + JS only
Remove the validate-patches job (no more patch files in v3.0).
Update lint to target TrueNAS.pm instead of FreeNAS.pm.
Rebuild staging assembly: TrueNAS.pm + truenas-storage.js are the
only payload — no patch dirs, no REST-Client.pm, no triggers file.
Add $VERSION = '3.0.0' to TrueNAS.pm as the single source of truth.
Add release/3.x as a testing-channel branch alongside master.
Pin actions to @v4 (upload/download-artifact, checkout).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:57:18 -04:00
Kevin Adams f6ca0c9ef5 fix: correct package name and API version for PVE 8.4.x
Package must match filename (TrueNAS.pm → PVE::Storage::Custom::TrueNAS)
so PVE's module loader finds the class when checking ISA PVE::Storage::Plugin.

api() returns 11 to match APIVER in PVE::Storage on PVE 8.4.x.
Returning 10 loaded successfully but triggered a deprecation warning.

Verified: pvedaemon loads plugin cleanly with no errors or warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:36:13 -04:00
Kevin Adams 432be1ba5c feat: implement TrueNAS v3.0 custom storage plugin (#225, #226, #227)
Full PVE::Storage::Custom implementation — zero patches to PVE system files.
Discovered automatically by Proxmox VE at runtime via Module::Load.

Implements:
- alloc_image:        create zvol via TrueNAS API + wire iSCSI extent/targetextent
- free_image:         tear down iSCSI association + delete zvol
- list_images:        enumerate zvols under configured pool/dataset
- status:             pool total/used/free via TrueNAS API (replaces SSH)
- path:               resolve /dev/disk/by-path from LUN ID via API
- activate_storage:   resolve target IQN + iscsiadm login
- deactivate_storage: iscsiadm logout + clear state cache
- activate_volume:    ensure session + wait for block device
- volume_has_feature: declare copy and snapshot support

iSCSI target resolution supports both explicit IQN (truenas_target) and
auto-discovery from TrueNAS portal config — the field being set or blank
acts as the gate between modes.

No SSH keys required. Bearer token auth only. REST API v2.0.
Transport: CORE 13.x + SCALE <= 24.10. WebSocket (SCALE 25.04+) in v3.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:31:39 -04:00
Kevin Adams 33faf6eb34 chore: v3.0 packaging cleanup — no patches, one .pm file
- Rename Custom/FreeNAS.pm → Custom/TrueNAS.pm (matches package name)
- Rewrite postinst: copy TrueNAS.pm + restart pveproxy (no patch logic)
- Rewrite postrm: remove TrueNAS.pm + restart (no restore-orig logic)
- Delete triggers: nothing to watch (no ZFSPlugin/pvemanagerlib/apidoc)
- Drop 'patch' dep, add 'open-iscsi'; update package description
- Delete patch-generation runbook (obsolete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:55:59 -04:00
Kevin Adams fbfb3f26e1 chore: strip v2.x patch infrastructure — v3.0 clean slate
Remove everything that existed solely to wedge into PVE system files:
- stable-5 through stable-8 versioned patch archives
- pve-manager/js patch files (no more pvemanagerlib.js patching)
- pve-docs/api-viewer patch files (no more apidoc.js patching)
- perl5/PVE/Storage/LunCmd/ (replaced by PVE::Storage::Custom plugin)
- perl5/PVE/Storage/ZFSPlugin-*.patch (no more ZFSPlugin.pm patching)

Add perl5/PVE/Storage/Custom/FreeNAS.pm as the v3.0 starting point.
v3.0 ships one .pm file; PVE discovers it automatically — zero patches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:50:55 -04:00
Kevin Adams e9907d0041 feat: deprecate basic auth with syslog warning (#244)
Basic auth (username/password) has been supported since the plugin's
origin but TrueNAS API keys have been available since TrueNAS 12 (2020).
Sending credentials on every API call is a security risk and TrueNAS
may stop accepting it in future releases.

Log a syslog(warn) on every connection attempt that uses Basic Auth so
operators see the deprecation message in their Proxmox logs. The warning
links to issue #244 which tracks removal in v3.0.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:42:39 -04:00
Kevin Scott Adams c166501873
feat: auto-detect and correct ZFS blocksize for TrueNAS SCALE/CORE (#241)
* feat: auto-detect and correct ZFS blocksize for TrueNAS SCALE/CORE (#241)

SCALE requires >= 16k volblocksize; CORE works with 8k. Without correction,
creating a disk on SCALE with blocksize=8k triggers a ZFS warning and wastes
space on SCALE's minimum-4k-block pool layout.

Three changes:
- freenas_get_recommended_blocksize: fixes freenas_api_connect → freenas_api_check
  so product_name is actually populated before the SCALE check (was always returning
  8192 before this fix)
- freenas_parse_blocksize: new helper to compare "8k"/"16k"/integer blocksize strings
- alloc_image (both 8.x and 8.4.x patches): always detect recommended blocksize for
  freenas provider; if configured < recommended, override for this call AND persist
  the correction back to storage.cfg via lock_storage_config
- on_add_hook (8.4.x patch only): detect at storage creation time and correct $scfg
  before write_config saves it — no extra write needed

Tested on pve01-hq (PVE 8.4.19) against Tank02 (SCALE 24.10.2.1):
- Disk created with blocksize=8k → task log shows correction message, storage.cfg
  updated to 16384, no volblocksize warning from TrueNAS
- Disk created with blocksize=16384 → no-op, clean TASK OK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: add validate-patches step for ZFSPlugin PVE 8.4.x patch

ZFSPlugin-8.4.14_1.pm.patch has been in the build since v2.3.0 but was
never dry-run validated in CI. Now that we have the 8.4 orig committed
(ZFSPlugin-8.4.14_1.pm.orig from libpve-storage-perl 8.3.8), wire it up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: replace explicit return undef with bare return (perlcritic)

Subroutines::ProhibitExplicitReturnUndef violation in
freenas_get_recommended_blocksize. Bare return in list context returns
an empty list rather than a list containing undef, which is the
correct Perl idiom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:49:05 -04:00
Kevin Adams 6571ab23d1 fix: remove zvol delete from run_delete_lu to eliminate false negative (#240)
ZFSPlugin's free_image deletes the zvol via SSH after calling our
run_delete_lu. Our earlier addition of freenas_delete_zvol in
run_delete_lu caused a double-delete: SSH would fail, free_image's
error recovery would call run_create_lu on the now-missing zvol,
producing a spurious "Unable to create lun (rolled back)" in the task
log even though the migration had already succeeded.

freenas_delete_zvol remains in run_create_lu's rollback path where it
correctly cleans up zvols orphaned by a failed LUN creation (#239).

Also documents ZFS blocksize requirements for TrueNAS SCALE (16k) vs
CORE (8k) in README configuration and troubleshooting sections (#241).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 11:50:12 -04:00
Kevin Adams 267e14ebfa 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>
2026-05-20 08:56:02 -04:00
Kevin Adams ec4085b244 fix: full eval-based rollback on LUN creation failure, add test trigger (#239)
Replace the ad-hoc if/rollback in run_create_lu with an eval block that
tracks both extent_id and link_id so either resource is cleaned up if
anything fails at any point during creation (#214 only covered the
link-creation step).

Also adds a FREENAS_TEST_ROLLBACK env-var trigger: set it in pvedaemon's
environment to force a rollback after a successful create, allowing
verification that orphaned extents and links are removed without needing
a real failure condition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 00:00:55 -04:00
Kevin Adams 168cf0adf7 fix: repair freenas_api_connect autovivification and ping URL passthrough (#238)
Two bugs caused clone/migrate to fail with 'setHost on undefined value'
followed by 'Loop recursion prevention':

1. freenas_api_check reads $freenas_server_list->{$apihost}{client} to
   check if initialized. Perl autovivifies {$apihost} as an empty {} on
   that read. freenas_api_connect then saw a defined (but empty) hash and
   skipped initialization, leaving client undef → setHost crash.
   Fix: guard also checks !defined ...{client}.

2. $ping was a local variable reset to v1.0 on each recursive call.
   TrueNAS 25.04+ removed the v1.0 endpoint (returns 404), so the
   v1.0→v2.0 upgrade branch looped until the runaway counter tripped.
   Fix: accept $ping as optional second parameter (defaults to v1.0)
   and pass it through on both recursive call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:34:58 -04:00
Kevin Scott Adams 4edd50b300
Fix dangling extent on failed LUN creation (issue #214)
Fix dangling extent on failed LUN creation (issue #214)
2026-05-18 17:14:12 -04:00