* 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>
Stable releases now produce X.Y.Z-1 (e.g. 2.3.0-1) so they sort higher
than any pre-release build in dpkg. Pre-release builds now use tilde
(~beta+sha, ~alpha+sha) which sorts below the base version, ensuring
apt upgrade always selects stable over a previously installed beta.
Previously -beta+sha sorted higher than X.Y.Z causing apt to refuse
upgrades from beta to stable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
softprops/action-gh-release@v3 adds Node 24 support (v2 was Node 20).
Pin cloudsmith-io/action to v0.6.14 (latest stable) instead of @master.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
Two install-time failures on PVE 8.4.x nodes:
1. 'patch' utility not in Depends — causes silent patch failure on fresh
nodes; all three PVE file patches silently skip, plugin appears to
install but storage operations fail. Added 'patch' to control.j2.
2. ZFSPlugin.pm routing not applied on PVE 8.4.x — tabs→spaces reformat
and get_base($scfg) signature change in 8.4 broke hunks #3 and #4
(the freenas elsif routing branches). Nodes with a prior partial
install are doubly affected: grep idempotency check passes on partial
freenas strings, so the routing is never applied, causing:
"freenas: unknown iscsi provider" on clone/migrate operations.
New ZFSPlugin-8.4.14_1.pm.patch generated against stock PVE 8.4.19,
bundled as patches/ZFSPlugin/8.4.patch alongside existing 8.patch.
Verified clean on pve01-hq (8.4.19) and pve02-hq (8.4.14).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The initComponent anchor text was not unique in 8.4.19 — it matched
pveLxcCPUInputPanel before ZFSInputPanel, causing tnsecret/tnconfirmsecret
to be declared in the wrong scope. The ZFSInputPanel dialog then threw a
ReferenceError on open, preventing the modal from appearing.
Fixed by anchoring hunk 6 on the already-modified setValues block
(which is unique to ZFSInputPanel after hunk 5 is applied), ensuring
tnsecret/tnconfirmsecret always land in the correct initComponent scope.
Verified on 8.4.14 and 8.4.19 stock files. Tested live on pve01-hq (8.4.19).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users on unsupported PVE versions now get clear guidance at every
layer — README, install time, and GitHub release notes — rather than
silently failing or getting confusing patch errors.
- README: replace bare compatibility table with a full matrix and
prominent callout blocks for PVE 7 (last v2.x), PVE 8 (EOL
2026-08-31), PVE 9+ (use v3.0), and PVE ≤6 (unsupported)
- postinst: add check_pve_version() that exits on PVE < 7, warns
loudly on PVE 7 (stay on v2.x, do not upgrade to v3.0), notes
the PVE 8 EOL date, and warns on PVE 9+ (v2.x not supported)
- build.yml release template: add a version compatibility table so
every GitHub release prominently states who should and should not
install it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PVE 8.4 reformatted pvemanagerlib.js (tabs→spaces, structural changes),
making the existing 8.0.5-era patch fail on all 8.4.x nodes.
- Add pvemanagerlib-8.4.14_1.js.patch covering PVE 8.4.x (all hunks
verified on 8.4.14 and 8.4.19)
- Update postinst to detect pve-manager major.minor (e.g. "8.4") and
try a minor-version-specific patch before falling back to the major
patch — 8.3.x nodes keep using 8.patch, 8.4.x nodes use 8.4.patch
- Update build.yml to bundle 8.4.patch alongside the existing 8.patch
Tested live on pve01-hq (8.4.19) and pve03-hq (8.3.2) — correct patch
selected in each case, pvemanagerlib patched successfully on 8.4.19.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All seven ADRs were in draft/proposed/decided states — promote all to
Accepted now that the decisions are implemented and stable.
Add .claude/cos/adrs/README.md documenting the ADR lifecycle process.
Add KSA Workspace Standards section to CLAUDE.md (sourced from scaffold
at claude-scaffold/CLAUDE.md) so future sessions inherit workspace-wide
rules without needing a separate file load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub is removing Node.js 20 from Actions runners on Sep 16, 2026
and forcing Node 24 as default from Jun 2, 2026. Upgrade all
first-party actions to latest major versions that ship with node24:
actions/checkout@v4 → v6
actions/upload-artifact@v4 → v7
actions/download-artifact@v4 → v8
Note: setup-python@v5 warning is inside cloudsmith-io/action@master
and is not directly addressable here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RequireUseStrict and RequireUseWarnings require that strict and warnings
are the first statements after the package declaration. Move our \$VERSION
to after the two use pragmas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
our \$VERSION = '2.3.0' is now the single source of truth for the release
series. The build.yml extracts it with a perl one-liner (no module load,
no stubs needed) and uses it as BASE_VERSION for alpha/beta channel builds.
Git tags remain the release trigger: pushing an exact vX.Y.Z tag publishes
to the stable Cloudsmith channel. A mismatch warning fires if the tag
version and \$VERSION disagree, catching forgotten version bumps.
The v2.3.0-pre anchor tag approach is superseded by this; the anchor tag
can stay in history as a breadcrumb but \$VERSION is now what drives builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two changes:
1. BASE_VERSION now strips pre-release suffixes from the nearest ancestor tag
(e.g. v2.3.0-pre → 2.3.0), so anchor tags like v2.3.0-pre make feature
branch builds say 2.3.0-alpha+<sha> without "pre" leaking into the version.
2. Stable release detection changed from glob (refs/tags/v*) to an exact
semver regex (^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$). Tags with a suffix
(v2.3.0-pre, v2.3.0-rc1) build and validate but do not publish to Cloudsmith
stable — they fall through to the 'none' channel.
This lets us place a v2.3.0-pre anchor tag to version the development series
correctly, then later push v2.3.0 as the actual stable release.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two problems in run_create_lu:
1. If freenas_iscsi_create_extent returned undef, the next line accessed
$extent->{'id'} unconditionally, causing a crash rather than a clean error.
2. If freenas_iscsi_create_target_to_extent failed after a successful extent
creation, the code just died with "Unable to create lun", leaving the
newly created extent orphaned on TrueNAS with no target association.
Subsequent delete_lu calls could not find or clean up this extent because
it was never in the LUN list, requiring manual TrueNAS cleanup.
Fix: check extent creation result before dereferencing; on target-to-extent
failure, call freenas_iscsi_remove_extent to roll back the extent before
dying. The rollback is best-effort (failure is logged but not re-thrown)
so a secondary API error does not mask the primary failure message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All of these were module-level scalars that the last freenas_api_check call
would overwrite. With two TrueNAS hosts in a Proxmox config (e.g., one CORE
and one SCALE), the second host's API check would clobber the first host's
product_name, dev_prefix, and api_version, causing wrong API paths and wrong
pool-name slash-vs-dash conversion on subsequent calls to the first host.
Changes:
- $freenas_server_list->{$host} now stores a hashref {client, product_name,
api_version, dev_prefix, runaway_count} instead of a bare REST::Client
- freenas_api_connect: recursion counter is now per-host (runaway_count);
$apiping made local so v1->v2 detection for one host does not affect others
- freenas_api_check: fixed broken init-guard (was testing $freenas_rest_connection->{$host}
which always evaluates undef on a REST::Client object); saves detected
product_name/api_version/dev_prefix back into the per-host hash after detection
- freenas_api_call: restores all module-level pointers (product_name, dev_prefix,
api_methods, api_variables) from per-host storage on every call, so the
correct host context is active regardless of which host was accessed last
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'! $method =~ /pattern/' is parsed as '(! $method) =~ /pattern/' due to
precedence, so the regex ran against "" and always returned false. The
guard never fired and any method string passed through to the API call.
Changed to '$method !~ /^(?:GET|DELETE|POST)$/' which correctly tests the
method string. Also replaced atomic group (?>...) with non-capturing (?:...)
since there is no backtracking concern here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TrueNAS 25.04 removed the v1.0 REST API entirely. Previously it redirected
(302) to v2.0; now it returns 404. The plugin treated 404 as a fatal
connection failure, preventing any operation on 25.04 hosts.
Fix: extend the v1.0→v2.0 upgrade condition in freenas_api_connect to also
trigger on HTTP 404, but only while probing the v1.0 endpoint (guarded by
`$apiping =~ /v1\.0/`). A 404 on the v2.0 endpoint still fails cleanly.
Also add a syslog warn when Bearer Token auth is attempted over plain HTTP:
TrueNAS 25.04+ revokes API keys sent without SSL, causing auth failures that
are otherwise invisible in the plugin log. The warning points admins to the
"Use SSL" storage config option.
Closes#205
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
freenas_iscsi_create_extent and freenas_iscsi_create_target_to_extent
both used `eval $value` to expand template variable names like '$name'
into their runtime values. Replace with a local %vars hash and an
`exists` lookup — same behavior, no stringy eval, no perlcritic warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dpkg-deb --build reads a binary control file and rejects the two-stanza
source+binary format. Rewrote control.j2 as a single binary-only stanza.
Removed the source stanza (Source, Standards-Version, Build-Depends fields)
which are only meaningful in source packages built with dpkg-buildpackage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
REST_CLIENT_PATH was defined but never referenced. REST/Client.pm is owned
by librest-client-perl and is intentionally not removed by our postrm
(documented in the comment above remove_plugin_files). Remove the dead variable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProhibitExplicitReturnUndef (severity 5): returning undef explicitly is
wrong because it forces scalar context on callers. 'return;' returns
undef in scalar context and an empty list in list context, which is the
correct idiom. Fixed all 7 occurrences in FreeNAS.pm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
perl -c requires all modules to be loadable at compile time. PVE::SafeSyslog
only exists on Proxmox VE hosts, so the syntax check always failed on
GitHub Actions runners. Fix:
- Install real Perl deps via apt (libwww-perl, libio-socket-ssl-perl,
librest-client-perl, libjson-perl)
- Create a minimal stub for PVE::SafeSyslog so perl -c can load FreeNAS.pm
- Pass -I/tmp/pve-stub to perl -c so the stub is found
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unquoted YAML plain scalars cannot contain ': ' (colon-space) because
the parser interprets it as a nested key-value mapping. The two step
names 'Dry-run patch: ZFSPlugin (PVE 8)' and 'Dry-run patch: apidoc.js
(PVE 8)' triggered this, causing GitHub Actions to reject the workflow
file before any jobs ran.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs that prevented any jobs from running:
1. action.yml contained only YAML comments (no 'on:' or 'jobs:' keys),
making it an invalid workflow file. GitHub rejected it at parse time.
Fixed by giving it a minimal valid structure with workflow_dispatch
trigger only, so it never fires automatically.
2. build.yml perlcritic step referenced perl5/PVE/Storage/Custom/FreeNAS.pm
which is not committed to the repository, causing a step failure.
Removed — only FreeNAS.pm (LunCmd) is linted for now.
3. build.yml publish job used 'if: |' block scalar for a multi-line
expression, which GitHub Actions rejects in expression position.
Changed to a single inline expression string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update README.md to be more structured and selective from a visitors view.
- Update the Roadmap section.
- Update the Donators list.
- Update the New Install Instructions section to allow the visitor to select the GPG location they want to use.
- Update the Activity section.
- Fix spelling errors.