RUNBOOK-002 says the docs have to be current before the tag is pushed.
This bumps $VERSION and adds the CHANGELOG entry plus a ROADMAP "Released"
entry for #291–#294 and #299.
The Debian changelogs had stopped at 3.2.4 / 3.2.2, so `apt changelog`
on a 4.x install showed v3 history. They get 4.0.2-1 entries now, and
RUNBOOK-002 gains a step so they don't fall behind again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TrueNASMultipath.pm calls the base plugin's private helpers, and from 4.0.2
it also calls SUPER::volume_resize. So it only works with the truenas-proxmox
built from the same commit. `>= 3.1.0` let apt pair 4.0.2 multipath with
a 3.2.5 or 4.0.1 base, which fails at runtime. `(= ${VERSION})` makes apt
upgrade or hold the two together. A CI check fails the build if the
template ever drifts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v4.0.0/4.0.1 shipped on the v4 track, but upgrade-paths.md still said v3→v4
was "future" with TBDs, and the README, getting-started and migrating-from-v2
docs still installed from v3. A v3 operator had no supported procedure.
v3→v4 is an in-place apt upgrade:
- storage.cfg is unchanged and the transport is auto-detected per host
- only the PVE daemons restart, not running VMs
- the multipath component moves along with the track
Added pre-checks (PVE 9, leftover v2 storages, orphan volumes, API key),
verification and rollback steps, and the v2→v4 ordering.
Also fixed the PVE 8 guidance. v3.1+ already needs PVE 9 (ADR-009), so
"stay on v3" isn't enough: on PVE 8 only 3.0.0-1 loads, and it has to be
pinned. CLAUDE.md had the API key field name wrong (truenas_api_token).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without an override the base Plugin.pm resize dies "can't resize this
image format", which blocked qm disk resize, the GUI and Terraform's
cloud-image provisioning. Grows the zvol via pool/dataset update
(1 MiB-aligned, integer volsize for SCALE's Pydantic). The WebSocket
transport gains the PUT -> pool.dataset.update mapping it needs.
Multipath additionally rescans the target's kernel sessions and runs
multipathd resize map when the device is active on this node, since
QMP block_resize on a host_device can't grow past the stale dm size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PVE's content API (and the bpg/proxmox Terraform provider through it)
calls volume_size_info in list context and dies "no format" when the
second value is missing, so every disk on a truenas storage failed to
refresh. Mirrors ZFSPoolPlugin: used comes from usedbydataset, and a
missing zvol now dies instead of reporting a size of 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alloc_image already creates cloud-init drives under PVE's standard name,
but parse_volname only knew disk-N and state-* volumes, so every later
operation on the drive - including qm destroy - died with "unable to
parse TrueNAS volume name". base- variant covers templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qm destroy calls path() and skips any disk whose owner != vmid. The
multipath plugin returned the LUN id (usually 0) as the owner and the
storeid as the vtype, so every truenas-multipath disk was silently left
behind on TrueNAS while the task reported OK.
Also make _naa_to_wwid return undef for a missing NAA: it returned a
truthy "3", which pointed path()/activate_volume at /dev/mapper/3 and
bypassed the #292 placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PVE's content delete (pvesm free, GUI Remove) and qm destroy call path()
only for the owner/vtype before vdisk_free. Dying when the zvol had no
iSCSI extent made orphans from failed allocs or v2-era leftovers
undeletable through PVE, even though free_image already handles the
no-extent case. activate_volume and qemu_blockdev_options keep dying on
the same condition, so the placeholder can never be opened by a VM.
Adds the first Perl unit-test harness (t/, prove in CI lint job) with a
mock TrueNAS API so plugin entry points can be tested off a PVE node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recorded so the finding (icons are hard-coded to Debian/Proxmox, so only
the label is changeable) isn't lost, without committing to the work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v4.0.1 multipath re-verification recorded VM 990 as destroyed, but its
config and zvol were still on pve01-hq a month later. The record should
reflect what actually happened, including why the eventual qm destroy left
the disk behind (#291).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The post-ADR-014 multipath re-check was only run as a standalone script,
the exact testing gap that caused #290 in the first place. Closed it by
re-verifying through a real pveproxy/pvedaemon-driven VM disk allocate +
start + teardown cycle against .92, and documented the result in ADR-014
and architecture.md §10 alongside the original AnyEvent-era verification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AnyEvent::WebSocket::Client's blocking ->recv() cannot run inside
pveproxy/pvedaemon (themselves built on AnyEvent as their core
reactor) without nesting event loops, which AnyEvent refuses by
design -- confirmed by a real user on the first real WebUI use of
v4.0.0, one day after release. Not a race condition: this fails on
every real "Add Storage" attempt against a WebSocket-transport host,
unconditionally. v4.0.0's testing never caught it because every
verification script ran standalone, never inside the actual daemon
process -- pvesm (a fresh CLI process) doesn't trigger it either, only
the real REST API path through pveproxy does.
Replaced _ws_connect/_ws_call's internals with Protocol::WebSocket::Client
+ IO::Socket::SSL -- a plain synchronous socket client with zero
event-loop dependency, so there's no shared reactor state to conflict
with pveproxy's. _api_ws's method-mapping logic is completely
unchanged; only the connection/call internals were rewritten.
Reproduced the exact bug via a real POST to /api2/json/storage through
pveproxy on a lab node, then confirmed the fix resolves it the same
way. Re-ran every check from v4.0.0's release (read path, write path,
snapshots, multipath) against the new transport -- zero regressions.
Full story: ADR-014, superseding ADR-012's library-choice section
only (everything else in ADR-012 stands). $VERSION bumped to 4.0.1.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Written after the v4.0.0 release housekeeping (CHANGELOG, ROADMAP,
issue closing) happened as an afterthought rather than before the
tag was pushed. Discipline checklist, not automatable -- the ROADMAP
write-up and issue-closing narrative need synthesis a script can't do.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CHANGELOG's [Unreleased] section becomes a dated [4.0.0] entry;
ROADMAP's "Upcoming" section moves to "Released", matching the
pattern every prior version got.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full volume_snapshot/_info/_rollback/_delete cycle against .92
succeeded with zero param-shape issues on the first try. Every API
call this plugin makes over the WebSocket transport is now
live-verified. See ADR-012.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Everything else in the write path (target/extent/targetextent/dataset
create+delete) is now live-confirmed, see ADR-012.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Adds _api_ws/_ws_connect/_ws_call and a _transport() auto-detector so
_api() dispatches to REST or WebSocket per host based on the detected
TrueNAS version -- storage.cfg is unchanged either way. Follows
ADR-012's decided design: AnyEvent::WebSocket::Client for the
transport, auth.login_with_api_key (not auth.login_ex, which needs a
username this plugin has no way to know), and the JSON-RPC envelope
hand-built with the existing JSON module rather than a dedicated RPC
library.
Live-verified end-to-end against .92 (TrueNAS-25.04.2.6, real SCALE
box) through the actual plugin module, not just the diagnostic script:
transport auto-detection, _api_global, portal/target queries, and the
single-ID lookup mapping used by path()/qemu_blockdev_options() all
confirmed working. This exercises every *read* (query) call the
plugin makes.
Caught and fixed one real bug during integration testing:
/system/product_type returns the license tier (e.g.
"COMMUNITY_EDITION"), not the product family -- CORE vs SCALE
detection uses the /system/version string's major-version magnitude
instead (SCALE's calendar versioning vs CORE's legacy 11.x-13.x range).
Scope note (see file header): the *write* path (create/delete/update
param shapes) follows TrueNAS's documented CRUDService conventions but
has not yet been independently live-tested -- verify against .91/.92
before trusting alloc_image/free_image/snapshot operations in
production on SCALE 25.04+.
$VERSION bumped to 4.0.0. Adds libanyevent-perl and
libanyevent-websocket-client-perl as package + CI lint dependencies.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Read-only tool for verifying ADR-012's transport assumptions against a
real TrueNAS SCALE 25.04+ host before any of this lands in TrueNAS.pm.
Not part of the packaged plugin. Used to confirm auth.login_with_api_key
and the full method-mapping table live against a Goldeneye (25.10.3.1)
lab node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CHANGELOG.md: new [Unreleased] section tracking v4.0.0 progress (added/
verified/fixed/deferred), per this project's rule to keep docs current
as work lands rather than batching into eventual release notes
- docs/architecture.md: new §5 "API Flow -- TrueNAS WebSocket JSON-RPC 2.0
(v4.0.0+)", inserted right after the REST API Flow section for narrative
continuity -- covers transport auto-detection, the /system/product_type
bug, connection/auth, the JSON-RPC envelope and JSON::PP::Boolean gotcha,
the full method-mapping table with both confirmed-broken positional-arg
cases called out, and multipath's free inheritance. Sections 5-10 renumbered
to 6-11 accordingly (TOC, cross-references, and the old release/N.x branch
workflow references all updated)
- Also fixed unrelated staleness found along the way: "Branch from master"
in the Contributing section (pre-ADR-013), and the ADR quick-reference
table, which was missing ADR-009 through ADR-013 entirely and still
described ADR-008 as current despite it being superseded by ADR-009
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full alloc_image/activate_volume/path/deactivate_volume/free_image
cycle against .92 with zero code changes to TrueNASMultipath.pm --
real dm-multipath device came up with 2 active paths. Also notes a
practical finding: .92 has only one TrueNAS portal object (0.0.0.0)
rather than two per-IP ones, and iscsiadm still produced two genuine
independent paths logging into different IPs against it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran the full read/write/snapshot suite against .90 (TrueNAS CORE,
real production box) to confirm _transport()'s dispatcher wrapper
doesn't regress the REST path for hosts that never touch WebSocket.
Zero regressions, confirmed back to original state afterward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every read and write API call the WebSocket transport makes is
live-verified against real TrueNAS SCALE hosts. Kevin's sign-off.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every API call the WebSocket transport makes is now live-verified
against a real TrueNAS SCALE host with zero param-shape issues
remaining. Left as Draft pending Kevin's own sign-off, per his
explicit instruction earlier.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alloc_image/path/volume_size_info/free_image confirmed working
end-to-end against .92 after fixing the delete param-shape bugs.
pool.dataset.create/delete both confirmed along the way. Only
zfs.snapshot.* remains entirely untested against the WS transport.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_api_ws/_ws_connect/_ws_call/_transport landed on next, live-verified
against .92 (25.04.2.6) through the real plugin module. Also notes
the /system/product_type bug caught during integration (returns
license tier, not product family -- fixed by parsing /system/version
magnitude instead).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kevin rebuilt .92 from scratch specifically to TrueNAS-25.04.2.6 -- the
exact SCALE minor version NAS-135643 was originally filed against, two
patch levels later. iscsi.target.query returned a real target cleanly,
no error. This was the one item ADR-012 flagged as an acknowledged gap
rather than a completed check; no remaining live-verification gaps for
this ADR now. Also documents a minor auth-flow gotcha found along the
way: retrying auth.login_ex on the same connection after a failed
attempt threw an unhandled RuntimeError on this fresh install,
reinforcing the login_with_api_key recommendation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
release/3.x -> main, release/4.x -> next, master -> archive/v2-legacy
(retired, was byte-identical to release/2.x). Branch identity is now
role-based, not version-numbered -- main and next are fixed names
that never get renamed at a major-version cutover; release/N.x is
reserved exclusively for already-superseded majors, created only at
the moment they're archived.
This directly fixes the structural cause of the bug found 2026-08-30:
build.yml hardcoded literal branch-name comparisons for the
testing/beta channel, and ADR-006's original "master -> beta channel"
rule was never superseded when release/3.x took over as the active
branch, leaving master as a silent, stale duplicate of release/2.x
that CI still treated as a legitimate publish source. Rewrote the
version-resolution logic to match roles instead: refs/heads/main,
refs/heads/next, and a refs/heads/release/*.x pattern that auto-
catches any future archived major with zero code changes needed.
Also tightened the push/PR triggers from a bare wildcard to an
explicit allowlist, and switched the release-notes CHANGELOG link to
a tag-relative permalink instead of a floating branch reference.
Research (DEP-14, openedx/frontend-base #273, semantic-release's
branch-pattern config) is cited in ADR-013; the openedx precedent in
particular fixed this identical bug the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.91 was upgraded straight to TrueNAS-25.10.6 (skipping the planned
Fangtooth stop) and re-tested clean: all 8 method-mapping families
work against real production-scale iSCSI config (2 targets, 9
extents), and NAS-135643 still doesn't reproduce. Also corrects the
auth.login_ex conclusion from the .92-only pass -- it works fine given
the right username (.91's key IS root, .92's isn't), so the
login_with_api_key recommendation stands for a different reason: no
username field exists in storage.cfg to use it correctly, not because
login_ex is broken. Notes the resulting gap (no 25.04.x node left in
the lab) and the revised lab-node role split.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Not committed to any release yet -- filed as a backlog idea alongside
today's #243/ADR-012 work, since a dual REST/WebSocket transport
reality in v4.0.0 makes visible version mismatches more useful to
surface.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two things surfaced since this ADR was last updated: confirmed
TrueNASMultipath.pm needs no separate WebSocket work (it calls the
shared _api() helper directly), and release/4.x now exists as the
implementation branch, including the dormant CI bug it exposed and
the fix for it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Multipath (truenas-proxmox-multipath, shipped v3.2.0/#256) had zero
coverage in README.md and was actively described as unsupported/future
work in docs/architecture.md and docs/migration-advanced.md. Added:
- README: Prerequisites, Installation, and Configuration sections for
the multipath package and its truenas-multipath storage type
- architecture.md: new §9 covering what TrueNASMultipath.pm overrides
vs. inherits, and the live failover behavior confirmed 2026-08-29
- migration-advanced.md: points to the multipath package instead of
calling it unsupported
- truenas-storage-help.html: single-path note pointing to the
multipath alternative
Also fixed README content that was stale from the v3.0 beta era: the
PVE 9 blockdev issue (#266) was fixed in v3.1.0 but still read as
open, "v3.0 (beta)" headings never updated to v3.x, a broken internal
anchor link, and a note claiming WebSocket support was "v3.0.0" scope
when ADR-009 placed it at v4.0.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "Publish to GitHub Pages testing dist" step ran for both 'testing'
and 'development' channel builds and does rm -rf pool/testing before
copying in the new .deb — so any push to a release/* branch other than
release/3.x silently overwrote the real testing dist with its own alpha
build. Confirmed happening 2026-08-30 when release/4.x was created and
its 3.2.5~alpha+d0ea761 build replaced release/3.x's legitimate beta
build there. Restricted the step to channel == 'testing' only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran a diagnostic against the .92 TrueNAS SCALE lab node (25.10.3.1) to
check ADR-012's open assumptions. Confirms auth.login_with_api_key over
auth.login_ex (the latter needs a username field this plugin doesn't
collect), verifies all 8 REST->JSON-RPC method mappings live, and finds
NAS-135643 no longer reproduces on this version. Also documents a real
Perl gotcha: JSON's decode_json returns a blessed JSON::PP::Boolean for
true/false, not a plain 1/0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scopes the transport-abstraction design called for in #243: confirms the
JSON-RPC 2.0 method mapping for all 8 REST endpoint families the plugin
uses, decides on AnyEvent::WebSocket::Client (apt-packaged, no CPAN-only
deps) after finding an independent prior implementation, and defers the
#249 per-variant file split to a future v4.1/v5.0. Still Draft pending
live verification against the lab's Fangtooth/Goldeneye TrueNAS nodes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four of the six tracked issues (#228, #234, #256, #260) had shipped
since this list was last touched, and #250 was closed too — the
instructions Claude reads at session start were actively wrong.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Debian trixie recommends deb822-style .sources files over the one-liner
.list format, and PVE 9 ships on trixie. Show deb822 as the primary form
in README (stable, v2 track, Cloudsmith migration, testing) and
getting-started.md/upgrade-paths.md, keeping the one-liner .list format
as a fallback for PVE 8 / bookworm users.
Fixes#285
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as #179: postinst/postrm only restarted pvedaemon/pveproxy,
never pvestatd/pve-ha-lrm/pve-ha-crm, so HA-managed VMs on truenas-multipath
storage could fail to start after install/upgrade. Mirrors the #179 fix —
bare statements so set -e aborts on a real failure, HA daemons restarted
via systemctl (they don't support a restart subcommand). Extended
test-restart-services.sh to cover both multipath scripts with the same
stub approach.
Fixes#287
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CHANGELOG.md stopped at 3.0.0 despite five releases since. Backfilled
from git/tag history so the changelog is a reliable release record again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
README and getting-started.md still described the legacy v2.x "ZFS over
iSCSI" storage type and pre-25.04 TrueNAS SCALE UI locations. Update both
to match the actual v3 "TrueNAS (ZFS/iSCSI)" panel fields, document the
SCALE 25.04+ API key location and the throwaway-share workaround for the
now-missing standalone portal/initiator-group screen, and sync the help
HTML per the same-commit doc-sync rule.
Fixes#286
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core.fileMode=false on this checkout (NTFS-mounted /mnt/c) meant the
previous commit recorded tests/stubs/* and tests/fail-stubs/* as 100644
despite being executable locally, so CI failed with "Permission denied"
trying to exec them via PATH lookup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pvedaemon/pveproxy picked up the patched PVE::Storage state, but
pve-ha-lrm/pve-ha-crm kept running against the pre-install state, so
HA-managed VMs on TrueNAS storage failed to start. They don't support
a `restart` subcommand (only start/stop/status), so they're restarted
via systemctl instead. Also switched every restart from `cmd && log
...` to bare statements — set -e does not fire on the left side of
&&, so a genuine restart failure was previously swallowed silently
instead of aborting the script.
Adds tests/test-restart-services.sh: sources the real postinst/postrm
against stub PVE binaries in CI to catch both classes of regression
(wrong subcommand, silently-swallowed failure) without needing a live
PVE cluster. Real HA-cluster validation is documented as a manual
runbook (RUNBOOK-001) since GitHub Actions can't run pve-ha-lrm/crm.
Bumps $VERSION to 3.2.4.
Fixes#179
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>