Compare commits

...

91 Commits

Author SHA1 Message Date
Kevin Adams 3e725244d0 fix: bump api() to 15 to match PVE 9 APIVER, silencing storage API warning
PVE 9.2.3 sets APIVER=15 in PVE::Storage. Our plugin was returning 14,
triggering "implementing an older storage API" on every pvedaemon/pveproxy
restart. No functional change — plugin loaded and worked correctly at 14.

Closes #282

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 14:00:46 -04:00
Kevin Adams 64b430f182 fix: create dists/error codename alias and fix cp -r overwrite bug
dists/error was never written to gh-pages because the codename alias
code was added after the v3.2.0 tag. The stable publish job only runs
on v*.*.* tags, so all subsequent pushes to release/3.x only touched
dists/testing. Bumping to v3.2.1 triggers stable publish to create it.

Also fix: cp -r src dst when dst already exists puts src inside dst
rather than replacing it. Add rm -rf before cp -r to guarantee a clean
replace on every stable release.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 08:25:46 -04:00
Kevin Adams c698801338 fix: generate codename alias dirs so error/limelight/rivendell work in sources.list
Codename was added to Release metadata but dists/<codename>/ directories were
never created on GitHub Pages, so apt would 404 on InRelease when using the
codename as the dist name. Copy each dist dir under its codename alias after
the main loop so both Suite and Codename values work interchangeably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:26:40 -04:00
Kevin Adams a604ec0c09 feat: add apt dist codenames (limelight, error, rivendell)
v2/main → limelight (Rush), v3 → error (The Warning),
v4 → rivendell (Lord of the Rings), testing stays as testing.
Suite field unchanged — codename is an alias, both work in sources.list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:55:28 -04:00
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 b7acb605ee fix: strip pre-existing unmanaged devices block before injecting managed stanza
If /etc/multipath.conf already had a manual devices{} block before package
install, postinst would append a second identical block, causing multipathd
to emit "duplicate keyword: devices" warnings. Strip any unmanaged devices
block in the append path to keep the file clean.

Fixes #279. Discovered 2026-06-20 on pve01-hq during SCALE 25.04 multipath testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:07:40 -04:00
Kevin Adams 1c723a8fb8 fix: clear pool/multipath/testing before publish to prevent stale version accumulation
Old multipath testing debs were never pruned, causing apt to see multiple
3.1.0~beta+<sha> versions and pick whichever sha sorts highest — not newest.
Mirror the existing pool/testing cleanup that already did this correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:31:20 -04:00
Kevin Adams da823569a7 feat: add UI panel for truenas-multipath storage type (#278)
Without a registered JS panel the truenas-multipath type was invisible in
the PVE Datacenter → Storage → Add dropdown. Adds truenas-multipath.js
with all base TrueNAS fields plus the required truenas_portals field, and
wires it into the multipath package install/remove scripts and CI build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:23:01 -04:00
Kevin Adams d0093f48b3 fix: safe multipath.conf management via BEGIN/END tagged block
On install: if /etc/multipath.conf already exists, append a tagged
block rather than skipping silently (old) or overwriting (dangerous).
On upgrade: replace only the tagged block, leaving the rest untouched.
On remove/purge: strip the tagged block; rest of the file preserved.

This means admins who added their own stanzas during manual testing
(e.g. before the package existed) won't lose config on install or
upgrade. Closes the silent "file already exists" footgun.

Also fixes pre-existing SC2015 shellcheck warnings in postrm.

Refs #256

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:54:12 -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 a534341d13 fix: add -Iperl5 so TrueNASMultipath.pm can find its parent in CI lint
TrueNASMultipath.pm inherits from TrueNAS.pm which lives in perl5/. The
-I/tmp/pve-stub flag alone doesn't make that path searchable, so the
'use base' failed at compile time. Adding -Iperl5 fixes it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:48:12 -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 0ce11dc6ba docs: update ROADMAP.md with v3.1.0 completed items and snapshot results
Snapshot interface (#234) confirmed working 2026-06-07:
- Disk-only: freeze/snap/thaw, rollback verified (ngrep/nmap gone)
- RAM: 292 MiB saved to TrueNAS SCALE zvol, rollback verified (/dev/shm proof)
Filed #277 (GET_LBA_STATUS iSCSI error — non-fatal log noise).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 15:04:09 -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 58a648e1a3 fix: use GITHUB_TOKEN for gh-pages push (ACCESS_TOKEN expired)
The ACCESS_TOKEN secret was created in 2022 and has expired. Switch both
gh-pages publish steps (stable and testing dist) to github.token, which is
generated fresh per run and never expires. Add permissions: contents: write
to the publish job so GITHUB_TOKEN can push to gh-pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:08:04 -04:00
Kevin Adams da0aaff0d2 fix: testing dist channel name and broken README badges (#275)
build.yml: testing dist publish condition used 'beta'/'alpha' but the
workflow channel values are 'testing'/'development' — step was always
skipped. Corrected to match actual channel names.

README: badge URLs referenced TheGrandWazoo/truenas-proxmox (repo not yet
renamed); corrected to freenas-proxmox. Workflow badge now includes
branch=release/3.x so it tracks the default branch explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:09:14 -04:00
Kevin Adams 1c98591ed4 fix: add Debian changelog to both packages (#273)
Adds packaging/changelog.Debian and packaging/DEBIAN-transitional/changelog.Debian
and wires them into the build staging steps. The gzipped changelog lands at
/usr/share/doc/truenas-proxmox/changelog.Debian.gz and
/usr/share/doc/freenas-proxmox/changelog.Debian.gz respectively, satisfying
Debian policy §12.7 and silencing the 'apt changelog' failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:02:24 -04:00
Kevin Adams 19a3ec2edd fix: restore Filename: prefix in CI apt Packages generation (#274)
Same sed bug as fixed on gh-pages: was stripping 'Filename: ' label
entirely, leaving a bare path that apt cannot use to download packages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:53:34 -04:00
Kevin Adams 03fe20b074 feat: GitHub Pages testing dist track; retire Cloudsmith testing (#272)
build.yml:
- New "Publish to GitHub Pages testing dist" step — runs on beta/alpha
  channel builds (pushes to release/3.x and master); replaces pool/testing/
  with the latest build so the dist always has exactly one version
- Cloudsmith now receives v2.x stable only; beta/alpha no longer published
  to Cloudsmith testing channel

README:
- Replace Cloudsmith testing install instructions with GitHub Pages testing
  dist track; same GPG key as stable, just different dist name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:45:16 -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 259bdd4fca fix(script): correct wrong dist track when GitHub Pages already configured
docs: update ROADMAP.md to reflect v3.0.0 shipped and current state

Migration script: the early-exit check now verifies the dist track matches
the installed major version. A v2 node that had v3 dist track from a test
run now gets corrected rather than skipped.

ROADMAP.md: full update — v3.0.0 moved to Released, GitHub Pages apt repo
added as Released, v3.1.0 scope updated with #272 and multipath redesign,
stale pre-release housekeeping removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:29:13 -04:00
Kevin Adams 95b71e9c7d docs: ADR-011 — apt components for optional feature packages
Establishes the pattern for distributing optional packages (starting with
truenas-proxmox-multipath) via apt components within existing dist tracks.
Users add 'multipath' to their sources.list component list to opt in;
standard users are never affected by apt upgrade.

References: #256 (multipath), #272 (testing track)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 07:44:56 -04:00
Kevin Adams adcd691eea ci: stop publishing stable v3+ releases to Cloudsmith
Stable v3.x and above go to GitHub Pages only. Cloudsmith still receives:
- v2.x stable releases (existing Cloudsmith user base on freenas-proxmox)
- beta/alpha/dev builds (testing channel, until GitHub Pages testing track
  ships as a v3.1.0 item)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 15:01:33 -04:00
Kevin Adams e57b74c46f fix: run full apt update in migration script to avoid false failure
The limited-scope apt-get update didn't flush the full apt cache, causing
apt-cache policy to miss the new GitHub Pages source on nodes that had a
beta build installed from the testing channel (pve02-hq: 3.0.0~beta).
Full apt update ensures the cache is consistent before the verification check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 14:48:05 -04:00
Kevin Adams 93d7ed1270 feat: Cloudsmith→GitHub Pages migration script; fix apt arch warning
scripts/migrate-repo-to-github-pages.sh:
- Detects installed version and picks the correct dist track automatically
- Finds Cloudsmith sources by URL content (not filename) to handle the
  varied naming conventions users have in sources.list.d/
- Removes old keyrings, imports GitHub Pages key, writes new source
- Confirms the package is visible from the new repo before exiting

build.yml publish job:
- Add empty binary-amd64 and binary-arm64 Packages files alongside
  binary-all, and set Architectures: all amd64 arm64 in Release
- Silences the "doesn't support architecture 'amd64'" warning on PVE
  hosts without duplicating package entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 14:29:15 -04:00
Kevin Adams 61373f9db7 docs: add Cloudsmith migration guide and upgrade-paths reference
- README: new "Migrating from Cloudsmith" section with v2 and v3
  command sequences and a one-line reason (Cloudsmith being phased out)
- docs/upgrade-paths.md: full matrix of every starting version × repo
  combination with links to the right guide; v4.x rows stubbed with
  placeholder instructions so users know what's coming and where to look

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 13:59:19 -04:00
Kevin Adams d3dfcb4452 docs: fix remaining SSH/Cloudsmith carryover from v2.x
README: v2.x Prerequisites relabeled legacy (not 'current stable');
condensed to three items. v3.0 does not require SSH.

architecture.md: remove incorrect claim that the Add Storage wizard
requires SSH via ZFSPoolPlugin.pm. Verified in truenas-storage.js:
truenas_pool is a plain text field. Pool is validated at runtime via
GET /pool/dataset, not SSH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:16:20 -04:00
Kevin Adams c9dff84c1e docs: remove SSH references and update install URLs to GitHub Pages
v3.0 requires no SSH — QEMU connects directly via iscsi://. README "How It
Works" and troubleshooting sections had v2.x SSH carryover. Compatibility
matrix now shows v3.x as stable with PVE 9 confirmed.

All install instructions updated from Cloudsmith to the GitHub Pages apt
repo (thegrandwazoo.github.io/freenas-proxmox, v3 dist track). Cloudsmith
testing channel retained for beta builds only.

architecture.md updated to reflect ADR-010 and the live GitHub Pages publish
step in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:12:00 -04:00
Kevin Adams dfbb4f6c31 feat: GitHub Pages apt repo with per-major-version dist tracks (#230)
Implements ADR-010: stable releases are now published to
https://thegrandwazoo.github.io/freenas-proxmox/ in addition to Cloudsmith.

Dist tracks:
  v3 main  — v3.x releases (new installs)
  main main — v2.x only (backward-compat, never auto-promoted to v3)
  v2 main  — v2.x alias (explicit pin)

CI: new "Publish to GitHub Pages APT repo" step in the publish job runs on
tagged stable releases. Downloads the built .deb, places it in pool/v{major},
regenerates Packages.gz and a GPG-signed InRelease for the relevant dist(s),
and pushes to the gh-pages branch.

Setup: scripts/setup-apt-signing-key.sh generates the GPG key pair and prints
the exact `gh secret set` commands to run. Requires APT_SIGNING_KEY and
APT_SIGNING_KEY_PASSPHRASE secrets to be added to the repo before the first
tagged release.

README updated to point new installs at GitHub Pages; Cloudsmith testing
channel retained for beta builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:03:57 -04:00
Kevin Adams 1d0cbca537 docs: ADR-010 — per-major-version apt dist tracks; supersedes ADR-003
Motivated by #271 (silent v2→v3 upgrade). Establishes main (v2 compat),
v2 (alias), and v3 dist tracks so apt upgrade never crosses a major
version boundary. Gated on #230 (GitHub Pages apt repo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:56:54 -04:00
Kevin Adams b50e8dc229 docs: record confirmed tested TrueNAS SCALE 25.04 and 25.10
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:11:05 -04:00
Kevin Adams 18a3aeade0 docs: mark v3.0.0 as released in CHANGELOG
Closes the [Unreleased] heading now that v3.0.0-1 is published to the
stable channel and the GitHub Release is live.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:07:36 -04:00
Kevin Adams 4056e23267 docs: accept ADR-009, mark ADR-008 superseded
ADR-009 (PVE version support v3.x + v4.0 strategy) accepted.
ADR-008 marked Superseded by ADR-009.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:17:23 -04:00
Kevin Adams 4e621a215c docs: ADR-009 draft + versioning strategy — v3.1 PVE 9 only, v4.0 WebSocket
ADR-009 (draft) supersedes ADR-008 with confirmed PVE 9 support, the lun
integer fix approach, and a clear version→PVE support mapping:
- v3.0.x: PVE 8 + PVE 9, REST API
- v3.1.x: PVE 9 only, REST API, api() bump, snapshots
- v4.0.x: WebSocket JSON-RPC 2.0 — genuine architectural change, major version

ROADMAP: v3.2.0 renamed to v4.0.0 for WebSocket milestone; v3.1 scope updated
to document PVE 8 drop and reasons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:13:21 -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 5919910b3c docs: update compatibility matrix to reflect actual tested state
- v3.x beta: tested on PVE 8.4.x, CORE 13.0-U6, SCALE 24.10 + 25.04 + 25.10
- v3.x PVE 9.x: split to planned row with known issue ref (#266)
- Version notices: PVE 8 now points to beta, PVE 9 warns of known issue
- Drop untested SCALE Cobia/Dragonfish from tested column
- "upcoming" → "beta", "In development" → "Beta — active development"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:04:33 -04:00
Kevin Adams 0a17a30838 docs: v2.x → v3.0 migration guides — beginner, advanced, troubleshooting (#228)
Three new docs covering the full migration path from freenas-proxmox v2.x
to truenas-proxmox v3.0:

- migrating-from-v2.md: step-by-step beginner guide with pre-migration
  checklist, TPM detection, capacity notes, Delete source semantics, FAQ
- migration-advanced.md: architecture diff, config field mapping, HA/portal
  failure warnings, bulk migration planning, rollback procedures
- migration-troubleshooting.md: Move Disk failure recovery matrix, dangling
  resource cleanup, API key expiration, common 422 errors

Reviewed by beginner and advanced user minions; incorporated feedback on
concept explanations, safety-critical gaps (HA, SPOF portal, concurrent
cutover staggering), and production edge cases.

Closes #228

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 09:44:39 -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 334f67e473 docs: mark #250 complete in ROADMAP, one item remains for v3.0.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:49:09 -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 728cb136cb docs: mark #264 (SCALE 25.04 compat) complete in ROADMAP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:01:51 -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 2979d54279 docs: add ROADMAP.md with milestone scope, decisions, and deferred items
Captures v3.0.0 remaining work, completed items, the GitHub repo rename
deferral rationale (business timing, no FUNDING.yml impact), and
upcoming v3.1.0/v3.2.0 scope. Process rule added: business decisions
go here and in GitHub issue comments — not only in AI memory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:59:55 -04:00
Kevin Adams 9b2ecb8093 chore: rename package freenas-proxmox → truenas-proxmox (#262)
Introduces truenas-proxmox as the real package and freenas-proxmox as a
transitional meta-package that Depends on truenas-proxmox. Existing
users running apt upgrade automatically pull in the new package; the
migration one-liner (apt install truenas-proxmox && apt purge
freenas-proxmox) completes the transition. The freenas-proxmox package
name will not exist in v4.0.

Changes:
- packaging/DEBIAN/: Package renamed to truenas-proxmox, INSTALL_DIR
  and LOG_FILE updated to /usr/share/truenas-proxmox and
  /var/log/truenas-proxmox-install.log; Replaces/Breaks fields added
- packaging/DEBIAN-transitional/: new empty meta-package that pulls in
  truenas-proxmox and prints a deprecation notice on configure
- build.yml: PACKAGE_NAME=truenas-proxmox; both debs built and
  published to Cloudsmith; transitional postinst added to shellcheck
- All GitHub repo URLs updated to TheGrandWazoo/truenas-proxmox
- apt install/remove/purge commands in docs updated to truenas-proxmox
- GitHub repo rename to truenas-proxmox pending (gh repo rename step)

Closes #262

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:49:28 -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 2e0b5acb9a chore: remove PayPal from FUNDING.yml, keep GitHub Sponsors only
Consolidating to a single payment path to reduce decision friction.
GitHub Sponsors has no platform fee; PayPal does.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:06:33 -04:00
Kevin Adams d6e7e159e3 docs: add prominent sponsor callout and expand Support section in README
Move sponsor ask above the fold with context (4M downloads, ISP/MSP
production use) so it reaches users who never scroll to the bottom.
Expand the Support section with tier descriptions and remove PayPal
in favour of GitHub Sponsors which has no platform fee.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:04:39 -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 0a8e7e7484 docs: add TPM limitation callout and v2.x→v3.0 migration path (#260, #228, #252)
- Add TPM state disk known limitation to v3.0 Prerequisites with clear
  workaround (local-lvm or NFS for tpmstate0, shared storage for live
  migration with TPM)
- Expand Upgrading section with v2.x→v3.0 Move Disk migration path,
  confirmed tested on PVE 8.4 with CORE 13.0-U6 and SCALE 24.10
- Note in-place rename is not supported; Move Disk is the supported path

Closes #260, #228, #252

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:34:40 -04:00
Kevin Adams 0ad5f88180 ci: opt in to Node.js 24 runner and pin checkout to v4.3.1
Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 env var to suppress deprecation
warnings ahead of GitHub's June 2nd forced cutover. Pin actions/checkout
to v4.3.1 (latest v4 patch). Upload/download-artifact left at @v4 pending
a separate audit of the v7/v8 breaking changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:28:41 -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 c48a992385 feat: add Help button to TrueNAS storage panel and ship docs
- Inject pveOnlineHelpInfo entry in truenas-storage.js so PVE wires
  the Help button automatically via the StorageBase framework
- Add onlineHelp: 'storage_truenas' to the input panel definition
- Add ui/truenas-storage-help.html — local help page installed to
  /usr/share/pve-docs/truenas-storage.html (no internet required)
- Add docs/getting-started.md and docs/architecture.md
- Update postinst/postrm to install and remove the help HTML file
- Update build.yml to include the help HTML in the package staging dir

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:08:41 -04:00
Kevin Adams feb1776aa1 docs: update CLAUDE.md to reflect v3.0 single-repo build
Remove all references to the external packer repo and v2.x patch
approach. Document the self-contained build.yml pipeline, packaging
layout, and current open issues accurately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:45:35 -04:00
Kevin Adams eb058266a9 docs: mark CORE 13.0-U6 and SCALE Electric Eel (24.10) as tested in v3.0 matrix
Cobia and Dragonfish remain listed as compatible but are not yet tested
against v3.0 — would require reinstalling those versions in the lab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:10:11 -04:00
Kevin Adams e286929535 chore: remove v2.x files not needed in v3.0
REST/Client.pm — v3.0 uses LWP::UserAgent directly
stable-6/, stable-7/ — PVE 6/7 EOL, v3.0 requires PVE 8+
perl5/PVE/Storage/LunCmd/ — v2.x wedge approach replaced by Custom plugin
pve-manager/js/ patch files — v3.0 uses ui/truenas-storage.js instead

FreeNAS.pm and REST/Client.pm history preserved on master and release/2.x.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:02: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 a4eb3e5557 docs: split Prerequisites by version — v3.0 needs no SSH keys or pre-created targets
v3.0 is fully API-driven: no SSH keys, no pre-created iSCSI targets.
v2.x still requires both. Separating them prevents confusion for users
on either version and removes incorrect guidance from the v3.0 path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:22:06 -04:00
Kevin Adams 678bf7a850 docs: add troubleshooting layer guide to distinguish plugin vs iSCSI vs Proxmox errors
Closes #257. Many support issues are misrouted because users don't know
which layer generated the error — the plugin (REST API orchestration),
the iSCSI/QEMU data path, or the Proxmox core storage stack. Added a
new section at the top of Troubleshooting that explains all three layers
with example log lines and a quick-triage table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:15:28 -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 187f3f9e3f docs: explain GiB vs GB disk size discrepancy in Troubleshooting
Proxmox enters and allocates disk sizes in GiB (base-2) while TrueNAS
and many storage UIs display sizes in GB (base-10). This causes the
reported size to appear ~7% larger than the number entered, which is
correct and expected behaviour. Adds a reference table for common sizes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 09:06:02 -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 ea1c2208e1 chore: update perlcriticrc — target TrueNAS.pm, clean up stale comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:58:14 -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 8a487ef4a8 feat: add TrueNAS UI panel via injected JS — no pvemanagerlib.js patch
Instead of diffing and patching the 50k-line pvemanagerlib.js each PVE
release, we ship truenas-storage.js as our own file and inject one
<script> line into index.html.tpl (rarely changes, ~60 lines).

truenas-storage.js adds 'truenas' to PVE.Utils.storageSchema at runtime
and defines PVE.storage.TrueNASInputPanel with all config fields:
  - TrueNAS host, API key, pool, dataset (column 1)
  - SSL toggle, cert verify, portal IP, target IQN (column 2)
API key field: required on create, optional on edit (only submitted if
the user types a new value).

postinst: copies TrueNAS.pm + truenas-storage.js, adds script tag
postrm:   removes both files, strips script tag

Closes #225 (core interface), #226 (iSCSI lifecycle activate/deactivate),
#227 (API pool listing replaces SSH).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:50:15 -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
143 changed files with 5099 additions and 185653 deletions

View File

@ -1,7 +1,7 @@
# ADR-003: APT Repository Hosting
**Date**: 2026-05-15
**Status**: Accepted
**Status**: Superseded by ADR-010
**Deciders**: Kevin Adams
## Context

View File

@ -1,7 +1,7 @@
# ADR-008: PVE Version Support Matrix
**Date**: 2026-05-19
**Status**: Accepted
**Status**: Superseded by ADR-009
**Deciders**: Kevin Adams
## Context

View File

@ -0,0 +1,86 @@
# ADR-009: PVE Version Support Matrix — v3.x and v4.0 Versioning Strategy
**Date**: 2026-05-31
**Status**: Accepted
**Deciders**: Kevin Adams
**Supersedes**: ADR-008
## Context
ADR-008 established the PVE version support matrix for v3.0. Three things emerged
during v3.0 development that require revised decisions:
1. **PVE 9 confirmed working** — pve01-hq was upgraded to PVE 9.2.3. One blocking
bug was found (#266, `lun` integer type in QEMU blockdev JSON) and fixed by
overriding `qemu_blockdev_options` in TrueNAS.pm.
2. **`api()` version conflict** — PVE 8 has `APIVER = 11`; PVE 9 has `APIVER = 14`.
A single `api()` return value cannot satisfy both without warning (return 11)
or fatally failing on PVE 8 (return 14). The warning is cosmetic and deferred.
3. **WebSocket API scope** — TrueNAS SCALE 25.04+ exposes a new JSON-RPC 2.0
WebSocket API alongside the REST API. This is a new transport layer, not an
incremental improvement. It warrants a major version bump when adopted.
4. **ADR-008 stated `volume_snapshot*` must ship in v3.0** — deferred to v3.1 (#234).
## Decision
### Version → PVE support mapping
| Version | PVE support | TrueNAS API | Notes |
|---------|------------|-------------|-------|
| v3.0.x | PVE 8.x + PVE 9.x | REST (v2.0) | `api()` = 11; cosmetic warning on PVE 9 |
| v3.1.x | PVE 9.x only | REST (v2.0) | `api()` bumped to PVE 9 APIVER; PVE 8 dropped |
| v4.0.x | PVE 9.x+ | WebSocket JSON-RPC 2.0 | New transport; genuine breaking change |
### Why v3.1 drops PVE 8 (not v4.0)
Dropping an EOL'd host platform is a support boundary decision, not a behavior
or API change. Users on PVE 8 stay on v3.0.x, which continues to work. The
plugin's `storage.cfg` format, TrueNAS API calls, and iSCSI behavior are
unchanged. Minor version bump is appropriate.
**PVE 8 EOL: 2026-08-31.** v3.1 must ship before that date so users have time
to migrate to PVE 9 before their platform is unsupported.
### Why v4.0 for WebSocket API
WebSocket JSON-RPC 2.0 is a fundamentally different transport layer. Adopting it
may require dropping or conditionally supporting the REST API for newer SCALE
versions, changing how the plugin establishes connections, and potentially
branching code paths per TrueNAS variant. This is an architectural change that
warrants a major version, not a minor one.
### v3.0.x specifics
- `api()` returns `11` — works on PVE 8 (exact match) and PVE 9 (within APIAGE range)
- PVE 9 emits "older storage API, upgrade recommended" — cosmetic, documented, expected
- `qemu_blockdev_options` override present — workaround for PVE `Plugin.pm` bug (#266)
- Remove when Proxmox fixes `lun => int($3)` in their code (see TrueNAS.pm comment)
- `volume_snapshot*` not implemented — deferred to v3.1 (#234)
### v3.1.x specifics
- Minimum PVE: 9.x
- `api()` bumped to match PVE 9's `APIVER` at time of release
- `volume_snapshot*` implementation (#234)
- Evaluate removal of `qemu_blockdev_options` override if Proxmox has fixed upstream
- `api()` bump tracked in #270
## Consequences
- v3.0.0 can be tagged — PVE 8 + PVE 9 both verified
- v3.1.0 milestone must add `api()` bump (#270) as a prerequisite
- ROADMAP updated: v3.2.0 renamed to v4.0.0 for WebSocket API (#243)
- ADR-008 marked Superseded when this ADR is accepted
## Alternatives Considered
**v4.0 for dropping PVE 8**: Rejected — dropping an EOL platform is not a
behavioral break. Users on PVE 8 keep using v3.0.x. A major version would
signal something fundamentally different about what the plugin does.
**v3.x for WebSocket API**: Rejected — WebSocket JSON-RPC 2.0 changes the
transport layer and may require dropping or splitting REST API support. That is
an architectural change deserving a major version signal to users.

View File

@ -0,0 +1,139 @@
# ADR-010: APT Repository Dist Tracks — Per-Major-Version Isolation
**Date**: 2026-06-02
**Status**: Accepted
**Deciders**: Kevin Adams
**Supersedes**: ADR-003
## Context
Issue #271 exposed a gap: a user running v2.4.0 with the stable Cloudsmith repo ran
`apt upgrade` and was silently promoted to v3.0.0 — a rewrite with breaking auth and
iSCSI model changes. The user had no warning, no release notes were published at the
time, and their host broke.
The root cause is that a single dist (`main`) serves all major versions. `apt upgrade`
has no way to know that crossing a major version boundary requires deliberate action.
Debian solves this with release codenames (`bookworm`, `trixie`): users choose a
release track explicitly and are never silently promoted across breaking boundaries.
ADR-003 established GitHub Pages as the target apt repo infrastructure and specified
a single `dists/stable/` dist. That structure does not support per-major-version
isolation. This ADR supersedes ADR-003 in full — the GitHub Pages hosting decision
is retained, but the dist layout, CI publishing rules, and Cloudsmith transition
plan replace what ADR-003 specified.
## Decision
### Dist track structure
| Dist | Content | Who uses it |
|------|---------|-------------|
| `main` | v2.x packages only | Existing users — backward-compatible; no change to their `sources.list` |
| `v2` | v2.x packages (alias for `main`) | Users who want to explicitly pin to v2 |
| `v3` | v3.x packages only | Users upgrading to v3, and all new installs |
`main` and `v2` serve identical package content. `main` exists purely for backward
compatibility with users who already have it in their `sources.list`. `v2` is the
explicit alias for those who add it knowingly.
When v4.0 ships, a `v4` dist is added. `v3` continues to receive v3.x point releases.
No existing user is auto-promoted.
### Directory layout (GitHub Pages apt repo)
```
dists/
main/ ← v2.x — backward-compat alias
Release
InRelease (GPG signed)
main/binary-all/
Packages
Packages.gz
v2/ ← v2.x — explicit alias (identical content to main/)
Release
InRelease
main/binary-all/
Packages
Packages.gz
v3/ ← v3.x only
Release
InRelease
main/binary-all/
Packages
Packages.gz
pool/
main/ ← shared pool — all .deb files for all versions
truenas-proxmox_2.*.deb
truenas-proxmox_3.*.deb
...
```
`main/` and `v2/` reference the same pool entries. CI generates both `Packages.gz`
files from the same v2.x package list — no filesystem symlinks needed.
### Sources.list lines users will have
```
# Existing users (v2.x, unchanged):
deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] https://thegrandwazoo.github.io/freenas-proxmox main main
# Explicit v2 pin (same content):
deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] https://thegrandwazoo.github.io/freenas-proxmox v2 main
# v3.x installs:
deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] https://thegrandwazoo.github.io/freenas-proxmox v3 main
```
### CI behavior
The build workflow publishes each `.deb` to the pool unconditionally, then regenerates
only the `Packages.gz` files for the dists that cover that major version:
- v2.x tag (`v2.*.*`) → regenerate `main/` and `v2/` dists
- v3.x tag (`v3.*.*`) → regenerate `v3/` dist only
- v4.x tag (`v4.*.*`) → regenerate `v4/` dist only (created when needed)
`main/` is never updated by a v3+ build. This is the enforcement mechanism.
### Cloudsmith transition
Cloudsmith will continue to serve packages in parallel during the GitHub Pages rollout
(per ADR-003). When the GitHub Pages repo is live and announced:
- New installs → GitHub Pages, `v3 main`
- Existing Cloudsmith users → stay on Cloudsmith until they choose to migrate
- Cloudsmith is not updated with per-version dists — it remains a transitional channel
Cloudsmith is dropped after v3.x is the stable generation and Cloudsmith user traffic
has subsided (tracked under issue #230).
## Consequences
- Users on `main` never receive a v3+ package unless they change their `sources.list`.
The v2.x → v3.0 silent-upgrade incident (#271) cannot recur for future major versions.
- New users must explicitly choose a dist track. The README and getting-started docs
must make `v3 main` the default for new installs.
- The CI build workflow (issue #230) must be updated to maintain multiple `Packages.gz`
files, keyed by the major version of the package being published.
- This structure is gated on #230 (GitHub Pages apt repo). Cloudsmith does not support
per-dist isolation within a single repo without additional paid repos.
## Alternatives Considered
**Option B — Separate Cloudsmith repo per major version** (`truenas-proxmox-v2`,
`truenas-proxmox-v3`): Works today without #230. Rejected because it requires users
to change their repo URL on every major version, and Cloudsmith repo sprawl adds cost
and management overhead. GitHub Pages dist tracks are cleaner long-term.
**Option C — Debian package epochs** (`1:2.4.0-1`, `2:3.0.0-1`): Epochs prevent
`apt upgrade` from crossing the boundary, but they are opaque to users, poorly
documented in the ecosystem, and do not solve the "wrong content in the right repo"
problem. Rejected as an internal hack with poor user-facing semantics.
## References
- Issue #271 — silent v2→v3 upgrade incident (motivation)
- Issue #230 — GitHub Pages apt repo (implementation prerequisite)
- ADR-003 — apt repo hosting decision (GitHub Pages approach retained; dist structure superseded)
- ADR-006 — versioning strategy (major/minor/patch semantics)

View File

@ -0,0 +1,114 @@
# ADR-011: APT Components for Optional Feature Packages
**Date**: 2026-06-06
**Status**: Accepted
**Deciders**: Kevin Adams
## Context
ADR-010 established per-major-version dist tracks (`main`, `v2`, `v3`, `v4`) on the
GitHub Pages apt repo. That decision covers *which version* a user receives. It does
not address *which features* within a version they receive.
The multipath plugin (#256) is the first optional feature that should not be installed
by default. Bundling it into the `truenas-proxmox` package would force all users to
carry `iscsiadm` dependencies and a `TrueNASMultipath.pm` code path they never use.
A separate package (`truenas-proxmox-multipath`) is the right model, but it needs a
distribution mechanism that:
- Does not require users to add a second, unrelated repo URL
- Does not appear in `apt upgrade` for users who never opted in
- Scales to future optional features (TPM workarounds, per-vendor drivers, etc.)
## Decision
Use **apt components** to distribute optional feature packages within the existing
dist tracks.
### Structure
```
dists/v3/
Release ← Components: main multipath
main/
binary-all/
Packages ← truenas-proxmox, freenas-proxmox (transitional)
multipath/
binary-all/
Packages ← truenas-proxmox-multipath
```
### Sources.list lines
```
# Standard install — only main component, no optional packages
deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main
# With multipath support
deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main multipath
```
Users who do not add `multipath` to their sources line never see
`truenas-proxmox-multipath` as a candidate — `apt upgrade` will not install it.
### Component naming convention
| Component | Contents | Who uses it |
|-----------|----------|-------------|
| `main` | Base plugin (`truenas-proxmox`) | All installs |
| `multipath` | `truenas-proxmox-multipath` | Users with multipath hardware |
Future components follow the same pattern: one component per optional feature,
named for the feature not the package.
### CI publishing
The build workflow generates a `Packages.gz` per component. Optional-feature packages
are built from subdirectories or separate workflow files and published into their
component directory. The `Release` file is regenerated after all components are
populated, listing all available components in the `Components:` field.
### Testing track
When the GitHub Pages testing track ships (#272), optional components exist there too:
```
dists/testing/
Release ← Components: main multipath
main/binary-all/Packages
multipath/binary-all/Packages
```
Beta users opt into beta multipath builds via `testing main multipath`.
## Consequences
- Users have a single repo URL for all packages from this project, regardless of
which features they use
- `apt upgrade` respects user intent: standard users never receive optional packages
- Adding a new optional feature is a new component — no changes to existing packages
or `main` component machinery
- The `Release` file must list all components; CI must regenerate it when any
component's content changes
- `truenas-proxmox-multipath` has a `Depends: truenas-proxmox` so apt enforces
install order and prevents the optional package from being installed alone
## Alternatives Considered
**Option A — Separate repo URL per optional feature** (e.g., a second GitHub Pages
site or Cloudsmith repo): Requires users to manage multiple repo entries and GPG keys.
Rejected — adds friction for users and management overhead.
**Option B — Bundle everything in `main`** with optional code gated by config flags:
Forces all users to carry dependencies they may never use; makes the base package
harder to audit. Rejected — this is what the v2.x `ZFSPlugin.pm` patch model did.
**Option C — GitHub Packages / OCI**: Does not support raw apt repos. Not viable.
## References
- ADR-010: per-major-version apt dist tracks (established the repo structure this extends)
- Issue #256: multipath plugin (first consumer of a non-`main` component)
- Issue #272: GitHub Pages testing track (testing component support needed here too)

View File

@ -1,70 +0,0 @@
# Runbook: Generate a Patch for a New Proxmox VE Version
When Proxmox VE releases an update that breaks the existing patches, follow these steps.
## Prerequisites
- A Proxmox VE node (or VM) running the new version
- SSH access to that node
- The current `FreeNAS.pm` changes you want to apply
## Steps
### 1. Copy the original files from the PVE node
```bash
# On your dev machine
PVE_HOST=your-proxmox-node
PVE_VER=$(ssh root@$PVE_HOST "dpkg-query --showformat='\${Version}' --show pve-manager")
mkdir -p stable-8/originals
scp root@$PVE_HOST:/usr/share/perl5/PVE/Storage/ZFSPlugin.pm \
stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig
scp root@$PVE_HOST:/usr/share/pve-manager/js/pvemanagerlib.js \
stable-8/pve-manager/js/pvemanagerlib.js.orig
scp root@$PVE_HOST:/usr/share/pve-docs/api-viewer/apidoc.js \
stable-8/pve-docs/api-viewer/apidoc.js.orig
```
### 2. Apply the desired modifications
Work on copies of the `.orig` files:
```bash
cp stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig /tmp/ZFSPlugin.pm
# ... make your changes manually or apply the known modifications ...
```
### 3. Generate the patch
```bash
diff -u stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig /tmp/ZFSPlugin.pm \
> stable-8/perl5/PVE/Storage/ZFSPlugin-${PVE_VER}.pm.patch
# Do the same for the other files
```
### 4. Test the patch
```bash
# On the PVE node or a copy:
patch --dry-run -p0 /usr/share/perl5/PVE/Storage/ZFSPlugin.pm \
< stable-8/perl5/PVE/Storage/ZFSPlugin-${PVE_VER}.pm.patch
```
### 5. Update the postinst version map
In `packaging/DEBIAN/postinst`, update the version-to-patch-file mapping to include the new PVE version.
### 6. Update the default `.patch` symlink or file
The `postinst` selects a patch file based on the installed PVE version. Make sure the new patch is in the version map.
## Notes
- The JS files are large (pvemanagerlib.js is several MB); patches are usually small diffs around the iSCSI provider section
- Use `--ignore-whitespace` with patch to handle indentation differences
- Test with `patch --dry-run` before committing

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1 @@
github: TheGrandWazoo
custom:
- "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TCLNEMBUYQUXN&source=url"

View File

@ -10,7 +10,7 @@ assignees: TheGrandWazoo
| Component | Version |
|-----------|---------|
| Plugin (`dpkg -l freenas-proxmox`) | |
| Plugin (`dpkg -l truenas-proxmox freenas-proxmox`) | |
| Proxmox VE (`pveversion`) | |
| TrueNAS type | CORE / SCALE |
| TrueNAS version | |

View File

@ -5,10 +5,12 @@ on:
branches: ["**"]
tags: ["v*.*.*"]
pull_request:
branches: [master]
branches: [master, "release/3.x"]
env:
PACKAGE_NAME: freenas-proxmox
PACKAGE_NAME: truenas-proxmox
TRANSITIONAL_PACKAGE_NAME: freenas-proxmox
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
# Cancel in-flight runs for the same branch on new push
concurrency:
@ -21,96 +23,81 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4.3.1
- name: Install lint tools
run: |
sudo apt-get update -qq
sudo apt-get install -y \
shellcheck libperl-critic-perl \
libwww-perl libio-socket-ssl-perl librest-client-perl libjson-perl
# PVE::SafeSyslog only exists on Proxmox hosts; stub it for syntax checking
mkdir -p /tmp/pve-stub/PVE
libwww-perl libio-socket-ssl-perl libjson-perl
# Stub PVE modules not available outside Proxmox hosts
mkdir -p /tmp/pve-stub/PVE/Storage
printf 'package PVE::SafeSyslog;\nuse Exporter "import";\nour @EXPORT = qw(syslog);\nsub syslog {}\n1;\n' \
> /tmp/pve-stub/PVE/SafeSyslog.pm
printf 'package PVE::Tools;\nuse Exporter "import";\nour @EXPORT_OK = qw(run_command);\nsub run_command {}\n1;\n' \
> /tmp/pve-stub/PVE/Tools.pm
printf 'package PVE::Storage::Plugin;\nsub new {}\nsub register {}\nsub lookup_types { [] }\nsub properties { {} }\nsub options { {} }\n1;\n' \
> /tmp/pve-stub/PVE/Storage/Plugin.pm
- name: Perl syntax check (all modules)
- name: Perl syntax check
run: |
echo "==> Checking Perl syntax..."
find perl5 stable-*/perl5 -name "*.pm" -print0 \
| xargs -0 -I{} perl -c -I/tmp/pve-stub {} \
&& echo "All .pm files OK"
perl -c -I/tmp/pve-stub perl5/PVE/Storage/Custom/TrueNAS.pm \
&& echo "TrueNAS.pm: OK"
perl -c -I/tmp/pve-stub -Iperl5 \
perl5/PVE/Storage/Custom/TrueNASMultipath.pm \
&& echo "TrueNASMultipath.pm: OK"
- name: Perl static analysis (perlcritic)
run: |
echo "==> Running perlcritic..."
perlcritic --profile .perlcriticrc \
perl5/PVE/Storage/LunCmd/FreeNAS.pm
perl5/PVE/Storage/Custom/TrueNAS.pm \
perl5/PVE/Storage/Custom/TrueNASMultipath.pm
- name: Shell script lint (shellcheck)
run: |
echo "==> Running shellcheck..."
shellcheck --severity=warning \
packaging/DEBIAN/postinst \
packaging/DEBIAN/postrm
packaging/DEBIAN/postrm \
packaging/DEBIAN-transitional/postinst \
packaging/DEBIAN-multipath/postinst \
packaging/DEBIAN-multipath/postrm
echo "Shell scripts OK"
# ── Job 2: Validate patches apply cleanly ──────────────────────────────────
validate-patches:
name: Validate Patches
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: "Dry-run patch: ZFSPlugin (PVE 8)"
run: |
patch --dry-run --ignore-whitespace \
stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig \
< stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \
&& echo "ZFSPlugin PVE-8 patch: OK"
- name: "Dry-run patch: ZFSPlugin (PVE 8.4.x)"
run: |
patch --dry-run --ignore-whitespace \
stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig \
< stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch \
&& echo "ZFSPlugin PVE-8.4 patch: OK"
- name: "Dry-run patch: apidoc.js (PVE 8)"
run: |
patch --dry-run --ignore-whitespace \
stable-8/pve-docs/api-viewer/apidoc.js.orig \
< stable-8/pve-docs/api-viewer/apidoc.js.patch \
&& echo "apidoc PVE-8 patch: OK"
# ── Job 3: Build .deb ────────────────────────────────────────────────────────
# ── Job 2: Build .deb ────────────────────────────────────────────────────────
build:
name: Build Package
runs-on: ubuntu-latest
needs: [lint, validate-patches]
needs: [lint]
outputs:
version: ${{ steps.vars.outputs.version }}
deb_file: ${{ steps.vars.outputs.deb_file }}
channel: ${{ steps.vars.outputs.channel }}
cloudsmith_repo: ${{ steps.vars.outputs.cloudsmith_repo }}
is_release: ${{ steps.vars.outputs.is_release }}
version: ${{ steps.vars.outputs.version }}
deb_file: ${{ steps.vars.outputs.deb_file }}
transitional_deb_file: ${{ steps.vars.outputs.transitional_deb_file }}
multipath_deb_file: ${{ steps.vars.outputs.multipath_deb_file }}
channel: ${{ steps.vars.outputs.channel }}
cloudsmith_repo: ${{ steps.vars.outputs.cloudsmith_repo }}
is_release: ${{ steps.vars.outputs.is_release }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4.3.1
with:
# Fetch full history so tag-based versioning works
fetch-depth: 0
# ── Version resolution ─────────────────────────────────────────────────
# Version strategy:
# Tagged release (v1.2.3) → 1.2.3 (stable channel)
# master branch → <tag>-beta+<sha> (testing channel)
# feature_* branch → <tag>-alpha+<sha> (development channel)
# any other branch / PR → <tag>-dev+<sha> (no publish)
# Tagged release (v3.x.x) → 3.x.x-1 (stable channel)
# release/3.x branch → <ver>~beta+<sha> (testing channel)
# master branch → <ver>~beta+<sha> (testing channel)
# release/* branches → <ver>~alpha+<sha> (development)
# everything else / PRs → <ver>~dev+<sha> (no publish)
#
# The base version is derived from the most recent git tag (vX.Y.Z).
# No VERSION file needed — the tag IS the version.
# Tilde (~) sorts BELOW the base version in dpkg, so pre-release builds
# never upgrade over a stable package. Stable uses "-1" Debian revision.
- name: Resolve version and channel
id: vars
run: |
@ -118,31 +105,25 @@ jobs:
REF="${{ github.ref }}"
IS_RELEASE="false"
# Base version from our $VERSION in FreeNAS.pm — the single source of truth.
# Bump that variable when starting a new release series; git tags trigger publishing.
# $VERSION in TrueNAS.pm is the single source of truth.
BASE_VERSION="$(perl -ne 'if (/our\s+\$VERSION\s*=\s*['"'"'"]([^'"'"'"]+)/) { print $1; exit }' \
perl5/PVE/Storage/LunCmd/FreeNAS.pm 2>/dev/null || echo '0.0.0')"
perl5/PVE/Storage/Custom/TrueNAS.pm 2>/dev/null || echo '0.0.0')"
# Only an exact vX.Y.Z tag (no suffix) is a stable release.
# Stable packages use a "-1" Debian revision so they sort higher than
# any pre-release build (e.g. 2.3.0~beta+sha < 2.3.0-1).
# Pre-release builds use "~" (tilde) which sorts BELOW the base version
# in dpkg, ensuring stable always wins on apt upgrade.
if [[ "$REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${REF#refs/tags/v}-1"
CHANNEL="stable"
CLOUDSMITH_REPO="truenas-proxmox"
IS_RELEASE="true"
if [[ "${REF#refs/tags/v}" != "$BASE_VERSION" ]]; then
echo "::warning::Tag version (${REF#refs/tags/v}) does not match \$VERSION in FreeNAS.pm ($BASE_VERSION)"
echo "::warning::Tag version (${REF#refs/tags/v}) does not match \$VERSION in TrueNAS.pm ($BASE_VERSION)"
fi
elif [[ "$REF" == refs/heads/master ]]; then
elif [[ "$REF" == refs/heads/master || "$REF" == refs/heads/release/3.x ]]; then
VERSION="${BASE_VERSION}~beta+${SHORT_SHA}"
CHANNEL="testing"
CLOUDSMITH_REPO="truenas-proxmox-testing"
elif [[ "$REF" == refs/heads/feature_* ]]; then
elif [[ "$REF" == refs/heads/release/* ]]; then
VERSION="${BASE_VERSION}~alpha+${SHORT_SHA}"
CHANNEL="development"
CLOUDSMITH_REPO="truenas-proxmox-snapshots"
@ -154,12 +135,16 @@ jobs:
fi
DEB_FILE="${PACKAGE_NAME}_${VERSION}_all.deb"
TRANSITIONAL_DEB_FILE="${TRANSITIONAL_PACKAGE_NAME}_${VERSION}_all.deb"
MULTIPATH_DEB_FILE="truenas-proxmox-multipath_${VERSION}_all.deb"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT"
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
echo "cloudsmith_repo=${CLOUDSMITH_REPO}" >> "$GITHUB_OUTPUT"
echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT"
echo "transitional_deb_file=${TRANSITIONAL_DEB_FILE}" >> "$GITHUB_OUTPUT"
echo "multipath_deb_file=${MULTIPATH_DEB_FILE}" >> "$GITHUB_OUTPUT"
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
echo "cloudsmith_repo=${CLOUDSMITH_REPO}" >> "$GITHUB_OUTPUT"
echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT"
{
echo "### Build Summary"
@ -177,9 +162,7 @@ jobs:
STAGING="dist"
mkdir -p "${STAGING}/DEBIAN"
mkdir -p "${STAGING}/usr/share/freenas-proxmox/patches/ZFSPlugin"
mkdir -p "${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib"
mkdir -p "${STAGING}/usr/share/freenas-proxmox/patches/apidoc"
mkdir -p "${STAGING}/usr/share/truenas-proxmox"
# Generate control file from template
sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN/control.j2 \
@ -188,79 +171,107 @@ jobs:
# Maintainer scripts
cp packaging/DEBIAN/postinst "${STAGING}/DEBIAN/postinst"
cp packaging/DEBIAN/postrm "${STAGING}/DEBIAN/postrm"
cp packaging/DEBIAN/triggers "${STAGING}/DEBIAN/triggers"
chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm"
# Plugin source files
cp perl5/PVE/Storage/LunCmd/FreeNAS.pm "${STAGING}/usr/share/freenas-proxmox/FreeNAS.pm"
cp perl5/REST/Client.pm "${STAGING}/usr/share/freenas-proxmox/REST-Client.pm"
# Plugin files (postinst copies these to their final destinations)
cp perl5/PVE/Storage/Custom/TrueNAS.pm "${STAGING}/usr/share/truenas-proxmox/TrueNAS.pm"
cp ui/truenas-storage.js "${STAGING}/usr/share/truenas-proxmox/truenas-storage.js"
cp ui/truenas-storage-help.html "${STAGING}/usr/share/truenas-proxmox/truenas-storage-help.html"
# PVE 8 patches (8.0.x 8.3.x)
cp stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \
"${STAGING}/usr/share/freenas-proxmox/patches/ZFSPlugin/8.patch"
# PVE 8.4.x ZFSPlugin patch — indentation changed in 8.4, incompatible with 8.patch
cp stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch \
"${STAGING}/usr/share/freenas-proxmox/patches/ZFSPlugin/8.4.patch"
echo "Bundled PVE 8.4.x ZFSPlugin patch"
cp stable-8/pve-manager/js/pvemanagerlib.js.patch \
"${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib/8.patch"
cp stable-8/pve-docs/api-viewer/apidoc.js.patch \
"${STAGING}/usr/share/freenas-proxmox/patches/apidoc/8.patch"
# PVE 8.4.x pvemanagerlib patch — JS was reformatted in 8.4, incompatible with 8.patch
cp stable-8/pve-manager/js/pvemanagerlib-8.4.14_1.js.patch \
"${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib/8.4.patch"
echo "Bundled PVE 8.4.x pvemanagerlib patch"
# PVE 7 patches (best-effort — use latest versioned patch available)
for type in ZFSPlugin pvemanagerlib apidoc; do
case "$type" in
ZFSPlugin) glob="stable-7/perl5/PVE/Storage/ZFSPlugin-*.pm.patch" ;;
pvemanagerlib) glob="stable-7/pve-manager/js/pvemanagerlib-*.js.patch" ;;
apidoc) glob="stable-7/pve-docs/api-viewer/apidoc-*.js.patch" ;;
esac
latest=$(ls $glob 2>/dev/null | sort -V | tail -1 || true)
if [ -n "$latest" ]; then
cp "$latest" "${STAGING}/usr/share/freenas-proxmox/patches/${type}/7.patch"
echo "Bundled PVE-7 ${type} patch: $(basename $latest)"
else
echo "No PVE-7 ${type} patch found — skipping"
fi
done
# Debian changelog (required by Debian policy §12.7)
mkdir -p "${STAGING}/usr/share/doc/truenas-proxmox"
gzip -9c packaging/changelog.Debian \
> "${STAGING}/usr/share/doc/truenas-proxmox/changelog.Debian.gz"
echo "==> Package contents:"
find "${STAGING}" | sort
- name: Build .deb
- name: Assemble transitional package staging directory
run: |
sudo dpkg-deb -Zgzip --build dist "${{ steps.vars.outputs.deb_file }}"
VERSION="${{ steps.vars.outputs.version }}"
STAGING="dist-transitional"
- name: Verify .deb
mkdir -p "${STAGING}/DEBIAN"
sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN-transitional/control.j2 \
> "${STAGING}/DEBIAN/control"
cp packaging/DEBIAN-transitional/postinst "${STAGING}/DEBIAN/postinst"
chmod 0755 "${STAGING}/DEBIAN/postinst"
# Debian changelog
mkdir -p "${STAGING}/usr/share/doc/freenas-proxmox"
gzip -9c packaging/DEBIAN-transitional/changelog.Debian \
> "${STAGING}/usr/share/doc/freenas-proxmox/changelog.Debian.gz"
echo "==> Transitional package contents:"
find "${STAGING}" | sort
- name: Assemble multipath package staging directory
run: |
echo "==> Package info:"
VERSION="${{ steps.vars.outputs.version }}"
STAGING="dist-multipath"
mkdir -p "${STAGING}/DEBIAN"
mkdir -p "${STAGING}/usr/share/truenas-proxmox-multipath"
sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN-multipath/control.j2 \
> "${STAGING}/DEBIAN/control"
cp packaging/DEBIAN-multipath/postinst "${STAGING}/DEBIAN/postinst"
cp packaging/DEBIAN-multipath/postrm "${STAGING}/DEBIAN/postrm"
chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm"
cp perl5/PVE/Storage/Custom/TrueNASMultipath.pm \
"${STAGING}/usr/share/truenas-proxmox-multipath/TrueNASMultipath.pm"
cp ui/truenas-multipath.js \
"${STAGING}/usr/share/truenas-proxmox-multipath/truenas-multipath.js"
cp packaging/DEBIAN-multipath/multipath.conf.example \
"${STAGING}/usr/share/truenas-proxmox-multipath/multipath.conf.example"
mkdir -p "${STAGING}/usr/share/doc/truenas-proxmox-multipath"
gzip -9c packaging/DEBIAN-multipath/changelog.Debian \
> "${STAGING}/usr/share/doc/truenas-proxmox-multipath/changelog.Debian.gz"
echo "==> Multipath package contents:"
find "${STAGING}" | sort
- name: Build .deb packages
run: |
sudo dpkg-deb -Zgzip --build dist "${{ steps.vars.outputs.deb_file }}"
sudo dpkg-deb -Zgzip --build dist-transitional "${{ steps.vars.outputs.transitional_deb_file }}"
sudo dpkg-deb -Zgzip --build dist-multipath "${{ steps.vars.outputs.multipath_deb_file }}"
- name: Verify .deb packages
run: |
echo "==> Package info (truenas-proxmox):"
dpkg-deb --info "${{ steps.vars.outputs.deb_file }}"
echo ""
echo "==> Package contents:"
dpkg-deb --contents "${{ steps.vars.outputs.deb_file }}"
echo "==> Package info (freenas-proxmox transitional):"
dpkg-deb --info "${{ steps.vars.outputs.transitional_deb_file }}"
echo ""
echo "==> Package info (truenas-proxmox-multipath):"
dpkg-deb --info "${{ steps.vars.outputs.multipath_deb_file }}"
- name: Upload package artifact
uses: actions/upload-artifact@v7
- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.vars.outputs.deb_file }}
path: ${{ steps.vars.outputs.deb_file }}
name: packages-${{ steps.vars.outputs.version }}
path: |
${{ steps.vars.outputs.deb_file }}
${{ steps.vars.outputs.transitional_deb_file }}
${{ steps.vars.outputs.multipath_deb_file }}
retention-days: 30
# ── Job 4: Security scan ─────────────────────────────────────────────────────
# ── Job 3: Security scan ─────────────────────────────────────────────────────
security:
name: Security Scan
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4.3.1
# Scan the repository for secrets and known vulnerabilities
- name: Run Trivy (repo scan — secrets + misconfig)
uses: aquasecurity/trivy-action@master
with:
@ -271,11 +282,10 @@ jobs:
exit-code: 1
format: table
# Download and scan the built .deb
- name: Download built package
uses: actions/download-artifact@v8
- name: Download built packages
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.deb_file }}
name: packages-${{ needs.build.outputs.version }}
- name: Extract and scan .deb contents
run: |
@ -292,23 +302,30 @@ jobs:
exit-code: 1
format: table
# ── Job 5: Publish ───────────────────────────────────────────────────────────
# ── Job 4: Publish ───────────────────────────────────────────────────────────
publish:
name: Publish
runs-on: ubuntu-latest
needs: [build, security]
# Only publish on direct pushes (not PRs) to tracked branches or tags
if: github.event_name == 'push' && needs.build.outputs.channel != 'none'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4.3.1
- name: Download built package
uses: actions/download-artifact@v8
- name: Download built packages
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.deb_file }}
name: packages-${{ needs.build.outputs.version }}
- name: Publish to Cloudsmith
- name: Publish truenas-proxmox to Cloudsmith
# Cloudsmith now serves v2.x stable only. v3+ stable → GitHub Pages.
# beta/alpha → GitHub Pages testing dist (see step below).
if: >-
startsWith(needs.build.outputs.version, '2.') &&
needs.build.outputs.channel == 'stable'
uses: cloudsmith-io/action@v0.6.14
with:
api-key: ${{ secrets.CLOUDSMITH_API_KEY }}
@ -320,39 +337,252 @@ jobs:
release: any-version
file: ${{ needs.build.outputs.deb_file }}
- name: Publish transitional freenas-proxmox to Cloudsmith
if: >-
startsWith(needs.build.outputs.version, '2.') &&
needs.build.outputs.channel == 'stable'
uses: cloudsmith-io/action@v0.6.14
with:
api-key: ${{ secrets.CLOUDSMITH_API_KEY }}
command: push
format: deb
owner: ksatechnologies
repo: ${{ needs.build.outputs.cloudsmith_repo }}
distro: debian
release: any-version
file: ${{ needs.build.outputs.transitional_deb_file }}
- name: Publish to GitHub Pages APT repo
if: needs.build.outputs.is_release == 'true'
env:
APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }}
APT_SIGNING_KEY_PASSPHRASE: ${{ secrets.APT_SIGNING_KEY_PASSPHRASE }}
GH_TOKEN: ${{ github.token }}
run: |
# Abort clearly if signing key not configured
if [[ -z "$APT_SIGNING_KEY" ]]; then
echo "::error::APT_SIGNING_KEY secret is not set — run scripts/setup-apt-signing-key.sh and add the secret"
exit 1
fi
# Determine major version from package version (e.g. 3.0.0-1 → 3)
VERSION="${{ needs.build.outputs.version }}"
MAJOR="${VERSION%%.*}"
# Import GPG signing key
echo "$APT_SIGNING_KEY" | base64 -d | gpg --batch --import
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \
| awk -F: '/^sec/{print $5; exit}')"
# Checkout gh-pages branch into a temp directory
PAGES_DIR="$(mktemp -d)"
git clone --branch gh-pages \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
"$PAGES_DIR"
# Add .deb files to the versioned pool directories
mkdir -p "${PAGES_DIR}/pool/v${MAJOR}"
mkdir -p "${PAGES_DIR}/pool/multipath/v${MAJOR}"
cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/"
cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/"
cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/v${MAJOR}/"
# Install apt tooling
sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils
# Determine which dists to update
if [[ "$MAJOR" == "2" ]]; then
DISTS="v2 main"
else
DISTS="v${MAJOR}"
fi
_gen_component() {
local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64"
dpkg-scanpackages --multiversion "$POOL" \
| sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \
> "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
}
_dist_codename() {
case "$1" in
v2|main) echo "limelight" ;;
v3) echo "error" ;;
v4) echo "rivendell" ;;
*) echo "$1" ;;
esac
}
for DIST in $DISTS; do
_gen_component "$PAGES_DIR" "$DIST" "main" \
"${PAGES_DIR}/pool/v${MAJOR}"
_gen_component "$PAGES_DIR" "$DIST" "multipath" \
"${PAGES_DIR}/pool/multipath/v${MAJOR}"
CODENAME=$(_dist_codename "$DIST")
# Generate Release file listing both components
apt-ftparchive \
-o "APT::FTPArchive::Release::Origin=truenas-proxmox" \
-o "APT::FTPArchive::Release::Label=truenas-proxmox" \
-o "APT::FTPArchive::Release::Suite=${DIST}" \
-o "APT::FTPArchive::Release::Codename=${CODENAME}" \
-o "APT::FTPArchive::Release::Components=main multipath" \
-o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \
release "${PAGES_DIR}/dists/${DIST}" \
> "${PAGES_DIR}/dists/${DIST}/Release"
gpg --batch --yes \
--passphrase "${APT_SIGNING_KEY_PASSPHRASE}" \
--default-key "${GPG_KEY_ID}" \
--clearsign \
-o "${PAGES_DIR}/dists/${DIST}/InRelease" \
"${PAGES_DIR}/dists/${DIST}/Release"
done
# Create codename alias directories (e.g. dists/error/ mirrors dists/v3/)
# so that both Suite ("v3") and Codename ("error") work in sources.list.
# rm -rf first: cp -r puts src inside dst when dst already exists.
for DIST in $DISTS; do
CODENAME=$(_dist_codename "$DIST")
if [[ "$CODENAME" != "$DIST" ]]; then
rm -rf "${PAGES_DIR}/dists/${CODENAME}"
cp -r "${PAGES_DIR}/dists/${DIST}" "${PAGES_DIR}/dists/${CODENAME}"
fi
done
# Commit and push
cd "$PAGES_DIR"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git diff --staged --quiet || \
git commit -m "apt: publish ${VERSION} to dist(s): ${DISTS}"
git push
- name: Publish to GitHub Pages testing dist
if: >-
needs.build.outputs.channel == 'testing' ||
needs.build.outputs.channel == 'development'
env:
APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }}
APT_SIGNING_KEY_PASSPHRASE: ${{ secrets.APT_SIGNING_KEY_PASSPHRASE }}
GH_TOKEN: ${{ github.token }}
run: |
if [[ -z "$APT_SIGNING_KEY" ]]; then
echo "::error::APT_SIGNING_KEY secret is not set"
exit 1
fi
VERSION="${{ needs.build.outputs.version }}"
echo "$APT_SIGNING_KEY" | base64 -d | gpg --batch --import
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \
| awk -F: '/^sec/{print $5; exit}')"
PAGES_DIR="$(mktemp -d)"
git clone --branch gh-pages \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
"$PAGES_DIR"
# Replace pool/testing/ contents with the latest build only
rm -rf "${PAGES_DIR}/pool/testing"
rm -rf "${PAGES_DIR}/pool/multipath/testing"
mkdir -p "${PAGES_DIR}/pool/testing"
mkdir -p "${PAGES_DIR}/pool/multipath/testing"
cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/testing/"
cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/testing/"
cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/testing/"
sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils
DIST="testing"
_gen_component() {
local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64"
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64"
dpkg-scanpackages --multiversion "$POOL" \
| sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \
> "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
}
_gen_component "$PAGES_DIR" "$DIST" "main" "${PAGES_DIR}/pool/testing"
_gen_component "$PAGES_DIR" "$DIST" "multipath" "${PAGES_DIR}/pool/multipath/testing"
apt-ftparchive \
-o "APT::FTPArchive::Release::Origin=truenas-proxmox" \
-o "APT::FTPArchive::Release::Label=truenas-proxmox" \
-o "APT::FTPArchive::Release::Suite=${DIST}" \
-o "APT::FTPArchive::Release::Codename=testing" \
-o "APT::FTPArchive::Release::Components=main multipath" \
-o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \
release "${PAGES_DIR}/dists/${DIST}" \
> "${PAGES_DIR}/dists/${DIST}/Release"
gpg --batch --yes \
--passphrase "${APT_SIGNING_KEY_PASSPHRASE}" \
--default-key "${GPG_KEY_ID}" \
--clearsign \
-o "${PAGES_DIR}/dists/${DIST}/InRelease" \
"${PAGES_DIR}/dists/${DIST}/Release"
cd "$PAGES_DIR"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git diff --staged --quiet || \
git commit -m "apt: publish ${VERSION} to testing dist"
git push
- name: Create draft GitHub Release
if: needs.build.outputs.is_release == 'true'
uses: softprops/action-gh-release@v3
with:
name: "v${{ needs.build.outputs.version }}"
draft: true
files: ${{ needs.build.outputs.deb_file }}
files: |
${{ needs.build.outputs.deb_file }}
${{ needs.build.outputs.transitional_deb_file }}
${{ needs.build.outputs.multipath_deb_file }}
generate_release_notes: false
body: |
## freenas-proxmox v${{ needs.build.outputs.version }}
## truenas-proxmox v${{ needs.build.outputs.version }}
> **Edit before publishing** — fill in tested versions and remove inapplicable notices below.
> **Edit before publishing** — fill in tested versions before publishing.
### ⚠️ Version Compatibility Notices
### Compatibility
| Your Proxmox VE | What to do |
|:----------------|:-----------|
| **PVE 9+** | ❌ Do **not** install v2.x — use v3.0 when released |
| **PVE 8.x** | ✅ Supported. Note: PVE 8 EOL is **2026-08-31** — plan your upgrade to PVE 9 + v3.0 |
| **PVE 7.x** | ⚠️ Best-effort only. This is the **last v2.x release** supporting PVE 7. Do **not** upgrade to v3.0 — stay on v2.x |
| **PVE 6 or older** | ❌ Not supported |
| Proxmox VE | Status |
|:-----------|:-------|
| **PVE 9+** | ✅ Supported |
| **PVE 8.x** | ✅ Supported (EOL 2026-08-31 — plan upgrade to PVE 9) |
| **PVE 7 or older** | ❌ Not supported — use v2.x |
### Tested Proxmox VE Versions
- Proxmox VE 8.4.x (tested: )
- Proxmox VE 8.3.x (tested: )
- Proxmox VE 7.x (best-effort: )
- Proxmox VE 9.x (tested: )
### Tested TrueNAS Versions
- TrueNAS CORE:
- TrueNAS SCALE:
- TrueNAS CORE 13.x (tested: )
- TrueNAS SCALE 24.10.x (tested: )
### Installation
See [README → Installation](https://github.com/TheGrandWazoo/freenas-proxmox#installation).
See [README → Installation](https://github.com/TheGrandWazoo/truenas-proxmox#installation).
### Changes
See [CHANGELOG.md](https://github.com/TheGrandWazoo/freenas-proxmox/blob/master/CHANGELOG.md#unreleased).
See [CHANGELOG.md](https://github.com/TheGrandWazoo/truenas-proxmox/blob/release/3.x/CHANGELOG.md).

View File

@ -1,6 +1,6 @@
# Perl::Critic configuration for freenas-proxmox
# Severity scale: 1 (brutal) → 5 (gentle). We start at 4 and tighten over time.
# Run: perlcritic --profile .perlcriticrc perl5/PVE/Storage/LunCmd/FreeNAS.pm
# Run: perlcritic --profile .perlcriticrc perl5/PVE/Storage/Custom/TrueNAS.pm
severity = 4
theme = core
@ -10,16 +10,16 @@ verbose = %f:%l:%c [%p] %m\n
# These are known issues being fixed in Phase 2 (code hardening).
# Remove entries here as the underlying code is fixed.
# eval $value pattern — being replaced with explicit substitution map (Phase 2)
# eval { } block is fine; only string eval is risky — keep disabled
[-BuiltinFunctions::ProhibitStringyEval]
# Postfix conditionals (if/unless at end of line) — style preference, keep
[-ControlStructures::ProhibitPostfixControls]
# Long subs — FreeNAS.pm has some; will be refactored in Phase 3
# TrueNAS.pm has necessarily large subs (PVE plugin interface methods)
[-Subroutines::ProhibitExcessComplexity]
# Global variables — known issue, fixing in Phase 2
# our $VERSION and our $state are intentional package-level declarations
[-Variables::ProhibitPackageVars]
# ── Rules with custom settings ────────────────────────────────────────────────

View File

@ -4,11 +4,11 @@ All notable changes to this project will be documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) independent of Proxmox VE or TrueNAS versions.
See each [GitHub Release](https://github.com/TheGrandWazoo/freenas-proxmox/releases) for the specific Proxmox VE and TrueNAS versions supported.
See each [GitHub Release](https://github.com/TheGrandWazoo/truenas-proxmox/releases) for the specific Proxmox VE and TrueNAS versions supported.
---
## [Unreleased] — v3.0.0
## [3.0.0] — 2026-05-31
### Added
- New `PVE::Storage::Custom::TrueNASPlugin` — proper Proxmox VE custom storage type (no more ZFSPlugin.pm patching)
@ -64,4 +64,4 @@ See each [GitHub Release](https://github.com/TheGrandWazoo/freenas-proxmox/relea
## [2.1.x] and Earlier
See the [commit history](https://github.com/TheGrandWazoo/freenas-proxmox/commits/master) for earlier changes.
See the [commit history](https://github.com/TheGrandWazoo/truenas-proxmox/commits/master) for earlier changes.

100
CLAUDE.md
View File

@ -2,79 +2,87 @@
## What This Project Is
A storage plugin "wedge" for Proxmox VE (PVE) that allows PVE to manage iSCSI LUNs on TrueNAS/FreeNAS via the TrueNAS REST API instead of the traditional SSH-based `iscsiadm` approach.
A native `PVE::Storage::Custom` plugin for Proxmox VE (PVE) that manages TrueNAS ZFS volumes over iSCSI via the TrueNAS REST API. Each VM gets its own dedicated iSCSI target; QEMU drives the connection directly via `iscsi://` paths — no `iscsiadm`, no SSH keys, no patches to PVE core files.
The plugin installs as a Debian package and works by:
1. Deploying a new Perl LunCmd handler (`FreeNAS.pm`) to `/usr/share/perl5/PVE/Storage/LunCmd/`
2. Patching three Proxmox VE system files at install time via `dpkg triggers`
1. Copying `TrueNAS.pm` to `/usr/share/perl5/PVE/Storage/Custom/` (auto-discovered by PVE)
2. Copying `truenas-storage.js` to `/usr/share/pve-manager/js/` and injecting one `<script>` tag into `index.html.tpl`
## Repository Layout
```
freenas-proxmox/
├── perl5/PVE/Storage/
│ ├── Custom/FreeNAS.pm # Old attempt at a full custom storage type (unfinished)
│ ├── LunCmd/FreeNAS.pm # MAIN backend: iSCSI LunCmd via TrueNAS REST API
│ ├── LunCmd/FreeNAS-ng.pm # Next-gen draft (not in use)
│ └── ZFSPlugin-*.pm.patch # Per-PVE-version patches for ZFSPlugin.pm
├── pve-manager/js/
│ └── pvemanagerlib-*.js.patch # Per-PVE-version patches for the Proxmox UI JS
├── pve-docs/api-viewer/
│ └── apidoc-*.js.patch # Per-PVE-version patches for the API docs JS
├── perl5/REST/Client.pm # Bundled REST::Client (also an apt dependency)
├── stable-5/, stable-6/, stable-7/, stable-8/
│ # Per-major-version snapshots of patches + originals
└── .github/workflows/action.yml # Currently just dispatches to external packer repo
├── perl5/PVE/Storage/Custom/TrueNAS.pm # MAIN plugin — PVE::Storage::Custom subclass
├── ui/truenas-storage.js # Proxmox UI panel (storageSchema + input panel)
├── packaging/
│ └── DEBIAN/
│ ├── control.j2 # Package metadata template (VERSION substituted at build time)
│ ├── postinst # Install: copies .pm + .js, injects <script> tag, restarts PVE
│ └── postrm # Remove/purge: removes files, strips <script> tag, restarts PVE
└── .github/workflows/
├── build.yml # Active CI: lint → build .deb → security scan → publish
└── action.yml # Deprecated no-op (retained so old external links don't 404)
```
## How the Current Build Works (Two-Repo Problem)
## How the Build Works
1. A push to this repo triggers `action.yml` which fires a `repository_dispatch` event to `TheGrandWazoo/freenas-proxmox-packer`
2. That separate repo holds the DEBIAN package structure (`DEBIAN/control`, `postinst`, `postrm`, `triggers`)
3. Its CI builds the `.deb` with `dpkg-deb` and pushes to Cloudsmith
Everything lives in this repo — there is no external packer repo.
The `postinst` script at install time **git-clones this repo** to `/usr/local/src/freenas-proxmox` and applies patches from there. This is the key fragility — internet access required at package install time.
`.github/workflows/build.yml` runs on every push and tag:
## Three Files That Get Patched at Install Time
| Job | What it does |
|-----|-------------|
| **lint** | `perl -c` syntax check + perlcritic + shellcheck on postinst/postrm |
| **build** | Resolves version from `$VERSION` in `TrueNAS.pm`, assembles `dist/` staging dir, runs `dpkg-deb`, uploads `.deb` as a workflow artifact |
| **security** | Trivy repo scan (secrets + misconfig) + Trivy scan of extracted `.deb` contents |
| **publish** | Pushes `.deb` to Cloudsmith; on `v*.*.*` tags also creates a draft GitHub Release |
| File | What the patch adds |
|------|---------------------|
| `/usr/share/perl5/PVE/Storage/ZFSPlugin.pm` | Adds `freenas` as a valid iSCSI provider, routes `run_lun_command` to `FreeNAS.pm`, adds custom properties/options |
| `/usr/share/pve-manager/js/pvemanagerlib.js` | Adds `FreeNAS/TrueNAS API` to the iSCSI provider dropdown; adds UI fields for API host, user, secret, SSL, token auth |
| `/usr/share/pve-docs/api-viewer/apidoc.js` | Registers TrueNAS-specific properties in the API docs |
### Version / Channel mapping
## Authentication Modes Supported
| Trigger | Version format | Cloudsmith repo |
|---------|---------------|-----------------|
| `v*.*.*` tag | `X.Y.Z-1` | `truenas-proxmox` (stable) |
| `release/3.x` or `master` branch | `X.Y.Z~beta+<sha>` | `truenas-proxmox-testing` |
| other `release/*` branches | `X.Y.Z~alpha+<sha>` | `truenas-proxmox-snapshots` |
| PRs / feature branches | `X.Y.Z~dev+<sha>` | not published |
- **Basic Auth**: `freenas_user` + `freenas_password` (deprecated but still works)
- **Bearer Token**: `truenas_token_auth=true` + `truenas_secret` (preferred for TrueNAS SCALE)
`$VERSION` in `TrueNAS.pm` is the single source of truth. A version tag that doesn't match emits a CI warning.
## TrueNAS API Version Detection
## Authentication
The plugin auto-detects v1.0 vs v2.0 API based on the TrueNAS version string and HTTP response:
- `>= 11.03.01.00` → uses v2.0 API
- Older → uses v1.0 API
- **Bearer Token** (required): `truenas_api_token` — set in PVE storage config
Basic auth (`freenas_user` / `freenas_password`) was removed in v3.0. Bearer token is the only supported mode.
## TrueNAS API
v3.0 uses the TrueNAS v2.0 REST API exclusively. Supported:
- TrueNAS CORE 11.3+ (confirmed tested: CORE 13.0-U6)
- TrueNAS SCALE (confirmed tested: SCALE 24.10 Electric Eel, SCALE 25.04, SCALE 25.10)
## Known Issues / Active Work
- Versioned patches are fragile — each PVE minor release may need a new patch
- `postinst` clones the repo at install time (requires internet, fragile)
- `Custom/FreeNAS.pm` is unfinished (duplicate `properties()` and `options()` subs)
- REST::Client is both an apt dependency AND manually bundled
- Everything is named `FreeNAS` internally but the product is now `TrueNAS`
- SSH keys still required for ZFS pool listing (separate Proxmox code path via `ZFSPoolPlugin.pm`)
- Max LUN limit bug (#150)
- **#228** — v2.x→v3.0 migration: "Move Disk" confirmed working; in-place zvol rename script TBD
- **#234** — Snapshot interface (PVE 9+): not started
- **#243** — WebSocket API (SCALE 25.04+): future
- **#249** — Per-variant dispatch: deferred to v3.1+
- **#250** — alloc_image partial-failure rollback: narrow edge case, open
- **#256** — Multipath support: deferred (no test hardware)
- **#260** — TPM state disks incompatible with `iscsi://` paths: known limitation, store TPM state on local-lvm or NFS
## Key Perl Modules
## Key Perl Module
- `PVE::Storage::LunCmd::FreeNAS` — the main plugin. Entry point: `run_lun_command()`
- Functions: `freenas_api_connect`, `freenas_api_check`, `freenas_api_call`, `freenas_list_lu`, `run_create_lu`, `run_delete_lu`, `run_modify_lu`
- `PVE::Storage::Custom::TrueNAS` (`perl5/PVE/Storage/Custom/TrueNAS.pm`) — the entire plugin
- Entry points: `alloc_image`, `free_image`, `volume_size_info`, `path`, `activate_volume`, `deactivate_volume`
- API helpers: `_api_call`, `_ensure_target`, `_running_vms_on_storage`
## Packaging / Deployment Notes
- Package name: `freenas-proxmox`
- Apt repos: Cloudsmith (`ksatechnologies/truenas-proxmox` stable, `ksatechnologies/truenas-proxmox-testing` beta)
- Install: `apt install freenas-proxmox` after adding the repo
- The package uses dpkg `triggers` to re-apply patches when Proxmox VE packages are upgraded
- Install: `apt install freenas-proxmox` after adding the Cloudsmith repo
- No dpkg triggers — install/remove are handled entirely by `postinst` / `postrm`
- PVE auto-discovers the plugin via `/usr/share/perl5/PVE/Storage/Custom/` on daemon restart
## ADRs / Plans / Runbooks

View File

@ -1,4 +1,4 @@
# Contributing to freenas-proxmox
# Contributing to truenas-proxmox
Thank you for your interest in contributing. This document covers how to report bugs, request features, and submit code.
@ -22,16 +22,16 @@ Be respectful. This is a community project maintained in spare time. Constructiv
## Reporting Bugs
Use the [bug report issue template](https://github.com/TheGrandWazoo/freenas-proxmox/issues/new?template=bug_report.md).
Use the [bug report issue template](https://github.com/TheGrandWazoo/truenas-proxmox/issues/new?template=bug_report.md).
Before filing:
- Check existing [open and closed issues](https://github.com/TheGrandWazoo/freenas-proxmox/issues?q=is%3Aissue) for duplicates
- Check existing [open and closed issues](https://github.com/TheGrandWazoo/truenas-proxmox/issues?q=is%3Aissue) for duplicates
- Reproduce the issue on the latest release if possible
**Always include:**
- Proxmox VE version (`proxmox-ve` package version)
- TrueNAS version and type (CORE / SCALE)
- Plugin version (`dpkg -l freenas-proxmox`)
- Plugin version (`dpkg -l truenas-proxmox`)
- Relevant log lines from syslog (`grep -i freenas /var/log/syslog`)
- The storage configuration (redact passwords/tokens)
@ -39,7 +39,7 @@ Before filing:
## Requesting Features
Use the [feature request issue template](https://github.com/TheGrandWazoo/freenas-proxmox/issues/new?template=feature_request.md).
Use the [feature request issue template](https://github.com/TheGrandWazoo/truenas-proxmox/issues/new?template=feature_request.md).
Feature requests are evaluated against the project roadmap. Large changes should be discussed in an issue before a pull request is opened.
@ -57,14 +57,14 @@ Feature requests are evaluated against the project roadmap. Large changes should
### Local Build
```bash
git clone https://github.com/TheGrandWazoo/freenas-proxmox.git
cd freenas-proxmox
git clone https://github.com/TheGrandWazoo/truenas-proxmox.git
cd truenas-proxmox
# Build the package (once packaging/ directory exists in v3.x)
dpkg-deb -Zgzip --build packaging freenas-proxmox_dev_all.deb
dpkg-deb -Zgzip --build packaging truenas-proxmox_dev_all.deb
# Install locally for testing
dpkg -i freenas-proxmox_dev_all.deb
dpkg -i truenas-proxmox_dev_all.deb
```
### Testing Changes to FreeNAS.pm

313
README.md
View File

@ -2,10 +2,12 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/TheGrandWazoo/freenas-proxmox?sort=semver)](https://github.com/TheGrandWazoo/freenas-proxmox/releases/latest)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TheGrandWazoo/freenas-proxmox/build.yml?label=build)](https://github.com/TheGrandWazoo/freenas-proxmox/actions/workflows/build.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TheGrandWazoo/freenas-proxmox/build.yml?branch=release%2F3.x&label=build)](https://github.com/TheGrandWazoo/freenas-proxmox/actions/workflows/build.yml)
[![GitHub issues](https://img.shields.io/github/issues/TheGrandWazoo/freenas-proxmox)](https://github.com/TheGrandWazoo/freenas-proxmox/issues)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/TheGrandWazoo?label=Sponsors)](https://github.com/sponsors/TheGrandWazoo)
> This plugin is maintained by one person and has 4 million downloads — used in production at ISPs and MSPs worldwide. If it saves you time or headaches, [please consider sponsoring](https://github.com/sponsors/TheGrandWazoo) to keep it actively maintained and funded.
A Proxmox VE storage plugin that manages ZFS-over-iSCSI volumes on TrueNAS (CORE and SCALE) through the TrueNAS REST API — no SSH-based LUN management, no `iscsiadm` scripting.
---
@ -28,14 +30,12 @@ A Proxmox VE storage plugin that manages ZFS-over-iSCSI volumes on TrueNAS (CORE
## How It Works
Proxmox VE's built-in ZFS-over-iSCSI storage type uses SSH to manage LUNs on the storage server. This plugin replaces that SSH-based management layer with direct calls to the **TrueNAS REST API**, giving you:
This plugin is a native `PVE::Storage::Custom` type — Proxmox discovers it automatically and treats it like any other storage backend. When you create a VM disk, the plugin calls the **TrueNAS REST API** to provision a ZFS volume (zvol) and a dedicated per-VM iSCSI target, then hands Proxmox an `iscsi://` path. QEMU opens that path directly — no SSH, no `iscsiadm`, no kernel iSCSI sessions on the Proxmox host.
- API token (Bearer) or username/password authentication
- Automatic TrueNAS API version detection (v1 and v2)
- Support for both TrueNAS CORE and TrueNAS SCALE
- Proper rollback when operations fail (no dangling iSCSI extents)
> **Note:** Proxmox still uses `iscsiadm` to connect and disconnect the iSCSI session on the Proxmox host itself — that part is handled by the core Proxmox code and does not require SSH. The SSH keys documented in the [Proxmox ZFS-over-iSCSI wiki](https://pve.proxmox.com/wiki/Storage:_ZFS_over_iSCSI) are still required for the ZFS pool listing step.
- Bearer Token (API key) authentication — no username/password, no SSH keys
- Per-VM iSCSI targets (`proxmox-vm-<vmid>`) — removing one disk never affects other VMs
- Automatic rollback when operations fail (no dangling iSCSI extents)
- Supports TrueNAS CORE 13.x and TrueNAS SCALE 24.1025.10
---
@ -45,94 +45,194 @@ Proxmox VE's built-in ZFS-over-iSCSI storage type uses SSH to manage LUNs on the
| Plugin Version | Proxmox VE | TrueNAS CORE | TrueNAS SCALE | Status |
|:--------------:|:----------:|:------------:|:-------------:|:------:|
| **3.x** *(upcoming)* | 8.x, 9.x | 13.0-U6+ | Cobia (23.10)+, Dragonfish (24.04)+ | In development |
| **2.x** *(current)* | 7.x ⚠️, 8.08.3 ✅, 8.4.x ✅ | 11.3+ | 22.02+ | Active |
| **3.x** *(stable)* | 8.4.x ✅, 9.x ✅ | 13.0-U6+ ✅ | Electric Eel (24.10) ✅, Fangtooth (25.04) ✅, Goldeye (25.10) ✅ | Stable |
| **2.x** *(stable)* | 7.x ⚠️, 8.08.3 ✅, 8.4.x ✅ | 11.3+ | 22.02+ | Active |
| **1.x** *(legacy)* | 5.x, 6.x | 11.x | — | Unsupported |
Tested combinations are marked ✅. Untested combinations may work but are not validated. Check the [Releases page](https://github.com/TheGrandWazoo/freenas-proxmox/releases) for the specific versions tested against each release.
### Important Version Notices
> **Proxmox VE 7 users**
>
> v2.x is the **last release series that supports PVE 7**. PVE 7 support is best-effort only — no new patches will be developed for it.
>
> **Do not upgrade to v3.0** when it releases — v3.0 requires Proxmox VE 8 or later. Stay on the latest v2.x release.
> v3.0 requires Proxmox VE 8 or later. Stay on the latest v2.x release.
> **Proxmox VE 8 users**
>
> Proxmox VE 8 reaches **end-of-life on 2026-08-31**. Plan your upgrade to PVE 9 before that date.
> v3.0 beta is tested and working on PVE 8.4.x. This is the recommended version for running v3.0 beta.
>
> v2.x works on PVE 8. When you upgrade to PVE 9, migrate to v3.0 (coming before the EOL date).
> Proxmox VE 8 reaches **end-of-life on 2026-08-31**. PVE 9 support is targeted for v3.1.0, planned well before that date.
> **Proxmox VE 9+ users**
>
> v2.x is **not supported on PVE 9**. Use v3.0 when it is released. Do not install v2.x on a PVE 9 node.
> v3.0 beta has a **known issue on PVE 9.x** — VMs may fail to start due to a LUN type mismatch in QEMU's blockdev layer ([#266](https://github.com/TheGrandWazoo/freenas-proxmox/issues/266)). PVE 9.x support is actively being investigated for **v3.1.0**.
>
> v2.x is not supported on PVE 9. Stay on PVE 8.x until v3.1.0 is available.
> **Proxmox VE 5 or 6 users**
>
> These versions are not supported. PVE 5 reached end-of-life in 2019, PVE 6 in 2022. Please upgrade your Proxmox VE installation.
Check the [Releases page](https://github.com/TheGrandWazoo/freenas-proxmox/releases) for the specific Proxmox and TrueNAS versions tested against each release.
Check the [Releases page](https://github.com/TheGrandWazoo/truenas-proxmox/releases) for the specific Proxmox and TrueNAS versions tested against each release.
---
## Prerequisites
Before installing, ensure the following are in place on your **Proxmox VE node**:
### v3.0 (beta)
1. **SSH keys** configured between Proxmox and TrueNAS — required for ZFS pool listing by the Proxmox core (see the [Proxmox wiki](https://pve.proxmox.com/wiki/Storage:_ZFS_over_iSCSI), section starting with `mkdir /etc/pve/priv/zfs`).
v3.0 is a fully API-driven custom storage plugin. No SSH keys required.
2. On **TrueNAS**, an iSCSI target and initiator group must exist and be configured. The plugin manages extents and target-to-extent mappings, but the target itself must be pre-created.
1. On **TrueNAS**, ensure the iSCSI service is running and an iSCSI **portal** and **initiator group** are configured. The plugin creates per-VM iSCSI targets automatically — you do not need to pre-create a target.
3. On **TrueNAS SCALE** or **TrueNAS CORE 13+**, generate an API key:
2. Generate a TrueNAS API key:
- TrueNAS SCALE: *System Settings → API Keys → Add*
- TrueNAS CORE 13: *gear icon (top-right) → API Keys → Add*
Copy the key — you will need it during storage configuration in Proxmox.
> **Known limitation — TPM state disks**
>
> `tpmstate0` (virtual TPM) disks **cannot** be stored on v3.0 iSCSI storage. The `swtpm` backend requires a local filesystem path and cannot use the `iscsi://` URIs that v3.0 provides to QEMU.
>
> If your VM uses Secure Boot or TPM, store the `tpmstate0` disk on a separate storage (e.g. `local-lvm` or NFS). All other disk types — virtio, scsi, IDE, EFI — work normally.
>
> For live VM migration with TPM, the TPM state disk must also be on shared filesystem storage (NFS or CephFS), not on this plugin's storage.
### v2.x (legacy)
> v2.x is no longer the recommended version. See [Migrating from v2.x](docs/migrating-from-v2.md).
1. **SSH keys** configured between each Proxmox node and TrueNAS — required by the Proxmox core for ZFS pool listing (see the [Proxmox wiki](https://pve.proxmox.com/wiki/Storage:_ZFS_over_iSCSI), section starting with `mkdir /etc/pve/priv/zfs`).
2. On **TrueNAS**, a pre-created iSCSI **target** and **initiator group**.
3. A TrueNAS API key (SCALE: *System Settings → API Keys*; CORE 13: *gear icon → API Keys*).
---
## Installation
### Stable Release
Add the repository and install:
### Stable Release (v3.x)
```bash
# Import the GPG key
curl -fsSL https://dl.cloudsmith.io/public/ksatechnologies/truenas-proxmox/gpg.284C106104A8CE6D.key \
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| tee /usr/share/keyrings/ksatechnologies-truenas-proxmox-keyring.gpg > /dev/null
| sudo tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Add the repository
cat > /etc/apt/sources.list.d/ksatechnologies-repo.list << 'EOF'
deb [signed-by=/usr/share/keyrings/ksatechnologies-truenas-proxmox-keyring.gpg] \
https://dl.cloudsmith.io/public/ksatechnologies/truenas-proxmox/deb/debian any-version main
EOF
# Add the v3 repository track
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
# Install
apt update && apt install freenas-proxmox
sudo apt update && sudo apt install truenas-proxmox
```
`apt upgrade` will deliver v3.x point releases automatically. You will never be
automatically promoted to a future v4 — that requires changing the dist track in
your `sources.list` to `v4`.
### Upgrading from v2.x
**v3.0 is a breaking change.** Do not run `apt upgrade` until you have read the
migration guide — your storage configuration will need to be updated.
See [docs/migrating-from-v2.md](docs/migrating-from-v2.md).
### Staying on v2.x
If you are on v2.x and want to continue receiving v2.x point releases without
risking a v3 upgrade, switch to the `main` dist track (v2.x only):
```bash
# Import the GPG key (if not already done)
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Add the v2 repository track
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox main main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
sudo apt update
```
### Migrating from Cloudsmith
> Cloudsmith is being phased out. GitHub Pages is the permanent home for this project's packages.
If you installed from the Cloudsmith apt repo, swap your source to GitHub Pages. Your installed package is not affected — this only changes where future updates come from.
**On v3.x (Cloudsmith stable):**
```bash
# Remove the old Cloudsmith source
sudo rm -f /etc/apt/sources.list.d/truenas-proxmox*.list
sudo rm -f /usr/share/keyrings/ksatechnologies-truenas-proxmox*.gpg
# Import the GitHub Pages GPG key
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Point to the v3 dist track
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
sudo apt update
```
**On v2.x (Cloudsmith stable) — staying on v2:**
```bash
# Remove the old Cloudsmith source
sudo rm -f /etc/apt/sources.list.d/freenas-proxmox*.list /etc/apt/sources.list.d/truenas-proxmox*.list
sudo rm -f /usr/share/keyrings/ksatechnologies-truenas-proxmox*.gpg
# Import the GitHub Pages GPG key
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Point to the v2 dist track (v2.x only — you will never receive a v3 package)
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox main main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
sudo apt update
```
For a full upgrade path matrix — including what to do when v4 ships — see [docs/upgrade-paths.md](docs/upgrade-paths.md).
---
### Testing / Beta Release
For early access to new features (may be unstable):
For early access to new features (may be unstable). Beta builds are published on
every push to `release/3.x` — always the latest build, not accumulated history.
```bash
# Import the GPG key
curl -fsSL https://dl.cloudsmith.io/public/ksatechnologies/truenas-proxmox-testing/gpg.CACC9EE03F2DFFCC.key \
# Import the GPG key (skip if already done for the stable track)
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| tee /usr/share/keyrings/ksatechnologies-truenas-proxmox-testing-keyring.gpg > /dev/null
| sudo tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Add the repository
cat > /etc/apt/sources.list.d/ksatechnologies-testing-repo.list << 'EOF'
deb [signed-by=/usr/share/keyrings/ksatechnologies-truenas-proxmox-testing-keyring.gpg] \
https://dl.cloudsmith.io/public/ksatechnologies/truenas-proxmox-testing/deb/debian any-version main
EOF
# Add the testing dist track
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox testing main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
# Install
apt update && apt install freenas-proxmox
sudo apt update && sudo apt install truenas-proxmox
```
To switch back to stable, replace `testing` with `v3` in your sources.list and run `apt update`.
---
## Configuration
@ -187,24 +287,53 @@ The disk is created successfully despite this warning, but the suboptimal block
Edit `/etc/pve/storage.cfg` on any cluster node and change `blocksize 8192` to `blocksize 16384` for your TrueNAS SCALE storage entry. No data migration is needed — only newly created zvols use the updated value. Existing zvols are unaffected.
> **Note:** Automatic blocksize detection based on TrueNAS version is planned for v2.4.0 (see [#241](https://github.com/TheGrandWazoo/freenas-proxmox/issues/241)).
> **Note:** Automatic blocksize detection based on TrueNAS version is planned for v2.4.0 (see [#241](https://github.com/TheGrandWazoo/truenas-proxmox/issues/241)).
---
## Upgrading
The package integrates with Proxmox VE's standard upgrade mechanism. On `apt upgrade`, the package will automatically re-apply any patches needed after a Proxmox VE update:
### v2.x → v2.x (patch / minor upgrade)
Standard apt upgrade — the package re-applies any patches needed after a Proxmox VE update automatically:
```bash
apt update && apt full-upgrade
```
### v2.x → v3.0 (migration)
v3.0 is a different storage plugin type (`PVE::Storage::Custom::TrueNAS`) and uses a different architecture (per-VM iSCSI targets, `iscsi://` paths). There is no in-place upgrade — disk data stays on TrueNAS and you move VM disks across using Proxmox's built-in **Move Disk** function.
> **Tested migration path (confirmed in lab):**
> Migrating live VM disks from a v2.x storage to a v3.0 storage via Move Disk was validated on Proxmox VE 8.4 with TrueNAS CORE 13.0-U6 and TrueNAS SCALE 24.10. VMs remained running throughout the migration.
#### Step-by-step
1. **Install v3.0** on all Proxmox nodes (see [Installation](#installation) — use the testing channel until v3.0 is stable-released).
2. **Add a new v3.0 storage** in Proxmox (*Datacenter → Storage → Add → TrueNAS (ZFS/iSCSI)*). Use the same TrueNAS pool as your existing v2.x storage. Give it a distinct ID (e.g. `truenas-v3`).
3. **For each VM**, move its disks from the v2.x storage to the new v3.0 storage:
- Select the VM → **Hardware** tab
- Select the disk → **Move Disk**
- Choose the new v3.0 storage as the target
- Check **Delete source** if you want the old zvol removed after the move
Repeat for each disk (including EFI disk if present). VMs can remain running during Move Disk.
4. **Verify** the VM boots and its disks are accessible after migration.
5. **Remove the v2.x storage** from Proxmox once all VMs are migrated (*Datacenter → Storage → Remove*). The iSCSI targets and extents that belonged to the old storage will need to be cleaned up from TrueNAS manually if they were not auto-removed.
> **Note:** EFI disks (`efidisk0`) and data disks can be migrated with Move Disk. TPM state disks (`tpmstate0`) must stay on local-lvm or NFS — see the [TPM limitation](#v30-upcoming) in Prerequisites.
---
## Uninstalling
```bash
apt remove freenas-proxmox
apt remove truenas-proxmox
```
This removes the plugin and reverses all patches, returning your Proxmox VE installation to its unmodified state. Any storage configurations using this plugin should be removed from Proxmox before uninstalling.
@ -213,6 +342,64 @@ This removes the plugin and reverses all patches, returning your Proxmox VE inst
## Troubleshooting
### Understanding what this plugin does — and does not — do
Before reporting an issue, it helps to know which layer the error is coming from. This plugin is an **orchestrator**: it calls the TrueNAS REST API to provision resources (create zvols, iSCSI extents, and target mappings). It does not carry data. The actual disk I/O path — QEMU reading and writing blocks — runs directly between Proxmox and TrueNAS over iSCSI, with no involvement from this plugin.
There are three distinct layers where errors can appear:
**Layer 1 — Plugin (TrueNAS REST API)**
The plugin called the TrueNAS API and got an error, or the API was unreachable. These errors come from the plugin code and appear in the Proxmox task log with prefixes like `freenas-proxmox:` or `[TrueNAS::]`.
Common causes:
- Wrong API host IP or hostname
- HTTP vs HTTPS mismatch (toggle **Use SSL**)
- API token expired, revoked, or not entered correctly
- TrueNAS iSCSI service not running
- TrueNAS API service not reachable from the Proxmox node
Example log line:
```
freenas-proxmox: Unable to connect to the TrueNAS API at '192.168.1.10' using HTTPS (500)
```
**Layer 2 — iSCSI / QEMU data path**
The VM's QEMU process (v3.0) or the Proxmox kernel iSCSI stack (v2.x) failed to connect or lost its session to TrueNAS. These errors come from QEMU or `iscsiadm` — not from this plugin.
In **v3.0**, look for QEMU log lines referencing `iscsi://` paths. In **v2.x**, look for `iscsiadm` lines in syslog.
Common causes:
- TrueNAS iSCSI service stopped while a VM was running
- Initiator group does not permit the Proxmox node's IP
- Network path to the iSCSI portal is down
- CHAP authentication configured on the TrueNAS target but not in Proxmox (note: this plugin does not configure CHAP — it must be set to None or configured separately)
Example log line (v2.x):
```
iscsiadm: No active sessions.
```
**Layer 3 — Proxmox core storage stack**
Errors from PVE's own storage subsystem — pvedaemon, pvestatd, or storage.cfg parsing.
Common causes:
- `storage.cfg` syntax error or stale config keys (especially after migrating from v2.x — see [migration guide](docs/migrating-from-v2.md))
- `pvedaemon` or `pvestatd` service crashed
**Quick triage:**
| Symptom | Likely layer | First check |
|---------|-------------|-------------|
| Disk creation fails, API error in task log | Plugin (Layer 1) | API key, SSL setting, TrueNAS API reachable |
| Disk created on TrueNAS but Proxmox reports error | Plugin (Layer 1) | syslog for `truenas-proxmox:` lines |
| VM won't start, iSCSI session error | iSCSI/QEMU (Layer 2) | TrueNAS iSCSI service, initiator group ACL |
| Storage shows unavailable | Proxmox core (Layer 3) | `pvedaemon` service, storage.cfg |
---
### After install, the "FreeNAS/TrueNAS API" option is not visible
Refresh your browser (force-refresh with Ctrl+Shift+R or Cmd+Shift+R). The Proxmox UI JavaScript is cached aggressively.
@ -246,7 +433,29 @@ TrueNAS SCALE 25.04 **revokes all existing API keys** that were created with whi
**Additionally**, TrueNAS SCALE 25.04 enforces HTTPS for API key authentication — keys transmitted over plain HTTP are automatically revoked. Ensure **Use SSL** is enabled in your Proxmox storage config when using token auth.
> **Note:** TrueNAS SCALE 25.04 also deprecates the REST API used by this plugin (v2.x). Full removal is planned for SCALE 26.x. Plugin v3.0.0 will add WebSocket JSON-RPC 2.0 support. See [issue #243](https://github.com/TheGrandWazoo/freenas-proxmox/issues/243).
> **Note:** TrueNAS SCALE 25.04 also deprecates the REST API used by this plugin (v2.x). Full removal is planned for SCALE 26.x. Plugin v3.0.0 will add WebSocket JSON-RPC 2.0 support. See [issue #243](https://github.com/TheGrandWazoo/truenas-proxmox/issues/243).
### Disk size in Proxmox shows larger than what I entered
This is expected. Proxmox creates disks in **GiB** (gibibytes, base-2) but displays them in **GB** (gigabytes, base-10) in some views.
| Unit | Base | 1 unit = |
|------|------|----------|
| GiB (gibibyte) | 2¹⁰ = 1024 | 1,073,741,824 bytes |
| GB (gigabyte) | 10³ = 1000 | 1,000,000,000 bytes |
When you enter **80 GiB** in the Proxmox disk creation dialog, the zvol is created as exactly 85,899,345,920 bytes. TrueNAS reports that in decimal: **85.90 GB**. The disk inside the VM is still exactly 80 GiB — nothing is lost or added.
A quick reference:
| Entered in Proxmox | TrueNAS / decimal display |
|--------------------|---------------------------|
| 10 GiB | 10.74 GB |
| 32 GiB | 34.36 GB |
| 80 GiB | 85.90 GB |
| 100 GiB | 107.37 GB |
| 500 GiB | 536.87 GB |
| 1 TiB | 1,099.51 GB (≈ 1.10 TB) |
### Dangling extents on TrueNAS after a failed operation
@ -254,7 +463,7 @@ If you see iSCSI extents in TrueNAS that are not associated with any target, the
### Filing a Bug Report
Please use the [GitHub issue tracker](https://github.com/TheGrandWazoo/freenas-proxmox/issues) and include the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md). Include relevant log lines from `syslog` (search for `FreeNAS::`).
Please use the [GitHub issue tracker](https://github.com/TheGrandWazoo/truenas-proxmox/issues) and include the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md). Include relevant log lines from `syslog` (search for `FreeNAS::`).
---
@ -268,10 +477,14 @@ For significant changes, open an issue first to discuss the approach.
## Support the Project
If this plugin saves you time, consider supporting its development:
This plugin is built and maintained by one person. Sponsorship directly funds the hardware lab used to test every release against real Proxmox and TrueNAS nodes before it ships to you.
- **GitHub Sponsors**: [github.com/sponsors/TheGrandWazoo](https://github.com/sponsors/TheGrandWazoo)
- **PayPal**: [Donate via PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TCLNEMBUYQUXN&source=url)
**[Become a sponsor on GitHub](https://github.com/sponsors/TheGrandWazoo)** — GitHub Sponsors has no platform fee and goes straight to development.
Sponsor tiers:
- **$5/month** — Supporter. You keep this maintained.
- **$10/month** — Backer. Listed in DONORS.md.
- **$25/month** — Sustainer. Priority issue responses.
Donor support has funded a 4-node Proxmox cluster and TrueNAS test lab used for development and validation. See [DONORS.md](DONORS.md) for a full list of donors.

182
ROADMAP.md Normal file
View File

@ -0,0 +1,182 @@
# freenas-proxmox / truenas-proxmox — Roadmap
This file captures release scope, business decisions, and deferred items. It is updated in the same commit as any scope or decision change — not just in issues or memory.
---
## Released — v3.0.0 (TrueNAS Custom Plugin)
**Released:** 2026-05-31
**Branch:** `release/3.x` (default branch)
**GitHub Release:** https://github.com/TheGrandWazoo/freenas-proxmox/releases/tag/v3.0.0
Full rewrite as a native `PVE::Storage::Custom` plugin. No patching of PVE files, no SSH, full TrueNAS REST API, bearer token auth only.
### Confirmed tested
| Component | Versions |
|-----------|---------|
| Proxmox VE | 8.4.x, 9.2.x |
| TrueNAS CORE | 13.0-U6 |
| TrueNAS SCALE | 24.10 Electric Eel, 25.04, 25.10 |
### What shipped
| # | Fix |
|---|-----|
| [#266](https://github.com/TheGrandWazoo/freenas-proxmox/issues/266) | PVE 9: `lun` string vs integer in QEMU blockdev JSON |
| [#269](https://github.com/TheGrandWazoo/freenas-proxmox/issues/269) | SCALE 25.10 strict Pydantic rejects volsize as string |
| [#267](https://github.com/TheGrandWazoo/freenas-proxmox/issues/267) | free_image 422 on targetextent delete when VM is running |
| [#265](https://github.com/TheGrandWazoo/freenas-proxmox/issues/265) | Loop over all targetextent rows in free_image |
| [#264](https://github.com/TheGrandWazoo/freenas-proxmox/issues/264) | SCALE 25.04 integer type coercion + alias uniqueness |
| [#261](https://github.com/TheGrandWazoo/freenas-proxmox/issues/261) | API token keyfile (`/etc/pve/priv/truenas-<id>.key`) |
| [#262](https://github.com/TheGrandWazoo/freenas-proxmox/issues/262) | Package rename: `freenas-proxmox``truenas-proxmox` + transitional package |
| [#263](https://github.com/TheGrandWazoo/freenas-proxmox/issues/263) | Fix `truenas_target` full-IQN match |
| [#260](https://github.com/TheGrandWazoo/freenas-proxmox/issues/260) | TPM state disk limitation callout |
| [#250](https://github.com/TheGrandWazoo/freenas-proxmox/issues/250) | Rollback orphaned TrueNAS resources on `alloc_image` partial failure |
| [#228](https://github.com/TheGrandWazoo/freenas-proxmox/issues/228) | Migration docs: beginner, advanced, troubleshooting |
---
## Released — GitHub Pages APT Repository
**Completed:** 2026-06-06
**Closed:** [#230](https://github.com/TheGrandWazoo/freenas-proxmox/issues/230)
**URL:** https://thegrandwazoo.github.io/freenas-proxmox
Replaced Cloudsmith as the primary package distribution channel. Per-major-version dist tracks prevent silent cross-version upgrades (ADR-010). Optional feature packages distributed via apt components (ADR-011).
### Dist tracks
| Dist | Content | Sources.list |
|------|---------|--------------|
| `main` | v2.x — backward-compat alias | `... freenas-proxmox main main` |
| `v2` | v2.x — explicit pin | `... freenas-proxmox v2 main` |
| `v3` | v3.x | `... freenas-proxmox v3 main` |
| `testing` | beta builds | Future — [#272](https://github.com/TheGrandWazoo/freenas-proxmox/issues/272) |
### Components (ADR-011)
| Component | Content |
|-----------|---------|
| `main` | Base plugin (all installs) |
| `multipath` | `truenas-proxmox-multipath` — optional add-on, [#256](https://github.com/TheGrandWazoo/freenas-proxmox/issues/256) |
### Cloudsmith transition state (2026-06-06)
| Channel | State |
|---------|-------|
| Stable (`truenas-proxmox`) | v3.x+ no longer published here; v2.x still published |
| Testing (`truenas-proxmox-testing`) | Still active until #272 ships |
### Migration tooling
`scripts/migrate-repo-to-github-pages.sh` — auto-detects installed version, finds Cloudsmith sources by URL content, switches to correct dist track. Run on each Proxmox node as root.
---
## Deferred Business Decisions
### GitHub Repo Rename: `freenas-proxmox``truenas-proxmox`
**Status:** Deferred — no timeline set
**Decision date:** 2026-05-25
**Tracked in:** [#229](https://github.com/TheGrandWazoo/freenas-proxmox/issues/229)
All code is ready. GitHub auto-redirects old URLs so there is no technical urgency.
```bash
gh repo rename truenas-proxmox --repo TheGrandWazoo/freenas-proxmox
```
**Note:** `FUNDING.yml` is tied to the user account, not the repo name. Rename has no effect on GitHub Sponsors.
---
## Released — v3.1.0 (PVE 9 + Snapshots)
**Released:** 2026-06-07
**PVE support:** PVE 9.x only — PVE 8 support dropped
**Decision date:** 2026-05-31 — see ADR-009
**GitHub Release:** https://github.com/TheGrandWazoo/freenas-proxmox/releases/tag/v3.1.0
v3.1 is the first release that requires PVE 9. Dropping PVE 8 allows:
- Bumping `api()` to PVE 9's `APIVER` (silences "older storage API" warning)
- Implementing snapshot-as-volume-chains (PVE 9 feature)
- Removing the `qemu_blockdev_options` override if Proxmox fixes `Plugin.pm` upstream
### Completed in v3.1.0
| # | Title | Commit |
|---|-------|--------|
| [#270](https://github.com/TheGrandWazoo/freenas-proxmox/issues/270) | Bump `api()` to 14 — silences PVE 9 "older storage API" warning | fc74a42 |
| [#272](https://github.com/TheGrandWazoo/freenas-proxmox/issues/272) | GitHub Pages testing dist track (retires Cloudsmith testing) | 03fe20b |
| [#273](https://github.com/TheGrandWazoo/freenas-proxmox/issues/273) | Debian changelog in package (`apt changelog` was failing) | 1c98591 |
| [#274](https://github.com/TheGrandWazoo/freenas-proxmox/issues/274) | Filename: prefix stripped from Packages — packages not downloadable | 19a3ec2 |
| [#275](https://github.com/TheGrandWazoo/freenas-proxmox/issues/275) | README badges pointed to non-existent repo | da0aaff |
| [#276](https://github.com/TheGrandWazoo/freenas-proxmox/issues/276) | parse_volname state volume crash — RAM snapshots left orphans | 80a7889 |
| [#234](https://github.com/TheGrandWazoo/freenas-proxmox/issues/234) | Snapshot interface — ZFS snapshots via TrueNAS REST API | 59d8a81 |
**Snapshot implementation (2026-06-07):** Verified on PVE 9.2.3 against TrueNAS CORE 13.0-U6 and SCALE 24.10. Disk-only and RAM snapshots both confirmed working end-to-end including rollback.
### Carried forward
| # | Title | Notes |
|---|-------|-------|
| [#277](https://github.com/TheGrandWazoo/freenas-proxmox/issues/277) | iSCSI GET_LBA_STATUS error on VM start | Log noise, non-fatal — open |
---
## Released — v3.2.0 (Multipath)
**Released:** 2026-06-20
**GitHub Release:** https://github.com/TheGrandWazoo/freenas-proxmox/releases/tag/v3.2.0
Introduces the `truenas-proxmox-multipath` optional add-on package. Active-active iSCSI multipath via dm-multipath. Separate plugin (`TrueNASMultipath.pm`) in the `multipath` apt component — not part of the base plugin. See ADR-011.
### Confirmed tested
| Component | Versions |
|-----------|---------|
| Proxmox VE | 9.2.x |
| TrueNAS CORE | 13.0-U6 (FreeBSD / ALUA) |
| TrueNAS SCALE | 24.10 Electric Eel, 25.04 |
**Path mode:** Active-active (`multibus` + `service-time 0`) — both paths carry I/O simultaneously. Failover and path recovery confirmed on CORE and SCALE 25.04.
### What shipped
| # | Title | Commit |
|---|-------|--------|
| [#256](https://github.com/TheGrandWazoo/freenas-proxmox/issues/256) | Multipath plugin — `truenas-proxmox-multipath` package (ADR-011) | da82356 |
| [#278](https://github.com/TheGrandWazoo/freenas-proxmox/issues/278) | Multipath UI panel (`truenas-multipath` in Datacenter → Storage → Add) | da82356 |
| [#279](https://github.com/TheGrandWazoo/freenas-proxmox/issues/279) | postinst: strip pre-existing unmanaged devices block in multipath.conf | b7acb60 |
### Known differences: CORE vs SCALE multipath
| | TrueNAS CORE (FreeBSD) | TrueNAS SCALE (Linux) |
|---|---|---|
| iSCSI target | `ctld` | Linux LIO |
| ALUA | Yes — `hwhandler='1 alua'`, `prio=50` | No — `hwhandler='0'`, `prio=1` |
| Portal config | `0.0.0.0` (all interfaces) | Per-IP listen addresses required |
| Failover time | ~2s (ALUA state query) | <0.1s |
| SCALE 25.04 note | — | `port` field rejected in `PUT /api/v2.0/iscsi/portal` — omit it ([#280](https://github.com/TheGrandWazoo/freenas-proxmox/issues/280)) |
---
## Upcoming — v4.0.0 (WebSocket API, SCALE 25.x)
**Target:** After PoC testing on SCALE 25.04+
**Why major version:** WebSocket JSON-RPC 2.0 is a new transport — genuine architectural change.
| # | Title |
|---|-------|
| [#243](https://github.com/TheGrandWazoo/freenas-proxmox/issues/243) | WebSocket JSON-RPC 2.0 API support (TrueNAS SCALE 25.04+) |
---
## Process Rules
- Every business decision (defer, hold, change scope) is recorded here **and** as a comment on the relevant GitHub issue — not only in conversation or AI memory.
- ADRs (in `.claude/cos/adrs/`) capture architectural decisions.
- This file captures timing, business rationale, and deferred actions.

622
docs/architecture.md Normal file
View File

@ -0,0 +1,622 @@
# freenas-proxmox v3.0 — Architecture & Developer Guide
## Table of Contents
1. [Architecture Overview](#1-architecture-overview)
2. [Per-VM iSCSI Target Model](#2-per-vm-iscsi-target-model)
3. [The `iscsi://` Path Model](#3-the-iscsi-path-model)
4. [API Flow — TrueNAS REST v2.0](#4-api-flow--truenas-rest-v20)
5. [alloc_image Deep Dive](#5-alloc_image-deep-dive)
6. [free_image Deep Dive](#6-free_image-deep-dive)
7. [Build Pipeline and Version/Channel Routing](#7-build-pipeline-and-versionchannel-routing)
8. [Debugging Guide](#8-debugging-guide)
9. [Contributing](#9-contributing)
---
## 1. Architecture Overview
### Where This Plugin Sits
Proxmox VE organizes storage backends through a plugin registry. Every storage type — directory, LVM, Ceph, ZFS-over-iSCSI — is a Perl module that subclasses `PVE::Storage::Plugin`. The `pvedaemon` process loads all registered storage plugins at startup and delegates storage operations (create volume, delete volume, list volumes, etc.) to the correct plugin based on the `type` field in `/etc/pve/storage.cfg`.
`PVE::Storage::Custom::TrueNAS` is loaded by PVE's auto-discovery mechanism. Any module installed under `/usr/share/perl5/PVE/Storage/Custom/` is automatically registered as a custom storage type. No patching of PVE's core `ZFSPlugin.pm`, `pvemanagerlib.js`, or `apidoc.js` is required — this is the fundamental architectural change from v2.x.
```
Proxmox VE (pvedaemon)
└─ PVE::Storage # core storage dispatch
└─ PVE::Storage::Custom::TrueNAS # this plugin
└─ TrueNAS REST API v2.0 # all state lives here
├─ /pool/dataset # zvol create/delete/resize
└─ /iscsi/* # extent, target, targetextent CRUD
```
### What the Plugin Manages
The plugin manages the entire lifecycle of iSCSI-backed block devices on behalf of Proxmox VE:
- **ZFS volumes (zvols)** — the actual block storage, created and destroyed via `POST /pool/dataset` and `DELETE /pool/dataset/id/{id}`
- **iSCSI extents** — TrueNAS's representation of a device to export over iSCSI, one per zvol
- **iSCSI targets** — the access point a client connects to, one per VM (`proxmox-vm-<vmid>`)
- **targetextent mappings** — the join record linking an extent to a target at a specific LUN ID
The plugin does NOT manage the iSCSI initiator side. QEMU's built-in libiscsi opens the `iscsi://` URI returned by `path()` directly, without any host-side session management via `iscsiadm`.
### What the Plugin Does Not Do
- **Pool name auto-discovery**: The user types the TrueNAS pool name directly in the Add Storage dialog. The plugin does not enumerate available pools. Pool capacity is returned via the TrueNAS API (`GET /pool/dataset`), not SSH.
- **Snapshots on PVE 8.x**: The `volume_snapshot` family of methods is defined but will return an unsupported error on PVE 8. PVE 9.0's Snapshot-as-Volume-Chains feature is the target integration point (v3.1.0, ADR-008).
- **TPM state disks**: swtpm (the virtual TPM backend) cannot use `iscsi://` URIs; it requires a local filesystem path. TPM disks must be placed on a different storage type.
### Installed File Locations
| File | Destination on Proxmox host |
|---|---|
| `perl5/PVE/Storage/Custom/TrueNAS.pm` | `/usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm` |
| `ui/truenas-storage.js` | `/usr/share/pve-manager/js/truenas-storage.js` |
| One `<script>` tag | Injected into `/usr/share/pve-manager/index.html.tpl` |
`postinst` copies both files, injects the script tag into `index.html.tpl` (idempotent — skipped if already present), and restarts `pvedaemon` and `pveproxy`.
---
## 2. Per-VM iSCSI Target Model
### Why Per-VM Targets
The v2.x approach used a single shared iSCSI target for all VMs. Every disk from every VM was a different LUN on the same target IQN. This created several problems:
- Deleting one disk required stopping the iSCSI service (`force=true` on extent delete does not work cleanly when multiple active sessions are on the same target)
- Live migration of a single disk while others on the same target are active caused session disruption
- LUN exhaustion — TrueNAS has a practical limit of 256 LUNs per target
In v3.0, each VM gets its own iSCSI target: `proxmox-vm-<vmid>`. Disks belonging to that VM become LUN 0, LUN 1, LUN 2, etc. on that target.
### The TrueNAS Object Graph
For a VM with `vmid=100` and two disks, TrueNAS holds:
```
ZFS pool "tank"
└── tank/proxmox/vdisks/
├── vm-100-disk-0 (zvol, 32 GiB)
└── vm-100-disk-1 (zvol, 64 GiB)
iSCSI extents
├── vm-100-disk-0 (type=DISK, disk=zvol/tank/proxmox/vdisks/vm-100-disk-0)
└── vm-100-disk-1 (type=DISK, disk=zvol/tank/proxmox/vdisks/vm-100-disk-1)
iSCSI target
└── proxmox-vm-100 (IQN: iqn.2005-10.org.freenas.ctl:proxmox-vm-100)
targetextent mappings
├── target=proxmox-vm-100, extent=vm-100-disk-0, lunid=0
└── target=proxmox-vm-100, extent=vm-100-disk-1, lunid=1
```
### Target Lifecycle
**Creation**: `_resolve_vm_target($scfg, $vmid)` is called on the first `alloc_image` for a VM. It queries `GET /iscsi/target`, looks for an entry with `name eq "proxmox-vm-$vmid"`, and creates one via `POST /iscsi/target` if it does not exist. The new target is assigned the same portal group and initiator group settings as an existing reference target (the `truenas_target` config value, or any existing target on the configured portal IP, as a fallback).
**Portal group discovery** (`_portal_groups_for_new_target`): Queries `GET /iscsi/portal` and finds all portals listening on the configured `truenas_portal_ip` (or `0.0.0.0`/`::`). Then queries `GET /iscsi/target` and finds the groups from the base target that reference those portal IDs. These groups are cloned for the new per-VM target, preserving `portal`, `initiator`, `authmethod`, and `auth` settings.
**Deletion**: `_maybe_cleanup_vm_target($scfg, $vmid, $target_id)` is called after each disk is removed. It queries `GET /iscsi/targetextent?target=<id>` to check for remaining extents. If the target is now empty, it is deleted via `DELETE /iscsi/target/id/<id>`. This keeps TrueNAS clean — no accumulation of empty targets.
**Runtime caching**: Both the target lookup and the global iSCSI config (`GET /iscsi/global`) are cached in the module-level `$state` hashref, keyed by `truenas_host`. `deactivate_storage` clears the entire cache for a host.
---
## 3. The `iscsi://` Path Model
### How QEMU Connects
The `path()` method returns a URI of the form:
```
iscsi://<portal_ip>/<iqn>/<lun_id>
```
Example:
```
iscsi://192.168.1.50/iqn.2005-10.org.freenas.ctl:proxmox-vm-100/0
```
QEMU implements libiscsi internally. When it receives an `iscsi://` path, it opens an iSCSI session directly from the QEMU process to the TrueNAS portal. No `iscsiadm` command is run, no kernel iSCSI session is created, and no `/dev/sdX` or `/dev/disk/by-path/` device node appears on the Proxmox host.
### How `path()` Works Internally
```perl
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
my $ext = _find_extent($scfg, $volname);
my $t = _api($scfg, 'GET', "/iscsi/target/id/$ext->{target_id}");
my $iqn = _basename($scfg) . ":$t->{name}";
my $portal = _portal($scfg);
return ("iscsi://$portal/$iqn/$ext->{lun_id}", $vmid, $vtype);
}
```
`_find_extent` calls `GET /iscsi/extent` to locate the extent by name, then `GET /iscsi/targetextent?extent=<id>` to find the LUN ID and target association. The IQN is assembled from the cached global basename (`GET /iscsi/global` → `basename` field) and the target name.
### activate_volume and deactivate_volume
Because QEMU manages the connection itself, `activate_volume` only verifies that the extent exists (catching configuration errors early) and logs readiness. It does not call `iscsiadm login` or any iSCSI session management command. `deactivate_volume` is a no-op.
### Constraints of the iscsi:// Model
- **No multipath**: libiscsi in QEMU does not do multipath. If the portal is unreachable, QEMU retries but does not fail over to a second portal. Multi-portal TrueNAS setups are not effective with this model (tracked for v3.1.0, #256).
- **TPM disks excluded**: `swtpm` cannot use `iscsi://` URIs. Any disk assigned to a VM's TPM must be on a different storage type (e.g., local-lvm, directory).
- **No host-visible device**: Because there is no kernel iSCSI session, tools like `lsscsi`, `iscsiadm --mode session`, or `ls /dev/disk/by-path` will not show the disk on the Proxmox host. This is expected and correct.
---
## 4. API Flow — TrueNAS REST v2.0
### Authentication
All requests use `Authorization: Bearer <token>` with the API key stored in `truenas_api_key` in storage.cfg. Basic auth was removed in v3.0 (ADR-005). The API key is generated in TrueNAS under:
- SCALE: System Settings → API Keys → Add
- CORE 13: gear icon (top right) → API Keys → Add
### Transport
The plugin uses `LWP::UserAgent` with a 30-second timeout. A single `LWP::UserAgent` instance is created per `truenas_host` and cached in `$state->{$host}{ua}`. SSL verification is off by default (`truenas_ssl_verify => 0`) to accommodate self-signed certificates. When `truenas_ssl_verify` is false, `ssl_opts(verify_hostname => 0, SSL_verify_mode => 0)` is applied to the UA.
### The `_api` Function
All TrueNAS calls go through `_api($scfg, $method, $path, $data)`:
```perl
sub _api {
my ($scfg, $method, $path, $data) = @_;
my $url = "$scheme://$scfg->{truenas_host}/api/v2.0$path";
my $req = HTTP::Request->new(uc($method) => $url);
$req->header('Authorization' => "Bearer $scfg->{truenas_api_key}");
$req->content(encode_json($data)) if defined $data;
my $res = _ua($scfg)->request($req);
die "TrueNAS API $method $path: " . $res->status_line unless $res->is_success;
return decode_json($res->content);
}
```
On HTTP error, the function extracts the `message` field from the JSON response body, logs it via `syslog('err', ...)`, and dies with a descriptive string. HTTP 204 (No Content) responses — returned by most DELETE calls — return `undef` cleanly.
### Endpoints Used
| Operation | Method | Endpoint |
|---|---|---|
| Global iSCSI config | GET | `/iscsi/global` |
| List portals | GET | `/iscsi/portal` |
| List targets | GET | `/iscsi/target` |
| Get target by ID | GET | `/iscsi/target/id/{id}` |
| Create target | POST | `/iscsi/target` |
| Delete target | DELETE | `/iscsi/target/id/{id}` |
| List extents | GET | `/iscsi/extent` |
| Create extent | POST | `/iscsi/extent` |
| Delete extent | DELETE | `/iscsi/extent/id/{id}` |
| List targetextents (by target) | GET | `/iscsi/targetextent?target={id}` |
| List targetextents (by extent) | GET | `/iscsi/targetextent?extent={id}` |
| Create targetextent | POST | `/iscsi/targetextent` |
| Delete targetextent | DELETE | `/iscsi/targetextent/id/{id}` |
| Reload iSCSI service | POST | `/service/reload` |
| Get pool dataset | GET | `/pool/dataset?id={pool}` |
| Get zvol by ID | GET | `/pool/dataset/id/{encoded_path}` |
| List all zvols | GET | `/pool/dataset?type=VOLUME` |
| Create zvol | POST | `/pool/dataset` |
| Delete zvol | DELETE | `/pool/dataset/id/{encoded_path}` |
### URL Encoding for Dataset Paths
TrueNAS uses the full ZFS dataset path as the resource identifier in DELETE and GET by-ID calls. Forward slashes must be URL-encoded. The plugin uses `URI::Escape::uri_escape($path, "^A-Za-z0-9\\-_.~")` which encodes `/` as `%2F`:
```perl
my $enc = uri_escape("tank/proxmox/vdisks/vm-100-disk-0", "^A-Za-z0-9\\-_.~");
# Result: tank%2Fproxmox%2Fvdisks%2Fvm-100-disk-0
_api($scfg, 'DELETE', "/pool/dataset/id/$enc", { recursive => JSON::true });
```
### iSCSI Service Reload
After creating a new extent and targetextent, the plugin calls:
```perl
_api($scfg, 'POST', '/service/reload', { service => 'iscsitarget' });
```
This signals TrueNAS to reload its iSCSI configuration so the new LUN is visible to connecting initiators. The reload is non-fatal — if it fails, a warning is logged but `alloc_image` still succeeds.
---
## 5. alloc_image Deep Dive
`alloc_image($class, $storeid, $scfg, $vmid, $fmt, $name, $size_kb)` is called by PVE when a new virtual disk is created.
### Step 1: Name Resolution
If `$name` is not supplied, `find_free_diskname` (base-class method) generates `vm-<vmid>-disk-<N>` where `N` is the lowest unused integer. Only `raw` format is accepted.
### Step 2: Zvol Creation
```perl
_api($scfg, 'POST', '/pool/dataset', {
name => "$prefix/$name", # e.g. tank/proxmox/vdisks/vm-100-disk-0
type => 'VOLUME',
volsize => $size_kb * 1024, # API takes bytes
sparse => JSON::true,
});
```
`$prefix` is assembled by `_zvol_prefix`: `truenas_pool` + optionally `/ truenas_dataset`. Sparse provisioning is always on.
### Step 3: iSCSI Extent Creation
```perl
_api($scfg, 'POST', '/iscsi/extent', {
name => $name, # e.g. vm-100-disk-0
type => 'DISK',
disk => "zvol/$prefix/$name", # zvol/ prefix required by TrueNAS
ro => JSON::false,
});
```
The extent name matches the volume name exactly. This 1:1 naming is load-bearing — `_find_extent` searches extents by name.
### Step 4: Per-VM Target Resolution
`_resolve_vm_target($scfg, $vmid)` returns `{ id, iqn }`. If the target does not yet exist, it is created here. LUN ID assignment (`_next_lun_id`): queries `GET /iscsi/targetextent?target=<id>` and returns the lowest unused non-negative integer.
### Step 5: targetextent Creation
```perl
_api($scfg, 'POST', '/iscsi/targetextent', {
target => $target_id,
extent => $extent_id,
lunid => $lun_id,
});
```
### Step 6: Service Reload and Return
`_reload_iscsi` triggers the TrueNAS iSCSI service reload. The function returns `$name`, which PVE stores in the VM config.
### Rollback on Failure
The current implementation does not explicitly roll back a partially-created extent if targetextent creation fails — the extent and zvol remain on TrueNAS. ADR-004 documents the intended `eval {}` rollback pattern. This is tracked as a known gap in issue #250.
---
## 6. free_image Deep Dive
`free_image($class, $storeid, $scfg, $volname, $isBase)` is called when a disk is deleted. The deletion order is deliberately sequenced to avoid dangling references.
### Step 1: Locate the Extent
`_find_extent($scfg, $volname)` calls `GET /iscsi/extent` (by name) then `GET /iscsi/targetextent?extent=<id>`. Returns `{ extent_id, targetextent_id, lun_id, target_id }`. If no extent exists, a warning is logged and extent removal is skipped.
### Step 2: Unmap the targetextent
```perl
_api($scfg, 'DELETE', "/iscsi/targetextent/id/$ext->{targetextent_id}");
```
This must happen before the extent DELETE. Removing the targetextent severs this LUN's association without affecting the target session or other LUNs on the same target — critical for the live-migration case.
### Step 3: Delete the Extent
```perl
_api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}", { force => JSON::true });
```
`force=true` is required to delete an extent that may still have a residual QEMU libiscsi session open (e.g., detached disk on a running VM).
### Step 4: Clean Up the Per-VM Target if Empty
`_maybe_cleanup_vm_target($scfg, $vmid, $target_id)` queries `GET /iscsi/targetextent?target=<id>` after the targetextent was removed. If no mappings remain, `DELETE /iscsi/target/id/<id>` removes the empty target.
### Step 5: Delete the Zvol
```perl
my $enc = uri_escape($zvol, "^A-Za-z0-9\\-_.~");
_api($scfg, 'DELETE', "/pool/dataset/id/$enc", { recursive => JSON::true });
```
`recursive=true` handles any ZFS snapshots under the zvol. Zvol deletion is last — if it fails, the error propagates after extent and targetextent are already cleaned up.
---
## 7. Build Pipeline and Version/Channel Routing
### Single-Repo Architecture (ADR-001)
All build and packaging logic lives in `.github/workflows/build.yml`. The old two-repo dispatch approach (this repo → `freenas-proxmox-packer`) was eliminated. Files are embedded in the `.deb` at build time; no `git clone` or `patch` runs at install time.
### Four-Job Pipeline
```
lint → build → security → publish
```
**Job 1: Lint**
- `perl -c -I/tmp/pve-stub perl5/PVE/Storage/Custom/TrueNAS.pm` — syntax check with PVE module stubs
- `perlcritic --profile .perlcriticrc` — static analysis at severity 4
- `shellcheck --severity=warning packaging/DEBIAN/postinst packaging/DEBIAN/postrm`
**Job 2: Build**
- Reads `our $VERSION` from `TrueNAS.pm` as the base version (ADR-006)
- Resolves version string and target channel based on `$GITHUB_REF`
- Generates `dist/DEBIAN/control` from `packaging/DEBIAN/control.j2` via `sed`
- Runs `dpkg-deb -Zgzip --build dist <deb_file>`
- Uploads the `.deb` as a workflow artifact (30-day retention)
**Job 3: Security**
- Trivy filesystem scan (secrets + misconfig) on the repository
- Trivy scan of extracted `.deb` contents (vuln + secret)
- Both scans use `exit-code: 1` — HIGH or CRITICAL findings fail the pipeline
**Job 4: Publish**
- Skipped for PRs and branches with `channel=none`
- Cloudsmith `deb` push to the resolved repo (parallel channel, transitional)
- GitHub Pages apt repo update: `.deb` added to `pool/v{major}/`, `Packages.gz` and signed `InRelease` regenerated for the relevant dist tracks, committed to `gh-pages` branch
- On tagged releases: creates a draft GitHub Release with the `.deb` attached
### Version String Logic
| Git ref | Debian version string | Channel | Cloudsmith repo | GitHub Pages dist |
|---|---|---|---|---|
| `refs/tags/v3.0.0` | `3.0.0-1` | stable | `truenas-proxmox` | `v3` |
| `refs/heads/master` or `refs/heads/release/3.x` | `3.0.0~beta+abc1234` | testing | `truenas-proxmox-testing` | — |
| `refs/heads/release/*` (other) | `3.0.0~alpha+abc1234` | development | `truenas-proxmox-snapshots` | — |
| feature branches, PRs | `3.0.0~dev+abc1234` | none | not published |
The tilde (`~`) in the Debian version sorts below the base version in `dpkg`, guaranteeing pre-release builds never auto-upgrade over a stable release.
### Running the Build Locally
```bash
# Install dependencies
sudo apt-get install -y libperl-critic-perl shellcheck libwww-perl libjson-perl liburi-perl
# Create PVE stubs
mkdir -p /tmp/pve-stub/PVE/Storage
printf 'package PVE::SafeSyslog;\nuse Exporter "import";\nour @EXPORT = qw(syslog);\nsub syslog {}\n1;\n' \
> /tmp/pve-stub/PVE/SafeSyslog.pm
printf 'package PVE::Tools;\nuse Exporter "import";\nour @EXPORT_OK = qw(run_command);\nsub run_command {}\n1;\n' \
> /tmp/pve-stub/PVE/Tools.pm
printf 'package PVE::Storage::Plugin;\nsub new {}\nsub register {}\nsub lookup_types { [] }\nsub properties { {} }\nsub options { {} }\n1;\n' \
> /tmp/pve-stub/PVE/Storage/Plugin.pm
# Syntax check + lint
perl -c -I/tmp/pve-stub perl5/PVE/Storage/Custom/TrueNAS.pm
perlcritic --profile .perlcriticrc perl5/PVE/Storage/Custom/TrueNAS.pm
shellcheck --severity=warning packaging/DEBIAN/postinst packaging/DEBIAN/postrm
# Build the package
VERSION="3.0.0-dev"
mkdir -p dist/DEBIAN dist/usr/share/freenas-proxmox
sed "s/\${VERSION}/$VERSION/" packaging/DEBIAN/control.j2 > dist/DEBIAN/control
cp packaging/DEBIAN/postinst packaging/DEBIAN/postrm dist/DEBIAN/
chmod 0755 dist/DEBIAN/postinst dist/DEBIAN/postrm
cp perl5/PVE/Storage/Custom/TrueNAS.pm dist/usr/share/freenas-proxmox/TrueNAS.pm
cp ui/truenas-storage.js dist/usr/share/freenas-proxmox/truenas-storage.js
sudo dpkg-deb -Zgzip --build dist freenas-proxmox_${VERSION}_all.deb
```
---
## 8. Debugging Guide
### Layer Model
Errors fall into one of four distinct layers:
```
PVE task / VM operation
└─ Plugin (TrueNAS.pm) — syslog "TrueNASPlugin: ..."
└─ TrueNAS REST API — HTTP errors, JSON responses
└─ iSCSI / QEMU libiscsi — QEMU logs, dmesg
└─ ZFS — TrueNAS ZFS pool logs
```
### Plugin Logs (Layer 1)
```bash
# All plugin messages
grep -i 'TrueNASPlugin' /var/log/syslog
# Follow live
tail -f /var/log/syslog | grep -i TrueNAS
```
Log levels: `info` (normal operation), `warning` (non-fatal issues), `err` (fatal — operation dies).
Example sequence for a successful `alloc_image`:
```
TrueNASPlugin: alloc_image: creating zvol tank/proxmox/vdisks/vm-100-disk-0 (34359738368 bytes) for VM 100
TrueNASPlugin: Created per-VM target: iqn.2005-10.org.freenas.ctl:proxmox-vm-100 (id=42)
TrueNASPlugin: alloc_image: vm-100-disk-0 ready at lun 0 on iqn.2005-10.org.freenas.ctl:proxmox-vm-100
```
### PVE Task Logs (Layer 1)
```bash
# Recent storage tasks
pvesh get /nodes/<nodename>/tasks --limit 50 | grep -i storage
# Specific task log
pvesh get /nodes/<nodename>/tasks/<upid>/log
# VM start failures
tail -f /var/log/pve/qemu-server/<vmid>.log
```
### TrueNAS API Errors (Layer 2)
Common errors and causes:
| Error | Likely cause |
|---|---|
| `401 Unauthorized` | API key missing, revoked, or wrong |
| `404 Not Found` on extent DELETE | Extent already deleted or never created |
| `422 Unprocessable Entity` | Payload validation error — check TrueNAS system log for detail |
| `500 Internal Server Error` | TrueNAS-side error — check TrueNAS system log |
| Connection refused / timeout | TrueNAS host unreachable; check `truenas_host` or `truenas_portal_ip` |
**Probe the API manually:**
```bash
# Test connectivity and auth
curl -sk -H "Authorization: Bearer <token>" \
https://<truenas_host>/api/v2.0/iscsi/global | python3 -m json.tool
# List all extents
curl -sk -H "Authorization: Bearer <token>" \
https://<truenas_host>/api/v2.0/iscsi/extent | python3 -m json.tool
# List all targetextents
curl -sk -H "Authorization: Bearer <token>" \
https://<truenas_host>/api/v2.0/iscsi/targetextent | python3 -m json.tool
```
### Dangling Resources (Layer 2)
If `alloc_image` fails partway through, orphaned extents may remain on TrueNAS. Identify unmapped extents:
```bash
# Get all extent IDs
curl -sk -H "Authorization: Bearer $TOKEN" \
https://<truenas>/api/v2.0/iscsi/extent | python3 -c \
"import json,sys; [print(e['id'],e['name']) for e in json.load(sys.stdin)]"
# Get all mapped extent IDs
curl -sk -H "Authorization: Bearer $TOKEN" \
https://<truenas>/api/v2.0/iscsi/targetextent | python3 -c \
"import json,sys; [print(te['extent']) for te in json.load(sys.stdin)]"
```
Manual cleanup of a dangling extent:
```bash
curl -sk -X DELETE -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"force": true}' \
https://<truenas>/api/v2.0/iscsi/extent/id/<id>
```
### iSCSI / QEMU Connection Issues (Layer 3)
```bash
tail -f /var/log/pve/qemu-server/<vmid>.log
```
Common libiscsi errors:
| Error | Meaning |
|---|---|
| `Failed to connect to iSCSI portal` | Portal IP wrong, TrueNAS iSCSI service not running, or firewall |
| `Initiator has no access to this target` | Initiator group does not include the Proxmox node's IQN |
| `LUN not found` | Extent not mapped, or iSCSI service not reloaded after extent creation |
| `Target not found` | IQN mismatch — check `_basename()` vs TrueNAS's actual global basename |
Check the Proxmox node's initiator IQN:
```bash
cat /etc/iscsi/initiatorname.iscsi
```
### Plugin Not Loading
```bash
grep -i 'TrueNAS\|Storage::Custom\|Can.t locate' /var/log/syslog
# Verify syntax directly
perl -c /usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm
# Check required Perl modules
dpkg -l libwww-perl libjson-perl liburi-perl libio-socket-ssl-perl
```
### Install / Removal Log
```bash
cat /var/log/freenas-proxmox-install.log
```
---
## 9. Contributing
### Quick Iteration on a Live Proxmox Node
For changes to `TrueNAS.pm` only:
```bash
scp perl5/PVE/Storage/Custom/TrueNAS.pm \
root@<proxmox-node>:/usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm
ssh root@<proxmox-node> "pvedaemon restart && pveproxy restart"
```
For UI changes (`truenas-storage.js`):
```bash
scp ui/truenas-storage.js \
root@<proxmox-node>:/usr/share/pve-manager/js/truenas-storage.js
# Hard-refresh browser (Ctrl+Shift+R) — no service restart needed
```
### Coding Standards
**Perl:**
- `use strict` and `use warnings` — no exceptions
- All TrueNAS API calls through `_api()` — never raw `LWP::UserAgent` outside that function
- Multi-step operations must use `eval {}` rollback: if step N fails, undo steps 1 through N-1 in reverse before dying (ADR-004)
- `JSON::true` / `JSON::false` for boolean JSON fields
**Shell (`postinst`/`postrm`):**
- `set -e` at top
- All output via the `log()` function (writes to `$LOG_FILE`)
- Operations must be idempotent — check before acting
- `shellcheck --severity=warning` must pass
### Branch and PR Workflow
1. Open a GitHub issue before writing code
2. Branch from `master`: `git checkout -b feature/short-description`
3. Run lint locally before pushing
4. Open a PR against `master` — CI runs lint → build → security automatically
5. Close the issue with commit SHA and version when the change ships
### Architecture Decision Records
Major design decisions live in `.claude/cos/adrs/`. Check before making changes that affect the plugin's architecture, auth model, build pipeline, or supported platform matrix.
| ADR | Decision |
|---|---|
| ADR-001 | Consolidate build pipeline into this repo; no install-time git clone |
| ADR-002 | Full `PVE::Storage::Custom` plugin; no ZFSPlugin.pm or pvemanagerlib.js patches |
| ADR-003 | Superseded by ADR-010 |
| ADR-010 | GitHub Pages apt repo with per-major-version dist tracks (`v3`, `main`/`v2` for backward compat) |
| ADR-004 | `eval {}` rollback pattern for all multi-step TrueNAS operations |
| ADR-005 | Bearer token (API key) as primary auth; basic auth removed in v3.0 |
| ADR-006 | `$VERSION` in `TrueNAS.pm` is the single source of truth for package versioning |
| ADR-007 | v2.x keeps `FreeNAS` naming; v3.x uses `TrueNAS` everywhere |
| ADR-008 | v3.0 supports PVE 8+ (core); PVE 9+ snapshot interface targeted for v3.1.0 |
---
## Key File Reference
| File | Purpose |
|---|---|
| [perl5/PVE/Storage/Custom/TrueNAS.pm](../perl5/PVE/Storage/Custom/TrueNAS.pm) | Main plugin — all storage operations |
| [ui/truenas-storage.js](../ui/truenas-storage.js) | Ext.js UI panel for the `truenas` storage type |
| [packaging/DEBIAN/postinst](../packaging/DEBIAN/postinst) | Install script — copies files, injects script tag, restarts PVE |
| [packaging/DEBIAN/postrm](../packaging/DEBIAN/postrm) | Removal script — reverses postinst changes |
| [packaging/DEBIAN/control.j2](../packaging/DEBIAN/control.j2) | Debian package control file template |
| [.github/workflows/build.yml](../.github/workflows/build.yml) | Full CI/CD pipeline |
| [.claude/cos/adrs/](../.claude/cos/adrs/) | Architecture Decision Records |

329
docs/getting-started.md Normal file
View File

@ -0,0 +1,329 @@
# Getting Started with freenas-proxmox v3.0
## 1. What This Plugin Does
Proxmox VE is a hypervisor — it runs your virtual machines. Those VMs need somewhere to store their disk images. If you have a TrueNAS storage server on your network, you might want to store those disks on TrueNAS so they live on dedicated, reliable, ZFS-managed storage instead of inside the Proxmox host itself. This plugin makes that possible. It registers a new storage type called **TrueNAS (ZFS/iSCSI)** inside Proxmox. When you add a VM disk to that storage, Proxmox talks to your TrueNAS server through its REST API, creates a ZFS volume on TrueNAS, exposes it as an iSCSI block device, and gives your VM a direct high-speed path to it — all automatically. You do not need to touch the TrueNAS UI every time you create or delete a VM disk.
---
## 2. Prerequisites
Complete all of these before installing the plugin.
### 2.1 Proxmox VE Version
Your Proxmox VE node (or cluster) must be running **PVE 8.x or PVE 9.x**. Check with:
```bash
pveversion
```
If the output shows `pve-manager/7.x`, stay on plugin v2.x — v3.0 does not support PVE 7.
### 2.2 TrueNAS Version
| TrueNAS product | Minimum version |
|:----------------|:----------------|
| TrueNAS SCALE | Electric Eel (24.10) or later |
| TrueNAS CORE | 13.0-U6 or later |
### 2.3 iSCSI Service on TrueNAS
The plugin creates per-VM iSCSI targets for you automatically. It does **not** create the iSCSI service infrastructure — you must set that up once.
**On TrueNAS SCALE:**
1. Go to **Shares → iSCSI → Configure**
2. Under **Portals**, add a portal that listens on the IP address your Proxmox nodes use to reach TrueNAS (or `0.0.0.0` to listen on all interfaces). Note the portal's IP address — you will need it.
3. Under **Initiators**, add an initiator group. You can leave it open (allow all) for initial setup; lock it down to specific Proxmox node IPs later.
4. Enable the iSCSI service and confirm it is running.
**On TrueNAS CORE:**
1. Go to **Sharing → Block Shares (iSCSI)**
2. Under **Portals**, add a portal (same as above)
3. Under **Initiators**, add an initiator group
4. Go to **Services** and enable **iSCSI**
You do **not** need to create any targets or extents by hand — the plugin creates those automatically when you allocate VM disks.
### 2.4 TrueNAS API Key
The plugin authenticates with TrueNAS using an API key (Bearer token). No username or password is used.
**On TrueNAS SCALE:**
Go to **System Settings → API Keys → Add**. Give it a descriptive name such as `proxmox-plugin`. Copy the key — it is shown only once.
**On TrueNAS CORE:**
Click the gear icon in the top-right corner → **API Keys → Add**. Copy the key.
Store the key somewhere safe temporarily. You will paste it into the Proxmox UI during storage configuration.
### 2.5 Network Reachability
- Every Proxmox node must be able to reach the TrueNAS host on **TCP 443** (HTTPS API) or **TCP 80** (HTTP, not recommended)
- Every Proxmox node must be able to reach the iSCSI portal on **TCP 3260**
No SSH keys are required. No pre-created iSCSI targets are required.
### 2.6 What You Cannot Store on This Storage
TPM state disks (`tpmstate0`) cannot live on this storage type. If you plan to create VMs with Secure Boot / TPM enabled, configure a separate local-lvm or NFS storage for those disks. All other disk types (virtio, scsi, IDE, EFI) work normally.
---
## 3. Installation
Run all of these commands on **each Proxmox node** in your cluster, or on your single Proxmox host. Run them as root.
### Step 1 — Import the GPG Signing Key
```bash
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
```
### Step 2 — Add the Package Repository
```bash
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main" \
| tee /etc/apt/sources.list.d/truenas-proxmox.list
```
### Step 3 — Install the Package
```bash
apt update && apt install truenas-proxmox
```
The installer:
- Copies `TrueNAS.pm` to `/usr/share/perl5/PVE/Storage/Custom/`
- Copies `truenas-storage.js` to `/usr/share/pve-manager/js/`
- Adds one `<script>` tag to `/usr/share/pve-manager/index.html.tpl` so the UI loads the new panel
- Restarts `pvedaemon` and `pveproxy`
It does **not** patch any PVE system files.
### Step 4 — Refresh Your Browser
Open (or reload) the Proxmox web UI. Do a hard refresh: **Ctrl+Shift+R** (Windows/Linux) or **Cmd+Shift+R** (Mac). The TrueNAS storage type will not appear until the browser picks up the new JavaScript.
---
## 4. First Storage Configuration in the Proxmox UI
In the Proxmox web UI:
1. Click **Datacenter** in the left tree
2. Click **Storage** in the top tabs
3. Click **Add**
4. Choose **TrueNAS (ZFS/iSCSI)** from the dropdown
If **TrueNAS (ZFS/iSCSI)** does not appear in the list, see [section 6.1](#61-truenas-zfsiscsi-does-not-appear-in-the-add-storage-dropdown) below.
### Field Reference
Fill in the form using the table below. Fields not listed can be left at their defaults.
| Field | What to enter | Notes |
|-------|---------------|-------|
| **ID** | A short name, e.g. `truenas-vms` | How Proxmox refers to the storage internally. Letters, numbers, and hyphens only. Cannot be changed later. |
| **TrueNAS Host** | IP address or hostname of your TrueNAS server | Example: `192.168.10.50` or `truenas.local`. Also used as the iSCSI portal address unless you set Portal IP separately. |
| **API Key** | Paste the API key you generated in step 2.4 | Treated as a password — hidden by default. Click the eye icon to show it while pasting. |
| **Pool / Dataset Path** | The ZFS pool or dataset path where VM disks should be created | Use `tank` for the pool root, or `tank/proxmox/vdisks` for a specific dataset. |
| **Use SSL** | Leave checked (on) | Recommended. Turn off only if TrueNAS has no HTTPS configured. |
| **Verify SSL Certificate** | Leave unchecked | Turn on only if TrueNAS has a valid CA-signed certificate. Most homelab setups use self-signed certs — leave this off. |
| **Shared** | Leave checked (on) | Required for clusters. Tells Proxmox all nodes can access this storage. |
| **Portal IP** | Optional — leave blank | If your TrueNAS management interface and iSCSI data interface are on different IPs, put the iSCSI data IP here. Otherwise the TrueNAS Host IP is used for both. |
| **Target IQN** | Optional — leave blank | The plugin auto-discovers the portal and initiator group from existing iSCSI targets. Leave blank unless you want to force it to copy settings from a specific target. |
Click **Add**. Proxmox contacts TrueNAS and confirms the pool is reachable. If it fails, check the error message — see [section 6](#6-common-first-run-problems) for common causes.
### 4.1 Securing the API Token (Recommended for Production)
By default the API token is stored in `/etc/pve/storage.cfg`, which is replicated in plaintext across all cluster nodes via the PVE cluster filesystem. For production deployments, move the token into a private keyfile that only root can read and that is not replicated.
Run these commands **on each Proxmox node** after adding the storage:
```bash
# Replace 'truenas-vms' with your actual storage ID
STORAGEID="truenas-vms"
KEYFILE="/etc/pve/priv/truenas-${STORAGEID}.key"
# Write the token to the keyfile (replace the value with your actual token)
echo -n "your-api-token-here" > "$KEYFILE"
chmod 600 "$KEYFILE"
# Remove the token from storage.cfg now that the keyfile is in place
pvesm set "$STORAGEID" --truenas_api_key ""
```
The plugin automatically checks `/etc/pve/priv/truenas-<storeid>.key` at startup. If the file exists, it is used and `truenas_api_key` in storage.cfg is ignored.
> **Note:** The keyfile must exist on **every Proxmox node** in your cluster. The `/etc/pve/priv/` directory is not replicated via pmxcfs — copy the file to each node manually (use `scp` or your configuration management tool).
---
## 5. Creating Your First VM Disk on TrueNAS Storage
### During VM Creation
1. In the Proxmox UI, click **Create VM**
2. On the **Disks** tab, change **Storage** from `local-lvm` to the storage ID you just created (e.g., `truenas-vms`)
3. Set the disk size
4. Complete the rest of the VM setup and click **Finish**
Proxmox will:
- Create a ZFS volume (`zvol`) on TrueNAS sized to your request
- Create an iSCSI extent pointing to that zvol
- Create a dedicated iSCSI target named `proxmox-vm-<vmid>` on TrueNAS (e.g., `proxmox-vm-100`)
- Map the extent to the target
- Give QEMU an `iscsi://` path to connect directly
When the VM starts, QEMU opens an iSCSI connection directly to TrueNAS — no kernel session management (`iscsiadm`) is involved.
### Adding a Disk to an Existing VM
1. Select the VM in the left tree
2. Click **Hardware**
3. Click **Add → Hard Disk**
4. Set **Storage** to your TrueNAS storage
5. Set the disk size
6. Click **Add**
### What You Will See on TrueNAS
After creating a VM disk, log into TrueNAS and check:
- **Datasets** (SCALE) or **Storage → Pools** (CORE): a new zvol named `vm-100-disk-0` (or similar) will appear under your configured pool/dataset
- **Shares → iSCSI → Targets**: a target named `proxmox-vm-100` will appear
- **Shares → iSCSI → Extents**: an extent named `vm-100-disk-0` will appear
When you delete the disk from Proxmox, all three are removed automatically. If the deleted disk was the last disk on that VM's target, the target is removed too.
---
## 6. Common First-Run Problems
### 6.1 "TrueNAS (ZFS/iSCSI)" Does Not Appear in the Add Storage Dropdown
**Cause:** Browser has cached the old Proxmox JavaScript.
**Fix:** Hard-refresh the browser: **Ctrl+Shift+R** (Windows/Linux) or **Cmd+Shift+R** (Mac). If it still does not appear, check that the package installed correctly:
```bash
ls /usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm
ls /usr/share/pve-manager/js/truenas-storage.js
```
Both files should exist. If they are missing, re-run `apt install truenas-proxmox`. Also check whether `pvedaemon` and `pveproxy` restarted cleanly:
```bash
systemctl status pvedaemon pveproxy
```
### 6.2 Storage Shows as Unavailable or Add Dialog Returns an Error
**Cause:** The plugin cannot reach the TrueNAS API.
**Check the logs first:**
```bash
journalctl -u pvedaemon --since "10 minutes ago" | grep -i truenas
```
Common specific errors:
| Log message | Fix |
|-------------|-----|
| `TrueNAS API key is not configured` | API key field was left blank; edit the storage and paste the key |
| `401 Unauthorized` | API key is wrong, expired, or revoked; generate a new one in TrueNAS |
| `500 Internal Server Error` or connection refused | SSL mismatch — if TrueNAS uses a self-signed certificate and Use SSL is on, try toggling Verify SSL Certificate off. Or check that TrueNAS is reachable on HTTPS from the Proxmox node |
| `Pool dataset 'tank' not found` | The pool name you typed does not match what TrueNAS has; check under **Storage → Pools** in TrueNAS |
**Quick connectivity test** from the Proxmox node shell:
```bash
# Replace 192.168.10.50 and your-api-key with real values
curl -sk -H "Authorization: Bearer your-api-key" \
https://192.168.10.50/api/v2.0/iscsi/global | python3 -m json.tool
```
If this returns JSON with a `basename` field, the API is working. If it returns an HTML login page or a TLS error, you have a connectivity or SSL problem.
### 6.3 Disk Creation Fails with "No iSCSI Portals Found"
**Cause:** The plugin could not match the TrueNAS Host IP (or Portal IP) to any portal configured in TrueNAS iSCSI settings.
**Fix:** In TrueNAS, go to **Shares → iSCSI → Portals** and check what IP the portal is listening on. Either:
- Set the portal to listen on `0.0.0.0` (all interfaces), or
- In Proxmox, edit the storage and set **Portal IP** to the exact IP the TrueNAS portal is listening on
### 6.4 VM Starts but Disk Is Not Accessible / VM Won't Boot
**Cause:** The iSCSI data path is blocked. Even if the plugin successfully created the disk (API path), the VM's QEMU process must open a separate iSCSI TCP connection to TrueNAS.
**Checks:**
1. Confirm TrueNAS iSCSI service is running
2. From the Proxmox node, test TCP connectivity to the iSCSI port:
```bash
nc -zv 192.168.10.50 3260
```
3. Check that the initiator group in TrueNAS does not block the Proxmox node's IP
4. Check that the per-VM target in TrueNAS (`proxmox-vm-<vmid>`) has the extent associated with it under **iSCSI → Target/Extent**
### 6.5 API Key Stopped Working After Upgrading TrueNAS SCALE to 25.04
TrueNAS SCALE 25.04 may revoke API keys created under certain conditions during upgrade, and enforces HTTPS for API key authentication.
**Fix:**
1. Log into TrueNAS SCALE → **Credentials → API Keys** → generate a new key
2. In Proxmox: **Datacenter → Storage** → select your TrueNAS storage → **Edit**
3. Paste the new API key into the **API Key** field
4. Ensure **Use SSL** is checked
### 6.6 Disk Size Shown in TrueNAS Is Larger Than What I Entered in Proxmox
This is expected and not an error. Proxmox creates disks in GiB (base-2) but TrueNAS reports sizes in GB (base-10). An 80 GiB disk shows as approximately 85.90 GB in TrueNAS. The disk inside your VM is exactly what you asked for.
### 6.7 TPM State Disk Fails to Create on TrueNAS Storage
TPM state (`tpmstate0`) is a known limitation — this storage type cannot hold TPM state disks. When creating a VM with TPM/Secure Boot, configure a different storage (such as `local-lvm` or NFS) for the `tpmstate0` disk. All other disk types work normally.
---
## 7. Uninstalling
Before uninstalling, remove any storage configurations that use this plugin from Proxmox, and migrate any VM disks off TrueNAS storage. Uninstalling while VMs are still using TrueNAS disks will not delete your data, but Proxmox will lose track of those disks.
### Step 1 — Remove Storage Configurations
In the Proxmox UI: **Datacenter → Storage** → select each TrueNAS storage entry → **Remove**.
### Step 2 — Uninstall the Package
```bash
apt remove truenas-proxmox
```
The removal script:
- Deletes `/usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm`
- Deletes `/usr/share/pve-manager/js/truenas-storage.js`
- Removes the `<script>` tag from `/usr/share/pve-manager/index.html.tpl`
- Restarts `pvedaemon` and `pveproxy`
To also remove install logs:
```bash
apt purge truenas-proxmox
```
### Step 3 — Refresh Your Browser
Hard-refresh the browser after uninstalling. The TrueNAS option will disappear from the Add Storage dropdown.
---
## Getting Help
- GitHub Issues: https://github.com/TheGrandWazoo/truenas-proxmox/issues
- When reporting a bug, include log lines from `journalctl -u pvedaemon | grep -i truenas` and the output of `pveversion`

197
docs/migrating-from-v2.md Normal file
View File

@ -0,0 +1,197 @@
# Migrating from v2.x to v3.0
This guide covers moving from the old `freenas-proxmox` v2.x plugin to the new `truenas-proxmox` v3.0 plugin.
v3.0 is a clean break from v2.x — a completely rewritten native Proxmox storage plugin. You cannot upgrade in place. The migration involves installing the new plugin, adding a new storage entry, and moving your VM disks across using Proxmox's built-in Move Disk function. **VMs can stay running throughout.**
---
## What's a zvol?
A **zvol** (ZFS volume) is TrueNAS's name for a dedicated chunk of storage carved out of a pool — think of it as a virtual hard drive sitting inside TrueNAS. Each VM disk in this plugin is its own zvol. When you "move a disk," Proxmox creates a new zvol, copies the data to it, and deletes the old one.
---
## What changed from v2.x
**Authentication:** v2.x used a username and password (or a secret token). v3.0 uses a TrueNAS API key only — username/password is gone.
**iSCSI connection model:** v2.x ran `iscsiadm` on the Proxmox host to create kernel iSCSI sessions. You could see block devices like `/dev/disk/by-path/ip-...` on your Proxmox node. In v3.0, QEMU connects directly to TrueNAS via `iscsi://` — no kernel sessions, no block devices on the host. **This is expected and correct.** If you look in `/dev/` and don't see the disk, that's fine — QEMU holds the connection, not the host.
**Per-VM targets:** v2.x shared iSCSI targets across VMs. v3.0 creates a dedicated iSCSI target for each VM (`proxmox-vm-<vmid>`). This means removing one disk never affects other VMs.
**Package name:** `freenas-proxmox` is now a transitional stub that installs `truenas-proxmox`. Both can coexist during migration.
---
## Pre-migration checklist
Do these checks **before** you start:
```
[ ] All Proxmox nodes running PVE 8.0 or newer
pveversion | grep pve-manager
[ ] TrueNAS reachable from each Proxmox node (API + iSCSI)
curl -k https://<truenas_host>/api/v2.0/system/info
nc -zv <truenas_host> 3260
[ ] iSCSI service enabled on TrueNAS
Sharing → iSCSI → Target Global Configuration: check "Enable"
[ ] TrueNAS pool is healthy (no resilvering, no scrub running)
TrueNAS → Storage → Pools: all pools show "Healthy"
[ ] TrueNAS pool has free space ≥ your largest single disk
(only one copy is needed at a time if you move one disk at a time)
[ ] Check each VM for tpmstate0 disks (see TPM section below)
Proxmox → VM → Hardware: look for tpmstate0
[ ] Back up any VMs you're not comfortable losing (optional but recommended)
```
**Ports required from Proxmox to TrueNAS:**
| Port | Protocol | Used for |
|------|----------|---------|
| 443 | HTTPS | Plugin API calls (REST v2.0) |
| 3260 | TCP | iSCSI disk I/O (QEMU to TrueNAS) |
| 22 | SSH | Not needed — removed in v3.0 |
---
## Check for TPM state disks first
Before you do anything else, check if any of your VMs have a TPM state disk. QEMU requires TPM state on a local filesystem path — it cannot use `iscsi://` URIs. You cannot migrate `tpmstate0` to v3.0 iSCSI storage.
**How to check:** In Proxmox, click each VM → **Hardware** tab. Look for a disk labeled `tpmstate0`. If you see one, you need to move it to `local-lvm` or NFS storage **before** migrating that VM's other disks.
> **Note for HA users:** If the VM is managed by Proxmox HA, `local-lvm` is not suitable for `tpmstate0` (it won't be accessible on other nodes if the VM is restarted). Use NFS or another shared filesystem storage for `tpmstate0` instead.
---
## Step 1 — Install v3.0 on every Proxmox node
Run this on **every node in the cluster** before adding the new storage:
```bash
# Import the GPG key
curl -fsSL https://thegrandwazoo.github.io/freenas-proxmox/public.gpg.key \
| gpg --dearmor \
| tee /etc/apt/keyrings/truenas-proxmox.gpg > /dev/null
# Add the v3 repository track
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v3 main" \
| tee /etc/apt/sources.list.d/truenas-proxmox.list
# Install
apt update && apt install truenas-proxmox
```
Verify the install on each node:
```bash
dpkg -l truenas-proxmox
```
---
## Step 2 — Create an API key on TrueNAS
**TrueNAS SCALE:**
1. Log in to the TrueNAS web UI
2. Go to **System Settings → API Keys**
3. Click **Add**, give it a name (e.g. `proxmox-v3`), click **Generate Key**
4. Copy the key — you will only see it once
**TrueNAS CORE:**
1. Log in to the TrueNAS web UI
2. Click the gear icon (top right) → **API Keys**
3. Click **Add**, give it a name, click **Submit**
4. Copy the key
Keep the key somewhere safe — you will paste it into Proxmox next.
---
## Step 3 — Add the v3.0 storage in Proxmox
> Keep your existing v2.x storage entry active — **do not remove it yet**. Both entries can coexist while you migrate.
1. In the Proxmox web UI, go to **Datacenter → Storage → Add → TrueNAS**
2. Fill in the fields:
| Field | What to enter |
|-------|--------------|
| ID | A new name, e.g. `truenas-v3` — must be different from your v2 storage ID |
| TrueNAS Host | Same IP/hostname as your v2 storage |
| API Key | The key you just created |
| Pool | Same ZFS pool path as before (e.g. `tank/proxmox/disks`) |
| Nodes | Select all cluster nodes |
3. Click **Add**. If the storage turns green and shows free/used space, you are connected.
**What is "Portal IP"?** Leave it blank for a typical setup with one TrueNAS box on one network. Only fill this in if your TrueNAS has separate management and iSCSI data network interfaces and you want iSCSI traffic on a specific IP.
---
## Step 4 — Move your VM disks
For each VM disk, use Proxmox's Move Disk function. VMs can stay powered on. Each move copies the disk data, then cuts over — you may notice a brief I/O pause (typically under a second) at the moment of cutover. For database or latency-sensitive workloads, consider scheduling moves during low-traffic windows.
1. Click the VM → **Hardware** tab
2. Click a disk (e.g. `scsi0`) → **Move Disk** in the top toolbar
3. Set **Target Storage** to `truenas-v3`
4. **Delete source:** if checked, Proxmox will automatically delete the old zvol immediately after the copy finishes and the VM switches to the new disk. If unchecked, both zvols remain — you can delete the old one manually from TrueNAS later. When in doubt, leave unchecked and delete manually after verifying the VM works.
5. Click **Move Disk** and wait for the task to complete before starting the next move
**Capacity note:** Each move requires free pool space equal to the size of the disk being moved. If you move one disk at a time and check "Delete source," you only need space for one disk. If you run moves in parallel, multiply accordingly.
**Timing:** A 100 GB disk over a 1 Gbps link takes roughly 1015 minutes. Plan accordingly for large disks.
Repeat for every disk on every VM. EFI disks, CloudInit drives, and data disks all follow the same process. Skip `tpmstate0` disks — handle those separately as described above.
---
## Step 5 — Verify and clean up
After moving all disks for a VM:
1. **Check the Hardware tab** — all disks should reference `truenas-v3`
2. **Restart the VM** (or stop/start) and confirm it boots normally
3. **Check TrueNAS** — go to Sharing → iSCSI → Targets. You should see one target named `proxmox-vm-<vmid>` for each migrated VM, with the correct number of LUNs
Once all VMs are migrated:
1. Confirm no disks reference the old v2.x storage: run this on any Proxmox node and expect no output:
```bash
grep -r 'old-storage-id:' /etc/pve/nodes/*/qemu-server/ /etc/pve/nodes/*/lxc/ 2>/dev/null
```
*(Replace `old-storage-id` with your actual v2 storage ID)*
2. Go to **Datacenter → Storage**, select the old v2.x storage, click **Remove**
3. Clean up any leftover iSCSI extents or targets on TrueNAS that were not auto-removed (see [migration-troubleshooting.md](migration-troubleshooting.md))
---
## Frequently asked questions
**Can I run v2.x and v3.0 at the same time?**
Yes — both storage entries coexist in Proxmox during migration. Remove the v2.x entry only after all disks are moved.
**What if something goes wrong — can I go back?**
Yes. The original disk is not deleted until the move fully completes (and only if you checked "Delete source"). If a move fails, the VM is still running on its original disk. You can also move disks back from v3.0 to v2.x storage using the same Move Disk process in reverse.
**Will my VMs have downtime?**
No shutdown required. Move Disk with a running VM uses live storage migration. Expect a brief I/O pause (typically under a second) at cutover.
**What happened to my block devices in `/dev/`?**
In v3.0, QEMU talks to iSCSI directly — no kernel sessions are created, so no block devices appear on the Proxmox host. This is correct behavior. Tools like `iscsiadm` and `lsscsi` will not show your VM disks.
**What about snapshots on my old disks?**
Proxmox-level snapshots taken via the v2.x plugin will not transfer. If you have important snapshot data, revert to a snapshot before migrating. ZFS-level snapshots on TrueNAS are unaffected.
**My old storage still shows orphaned extents on TrueNAS after removal.**
See [Dangling resources after removal](migration-troubleshooting.md#dangling-resources-after-removal) in the troubleshooting guide.

210
docs/migration-advanced.md Normal file
View File

@ -0,0 +1,210 @@
# Migration Advanced Reference: v2.x to v3.0
This document is for administrators who want a full understanding of what changes between v2.x and v3.0, how to plan large-scale migrations, and how to handle edge cases. For a step-by-step walkthrough, start with the [Migration Guide](migrating-from-v2.md).
---
## What changed architecturally
### v2.x: Host-managed iSCSI sessions
v2.x used `iscsiadm` on the Proxmox host to establish kernel-level iSCSI sessions. All VMs on a storage shared a small number of iSCSI targets, with each VM disk assigned to a different LUN number. Device nodes like `/dev/disk/by-path/ip-...-lun-0` appeared on the Proxmox host and were passed to QEMU as block devices.
**Consequences:**
- iSCSI sessions visible on the host (`iscsiadm -m session`)
- Block device nodes in `/dev/` on the Proxmox host
- LUN limit per target (256 max)
- Removing one disk from a running VM could disrupt other VMs on the same target
- Depended on SSH keys from Proxmox nodes to TrueNAS for pool enumeration
### v3.0: Per-VM targets, QEMU-native iSCSI
v3.0 creates a dedicated iSCSI target for each VM (`proxmox-vm-<vmid>`) and passes an `iscsi://` URI directly to QEMU. QEMU's built-in libiscsi library opens the session — no kernel iSCSI daemon involved.
**Consequences:**
- No iSCSI sessions visible on the Proxmox host
- No block device nodes in `/dev/` — this is expected and correct
- Each VM has exactly one target; all its disks are LUNs on that target
- Removing a disk from one VM never affects another VM's connection
- No SSH keys required — the plugin communicates only via TrueNAS REST API
The `iscsi://` URI format stored in the VM config:
```
iscsi://<portal_ip>/<iqn>:<target_name>/<lun_id>
```
Example:
```
iscsi://192.168.10.50/iqn.2005-10.org.freenas.ctl:proxmox-vm-100/0
```
---
## Configuration field mapping
| v2.x field | v3.0 field | Notes |
|-----------|-----------|-------|
| `type freenas` / `iscsiprovider freenas` | `type truenas` | New storage plugin type |
| `freenas_user` | *(removed)* | Basic auth no longer supported |
| `freenas_password` | *(removed)* | Basic auth no longer supported |
| `truenas_secret` | `truenas_api_key` | Same concept; renamed |
| `portal` | `truenas_portal_ip` | Optional; defaults to `truenas_host` |
| `target` | `truenas_target` | Optional; base IQN for auto-discovery |
| `pool` | `truenas_pool` | Same ZFS pool path |
| — | `truenas_dataset` | New: optional sub-dataset within the pool |
| `truenas_ssl` | `truenas_ssl` | Unchanged (default: 1) |
| `truenas_ssl_verify` | `truenas_ssl_verify` | Unchanged (default: 0) |
A v2.x storage config block like:
```
freenas: old-storage
freenas_user admin
freenas_password secret
portal 192.168.10.50
pool tank/proxmox/disks
truenas_ssl 1
truenas_ssl_verify 0
```
Becomes in v3.0:
```
truenas: truenas-v3
truenas_host 192.168.10.50
truenas_api_key 1-AbCdEf...
truenas_pool tank/proxmox/disks
truenas_ssl 1
truenas_ssl_verify 0
nodes pve01,pve02,pve03
shared 1
```
---
## HA (High Availability) considerations
If any VM is managed by Proxmox HA, disable HA for that VM before migrating its disks:
1. Datacenter → HA → Resources → select the VM → Remove (or set to disabled)
2. Migrate the disks
3. Re-enable HA after migration is complete
If a node fences (reboots unexpectedly) while Move Disk is in progress, the HA manager may attempt to restart the VM on another node. The VM config may still reference the old disk at that point. Check the VM config after recovery and re-run the migration if needed. The original disk is always preserved until Move Disk fully completes.
---
## Single portal — single point of failure
v3.0 does not support iSCSI multipath. QEMU's libiscsi opens one connection to the portal IP and retries on failure but does not fail over to a second IP.
If your TrueNAS has multiple network interfaces or portal IPs, `truenas_portal_ip` selects exactly one. **All Proxmox nodes must be able to reach that IP on port 3260.** A failure of that interface will cause all VMs on that storage to lose disk access.
Options for resilience:
- Use a floating/virtual IP at the network layer (bond, VRRP/CARP)
- Ensure the portal IP is on a highly available network segment
- Multipath support is tracked for a future release
---
## Running v2.x and v3.0 in parallel
Both storage entries can coexist in `/etc/pve/storage.cfg`. Proxmox routes disk allocation to whichever storage is specified — the two plugins do not interfere with each other. This allows a rolling, VM-by-VM migration without any maintenance window.
Recommended approach for clusters with many VMs:
1. Install v3.0 on all nodes first (required before adding the new storage type)
2. Add the v3.0 storage entry
3. Migrate VMs in batches — test one or two first before proceeding
4. Remove the v2.x storage entry only after confirming the last VM is migrated
---
## Planning for large deployments
**Capacity:** Move Disk creates a full copy of the zvol before deleting the old one. You need free pool space equal to the size of one disk at a time (or however many moves run concurrently). If free space is tight, migrate and verify one disk at a time with "Delete source" checked before starting the next.
**Concurrency:** Proxmox allows multiple Move Disk operations in parallel, but each operation is I/O-intensive. Limit concurrency to 23 simultaneous moves and **stagger start times by 510 minutes** — if multiple moves reach the cutover phase at the same time, multiple VMs will see simultaneous I/O pauses. Monitor TrueNAS pool load during migrations: `zpool iostat 1`. If the pool is above 6070% utilization, reduce concurrency.
**Network:** Move Disk transfers data through the Proxmox host, not directly on TrueNAS. Traffic path: TrueNAS → iSCSI → Proxmox host RAM → iSCSI → TrueNAS. For a 1 Gbps link, budget ~1015 minutes per 100 GB.
**Verification checklist per VM:**
- [ ] All disks listed in Hardware tab reference the new storage
- [ ] VM boots cleanly from the new storage
- [ ] `qm config <vmid>` shows no references to the old storage ID
- [ ] TrueNAS: one iSCSI target named `proxmox-vm-<vmid>` exists with the correct number of LUNs
- [ ] TrueNAS: old zvols from v2.x storage are gone (if Delete source was checked)
**Verification for the whole cluster when done:**
```bash
# Find any VM configs still referencing the old storage ID (replace 'old-storage' with your v2 ID)
grep -r 'old-storage:' /etc/pve/nodes/*/qemu-server/ /etc/pve/nodes/*/lxc/ 2>/dev/null
```
No output means all disks are migrated.
---
## Why there is no in-place rename path
An in-place rename would require:
1. Renaming each zvol on TrueNAS to match the new naming convention
2. Rebuilding the iSCSI extent, target, and target-extent association under v3.0's per-VM model
3. Rewriting the disk path in the Proxmox VM config from a block device to an `iscsi://` URI
Steps 2 and 3 require the VM to be offline and involve several API calls that can fail partway through. The Move Disk path is safer because Proxmox manages the cutover atomically and the VM can stay running. An offline rename script may be provided in a future release for large deployments where copying data is impractical.
---
## Rollback
If a Move Disk operation fails partway through:
1. The original disk is untouched — it is not deleted until the move completes successfully and you confirm
2. The new (partial) zvol and any iSCSI objects created on TrueNAS may be left behind — clean these up manually:
- On TrueNAS: **Sharing → iSCSI → Extents** — delete the orphaned extent
- On TrueNAS: **Sharing → iSCSI → Targets** — delete the orphaned target (if no other disks for that VM exist)
- On TrueNAS: **Storage → Pools → Browse** — delete the orphaned zvol
3. The VM's config in Proxmox still references the original disk — it is unaffected
If you decide to roll back entirely to v2.x after partially migrating:
- Disks still on v2.x storage will continue to work on v2.x
- Disks already moved to v3.0 storage must stay on v3.0 storage — moving back is the same Move Disk process in reverse
---
## TPM state disks
VMs with vTPM enabled have a `tpmstate0` disk. QEMU requires this to be a local filesystem path — the `iscsi://` URI model that v3.0 uses is incompatible.
**If your v2.x storage used the kernel iSCSI path model (block device), tpmstate0 may have worked there.** In v3.0 it will not. Before migrating a VM with TPM:
1. Move the `tpmstate0` disk to `local-lvm` or NFS storage first
2. Then migrate the remaining disks to v3.0 storage as normal
For live VM migration across cluster nodes (if needed), the `tpmstate0` disk must also be on shared filesystem storage (NFS, CephFS) — local-lvm does not support live migration.
---
## API key security
v2.x stored credentials in `/etc/pve/storage.cfg` in plain text. v3.0 supports the same (`truenas_api_key` in the config file), but also supports keyfiles stored outside the cluster config:
```bash
# Store on each node (not synced across the cluster)
mkdir -p /etc/pve/priv/truenas
echo "1-YourKeyHere" > /etc/pve/priv/truenas/truenas-v3.key
chmod 600 /etc/pve/priv/truenas/truenas-v3.key
```
The plugin looks for `/etc/pve/priv/truenas/<storeid>.key` and prefers it over the config file value. This keeps the API key out of the cluster config database (which is readable by all cluster members).
---
## Cluster node considerations
The storage config in `/etc/pve/storage.cfg` is cluster-wide. The plugin must be installed on every node that has the storage in its `nodes` list. If a node is missing the plugin, `pvesm status` will show an error for that storage on that node.
Install order:
1. Install `truenas-proxmox` on all nodes first
2. Then add the storage entry (or it may fail on nodes that don't have the plugin yet)
The API key and any keyfiles must be present on every node individually — keyfiles in `/etc/pve/priv/` are not synced across the cluster.

View File

@ -0,0 +1,187 @@
# Migration Troubleshooting
This guide covers problems specific to migrating from v2.x to v3.0. For general post-migration errors, see the troubleshooting section in the [README](../README.md).
---
## Move Disk fails immediately
**Symptom:** Task errors within seconds of starting Move Disk.
**Check the task log** (click the failed task in Proxmox):
| Error | Cause | Fix |
|-------|-------|-----|
| `TrueNAS API token not configured` | API key missing from the v3.0 storage config | Edit the storage in Datacenter → Storage and add the API key |
| `Pool dataset '...' not found` | `truenas_pool` path doesn't exist on TrueNAS | Verify the pool path in the storage config matches exactly what TrueNAS shows |
| `422 Unprocessable Entity` | API field validation error | Check TrueNAS version compatibility; enable logging (`journalctl -u pvedaemon -f`) for detail |
| `SSL: certificate verify failed` | `truenas_ssl_verify 1` but certificate is self-signed | Set `truenas_ssl_verify 0` in storage config, or install a valid certificate |
| `Connection refused` | Wrong host/IP or TrueNAS API not reachable | Verify host from the Proxmox node: `curl -k https://<host>/api/v2.0/system/info` |
---
## Move Disk hangs or times out
**Symptom:** Task starts, progress bar moves, but eventually fails with a timeout or connection error.
- **Network interruption:** iSCSI traffic is sensitive to packet loss. Check switch/VLAN configuration between Proxmox and TrueNAS.
- **TrueNAS pool under stress:** If the pool is rebuilding (resilver in progress) or highly loaded, I/O will be slow. Check TrueNAS → Storage → Pools for any active operations.
- **Large disk:** A 500 GB disk at 1 Gbps takes ~70 minutes. Proxmox's default task timeout may trigger. Check if the task actually completed on TrueNAS despite the Proxmox error.
After a failed move, check TrueNAS for a partial zvol:
```
Sharing → iSCSI → Extents # look for an extent with the new disk name
Storage → Pools → Browse # look for a partial zvol
```
Delete any partial resources before retrying.
---
## VM won't start after Move Disk
**Symptom:** VM was running before migration, fails to start after the disk move completes.
**Check the VM config:**
```bash
qm config <vmid>
```
Look for the moved disk's line. It should show `iscsi://...` as the path, not a block device path.
**Check the iSCSI target exists on TrueNAS:**
1. TrueNAS → Sharing → iSCSI → Targets — confirm `proxmox-vm-<vmid>` is present
2. TrueNAS → Sharing → iSCSI → Extents — confirm the extent for each disk is present
3. TrueNAS → Sharing → iSCSI → Associated Targets — confirm each extent is linked to the target at the correct LUN
If any of these are missing, the disk cannot be accessed. Re-run a Move Disk operation to recreate the iSCSI objects, or use the Proxmox task log to identify which step failed.
**Check the iSCSI portal is reachable:**
```bash
# From the Proxmox node, using qm's built-in check:
qm start <vmid>
# Read the full error in the task log — it often includes the iscsi:// URI
```
If the error is `Failed to open drive ... iscsi://...`: the QEMU process cannot reach the iSCSI portal. Check:
- `truenas_portal_ip` in storage config — should be the IP that Proxmox can reach TrueNAS on
- Firewall rules between Proxmox and TrueNAS on port 3260
- iSCSI service is running on TrueNAS (Sharing → iSCSI → Target Global Configuration → Enable)
---
## Disk shows wrong size or "undefined" after move
**Symptom:** The disk appears in the Hardware tab but shows "undefined" or an incorrect size.
This is usually transient — the Proxmox UI polls before the iSCSI setup is fully complete. Wait 3060 seconds and refresh.
If it persists:
1. Check `volume_size_info` is returning a value:
```bash
pvesm volume_info <storeid> <volid>
```
Example: `pvesm volume_info truenas-v3 truenas-v3:vm-100-disk-0`
2. If this returns an error, the plugin cannot find the zvol via the TrueNAS API. Verify the zvol exists:
```bash
curl -sk -H "Authorization: Bearer <apikey>" \
https://<truenas_host>/api/v2.0/pool/dataset/id/<pool>%2F<volname>
```
3. If the zvol exists but `volume_size_info` fails, check for SSL or API connectivity issues in the daemon log:
```bash
journalctl -u pvedaemon --since '5 minutes ago' | grep TrueNAS
```
---
## Dangling resources after removal
**Symptom:** After removing the v2.x storage from Proxmox, TrueNAS still has old iSCSI extents, targets, or zvols that weren't cleaned up.
The v2.x plugin may not have removed these automatically. They are harmless but waste space and clutter the iSCSI config.
**To clean up manually on TrueNAS:**
1. **Extents** (Sharing → iSCSI → Extents): Delete any extents whose name matches old VM disk names
2. **Target-Extent associations** (Sharing → iSCSI → Associated Targets): Delete associations for removed targets
3. **Targets** (Sharing → iSCSI → Targets): Delete targets that no longer have any Proxmox VMs using them
4. **Zvols** (Storage → Pools → Browse): Delete zvols that were not cleaned up (confirm the VM config no longer references them first)
Always verify a zvol is truly unused before deleting it — check `qm config` for all VMs.
---
## Old v2.x storage can't be removed from Proxmox
**Symptom:** "Remove" is greyed out or fails with "storage is still in use."
Proxmox blocks storage removal if any VM config still references it. Find the VMs:
```bash
grep -r 'old-storage:' /etc/pve/nodes/*/qemu-server/ /etc/pve/nodes/*/lxc/ 2>/dev/null
```
For each match, move or remove the disk first. Common leftovers:
- **Unused disks** (detached but not deleted): VM → Hardware tab, look for `unused0`, `unused1` etc. referencing the old storage
- **CloudInit drives**: Move or delete the CloudInit drive (it will be regenerated)
- **EFI disks**: Move via Move Disk the same as any other disk
---
## iSCSI service stops working after migration
**Symptom:** VMs that were running fine start failing with iSCSI errors after migration.
This is not a migration problem — it means the iSCSI service on TrueNAS is unhealthy. Check:
1. **TrueNAS → Sharing → iSCSI** — confirm the service shows as running
2. **TrueNAS System Logs** — look for any iSCSI service errors around the time of failure
3. **Proxmox node logs:**
```bash
dmesg | grep -i iscsi
journalctl -u pvedaemon --since '30 minutes ago' | grep TrueNAS
```
If TrueNAS rebooted or the iSCSI service was restarted, running VMs with iSCSI disks will lose connectivity. QEMU will retry the connection — VMs typically recover within 3060 seconds if the service comes back quickly. For production workloads, configure TrueNAS to start the iSCSI service automatically on boot.
---
## Move Disk failure recovery matrix
If a Move Disk operation fails partway through, use this table to determine what to clean up and whether it is safe to retry:
| What failed | State on TrueNAS | VM state | Action |
|-------------|-----------------|----------|--------|
| Zvol copy (mid-copy) | Partial zvol exists | Running on original disk | Delete partial zvol on TrueNAS, retry Move Disk |
| iSCSI extent creation | New zvol exists, no extent | Running on original disk | Delete new zvol on TrueNAS, retry Move Disk |
| iSCSI target creation | New zvol + extent exist | Running on original disk | Delete extent and new zvol, retry Move Disk |
| targetextent association | All iSCSI objects exist | Running on original disk | Delete targetextent, extent, new zvol, retry Move Disk |
| VM config rewrite | All iSCSI objects exist, config not updated | Running on original disk | Verify VM is still on old disk (`qm config`); delete new iSCSI objects and zvol, retry |
| Zvol deletion (Delete source) | New disk active, old zvol still present | Running on new disk | Verify VM config references new disk; delete old zvol manually with `zfs destroy` |
| Node crash mid-copy | Partial zvol may exist | Will recover to original disk | After node recovers, check TrueNAS for partial zvol; delete if found; retry Move Disk |
The original disk is never modified or deleted during the copy phase. If anything fails before the VM config is rewritten, the VM will continue running on its original disk.
---
## API key revoked or expired
**Symptom:** Move Disk or disk creation fails with `401 Unauthorized` or `TrueNAS API token not configured`.
TrueNAS API keys can be revoked manually or expire under rotation policies. If this happens:
1. Generate a new API key on TrueNAS (same process as initial setup)
2. In Proxmox: Datacenter → Storage → select the storage → Edit → update the API Key field
3. Retry the failed operation
---
## Getting help
If you hit a problem not covered here:
1. Collect the relevant log output:
```bash
journalctl -u pvedaemon --since '1 hour ago' | grep -i truenas > /tmp/pvedaemon.log
```
2. Note your versions: `dpkg -l truenas-proxmox`, PVE version (`pveversion`), TrueNAS version
3. Open an issue at https://github.com/TheGrandWazoo/freenas-proxmox/issues with the log and version info

103
docs/upgrade-paths.md Normal file
View File

@ -0,0 +1,103 @@
# Upgrade Paths
Quick reference for every migration scenario. Find your current version and repo source in the table, then follow the link for step-by-step instructions.
---
## Where am I, and where do I go?
| Current version | Current repo | → Stay on current major | → Move to v3.x | → Move to v4.x |
|---|---|---|---|---|
| **v2.x** | Cloudsmith stable | [Swap repo to GitHub Pages `main`](#cloudsmith-to-github-pages) | [Swap repo + config migration](migrating-from-v2.md) | TBD when v4 ships — will require v3 config first |
| **v2.x** | GitHub Pages `main` | Already on correct repo ✅ | [Config migration only](migrating-from-v2.md) | TBD when v4 ships — will require v3 config first |
| **v3.x** | Cloudsmith stable | [Swap repo to GitHub Pages `v3`](#cloudsmith-to-github-pages) | Already on v3 — swap repo only | [Change dist track to `v4` + v3→v4 guide](#future-v4) |
| **v3.x** | GitHub Pages `v3` | Already on correct repo ✅ | Already here ✅ | [Change dist track to `v4` + v3→v4 guide](#future-v4) |
**Not sure which version you have?** Run this on any Proxmox node:
```bash
dpkg -l truenas-proxmox freenas-proxmox 2>/dev/null | grep '^ii'
```
**Not sure which repo you're using?**
```bash
cat /etc/apt/sources.list.d/truenas-proxmox*.list /etc/apt/sources.list.d/freenas-proxmox*.list 2>/dev/null
```
---
## Cloudsmith → GitHub Pages {#cloudsmith-to-github-pages}
> Cloudsmith is being phased out. GitHub Pages is the permanent home for this project's packages. Your installed package is not affected — this only changes where future updates come from.
Full instructions are in the [README — Migrating from Cloudsmith](../README.md#migrating-from-cloudsmith) section.
In short:
1. Remove your existing Cloudsmith source file from `/etc/apt/sources.list.d/`
2. Remove the old Cloudsmith keyring from `/usr/share/keyrings/`
3. Import the GitHub Pages GPG key to `/etc/apt/keyrings/truenas-proxmox.gpg`
4. Add the GitHub Pages source pointing at the right dist track (`main` for v2, `v3` for v3)
5. Run `apt update`
---
## What changes between major versions
| | v2.x → v3.x | v3.x → v4.x (future) |
|---|---|---|
| **Breaking change?** | Yes — auth model, iSCSI model, package name | Yes — WebSocket API replaces REST |
| **In-place upgrade?** | No — VM disks must be moved | TBD |
| **Plugin restarts VMs?** | No — Move Disk is live | TBD |
| **Config changes?** | Yes — API key replaces username/password | TBD |
| **Full guide** | [migrating-from-v2.md](migrating-from-v2.md) | Will be added when v4 ships |
---
## v2.x → v3.x
See [migrating-from-v2.md](migrating-from-v2.md) for the full step-by-step guide.
High-level steps:
1. Install `truenas-proxmox` on every Proxmox node (from GitHub Pages `v3` dist)
2. Create a TrueNAS API key
3. Add a new v3 storage entry in Proxmox (keep the old v2 entry running alongside)
4. Move VM disks one at a time using Proxmox Move Disk (VMs stay running)
5. Remove the old v2 storage entry once all disks are moved
**Do not run `apt upgrade` from a v2 Cloudsmith install without reading the migration guide first.** v3.0 is a breaking change — your storage will need reconfiguration.
---
## v3.x → v4.x (future) {#future-v4}
v4.0 will replace the TrueNAS REST API with the WebSocket JSON-RPC 2.0 API introduced in TrueNAS SCALE 25.04. This is an architectural change and will be a major version with its own migration guide.
When v4.0 ships, this section will be updated with:
- What changes in v4.0 (API model, config fields, compatibility matrix)
- Whether an in-place upgrade is possible
- Step-by-step migration from v3.x
- Step-by-step migration from v2.x (if a direct v2→v4 path is supported)
**To receive v4.x packages when they ship**, you will need to change your `sources.list` dist track from `v3` to `v4`:
```bash
# When v4.0 is released — do NOT run this until the v4 migration guide is published
echo "deb [signed-by=/etc/apt/keyrings/truenas-proxmox.gpg] \
https://thegrandwazoo.github.io/freenas-proxmox v4 main" \
| sudo tee /etc/apt/sources.list.d/truenas-proxmox.list
sudo apt update
```
`apt upgrade` on the `v3` dist track will never pull in a v4 package — you must explicitly change the dist track. This is intentional.
---
## Dist track reference
| Dist track | Contains | Who should use it |
|---|---|---|
| `main` | v2.x packages only | Users staying on v2.x |
| `v2` | v2.x packages (alias for `main`) | Same as `main` — explicit pin |
| `v3` | v3.x packages only | All current v3 installs |
| `v4` | v4.x packages only | Added when v4.0 ships |
Packages on one dist track are never promoted to another. Changing major versions always requires an explicit `sources.list` edit.

View File

@ -0,0 +1,22 @@
truenas-proxmox-multipath (3.2.2-1) bookworm; urgency=low
* Version bump to match truenas-proxmox 3.2.2; api() fix inherited from TrueNAS.pm
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Mon, 23 Jun 2026 00:00:00 +0000
truenas-proxmox-multipath (3.2.1-1) bookworm; urgency=low
* Version bump to match truenas-proxmox 3.2.1; no functional changes
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 22 Jun 2026 00:00:00 +0000
truenas-proxmox-multipath (3.1.0-1) bookworm; urgency=low
* Initial release — kernel iSCSI multipath via iscsiadm + dm-multipath
* Registers 'truenas-multipath' PVE storage type
* Inherits all REST API and iSCSI resource management from truenas-proxmox
* path() returns /dev/mapper/<wwid> for QEMU block device access
* activate_volume() logs in via all configured portals
* Distributed via the 'multipath' apt component
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 07 Jun 2026 00:00:00 +0000

View File

@ -0,0 +1,19 @@
Package: truenas-proxmox-multipath
Version: ${VERSION}
Architecture: all
Maintainer: KSA Technologies, LLC <theprofessor@ksatechnologies.com>
Depends: truenas-proxmox (>= 3.1.0), open-iscsi, multipath-tools
Section: perl
Priority: optional
Homepage: https://github.com/TheGrandWazoo/truenas-proxmox
Description: TrueNAS Multipath Storage Plugin for Proxmox VE
Extends truenas-proxmox with kernel iSCSI multipath support via iscsiadm
and dm-multipath. Registers a separate 'truenas-multipath' storage type
that presents /dev/mapper/<wwid> block devices to QEMU instead of
iscsi:// URIs, enabling active/active multipath with failover.
.
Requires two or more network paths between Proxmox and TrueNAS and
TrueNAS iSCSI portals configured on each path.
.
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
Copyright (c) 2020 KSA Technologies, LLC.

View File

@ -0,0 +1,28 @@
# /etc/multipath.conf — reference for truenas-proxmox-multipath
#
# postinst installs this automatically if /etc/multipath.conf does not exist.
# If you already have a multipath.conf, postinst appends the tagged block below
# instead of overwriting your file.
#
# The BEGIN/END markers let postinst update the stanza on upgrade and postrm
# strip it on remove without touching the rest of your config.
defaults {
find_multipaths yes
}
# BEGIN truenas-proxmox-multipath
# Managed by truenas-proxmox-multipath. Do not edit between these markers.
# To customise, remove the package tags and maintain the block yourself.
devices {
device {
vendor "TrueNAS"
product "iSCSI Disk"
path_grouping_policy multibus
path_checker tur
failback immediate
rr_weight uniform
no_path_retry 5
}
}
# END truenas-proxmox-multipath

View File

@ -0,0 +1,112 @@
#!/bin/bash
# postinst: truenas-proxmox-multipath install/upgrade script
set -e
INSTALL_DIR="/usr/share/truenas-proxmox-multipath"
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNASMultipath.pm"
JS_DST="/usr/share/pve-manager/js/truenas-multipath.js"
TPL="/usr/share/pve-manager/index.html.tpl"
SCRIPT_TAG=' <script type="text/javascript" src="/pve2/js/truenas-multipath.js?ver=[% version %]"></script>'
MULTIPATH_CONF="/etc/multipath.conf"
LOG_FILE="/var/log/truenas-proxmox-install.log"
log() {
echo "[truenas-proxmox-multipath] $*" | tee -a "$LOG_FILE"
}
install_plugin() {
log "Installing ${PLUGIN_DST}"
mkdir -p "$(dirname "$PLUGIN_DST")"
cp "${INSTALL_DIR}/TrueNASMultipath.pm" "$PLUGIN_DST"
}
install_ui() {
log "Installing ${JS_DST}"
cp "${INSTALL_DIR}/truenas-multipath.js" "$JS_DST"
if grep -qF 'truenas-multipath.js' "$TPL" 2>/dev/null; then
log "index.html.tpl already has truenas-multipath.js — skipping"
return 0
fi
log "Adding <script> tag to ${TPL}"
sed -i "/pvemanagerlib\.js/a\\${SCRIPT_TAG}" "$TPL"
}
BLOCK_BEGIN="# BEGIN truenas-proxmox-multipath"
BLOCK_END="# END truenas-proxmox-multipath"
# Appends or replaces the tagged TrueNAS device stanza in multipath.conf.
# Safe to call on upgrade — idempotent via BEGIN/END markers.
install_multipath_conf() {
local stanza
stanza=$(cat <<STANZA
${BLOCK_BEGIN}
# Managed by truenas-proxmox-multipath. Do not edit between these markers.
# To customise, remove the package tags and maintain the block yourself.
devices {
device {
vendor "TrueNAS"
product "iSCSI Disk"
path_grouping_policy multibus
path_checker tur
failback immediate
rr_weight uniform
no_path_retry 5
}
}
${BLOCK_END}
STANZA
)
if [ ! -f "$MULTIPATH_CONF" ]; then
log "Creating ${MULTIPATH_CONF} with defaults + TrueNAS device stanza"
printf 'defaults {\n find_multipaths yes\n}\n\n%s\n' "$stanza" > "$MULTIPATH_CONF"
elif grep -qF "$BLOCK_BEGIN" "$MULTIPATH_CONF"; then
log "Updating existing TrueNAS stanza in ${MULTIPATH_CONF}"
# Replace the block between markers (inclusive) with the new stanza
perl -i -0777 -pe \
"s|\Q${BLOCK_BEGIN}\E.*?\Q${BLOCK_END}\E|${stanza}|s" \
"$MULTIPATH_CONF"
else
log "Appending TrueNAS device stanza to existing ${MULTIPATH_CONF}"
# Strip any pre-existing unmanaged devices block to prevent duplicate
# keyword warnings in multipathd (e.g. user had a manual block before install).
if grep -qE '^\s*devices\s*\{' "$MULTIPATH_CONF"; then
log "Removing pre-existing unmanaged devices block from ${MULTIPATH_CONF}"
perl -i -0777 -pe \
's/\n*\ndevices\s*\{(?:[^{}]*|\{[^{}]*\})*\}\n*//gs' \
"$MULTIPATH_CONF"
fi
printf '\n%s\n' "$stanza" >> "$MULTIPATH_CONF"
fi
systemctl enable multipathd 2>/dev/null || true
systemctl start multipathd 2>/dev/null || true
log "multipathd enabled and started"
}
restart_pve_services() {
log "Restarting Proxmox VE services ..."
pvedaemon restart && log "pvedaemon restarted"
pveproxy restart && log "pveproxy restarted"
log "Done. Refresh your Proxmox browser tab."
}
case "$1" in
configure)
log "Configuring truenas-proxmox-multipath (previous version: ${2:-none})"
install_plugin
install_ui
install_multipath_conf
restart_pve_services
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "$0: called with unknown argument '$1'" >&2
exit 0
;;
esac
exit 0

View File

@ -0,0 +1,54 @@
#!/bin/bash
# postrm: truenas-proxmox-multipath remove/purge script
set -e
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNASMultipath.pm"
JS_DST="/usr/share/pve-manager/js/truenas-multipath.js"
TPL="/usr/share/pve-manager/index.html.tpl"
MULTIPATH_CONF="/etc/multipath.conf"
BLOCK_BEGIN="# BEGIN truenas-proxmox-multipath"
BLOCK_END="# END truenas-proxmox-multipath"
LOG_FILE="/var/log/truenas-proxmox-install.log"
log() {
echo "[truenas-proxmox-multipath] $*" | tee -a "$LOG_FILE"
}
case "$1" in
remove|purge)
if [ -f "$PLUGIN_DST" ]; then
log "Removing ${PLUGIN_DST}"
rm -f "$PLUGIN_DST"
fi
if [ -f "$JS_DST" ]; then
log "Removing ${JS_DST}"
rm -f "$JS_DST"
fi
if [ -f "$TPL" ] && grep -qF 'truenas-multipath.js' "$TPL" 2>/dev/null; then
log "Removing <script> tag from ${TPL}"
sed -i '/truenas-multipath\.js/d' "$TPL"
fi
if [ -f "$MULTIPATH_CONF" ] && grep -qF "$BLOCK_BEGIN" "$MULTIPATH_CONF"; then
log "Removing TrueNAS stanza from ${MULTIPATH_CONF}"
perl -i -0777 -pe \
"s|\n?\Q${BLOCK_BEGIN}\E.*?\Q${BLOCK_END}\E\n?||s" \
"$MULTIPATH_CONF"
fi
log "Restarting Proxmox VE services ..."
if pvedaemon restart 2>/dev/null; then log "pvedaemon restarted"; fi
if pveproxy restart 2>/dev/null; then log "pveproxy restarted"; fi
log "Done."
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "$0: called with unknown argument '$1'" >&2
exit 0
;;
esac
exit 0

View File

@ -0,0 +1,31 @@
freenas-proxmox (3.2.2-1) bookworm; urgency=low
* Version bump to match truenas-proxmox 3.2.2; no functional changes
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Mon, 23 Jun 2026 00:00:00 +0000
freenas-proxmox (3.2.1-1) bookworm; urgency=low
* Version bump to match truenas-proxmox 3.2.1; no functional changes
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 22 Jun 2026 00:00:00 +0000
freenas-proxmox (3.0.0-1) bookworm; urgency=low
* Transitional package — installs truenas-proxmox automatically
* The freenas-proxmox package name is retired; truenas-proxmox is the successor
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 31 May 2026 00:00:00 +0000
freenas-proxmox (2.4.0-1) bookworm; urgency=low
* Quality of life improvements and minor fixes
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Fri, 22 May 2026 00:00:00 +0000
freenas-proxmox (2.3.0-1) bookworm; urgency=medium
* Hardening and stability improvements
* Bearer token authentication support
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Mon, 19 May 2026 00:00:00 +0000

View File

@ -0,0 +1,16 @@
Package: freenas-proxmox
Version: ${VERSION}
Architecture: all
Maintainer: KSA Technologies, LLC <theprofessor@ksatechnologies.com>
Depends: truenas-proxmox (>= 3.0.0)
Section: perl
Priority: optional
Homepage: https://github.com/TheGrandWazoo/truenas-proxmox
Description: Transitional package — replaced by truenas-proxmox
This is an empty transitional package. The plugin has been renamed from
freenas-proxmox to truenas-proxmox. Installing this package pulls in the
real package automatically.
.
To fully complete migration: apt install truenas-proxmox && apt purge freenas-proxmox
.
The freenas-proxmox package name will not exist in v4.0.

View File

@ -0,0 +1,27 @@
#!/bin/bash
# postinst: freenas-proxmox transitional package
# This package is empty — the real plugin is in truenas-proxmox.
set -e
LOG_FILE="/var/log/truenas-proxmox-install.log"
log() {
echo "[freenas-proxmox] $*" | tee -a "$LOG_FILE"
}
case "$1" in
configure)
log "freenas-proxmox is now a transitional package."
log "The plugin has moved to: truenas-proxmox"
log "To complete migration: apt install truenas-proxmox && apt purge freenas-proxmox"
log "Note: freenas-proxmox will not exist in v4.0."
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "$0: called with unknown argument '$1'" >&2
exit 0
;;
esac
exit 0

View File

@ -1,15 +1,18 @@
Package: freenas-proxmox
Package: truenas-proxmox
Version: ${VERSION}
Architecture: all
Maintainer: KSA Technologies, LLC <theprofessor@ksatechnologies.com>
Depends: librest-client-perl, patch
Depends: librest-client-perl, open-iscsi
Replaces: freenas-proxmox
Breaks: freenas-proxmox (<< 3.0.0)
Section: perl
Priority: optional
Homepage: https://github.com/TheGrandWazoo/freenas-proxmox
Description: TrueNAS ZFS-over-iSCSI Plugin for Proxmox VE
Manages iSCSI LUNs on TrueNAS (CORE and SCALE) via the TrueNAS REST API.
Supports Bearer Token and username/password authentication.
No SSH-based LUN management required.
Homepage: https://github.com/TheGrandWazoo/truenas-proxmox
Description: TrueNAS Custom Storage Plugin for Proxmox VE
Native PVE::Storage::Custom plugin for managing TrueNAS ZFS volumes over iSCSI.
Discovered automatically by Proxmox VE — no patches to PVE system files required.
Supports TrueNAS CORE and SCALE via the TrueNAS REST API with Bearer Token auth.
No SSH keys required.
.
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
Copyright (c) 2020 KSA Technologies, LLC.

View File

@ -1,212 +1,63 @@
#!/bin/bash
# postinst: freenas-proxmox install/upgrade/trigger script
# Applies patches to Proxmox VE system files and installs the TrueNAS API plugin.
# No internet access required — all files are bundled in the package.
# postinst: truenas-proxmox v3.x install/upgrade script
# Installs the TrueNAS custom storage plugin and UI panel for Proxmox VE.
# No patches to PVE core files — only adds our own files and one <script> line.
set -e
INSTALL_DIR="/usr/share/freenas-proxmox"
LIB_PATH="/usr/share"
LOG_FILE="/var/log/freenas-proxmox-install.log"
ZFSPLUGIN_PATH="/perl5/PVE/Storage/ZFSPlugin.pm"
PVEMANAGER_PATH="/pve-manager/js/pvemanagerlib.js"
APIDOC_PATH="/pve-docs/api-viewer/apidoc.js"
FREENAS_PM_PATH="/perl5/PVE/Storage/LunCmd/FreeNAS.pm"
REST_CLIENT_PATH="/perl5/REST/Client.pm"
INSTALL_DIR="/usr/share/truenas-proxmox"
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm"
JS_DST="/usr/share/pve-manager/js/truenas-storage.js"
HELP_DST="/usr/share/pve-docs/truenas-storage.html"
TPL="/usr/share/pve-manager/index.html.tpl"
SCRIPT_TAG=' <script type="text/javascript" src="/pve2/js/truenas-storage.js?ver=[% version %]"></script>'
LOG_FILE="/var/log/truenas-proxmox-install.log"
log() {
echo "[freenas-proxmox] $*" | tee -a "$LOG_FILE"
echo "[truenas-proxmox] $*" | tee -a "$LOG_FILE"
}
# Detect installed Proxmox VE major version (e.g. "8" from "8.4.19")
detect_pve_major() {
local ver
ver=$(dpkg-query --showformat='${Version}' --show proxmox-ve 2>/dev/null || echo "0")
echo "${ver%%.*}"
install_plugin() {
log "Installing ${PLUGIN_DST}"
mkdir -p "$(dirname "$PLUGIN_DST")"
cp "${INSTALL_DIR}/TrueNAS.pm" "$PLUGIN_DST"
}
# Detect pve-manager major.minor (e.g. "8.4" from "8.4.19").
# Used to select minor-version-specific patches within a major series.
detect_pve_manager_minor() {
local ver
ver=$(dpkg-query -W -f '${Version}' pve-manager 2>/dev/null || echo "0.0.0")
echo "${ver%.*}"
}
install_ui() {
log "Installing ${JS_DST}"
cp "${INSTALL_DIR}/truenas-storage.js" "$JS_DST"
# Find the best bundled patch for a given component and PVE version.
# Tries {minor}.patch (e.g. 8.4.patch) first, then walks down {major}.patch,
# {major-1}.patch, ... until a match is found.
find_patch() {
local component="$1"
local major="$2"
local minor="$3" # e.g. "8.4" — may be empty
local patch_dir="${INSTALL_DIR}/patches/${component}"
log "Installing ${HELP_DST}"
cp "${INSTALL_DIR}/truenas-storage-help.html" "$HELP_DST"
# Minor-version-specific patch takes priority (e.g. pvemanagerlib/8.4.patch)
if [ -n "$minor" ] && [ -f "${patch_dir}/${minor}.patch" ]; then
echo "${patch_dir}/${minor}.patch"
if grep -qF 'truenas-storage.js' "$TPL" 2>/dev/null; then
log "index.html.tpl already has truenas-storage.js — skipping"
return 0
fi
# Fall back to major-version patch, walking down until one is found
local ver="$major"
while [ "$ver" -ge 5 ]; do
if [ -f "${patch_dir}/${ver}.patch" ]; then
echo "${patch_dir}/${ver}.patch"
return 0
fi
ver=$(( ver - 1 ))
done
log "WARNING: No bundled patch found for ${component} on Proxmox VE ${major}.x"
return 1
}
# Apply a patch to a target file idempotently.
# Creates a .orig backup (via patch --backup) on first application.
apply_patch() {
local target_rel="$1"
local patch_file="$2"
local label="$3"
local target="${LIB_PATH}${target_rel}"
[ -f "$target" ] || { log "ERROR: Target not found: ${target}"; return 1; }
[ -f "$patch_file" ] || { log "Skipping ${label}: patch file missing"; return 0; }
if grep -q "freenas" "$target" 2>/dev/null; then
log "${label} is already patched — skipping"
return 0
fi
log "Patching ${target} ..."
if patch --backup --ignore-whitespace "$target" < "$patch_file" >> "$LOG_FILE" 2>&1; then
log "${label} patched successfully"
else
log "ERROR: Failed to patch ${label} — see ${LOG_FILE} for details"
return 1
fi
}
install_files() {
log "Installing ${LIB_PATH}${FREENAS_PM_PATH}"
mkdir -p "$(dirname "${LIB_PATH}${FREENAS_PM_PATH}")"
cp "${INSTALL_DIR}/FreeNAS.pm" "${LIB_PATH}${FREENAS_PM_PATH}"
log "Installing ${LIB_PATH}${REST_CLIENT_PATH}"
mkdir -p "$(dirname "${LIB_PATH}${REST_CLIENT_PATH}")"
cp "${INSTALL_DIR}/REST-Client.pm" "${LIB_PATH}${REST_CLIENT_PATH}"
log "Adding <script> tag to ${TPL}"
sed -i "/pvemanagerlib\.js/a\\${SCRIPT_TAG}" "$TPL"
}
restart_pve_services() {
log "Restarting Proxmox VE services ..."
pvedaemon restart && log "pvedaemon restarted"
pveproxy restart && log "pveproxy restarted"
pvestatd restart && log "pvestatd restarted"
systemctl restart pvescheduler.service && log "pvescheduler restarted"
log "Done. Refresh your Proxmox browser tab to load the updated UI."
pvedaemon restart && log "pvedaemon restarted"
pveproxy restart && log "pveproxy restarted"
log "Done. Refresh your Proxmox browser tab."
}
# Warn the user if their PVE version is outside the supported range for this
# plugin version. Exits non-zero (aborting the install) only for versions so
# old that no patches exist (< 7).
check_pve_version() {
local major="$1"
if [ "$major" -lt 7 ]; then
log "ERROR: Proxmox VE ${major}.x is not supported by this plugin."
log " PVE 5 and 6 reached end-of-life in 2019 and 2022 respectively."
log " Please upgrade your Proxmox VE installation before installing this plugin."
exit 1
fi
if [ "$major" -eq 7 ]; then
log "WARNING: *** Proxmox VE 7 — Important Notice ***"
log "WARNING: v2.x is the LAST release series that supports Proxmox VE 7."
log "WARNING: PVE 7 support is best-effort only; no new patches will be"
log "WARNING: developed for it."
log "WARNING: When v3.0 is released, DO NOT upgrade to it — v3.0 requires"
log "WARNING: Proxmox VE 8 or later. Stay on the latest v2.x release."
fi
if [ "$major" -eq 8 ]; then
log "INFO: Proxmox VE 8 reaches end-of-life on 2026-08-31."
log "INFO: Plan your upgrade to PVE 9 and migration to v3.0 before that date."
fi
if [ "$major" -ge 9 ]; then
log "WARNING: *** Proxmox VE ${major}.x — Not Supported by v2.x ***"
log "WARNING: This plugin (v2.x) is not supported on Proxmox VE 9 or later."
log "WARNING: Patches may fail to apply or the UI may not work correctly."
log "WARNING: Please use v3.0 of this plugin, which is designed for PVE 9+."
log "WARNING: See: https://github.com/TheGrandWazoo/freenas-proxmox/releases"
log "WARNING: Continuing anyway — but expect problems."
fi
}
# ── Entry point ──────────────────────────────────────────────────────────────
major=$(detect_pve_major)
pve_mgr_minor=$(detect_pve_manager_minor)
log "Proxmox VE major version: ${major}, pve-manager: ${pve_mgr_minor}"
check_pve_version "$major"
case "$1" in
triggered)
# dpkg fired us because one of the watched PVE files was updated.
log "Triggered by package update — re-applying patches for: $2"
for fullpath in $2; do
filename=$(basename "$fullpath")
filename="${filename%.*}" # strip extension
case "$filename" in
ZFSPlugin)
patch_file=$(find_patch "ZFSPlugin" "$major" "$pve_mgr_minor" || true)
apply_patch "$ZFSPLUGIN_PATH" "$patch_file" "ZFSPlugin.pm"
;;
pvemanagerlib)
patch_file=$(find_patch "pvemanagerlib" "$major" "$pve_mgr_minor" || true)
apply_patch "$PVEMANAGER_PATH" "$patch_file" "pvemanagerlib.js"
;;
apidoc)
patch_file=$(find_patch "apidoc" "$major" "$pve_mgr_minor" || true)
apply_patch "$APIDOC_PATH" "$patch_file" "apidoc.js"
;;
esac
done
install_files
exit 0
;;
configure)
log "Configuring freenas-proxmox (previous version: ${2:-none})"
CHANGED="no"
patch_file=$(find_patch "ZFSPlugin" "$major" "$pve_mgr_minor" || true)
apply_patch "$ZFSPLUGIN_PATH" "$patch_file" "ZFSPlugin.pm" && CHANGED="yes"
patch_file=$(find_patch "pvemanagerlib" "$major" "$pve_mgr_minor" || true)
apply_patch "$PVEMANAGER_PATH" "$patch_file" "pvemanagerlib.js" && CHANGED="yes"
patch_file=$(find_patch "apidoc" "$major" "$pve_mgr_minor" || true)
apply_patch "$APIDOC_PATH" "$patch_file" "apidoc.js" && CHANGED="yes"
install_files
CHANGED="yes"
[ "$CHANGED" = "yes" ] && restart_pve_services
exit 0
log "Configuring truenas-proxmox (previous version: ${2:-none})"
install_plugin
install_ui
restart_pve_services
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "$0: called with unknown argument '$1'" >&2
exit 0
;;
esac
exit 0

View File

@ -1,104 +1,52 @@
#!/bin/bash
# postrm: freenas-proxmox removal script
# Reverses patches applied to Proxmox VE system files and removes installed plugin files.
# postrm: truenas-proxmox v3.x removal script
set -e
LIB_PATH="/usr/share"
LOG_FILE="/var/log/freenas-proxmox-install.log"
ZFSPLUGIN_PATH="/perl5/PVE/Storage/ZFSPlugin.pm"
PVEMANAGER_PATH="/pve-manager/js/pvemanagerlib.js"
APIDOC_PATH="/pve-docs/api-viewer/apidoc.js"
FREENAS_PM_PATH="/perl5/PVE/Storage/LunCmd/FreeNAS.pm"
INSTALL_DIR="/usr/share/freenas-proxmox"
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNAS.pm"
JS_DST="/usr/share/pve-manager/js/truenas-storage.js"
HELP_DST="/usr/share/pve-docs/truenas-storage.html"
TPL="/usr/share/pve-manager/index.html.tpl"
LOG_FILE="/var/log/truenas-proxmox-install.log"
log() {
echo "[freenas-proxmox] $*" | tee -a "$LOG_FILE"
echo "[truenas-proxmox] $*" | tee -a "$LOG_FILE"
}
# Restore a file from the .orig backup that patch --backup created.
restore_orig() {
local target_rel="$1"
local label="$2"
local target="${LIB_PATH}${target_rel}"
local orig="${target}.orig"
remove_ui() {
rm -f "$JS_DST" && log "Removed ${JS_DST}"
rm -f "$HELP_DST" && log "Removed ${HELP_DST}"
if ! grep -q "freenas" "$target" 2>/dev/null; then
log "${label} does not appear to be patched — skipping restore"
return 0
if [ -f "$TPL" ] && grep -qF 'truenas-storage.js' "$TPL"; then
log "Removing <script> tag from ${TPL}"
sed -i '/truenas-storage\.js/d' "$TPL"
fi
if [ -f "$orig" ]; then
log "Restoring ${label} from .orig backup"
cp "$orig" "$target" && rm -f "$orig"
log "${label} restored"
else
log "WARNING: No .orig backup found for ${label} — Proxmox VE reinstall may be needed"
log " Run: apt install --reinstall pve-manager libpve-storage-perl pve-docs"
fi
}
remove_plugin_files() {
if [ -f "${LIB_PATH}${FREENAS_PM_PATH}" ]; then
log "Removing ${LIB_PATH}${FREENAS_PM_PATH}"
rm -f "${LIB_PATH}${FREENAS_PM_PATH}"
fi
# REST/Client.pm is owned by librest-client-perl; do not remove it here.
}
restart_pve_services() {
log "Restarting Proxmox VE services ..."
pvedaemon restart && log "pvedaemon restarted"
pveproxy restart && log "pveproxy restarted"
pvestatd restart && log "pvestatd restarted"
systemctl restart pvescheduler.service && log "pvescheduler restarted"
pvedaemon restart && log "pvedaemon restarted"
pveproxy restart && log "pveproxy restarted"
}
# ── Entry point ──────────────────────────────────────────────────────────────
case "$1" in
remove)
log "Removing freenas-proxmox — restoring Proxmox VE files ..."
restore_orig "$ZFSPLUGIN_PATH" "ZFSPlugin.pm"
restore_orig "$PVEMANAGER_PATH" "pvemanagerlib.js"
restore_orig "$APIDOC_PATH" "apidoc.js"
remove_plugin_files
log "Removing truenas-proxmox ..."
rm -f "$PLUGIN_DST" && log "Removed ${PLUGIN_DST}"
remove_ui
restart_pve_services
log "freenas-proxmox removed. Refresh your Proxmox browser tab."
exit 0
log "truenas-proxmox removed. Refresh your Proxmox browser tab."
;;
upgrade)
# On upgrade the new package's postinst will re-apply everything.
# Just restore the originals cleanly so the new patch applies to a fresh file.
log "Preparing for upgrade — restoring unpatched Proxmox VE files ..."
restore_orig "$ZFSPLUGIN_PATH" "ZFSPlugin.pm"
restore_orig "$PVEMANAGER_PATH" "pvemanagerlib.js"
restore_orig "$APIDOC_PATH" "apidoc.js"
remove_plugin_files
exit 0
;;
purge)
log "Purging freenas-proxmox ..."
rm -rf "$INSTALL_DIR"
rm -f "$LOG_FILE"
exit 0
log "Purging truenas-proxmox ..."
rm -f "$PLUGIN_DST" "$JS_DST" "$HELP_DST" "$LOG_FILE"
[ -f "$TPL" ] && sed -i '/truenas-storage\.js/d' "$TPL"
;;
failed-upgrade|disappear|abort-upgrade|abort-remove|abort-deconfigure)
upgrade|failed-upgrade|disappear|abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "$0: called with unknown argument '$1'" >&2
exit 0
;;
esac
exit 0

View File

@ -1,7 +0,0 @@
# dpkg trigger file for freenas-proxmox
#
# When Proxmox VE updates replace any of these three files, dpkg automatically
# re-fires the postinst script so patches are re-applied without manual action.
interest /usr/share/perl5/PVE/Storage/ZFSPlugin.pm
interest /usr/share/pve-manager/js/pvemanagerlib.js
interest /usr/share/pve-docs/api-viewer/apidoc.js

View File

@ -0,0 +1,42 @@
truenas-proxmox (3.2.2-1) bookworm; urgency=low
* Bump api() return value from 14 to 15 to match PVE 9 APIVER — silences
"implementing an older storage API" warning on every daemon restart
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Mon, 23 Jun 2026 00:00:00 +0000
truenas-proxmox (3.2.1-1) bookworm; urgency=low
* Fix apt codename alias 'error' — dists/error was never created because
codename alias code was added after v3.2.0 tag; also fix cp -r overwrite
bug that would corrupt dists/error on second+ stable releases
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 22 Jun 2026 00:00:00 +0000
truenas-proxmox (3.0.0-1) bookworm; urgency=medium
* Full rewrite as native PVE::Storage::Custom plugin — no PVE core patches
* Bearer token authentication only; basic auth (freenas_user/password) removed
* Per-VM dedicated iSCSI targets via TrueNAS REST API v2.0
* QEMU connects via iscsi:// directly — no iscsiadm, no kernel iSCSI sessions
* Supports TrueNAS CORE 13.0-U6 and SCALE 24.10, 25.04, 25.10
* Supports Proxmox VE 8.x and 9.x
* Package renamed from freenas-proxmox to truenas-proxmox; transitional package provided
* alloc_image partial-failure rollback (cleanup orphaned TrueNAS resources)
* Migration docs: beginner, advanced, troubleshooting guides
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 31 May 2026 00:00:00 +0000
truenas-proxmox (2.4.0-1) bookworm; urgency=low
* Quality of life improvements and minor fixes
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Fri, 22 May 2026 00:00:00 +0000
truenas-proxmox (2.3.0-1) bookworm; urgency=medium
* Hardening and stability improvements
* Bearer token authentication support
* Build and CI infrastructure improvements
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Mon, 19 May 2026 00:00:00 +0000

View File

@ -1,4 +0,0 @@
/ZFSPlugin.pm
/ZFSPoolPlugin.pm
/ZFSPlugin-new.pm
/ZFSPlugin-newver.patch

View File

@ -0,0 +1,813 @@
package PVE::Storage::Custom::TrueNAS;
# TrueNAS Custom Storage Plugin for Proxmox VE
#
# Manages ZFS volumes over iSCSI via the TrueNAS REST API.
# Discovered automatically by PVE — no patches to system files required.
#
# Supports: TrueNAS CORE 13.x, TrueNAS SCALE <= 24.10 (REST API v2.0)
# Auth: Bearer token (API key) only — basic auth removed in v3.0
# iSCSI: QEMU libiscsi (iscsi:// paths) — no iscsiadm session management
#
# Per-VM target architecture:
# Each VM gets its own iSCSI target (proxmox-vm-<vmid>).
# path() returns iscsi://portal/iqn:proxmox-vm-<vmid>/lun — QEMU connects via
# libiscsi. Deleting a disk from a running VM (e.g. removing an unused disk)
# requires force=true on both the targetextent and extent DELETEs — TrueNAS
# sees the target session as active even when the specific LUN is not in use.
use strict;
use warnings;
use JSON qw(encode_json decode_json);
use LWP::UserAgent ();
use HTTP::Request ();
use URI::Escape qw(uri_escape);
use Sys::Syslog qw(syslog);
use PVE::Storage::Plugin;
use base qw(PVE::Storage::Plugin);
our $VERSION = '3.2.2';
# Per-host runtime state cache
my $state = {};
# ── Plugin identity ───────────────────────────────────────────────────────────
sub api { return 15; }
sub type { return 'truenas'; }
sub plugindata {
return {
content => [ { images => 1 }, { images => 1 } ],
format => [ { raw => 1 }, 'raw' ],
};
}
sub properties {
return {
truenas_host => {
description => "TrueNAS hostname or IP address",
type => 'string',
},
truenas_api_key => {
description => "TrueNAS API key (Bearer token). "
. "Leave blank to use /etc/pve/priv/truenas-<storeid>.key instead.",
type => 'string',
},
truenas_ssl => {
description => "Use HTTPS for TrueNAS API (recommended)",
type => 'boolean',
default => 1,
},
truenas_ssl_verify => {
description => "Verify TrueNAS SSL certificate (disable for self-signed certs)",
type => 'boolean',
default => 0,
},
truenas_pool => {
description => "ZFS pool or dataset path where PVE volumes are created "
. "(e.g. 'tank' or 'tank/proxmox/vdisks'). "
. "Matches the 'pool' field from the v2.x plugin.",
type => 'string',
},
truenas_dataset => {
description => "Optional additional sub-dataset appended to Pool path. "
. "Leave blank — put the full path in Pool instead.",
type => 'string',
},
truenas_portal_ip => {
description => "iSCSI portal IP address. Defaults to truenas_host if not set.",
type => 'string',
},
truenas_target => {
description => "Base iSCSI target name (used to look up portal and initiator "
. "group settings for auto-created per-VM targets). "
. "Leave blank to auto-discover from the portal IP.",
type => 'string',
},
};
}
sub options {
return {
nodes => { optional => 1 },
disable => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
shared => { optional => 1 },
truenas_host => { fixed => 1 },
truenas_api_key => { optional => 1 },
truenas_ssl => { optional => 1 },
truenas_ssl_verify => { optional => 1 },
truenas_pool => { fixed => 1 },
truenas_dataset => { optional => 1 },
truenas_portal_ip => { optional => 1 },
truenas_target => { optional => 1 },
};
}
# ── Private: logging ──────────────────────────────────────────────────────────
sub _log {
my ($level, $msg) = @_;
syslog($level, "TrueNASPlugin: $msg");
return;
}
# ── Private: HTTP/API helpers ─────────────────────────────────────────────────
sub _ua {
my ($scfg) = @_;
my $host = $scfg->{truenas_host};
unless ($state->{$host}{ua}) {
my $ua = LWP::UserAgent->new(timeout => 30);
if ($scfg->{truenas_ssl} // 1) {
unless ($scfg->{truenas_ssl_verify} // 0) {
$ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0);
}
}
$state->{$host}{ua} = $ua;
}
return $state->{$host}{ua};
}
# Resolves the API token from /etc/pve/priv/truenas-<storeid>.key (preferred)
# or truenas_api_key in storage.cfg (backwards compat). Caches in $state so
# the keyfile is read at most once per daemon session per host.
sub _resolve_token {
my ($storeid, $scfg) = @_;
my $host = $scfg->{truenas_host};
return if $state->{$host}{api_token};
my $keyfile = "/etc/pve/priv/truenas-$storeid.key";
if (-r $keyfile) {
open(my $fh, '<', $keyfile)
or die "Cannot read TrueNAS token file '$keyfile': $!\n";
my $token = do { local $/; <$fh> };
close $fh;
$token =~ s/\s+\z//;
die "TrueNAS token file '$keyfile' is empty\n" unless length $token;
$state->{$host}{api_token} = $token;
return;
}
die "TrueNAS API token not configured for storage '$storeid'. "
. "Set truenas_api_key in storage.cfg or create $keyfile\n"
unless $scfg->{truenas_api_key};
$state->{$host}{api_token} = $scfg->{truenas_api_key};
return;
}
# Make a TrueNAS REST API v2.0 call.
# Dies with a descriptive message on HTTP error.
# Returns decoded JSON hashref/arrayref, or undef for empty 204 responses.
sub _api {
my ($scfg, $method, $path, $data) = @_;
my $host = $scfg->{truenas_host};
my $token = $state->{$host}{api_token}
or die "TrueNAS API token not resolved for '$host'\n";
my $scheme = ($scfg->{truenas_ssl} // 1) ? 'https' : 'http';
my $url = "$scheme://$host/api/v2.0$path";
my $req = HTTP::Request->new(uc($method) => $url);
$req->header('Content-Type' => 'application/json');
$req->header('Accept' => 'application/json');
$req->header('Authorization' => "Bearer $token");
$req->content(encode_json($data)) if defined $data;
my $res = _ua($scfg)->request($req);
unless ($res->is_success) {
my $detail = '';
eval {
my $body = decode_json($res->content);
if (ref $body eq 'HASH' && $body->{message}) {
$detail = " — $body->{message}";
} elsif (ref $body eq 'ARRAY' && @$body) {
# SCALE 25.04+ returns Pydantic validation errors as an array
my @msgs = map { $_->{message} // $_->{msg} // '' } @$body;
$detail = " — " . join('; ', grep { length } @msgs);
} elsif (ref $body eq 'HASH') {
# SCALE 25.10+ returns field-keyed validation errors: { "field": [{message}...] }
my @msgs;
for my $field (sort keys %$body) {
my $errs = $body->{$field};
next unless ref $errs eq 'ARRAY';
push @msgs, map { "$field: " . ($_->{message} // '') } @$errs;
}
$detail = " — " . join('; ', grep { length } @msgs) if @msgs;
}
};
my $msg = "TrueNAS API $method $path: " . $res->status_line . $detail;
_log('err', $msg);
die "$msg\n";
}
my $content = $res->content // '';
return unless length($content);
return decode_json($content);
}
# Returns (and caches) the iSCSI global config { basename, ... }
sub _api_global {
my ($scfg) = @_;
my $host = $scfg->{truenas_host};
$state->{$host}{global} //= _api($scfg, 'GET', '/iscsi/global') // {};
return $state->{$host}{global};
}
# ── Private: TrueNAS domain helpers ──────────────────────────────────────────
# Returns the zvol parent dataset path: "pool" or "pool/dataset"
sub _zvol_prefix {
my ($scfg) = @_;
my $prefix = $scfg->{truenas_pool};
$prefix .= "/$scfg->{truenas_dataset}" if $scfg->{truenas_dataset};
return $prefix;
}
# Returns the iSCSI portal IP/hostname to use
sub _portal {
my ($scfg) = @_;
return $scfg->{truenas_portal_ip} // $scfg->{truenas_host};
}
# Returns the iSCSI global basename (e.g. "iqn.2005-10.org.freenas.ctl")
sub _basename {
my ($scfg) = @_;
return _api_global($scfg)->{basename} // 'iqn.2005-10.org.freenas.ctl';
}
# Finds the [ { portal, initiator, authmethod, auth } ] group list to use when
# creating new per-VM targets. Copies the first group found on an existing
# target that uses our portal IP, so security settings (initiator group, auth)
# are inherited automatically.
sub _portal_groups_for_new_target {
my ($scfg) = @_;
my $portal_ip = _portal($scfg);
my $portals = _api($scfg, 'GET', '/iscsi/portal') // [];
my %our_portal_ids;
for my $p (@$portals) {
for my $listen (@{$p->{listen} // []}) {
my $ip = $listen->{ip} // '';
if ($ip eq $portal_ip || $ip eq '0.0.0.0' || $ip eq '::') {
$our_portal_ids{$p->{id}} = 1;
last;
}
}
}
die "No iSCSI portals found listening on $portal_ip — "
. "cannot create per-VM targets. Check truenas_portal_ip.\n"
unless %our_portal_ids;
my $targets = _api($scfg, 'GET', '/iscsi/target') // [];
# Prefer copying groups from the configured base target
if (my $base = $scfg->{truenas_target}) {
my ($bt) = grep { $_->{name} eq $base || $base =~ /:\Q$_->{name}\E$/ } @$targets;
if ($bt) {
my @gs = grep { $our_portal_ids{$_->{portal}} } @{$bt->{groups} // []};
return _clean_groups(\@gs) if @gs;
}
}
# Fall back to any existing target that uses our portal
for my $t (@$targets) {
my @gs = grep { $our_portal_ids{$_->{portal}} } @{$t->{groups} // []};
return _clean_groups(\@gs) if @gs;
}
# 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 => int($pid), authmethod => 'NONE' } ];
}
sub _clean_groups {
my ($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.
# Returns { id, iqn }.
sub _resolve_vm_target {
my ($scfg, $vmid) = @_;
my $host = $scfg->{truenas_host};
return $state->{$host}{vm_targets}{$vmid}
if $state->{$host}{vm_targets}{$vmid};
my $target_name = "proxmox-vm-$vmid";
my $targets = _api($scfg, 'GET', '/iscsi/target') // [];
my ($existing) = grep { $_->{name} eq $target_name } @$targets;
my ($t_id, $iqn);
if ($existing) {
$t_id = $existing->{id};
$iqn = _basename($scfg) . ":$target_name";
_log('info', "Using existing per-VM target: $iqn (id=$t_id)");
} else {
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,
});
$t_id = $new->{id};
$iqn = _basename($scfg) . ":$target_name";
_log('info', "Created per-VM target: $iqn (id=$t_id)");
}
# 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};
}
# Deletes the per-VM target if it has no remaining targetextent associations.
sub _maybe_cleanup_vm_target {
my ($scfg, $vmid, $target_id) = @_;
return unless defined $target_id;
my $tes = _api($scfg, 'GET', "/iscsi/targetextent?target=$target_id") // [];
return if @$tes; # still has extents — don't remove
eval { _api($scfg, 'DELETE', "/iscsi/target/id/$target_id") };
if ($@) {
_log('warning', "could not remove empty per-VM target id=$target_id (vm=$vmid): $@");
} else {
_log('info', "removed empty per-VM target id=$target_id (vm=$vmid)");
my $host = $scfg->{truenas_host};
delete $state->{$host}{vm_targets}{$vmid};
}
return;
}
# Returns true if the QEMU process for $vmid is alive.
# Returns the next unused LUN ID on the given target
sub _next_lun_id {
my ($scfg, $target_id) = @_;
my $tes = _api($scfg, 'GET', "/iscsi/targetextent?target=$target_id") // [];
my %used = map { $_->{lunid} => 1 } @$tes;
my $lun = 0;
$lun++ while $used{$lun}; # hash-key lookup stringifies $lun; int() restores IV type
return int($lun);
}
# Returns { extent_id, targetextents, targetextent_id, lun_id, target_id } for a volname,
# or undef if no extent exists. targetextents is the full array of all associations
# (normally one, but may be >1 if duplicate rows exist from a prior failed alloc_image).
sub _find_extent {
my ($scfg, $volname) = @_;
my $extents = _api($scfg, 'GET', '/iscsi/extent') // [];
my ($ext) = grep { $_->{name} eq $volname } @$extents;
return unless $ext;
my $tes = _api($scfg, 'GET', "/iscsi/targetextent?extent=$ext->{id}") // [];
my ($te) = @$tes;
return {
extent_id => $ext->{id},
naa => $ext->{naa},
targetextents => $tes,
targetextent_id => defined $te ? $te->{id} : undef,
lun_id => defined $te ? $te->{lunid} : 0,
target_id => defined $te ? $te->{target} : undef,
};
}
# Signal TrueNAS to reload the iSCSI service so new extents are visible
sub _reload_iscsi {
my ($scfg) = @_;
eval { _api($scfg, 'POST', '/service/reload', { service => 'iscsitarget' }) };
_log('warning', "iSCSI reload failed (non-fatal): $@") if $@;
return;
}
# ── PVE::Storage::Plugin interface ───────────────────────────────────────────
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ /^(vm|base)-(\d+)-disk-\d+$/) {
my ($prefix, $vmid) = ($1, $2);
my $isBase = $prefix eq 'base' ? 1 : 0;
return ('images', $volname, $vmid, undef, undef, $isBase, 'raw');
}
# RAM snapshot state volume: vm-<vmid>-state-<snapname>
# PVE allocates one of these per snapshot when "Include RAM" is selected.
if ($volname =~ /^vm-(\d+)-state-.+$/) {
return ('images', $volname, $1, undef, undef, 0, 'raw');
}
die "unable to parse TrueNAS volume name '$volname'\n";
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
_resolve_token($storeid, $scfg);
my $pool_name = (split m{/}, $scfg->{truenas_pool})[0];
my $datasets = _api($scfg, 'GET', "/pool/dataset?id=$pool_name") // [];
my ($ds) = grep { $_->{name} eq $pool_name } @$datasets;
die "Pool dataset '$pool_name' not found on $scfg->{truenas_host}\n" unless $ds;
my $free = $ds->{available}{parsed} // 0;
my $used = $ds->{used}{parsed} // 0;
my $total = $free + $used;
return ($total, $free, $used, 1);
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size_kb) = @_;
_resolve_token($storeid, $scfg);
die "Unsupported format '$fmt' — only raw is supported\n" if $fmt && $fmt ne 'raw';
$name //= $class->find_free_diskname($storeid, $scfg, $vmid, 'raw');
my $prefix = _zvol_prefix($scfg);
my $zvol = "$prefix/$name";
my $size_b = int($size_kb) * 1024;
_log('info', "alloc_image: creating zvol $zvol ($size_b bytes) for VM $vmid");
# 1. Create the zvol
# int() here produces a fresh IV — string interpolation in the log call above
# sets Perl's POK flag on $size_b, which causes JSON::XS to encode it as a
# string. SCALE 25.10 strict Pydantic rejects string-typed integers.
_api($scfg, 'POST', '/pool/dataset', {
name => $zvol,
type => 'VOLUME',
volsize => int($size_b),
sparse => JSON::true,
});
# Steps 2-4 wrapped in eval so the zvol is cleaned up on any failure.
my ($extent, $target, $lun_id);
eval {
# 2. Create the iSCSI extent
$extent = _api($scfg, 'POST', '/iscsi/extent', {
name => $name,
type => 'DISK',
disk => "zvol/$zvol",
ro => JSON::false,
});
# 3. Associate extent with the per-VM target
$target = _resolve_vm_target($scfg, $vmid);
$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 => int($target->{id}),
extent => int($extent->{id}),
lunid => int($lun_id),
});
};
if (my $err = $@) {
_log('warning', "alloc_image: partial failure — rolling back: $err");
# Reverse order, best-effort. Target: only removed if it has no
# remaining extents (i.e. it was just created for this disk).
if ($target) {
eval { _maybe_cleanup_vm_target($scfg, $vmid, $target->{id}) };
}
if ($extent) {
eval { _api($scfg, 'DELETE', "/iscsi/extent/id/$extent->{id}",
{ force => JSON::true }) };
_log('warning', "alloc_image rollback: extent delete failed: $@") if $@;
}
my $zvol_id = uri_escape($zvol, "^A-Za-z0-9\\-_.~");
eval { _api($scfg, 'DELETE', "/pool/dataset/id/$zvol_id",
{ recursive => JSON::true }) };
_log('warning', "alloc_image rollback: zvol delete failed: $@") if $@;
die $err;
}
_reload_iscsi($scfg);
_log('info', "alloc_image: $name ready at lun $lun_id on $target->{iqn}");
return $name;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
_resolve_token($storeid, $scfg);
_log('info', "free_image: removing $volname");
my ($vmid) = $volname =~ /^(?:vm|base)-(\d+)-/;
my $ext = _find_extent($scfg, $volname);
if ($ext) {
# Step 1: unmap ALL LUN associations for this extent. Normally there is
# exactly one targetextent row, but duplicate rows can exist if a prior
# alloc_image failed partway through and left an orphan. force=true
# (bare boolean) is required by SCALE 24.10 when the target has an active
# session — e.g. removing an unused disk from a running VM.
for my $te (@{ $ext->{targetextents} // [] }) {
eval { _api($scfg, 'DELETE', "/iscsi/targetextent/id/$te->{id}", JSON::true) };
_log('warning', "free_image: could not remove targetextent $te->{id}: $@") if $@;
}
# Step 2: delete the extent. force=true handles any residual session
# on a single-disk per-VM target (e.g. deleting a detached disk).
eval {
_api($scfg, 'DELETE', "/iscsi/extent/id/$ext->{extent_id}",
{ force => JSON::true });
};
die "free_image: could not delete iSCSI extent for '$volname': $@\n" if $@;
# Remove the per-VM target if this was its last disk
_maybe_cleanup_vm_target($scfg, $vmid, $ext->{target_id})
if $vmid && defined $ext->{target_id};
} else {
_log('warning', "free_image: no iSCSI extent found for $volname — skipping extent removal");
}
# Delete the zvol; recursive handles any leftover snapshots
my $zvol = _zvol_prefix($scfg) . "/$volname";
my $zvol_id = uri_escape($zvol, "^A-Za-z0-9\\-_.~");
_api($scfg, 'DELETE', "/pool/dataset/id/$zvol_id", { recursive => JSON::true });
_log('info', "free_image: $volname removed");
return;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $fmt, $ids) = @_;
_resolve_token($storeid, $scfg);
my $prefix = _zvol_prefix($scfg);
my $datasets = _api($scfg, 'GET', '/pool/dataset?type=VOLUME') // [];
my @vols;
for my $ds (@$datasets) {
my $name = $ds->{name} // '';
next unless $name =~ s{^\Q$prefix\E/}{};
next if $name =~ m{/};
next unless $name =~ /^(?:vm|base|subvol)-(\d+)-/;
my $ds_vmid = $1 + 0;
next if defined $vmid && $ds_vmid != $vmid;
my $volid = "$storeid:$name";
next if $ids && !$ids->{$volid};
my $size = 0;
eval { $size = $ds->{volsize}{parsed} // 0 };
push @vols, {
volid => $volid,
name => $name,
size => $size,
vmid => $ds_vmid,
format => 'raw',
};
}
return \@vols;
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
_resolve_token($storeid, $scfg);
my $zvol = _zvol_prefix($scfg) . "/$volname";
my $enc = uri_escape($zvol, "^A-Za-z0-9\\-_.~");
my $ds = _api($scfg, 'GET', "/pool/dataset/id/$enc") // {};
return $ds->{volsize}{parsed} // 0;
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
_resolve_token($storeid, $scfg);
my $ext = _find_extent($scfg, $volname);
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}. "
. "Was it created via this plugin?\n" unless $ext;
die "Volume '$volname' is not mapped to any iSCSI target.\n"
unless defined $ext->{target_id};
# Look up the target that owns this extent (per-VM or legacy shared)
my $t = _api($scfg, 'GET', "/iscsi/target/id/$ext->{target_id}") // {};
my $iqn = _basename($scfg) . ":$t->{name}";
my $portal = _portal($scfg);
my $dev_path = "iscsi://$portal/$iqn/$ext->{lun_id}";
my ($vtype, undef, $ds_vmid) = $class->parse_volname($volname);
return ($dev_path, $ds_vmid, $vtype);
}
# Override base-class qemu_blockdev_options to ensure lun is encoded as a JSON
# integer. PVE::Storage::Plugin does lun => "$3" (string capture from the
# iscsi:// URL regex) which QEMU 9's strict blockdev schema rejects. We build
# the hash directly from _find_extent so the value is always an IV. (#266)
# REMOVE this override once Proxmox fixes Plugin.pm to use int($3) — verify by
# checking pve-storage changelog for an iscsi lun integer fix, then confirm
# "grep 'lun =>' /usr/share/perl5/PVE/Storage/Plugin.pm" no longer quotes $3.
sub qemu_blockdev_options {
my ($class, $scfg, $storeid, $volname, $machine_version, $options) = @_;
_resolve_token($storeid, $scfg);
my $ext = _find_extent($scfg, $volname);
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}.\n" unless $ext;
die "Volume '$volname' is not mapped to any iSCSI target.\n"
unless defined $ext->{target_id};
my $t = _api($scfg, 'GET', "/iscsi/target/id/$ext->{target_id}") // {};
my $iqn = _basename($scfg) . ":$t->{name}";
return {
driver => 'iscsi',
portal => _portal($scfg),
target => $iqn,
lun => int($ext->{lun_id}),
transport => 'tcp',
};
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
_resolve_token($storeid, $scfg);
# Verify API reachability and warm up the global config cache
_api_global($scfg);
_log('info', "activate_storage: $storeid online");
return 1;
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
delete $state->{$scfg->{truenas_host}};
_log('info', "deactivate_storage: $storeid offline");
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
_resolve_token($storeid, $scfg);
# QEMU connects via iscsi:// (libiscsi) — no iscsiadm session needed here.
# Just verify the extent exists so we catch config errors early.
my $ext = _find_extent($scfg, $volname);
die "Volume '$volname' has no iSCSI extent — was it created via this plugin?\n"
unless $ext;
_log('info', "activate_volume: $volname (lun $ext->{lun_id}) ready");
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
return 1;
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
copy => { base => 1, current => 1 },
snapshot => { current => 1, snap => 1 },
};
my ($vtype, undef, undef, undef, undef, $isBase) = $class->parse_volname($volname);
my $key = $snapname ? 'snap' : ($isBase ? 'base' : 'current');
return 1 if $features->{$feature} && $features->{$feature}{$key};
return;
}
# ── Snapshot support ──────────────────────────────────────────────────────────
# Request a guest filesystem freeze before snapping a running VM's disk.
# Returning 1 causes PVE to ask the QEMU guest agent to freeze IO before the
# TrueNAS API snapshot call, ensuring write-consistency.
sub volume_snapshot_needs_fsfreeze { return 1; }
# Returns the full ZFS path for a volume: "pool[/dataset]/volname"
sub _snap_dataset {
my ($scfg, $volname) = @_;
return _zvol_prefix($scfg) . "/$volname";
}
# Returns the full ZFS snapshot ID: "pool[/dataset]/volname@snapname"
sub _snap_id {
my ($scfg, $volname, $snap) = @_;
return _zvol_prefix($scfg) . "/$volname\@$snap";
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
_resolve_token($storeid, $scfg);
my $dataset = _snap_dataset($scfg, $volname);
_log('info', "snapshot: creating $dataset\@$snap");
_api($scfg, 'POST', '/zfs/snapshot', { dataset => $dataset, name => $snap });
return;
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
_resolve_token($storeid, $scfg);
my $id = _snap_id($scfg, $volname, $snap);
_log('info', "snapshot: deleting $id");
# The snapshot API requires full URI encoding of the id path segment —
# unlike the dataset endpoint, it does not accept bare slashes in the path.
_api($scfg, 'DELETE', "/zfs/snapshot/id/" . uri_escape($id));
return;
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
_resolve_token($storeid, $scfg);
my $id = _snap_id($scfg, $volname, $snap);
_log('info', "snapshot: rolling back to $id");
_api($scfg, 'POST', '/zfs/snapshot/rollback', {
id => $id,
options => { force => JSON::false, recursive_clones => JSON::false },
});
return;
}
# ZFS only permits rollback to the most recent snapshot. If $snap is not the
# newest, list the blocking (newer) snapshots so PVE can report them.
sub volume_rollback_is_possible {
my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
my $snaps = $class->volume_snapshot_info($scfg, $storeid, $volname);
$blockers //= [];
my $found;
for my $name (sort { $snaps->{$a}{creation_time} <=> $snaps->{$b}{creation_time} }
keys %$snaps) {
if ($name eq $snap) {
$found = 1;
} elsif ($found) {
push @$blockers, $name;
}
}
die "snapshot '$snap' does not exist on '$volname'\n" if !$found;
die "can't rollback '$snap' on '$volname' — newer snapshots exist: "
. join(', ', @$blockers) . "\n"
if @$blockers;
return 1;
}
# Returns a hashref of snapshots for $volname, keyed by snapshot name:
# { 'snapname' => { id => 'pool/vol@snapname', creation_time => $epoch }, ... }
sub volume_snapshot_info {
my ($class, $scfg, $storeid, $volname) = @_;
_resolve_token($storeid, $scfg);
my $dataset = _snap_dataset($scfg, $volname);
my $enc_ds = uri_escape($dataset);
my $snaps = _api($scfg, 'GET', "/zfs/snapshot?dataset=$enc_ds&limit=500") // [];
my %result;
for my $s (@$snaps) {
# snapshot_name is the part after @; id is the full "dataset@name" string
my $name = $s->{snapshot_name}
// do { (my $n = $s->{id} // '') =~ s/^.*@//; $n };
my $ctime = $s->{properties}{creation}{rawvalue} // 0;
$result{$name} = { id => $s->{id}, creation_time => int($ctime) };
}
return \%result;
}
1;

View File

@ -0,0 +1,218 @@
package PVE::Storage::Custom::TrueNASMultipath;
# TrueNAS Multipath Storage Plugin for Proxmox VE
#
# Extends TrueNAS.pm with kernel iSCSI multipath support (iscsiadm + dm-multipath).
# Registers as PVE storage type 'truenas-multipath'.
#
# Requires: truenas-proxmox >= 3.1.0, open-iscsi, multipath-tools
# Auth: Same Bearer token model as TrueNAS.pm
# Path: /dev/mapper/<wwid> instead of iscsi:// — QEMU uses the block device directly
use strict;
use warnings;
use PVE::Storage::Custom::TrueNAS;
use base qw(PVE::Storage::Custom::TrueNAS);
# ── Plugin identity ───────────────────────────────────────────────────────────
sub type { return 'truenas-multipath'; }
sub plugindata {
return {
content => [ { images => 1 }, { images => 1 } ],
format => [ { raw => 1 }, 'raw' ],
};
}
# ── Config properties (extends parent) ───────────────────────────────────────
# Only declare NEW properties here. Inherited TrueNAS.pm properties (truenas_host,
# truenas_pool, etc.) are already registered globally by TrueNAS.pm — re-declaring
# them would cause a "duplicate property" error in PVE::SectionConfig.
sub properties {
return {
truenas_portals => {
description => "Comma-separated iSCSI portal IP addresses for multipath "
. "(e.g. '172.31.69.91,192.168.69.91'). Each portal is logged "
. "into separately; dm-multipath aggregates the sessions.",
type => 'string',
},
};
}
sub options {
my $opts = PVE::Storage::Custom::TrueNAS::options();
$opts->{truenas_portals} = { optional => 0 };
return $opts;
}
# ── Private helpers ───────────────────────────────────────────────────────────
# Parse truenas_portals into a list of IP strings.
sub _portals {
my ($scfg) = @_;
return grep { length } split /\s*,\s*/, ($scfg->{truenas_portals} // '');
}
# Convert TrueNAS NAA string (e.g. "0x6589cfc...") to the dm-multipath WWID
# used in /dev/mapper/ (e.g. "36589cfc...").
sub _naa_to_wwid {
my ($naa) = @_;
$naa =~ s/^0x//i;
return "3$naa";
}
# Run an iscsiadm command, returning (exit_code, output).
# Suppresses errors that indicate a session already exists.
sub _iscsiadm {
my (@args) = @_;
my $out = '';
open(my $fh, '-|', 'iscsiadm', @args, '2>&1')
or return (1, "failed to exec iscsiadm: $!");
$out = do { local $/; <$fh> };
close $fh;
my $rc = $? >> 8;
return ($rc, $out);
}
# Wait up to $timeout seconds for a block device path to appear.
sub _wait_for_device {
my ($path, $timeout) = @_;
$timeout //= 30;
while ($timeout-- > 0) {
return 1 if -b $path;
sleep 1;
}
return 0;
}
# ── Path — block device via dm-multipath ─────────────────────────────────────
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}.\n" unless $ext;
my $wwid = _naa_to_wwid($ext->{naa})
or die "Volume '$volname' has no NAA/WWID on $scfg->{truenas_host}.\n";
return ("/dev/mapper/$wwid", $ext->{lun_id}, $storeid);
}
# ── qemu_blockdev_options — delegate to Plugin.pm (host_device for /dev/mapper) ─
# TrueNAS.pm overrides qemu_blockdev_options to return an iscsi:// blockdev —
# that's wrong for multipath where path() returns /dev/mapper/<wwid>.
# We skip the parent and call the grandparent (PVE::Storage::Plugin) directly.
# Plugin.pm calls path(), sees /dev/mapper/<wwid> starts with '/', stats it as a
# block device (S_ISBLK), and returns { driver => 'host_device', filename => ... }.
# QEMU then accesses the dm-multipath block device directly.
sub qemu_blockdev_options {
my ($class, $scfg, $storeid, $volname, $machine_version, $options) = @_;
return PVE::Storage::Plugin::qemu_blockdev_options(
$class, $scfg, $storeid, $volname, $machine_version, $options // {});
}
# ── activate_volume — login via all portals, wait for mapper device ───────────
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}.\n" unless $ext;
die "Volume '$volname' is not mapped to any iSCSI target.\n"
unless defined $ext->{target_id};
my $t = PVE::Storage::Custom::TrueNAS::_api($scfg, 'GET',
"/iscsi/target/id/$ext->{target_id}") // {};
my $iqn = PVE::Storage::Custom::TrueNAS::_basename($scfg) . ":$t->{name}";
my $wwid = _naa_to_wwid($ext->{naa})
or die "Volume '$volname' has no NAA/WWID.\n";
my $mapper = "/dev/mapper/$wwid";
return if -b $mapper; # already active
my @portals = _portals($scfg);
die "No portals configured — set truenas_portals in storage config.\n" unless @portals;
PVE::Storage::Custom::TrueNAS::_log('info',
"multipath activate: $volname iqn=$iqn wwid=$wwid portals=" . join(',', @portals));
for my $ip (@portals) {
my $portal = "$ip:3260";
# Ensure node entry exists
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '-o', 'new');
# Login — ISCSI_ERR_SESS_EXISTS (15) is fine
my ($rc, $out) = _iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '--login');
if ($rc && $rc != 15) {
die "iscsiadm login to $portal failed (rc=$rc): $out\n";
}
}
# Signal multipathd to pick up the new sessions. Don't call multipath(8)
# directly — that conflicts with a running multipathd daemon.
if (-x '/usr/sbin/multipathd') {
system('multipathd', 'reconfigure');
} elsif (-x '/sbin/multipathd') {
system('/sbin/multipathd', 'reconfigure');
}
# multipathd may take a moment to create the dm device after sessions appear
unless (_wait_for_device($mapper, 30)) {
# Last resort: force a manual scan
system('multipath', '-v0') if !-b $mapper;
unless (_wait_for_device($mapper, 10)) {
my $found = `multipath -l 2>/dev/null` // '';
die "Multipath device $mapper did not appear after 40s. "
. "Check that multipathd is running and multipath.conf includes TrueNAS devices.\n"
. "Current multipath map:\n$found\n";
}
}
PVE::Storage::Custom::TrueNAS::_log('info', "multipath activate: $mapper ready");
return;
}
# ── deactivate_volume — logout from all portals, flush mapper device ──────────
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
return unless $ext && defined $ext->{target_id};
my $t = PVE::Storage::Custom::TrueNAS::_api($scfg, 'GET',
"/iscsi/target/id/$ext->{target_id}") // {};
my $iqn = PVE::Storage::Custom::TrueNAS::_basename($scfg) . ":$t->{name}";
my $wwid = _naa_to_wwid($ext->{naa} // '');
my $mapper = $wwid ? "/dev/mapper/$wwid" : undef;
# Flush multipath device first
if ($mapper && -b $mapper) {
system('multipath', '-f', $wwid);
}
for my $ip (_portals($scfg)) {
my $portal = "$ip:3260";
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '--logout');
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '-o', 'delete');
}
PVE::Storage::Custom::TrueNAS::_log('info', "multipath deactivate: $volname done");
return;
}
1;

View File

@ -1,905 +0,0 @@
package PVE::Storage::LunCmd::FreeNAS;
use strict;
use warnings;
our $VERSION = '2.4.0';
use Data::Dumper;
use PVE::SafeSyslog;
use IO::Socket::SSL;
use LWP::UserAgent;
use HTTP::Request;
use REST::Client;
use MIME::Base64;
use JSON;
# Global variable definitions
my $MAX_LUNS = 255; # Max LUNS per target on the iSCSI server
my $freenas_server_list = {}; # Per-host state: {client, product_name, api_version, dev_prefix, runaway_count}
my $freenas_rest_connection = undef; # REST::Client for the current active host
my $freenas_global_config_list = {}; # Per-host iSCSI global config cache
my $freenas_global_config = undef; # Pointer to entry in $freenas_global_config_list
my $dev_prefix = "";
my $product_name = undef;
# FreeNAS API definitions
my $freenas_api_version = "v1.0"; # Default to v1.0 of the API's
my $freenas_api_methods = undef; # API Methods Nested HASH Ref
my $freenas_api_variables = undef; # API Variable Nested HASH Ref
my $truenas_version = undef;
my $truenas_release_type = "Production";
# FreeNAS/TrueNAS (CORE) API Versioning HashRef Matrix
my $freenas_api_version_matrix = {
"v1.0" => {
"methods" => {
"config" => {
"resource" => "/api/v1.0/services/iscsi/globalconfiguration/",
},
"target" => {
"resource" => "/api/v1.0/services/iscsi/target/",
},
"extent" => {
"resource" => "/api/v1.0/services/iscsi/extent/",
"post_body" => {
"iscsi_target_extent_type" => "Disk",
"iscsi_target_extent_name" => "\$name",
"iscsi_target_extent_disk" => "\$device",
},
},
"targetextent" => {
"resource" => "/api/v1.0/services/iscsi/targettoextent/",
"post_body" => {
"iscsi_target" => "\$target_id",
"iscsi_extent" => "\$extent_id",
"iscsi_lunid" => "\$lun_id",
},
},
},
"variables" => {
"basename" => "iscsi_basename",
"lunid" => "iscsi_lunid",
"extentid" => "iscsi_extent",
"targetid" => "iscsi_target",
"extentpath" => "iscsi_target_extent_path",
"extentnaa" => "iscsi_target_extent_naa",
"targetname" => "iscsi_target_name",
}
},
"v2.0" => {
"methods" => {
"config" => {
"resource" => "/api/v2.0/iscsi/global",
},
"target" => {
"resource" => "/api/v2.0/iscsi/target/",
},
"extent" => {
"resource" => "/api/v2.0/iscsi/extent/",
"delete_body" => {
"remove" => \1,
"force" => \1,
},
"post_body" => {
"type" => "DISK",
"name" => "\$name",
"disk" => "\$device",
},
},
"targetextent" => {
"resource" => "/api/v2.0/iscsi/targetextent/",
"delete_body" => {
"force" => \1,
},
"post_body" => {
"target" => "\$target_id",
"extent" => "\$extent_id",
"lunid" => "\$lun_id",
},
},
},
"variables" => {
"basename" => "basename",
"lunid" => "lunid",
"extentid" => "extent",
"targetid" => "target",
"extentpath" => "path",
"extentnaa" => "naa",
"targetname" => "name",
},
},
};
#
#
#
sub get_base {
return '/dev/zvol';
}
#
# Subroutine called from ZFSPlugin.pm
#
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
syslog("info",(caller(0))[3] . " : $method(@params)");
if (defined($scfg->{'truenas_token_auth'}) && $scfg->{'truenas_token_auth'}) {
if (!defined($scfg->{'truenas_secret'})) {
die "Undefined `truenas_secret` variable.";
}
} elsif (!defined($scfg->{'freenas_user'}) || !defined($scfg->{'freenas_password'})) {
die "Undefined `freenas_user` and/or `freenas_password` variables.";
}
if (!defined $freenas_server_list->{defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal}}) {
freenas_api_check($scfg);
}
if($method eq "create_lu") {
return run_create_lu($scfg, $timeout, $method, @params);
}
if($method eq "delete_lu") {
return run_delete_lu($scfg, $timeout, $method, @params);
}
if($method eq "import_lu") {
return run_create_lu($scfg, $timeout, $method, @params);
}
if($method eq "modify_lu") {
return run_modify_lu($scfg, $timeout, $method, @params);
}
if($method eq "add_view") {
return run_add_view($scfg, $timeout, $method, @params);
}
if($method eq "list_view") {
return run_list_view($scfg, $timeout, $method, @params);
}
if($method eq "list_extent") {
return run_list_extent($scfg, $timeout, $method, @params);
}
if($method eq "list_lu") {
return run_list_lu($scfg, $timeout, $method, "name", @params);
}
syslog("error",(caller(0))[3] . " : unknown method $method");
return;
}
#
#
#
sub run_add_view {
return '';
}
#
# a modify_lu occur by example on a zvol resize. we just need to destroy and recreate the lun with the same zvol.
# Be careful, the first param is the new size of the zvol, we must shift params
#
sub run_modify_lu {
my ($scfg, $timeout, $method, @params) = @_;
syslog("info", (caller(0))[3] . " : called");
shift(@params);
run_delete_lu($scfg, $timeout, $method, @params);
return run_create_lu($scfg, $timeout, $method, @params);
}
#
#
#
sub run_list_view {
my ($scfg, $timeout, $method, @params) = @_;
syslog("info", (caller(0))[3] . " : called");
return run_list_lu($scfg, $timeout, $method, "lun-id", @params);
}
#
#
# Optimized
sub run_list_lu {
my ($scfg, $timeout, $method, $result_value_type, @params) = @_;
my $object = $params[0];
my $result = undef;
my $luns = freenas_list_lu($scfg);
syslog("info", (caller(0))[3] . " : called with (method: '$method'; result_value_type: '$result_value_type'; param[0]: '$object')");
$object =~ s/^\Q$dev_prefix//;
syslog("info", (caller(0))[3] . " : TrueNAS object to find: '$object'");
if (defined($luns->{$object})) {
my $lu_object = $luns->{$object};
$result = $result_value_type eq "lun-id" ? $lu_object->{$freenas_api_variables->{'lunid'}} : $dev_prefix . $lu_object->{$freenas_api_variables->{'extentpath'}};
syslog("info",(caller(0))[3] . " '$object' with key '$result_value_type' found with value: '$result'");
} else {
syslog("info", (caller(0))[3] . " '$object' with key '$result_value_type' was not found");
}
return $result;
}
#
#
# Optimzed
sub run_list_extent {
my ($scfg, $timeout, $method, @params) = @_;
my $object = $params[0];
syslog("info", (caller(0))[3] . " : called with (method: '$method'; params[0]: '$object')");
my $result = undef;
my $luns = freenas_list_lu($scfg);
$object =~ s/^\Q$dev_prefix//;
syslog("info", (caller(0))[3] . " TrueNAS object to find: '$object'");
if (defined($luns->{$object})) {
my $lu_object = $luns->{$object};
$result = $lu_object->{$freenas_api_variables->{'extentnaa'}};
syslog("info",(caller(0))[3] . " '$object' with key '$freenas_api_variables->{'extentnaa'}' found with value: '$result'");
} else {
syslog("info",(caller(0))[3] . " '$object' with key '$freenas_api_variables->{'extentnaa'}' was not found");
}
return $result;
}
#
#
#
sub run_create_lu {
my ($scfg, $timeout, $method, @params) = @_;
my $lun_path = $params[0];
syslog("info", (caller(0))[3] . " : called with (method=$method; param[0]=$lun_path)");
my $lun_id = freenas_get_first_available_lunid($scfg);
die "Maximum number of LUNs per target is $MAX_LUNS" if scalar $lun_id >= $MAX_LUNS;
die "$params[0]: LUN $lun_path exists" if defined(run_list_lu($scfg, $timeout, $method, "name", @params));
my $target_id = freenas_get_targetid($scfg);
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
my ($extent_id, $link_id);
eval {
my $extent = freenas_iscsi_create_extent($scfg, $lun_path);
die "Unable to create extent for $lun_path" if !defined($extent);
$extent_id = $extent->{'id'};
my $link = freenas_iscsi_create_target_to_extent($scfg, $target_id, $extent_id, $lun_id);
die "Unable to link extent to target for $lun_path" if !defined($link);
$link_id = $link->{'id'};
# Set FREENAS_TEST_ROLLBACK=1 in pvedaemon environment to exercise
# rollback without a real failure (#239)
die "TEST: forced rollback for #239 verification" if $ENV{FREENAS_TEST_ROLLBACK};
};
if (my $err = $@) {
syslog("err", (caller(0))[3] . " : rolling back after error for $lun_path: $err");
freenas_iscsi_remove_target_to_extent($scfg, $link_id) if defined $link_id;
freenas_iscsi_remove_extent($scfg, $extent_id) if defined $extent_id;
freenas_delete_zvol($scfg, $lun_path) if defined $extent_id;
die "Unable to create lun $lun_path (rolled back): $err";
}
syslog("info", (caller(0))[3] . "(lun_path=$lun_path, lun_id=$lun_id) : successful");
return "";
}
#
#
# Optimzied
sub run_delete_lu {
my ($scfg, $timeout, $method, @params) = @_;
my $lun_path = $params[0];
syslog("info", (caller(0))[3] . " : called with (method: '$method'; param[0]: '$lun_path')");
my $luns = freenas_list_lu($scfg);
my $lun = undef;
my $link = undef;
$lun_path =~ s/^\Q$dev_prefix//;
if (defined($luns->{$lun_path})) {
$lun = $luns->{$lun_path};
syslog("info",(caller(0))[3] . " lun: '$lun_path' found");
} else {
die "Unable to find the lun $lun_path for $scfg->{target}";
}
my $target_id = freenas_get_targetid($scfg);
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
# find the target to extent
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
syslog("info", (caller(0))[3] . " : searching for 'targetextent' with (target_id=$target_id; lun_id=$lun->{$freenas_api_variables->{'lunid'}}; extent_id=$lun->{id})");
foreach my $item (@$target2extents) {
if($item->{$freenas_api_variables->{'targetid'}} == $target_id &&
$item->{$freenas_api_variables->{'lunid'}} == $lun->{$freenas_api_variables->{'lunid'}} &&
$item->{$freenas_api_variables->{'extentid'}} == $lun->{'id'}) {
$link = $item;
syslog("info", (caller(0))[3] . " : found 'targetextent'(target_id=$item->{$freenas_api_variables->{'targetid'}}; lun_id=$item->{$freenas_api_variables->{'lunid'}}; extent_id=$item->{$freenas_api_variables->{'extentid'}})");
last;
}
}
die "Unable to find the link for the lun $lun_path for $scfg->{target}" if !defined($link);
# Remove the extent
my $remove_extent = freenas_iscsi_remove_extent($scfg, $lun->{'id'});
# Remove the link
my $remove_link = freenas_iscsi_remove_target_to_extent($scfg, $link->{'id'});
# NOTE: zvol deletion is intentionally left to ZFSPlugin's SSH path (zfs_delete_zvol).
# Deleting it here caused a double-delete: SSH would fail, ZFSPlugin's error recovery
# would call run_create_lu on a missing zvol, producing a false "Unable to create lun"
# error in the task log (#240). freenas_delete_zvol belongs only in run_create_lu's
# rollback path (#239).
if($remove_link == 1 && $remove_extent == 1) {
syslog("info", (caller(0))[3] . "(lun_path=$lun_path) : successful");
} else {
die "Unable to delete lun $lun_path";
}
return "";
}
sub freenas_api_connect {
my ($scfg, $ping) = @_;
$ping //= '/api/v1.0/system/version/';
syslog("info", (caller(0))[3] . " : called");
my $scheme = $scfg->{freenas_use_ssl} ? "https" : "http";
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
if (!defined $freenas_server_list->{$apihost} || !defined $freenas_server_list->{$apihost}{client}) {
$freenas_server_list->{$apihost} = {
client => REST::Client->new(),
product_name => undef,
api_version => 'v1.0',
dev_prefix => '',
runaway_count => 0,
};
}
my $host = $freenas_server_list->{$apihost};
$host->{client}->setHost($scheme . '://' . $apihost);
$host->{client}->addHeader('Content-Type', 'application/json');
if (defined($scfg->{'truenas_token_auth'}) && $scfg->{'truenas_token_auth'}) {
syslog("info", (caller(0))[3] . " : Authentication using Bearer Token Auth");
if (!$scfg->{freenas_use_ssl}) {
syslog("warn", (caller(0))[3] . " : Bearer Token used without SSL — TrueNAS 25.04+ revokes API keys sent over plain HTTP; enable 'Use SSL' in storage config");
}
$host->{client}->addHeader('Authorization', 'Bearer ' . $scfg->{truenas_secret});
} else {
syslog("warn", (caller(0))[3] . " : Authentication using Basic Auth (username/password) -- DEPRECATED. Generate an API key in TrueNAS and switch to token auth. Basic auth will be removed in v3.0.0.");
$host->{client}->addHeader('Authorization', 'Basic ' . encode_base64($scfg->{freenas_user} . ':' . $scfg->{freenas_password}));
}
# If using SSL, don't verify SSL certs
if ($scfg->{freenas_use_ssl}) {
$host->{client}->getUseragent()->ssl_opts(verify_hostname => 0);
$host->{client}->getUseragent()->ssl_opts(SSL_verify_mode => SSL_VERIFY_NONE);
}
# Check if the APIs are accessible via the selected host and scheme
my $api_response = $host->{client}->request('GET', $ping);
my $code = $api_response->responseCode();
my $type = $api_response->responseHeader('Content-Type');
syslog("info", (caller(0))[3] . " : REST connection header Content-Type:'" . $type . "'");
# Make sure we are not recursion calling.
if ($host->{runaway_count} > 2) {
freenas_api_log_error($host->{client});
die "Loop recursion prevention";
# Successful connection
} elsif ($code == 200 && ($type =~ /^text\/plain/ || $type =~ /^application\/json/)) {
syslog("info", (caller(0))[3] . " : REST connection successful to '" . $apihost . "' using the '" . $scheme . "' protocol");
$host->{runaway_count} = 0;
# A 302 or 200 (wrong content-type) means v1.0 is redirecting — upgrade to v2.0.
# TrueNAS 25.04+ removed v1.0 entirely and returns 404 instead of 302; treat
# that the same way so long as we are still probing the v1.0 endpoint.
} elsif ($code == 302 || $code == 200 || ($code == 404 && $ping =~ /v1\.0/)) {
syslog("info", (caller(0))[3] . " : v1.0 API unavailable (HTTP $code) — upgrading to v2.0");
$host->{runaway_count}++;
$ping =~ s/v1\.0/v2\.0/;
freenas_api_connect($scfg, $ping);
# A 307 from FreeNAS means redirect http to https.
} elsif ($code == 307) {
syslog("info", (caller(0))[3] . " : Redirecting to HTTPS protocol");
$host->{runaway_count}++;
$scfg->{freenas_use_ssl} = 1;
freenas_api_connect($scfg, $ping);
# For now, any other code we fail.
} else {
freenas_api_log_error($host->{client});
die "Unable to connect to the FreeNAS API service at '" . $apihost . "' using the '" . $scheme . "' protocol";
}
$freenas_rest_connection = $host->{client};
return;
}
#
# Check to see what FreeNAS version we are running and set
# the FreeNAS.pm to use the correct API version of FreeNAS
#
sub freenas_api_check {
my ($scfg, $timeout) = @_;
my $result = {};
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
syslog("info", (caller(0))[3] . " : called");
if (!defined $freenas_server_list->{$apihost}{client}) {
freenas_api_connect($scfg);
$result = $freenas_rest_connection->responseContent();
$result =~ s/"//g;
syslog("info", (caller(0))[3] . " : successful : Server version: " . $result);
if ($result =~ /^(TrueNAS|FreeNAS)-(\d+)\.(\d+)\-U(\d+)(?(?=\.)\.(\d+))$/) {
$product_name = $1;
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $4 || 0, $5 || 0);
} elsif ($result =~ /^(TrueNAS)-(\d+)\.(\d+)(?(?=\-U\d+)-U(\d+)|-\w+)(?(?=\.).(\d+))$/) {
$product_name = $1;
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $4 || 0, $6 || 0);
$truenas_release_type = $5 || "Production";
} elsif ($result =~ /^(TrueNAS-SCALE)-(\d+)\.(\d+)(?(?=\-)-(\w+))\.(\d+)(?(?=\.)\.(\d+))(?(?=\-)-(\d+))$/) {
$product_name = $1;
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $5 || 0, $7 || 0);
$truenas_release_type = $4 || "Production";
} else {
$product_name = "Unknown";
$truenas_release_type = "Unknown";
syslog("error", (caller(0))[3] . " : Could not parse the version of TrueNAS.");
}
syslog("info", (caller(0))[3] . " : ". $product_name . " Unformatted Version: " . $truenas_version);
if ($truenas_version >= 11030100) {
$freenas_api_version = "v2.0";
$dev_prefix = "/dev/";
}
if ($truenas_release_type ne "Production") {
syslog("warn", (caller(0))[3] . " : The '" . $product_name . "' release type of '" . $truenas_release_type . "' may not worked due to unsupported changes.");
}
# Save per-host metadata so subsequent calls restore state correctly
$freenas_server_list->{$apihost}{product_name} = $product_name;
$freenas_server_list->{$apihost}{api_version} = $freenas_api_version;
$freenas_server_list->{$apihost}{dev_prefix} = $dev_prefix;
} else {
syslog("info", (caller(0))[3] . " : REST Client already initialized");
}
syslog("info", (caller(0))[3] . " : Using " . $product_name . " API version " . $freenas_api_version);
$freenas_api_methods = $freenas_api_version_matrix->{$freenas_api_version}->{'methods'};
$freenas_api_variables = $freenas_api_version_matrix->{$freenas_api_version}->{'variables'};
$freenas_global_config = $freenas_global_config_list->{$apihost} = (!defined($freenas_global_config_list->{$apihost})) ? freenas_iscsi_get_globalconfiguration($scfg) : $freenas_global_config_list->{$apihost};
return;
}
#
### FREENAS API CALLING ROUTINE ###
#
sub freenas_api_call {
my ($scfg, $method, $path, $data) = @_;
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
syslog("info", (caller(0))[3] . " : called for host '" . $apihost . "'");
$method = uc($method);
if ($method !~ /^(?:GET|DELETE|POST)$/) {
syslog("info", (caller(0))[3] . " : Invalid HTTP RESTful service method '$method'");
die "Invalid HTTP RESTful service method '$method' used.";
}
if (!defined $freenas_server_list->{$apihost}{client}) {
freenas_api_check($scfg);
}
my $host = $freenas_server_list->{$apihost};
$freenas_rest_connection = $host->{client};
$freenas_global_config = $freenas_global_config_list->{$apihost};
$product_name = $host->{product_name};
$dev_prefix = $host->{dev_prefix};
$freenas_api_methods = $freenas_api_version_matrix->{$host->{api_version}}{methods};
$freenas_api_variables = $freenas_api_version_matrix->{$host->{api_version}}{variables};
my $json_data = (defined $data) ? encode_json($data) : undef;
$freenas_rest_connection->request($method, $path, $json_data);
syslog("info", (caller(0))[3] . " : successful");
return;
}
#
# Writes the Response and Content to SysLog
#
sub freenas_api_log_error {
my ($rest_connection) = @_;
my $connection = ((defined $rest_connection) ? $rest_connection : $freenas_rest_connection);
syslog("info","[ERROR]FreeNAS::API::" . (caller(1))[3] . " : Response code: " . $connection->responseCode());
syslog("info","[ERROR]FreeNAS::API::" . (caller(1))[3] . " : Response content: " . $connection->responseContent());
return 1;
}
#
#
#
sub freenas_iscsi_get_globalconfiguration {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'config'}->{'resource'}, $freenas_api_methods->{'config'}->{'get'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", (caller(0))[3] . " : target_basename=" . $result->{$freenas_api_variables->{'basename'}});
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Returns a list of all extents.
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-extent-
#
sub freenas_iscsi_get_extent {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'extent'}->{'resource'} . "?limit=0", $freenas_api_methods->{'extent'}->{'get'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", (caller(0))[3] . " : successful");
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Create an extent on FreeNas
# http://api.freenas.org/resources/iscsi/index.html#create-resource
# Parameters:
# - target config (scfg)
# - lun_path
#
sub freenas_iscsi_create_extent {
my ($scfg, $lun_path) = @_;
syslog("info", (caller(0))[3] . " : called with (lun_path=$lun_path)");
my $name = $lun_path;
$name =~ s/^.*\///; # all from last /
my $pool = $scfg->{'pool'};
# If TrueNAS-SCALE the slashes (/) need to be converted to dashes (-)
if ($product_name eq "TrueNAS-SCALE") {
$pool =~ s/\//-/g;
syslog("info", (caller(0))[3] . " : TrueNAS-SCALE slash to dash conversion '" . $pool ."'");
}
$name = $pool . ($product_name eq "TrueNAS-SCALE" ? '-' : '/') . $name;
syslog("info", (caller(0))[3] . " : " . $product_name . " extent '". $name . "'");
my $device = $lun_path;
$device =~ s/^\/dev\///; # strip /dev/
my %extent_vars = ('$name' => $name, '$device' => $device);
my $post_body = {};
while ((my $key, my $value) = each %{$freenas_api_methods->{'extent'}->{'post_body'}}) {
$post_body->{$key} = exists $extent_vars{$value} ? $extent_vars{$value} : $value;
}
freenas_api_call($scfg, 'POST', $freenas_api_methods->{'extent'}->{'resource'}, $post_body);
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 201) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", "FreeNAS::API::create_extent(lun_path=" . $result->{$freenas_api_variables->{'extentpath'}} . ") : successful");
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Explicitly delete the underlying ZFS zvol via the pool/dataset API.
# The extent DELETE with remove:true is unreliable on TrueNAS SCALE 24.10+
# (extent config is removed but the zvol is left behind). This function
# is a belt-and-suspenders call after every extent removal.
# Only runs on v2.0; v1.0 hosts are expected to handle it via remove:true.
# Parameters:
# - scfg
# - lun_path (either /dev/zvol/tank/... or zvol/tank/... form)
#
sub freenas_delete_zvol {
my ($scfg, $lun_path) = @_;
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
my $api_version = ($freenas_server_list->{$apihost}{api_version} // $freenas_api_version) // '';
return 1 unless $api_version eq 'v2.0';
my $dataset = $lun_path;
$dataset =~ s{^/dev/zvol/}{}; # strip /dev/zvol/ prefix (run_create_lu form)
$dataset =~ s{^zvol/}{}; # strip zvol/ prefix (run_delete_lu form after $dev_prefix strip)
$dataset =~ s{/}{%2F}g; # URL-encode path separators for REST endpoint
syslog("info", (caller(0))[3] . " : explicitly deleting zvol dataset '$dataset'");
freenas_api_call($scfg, 'DELETE', "/api/v2.0/pool/dataset/id/$dataset/", {"recursive" => \0});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 204) {
syslog("info", (caller(0))[3] . " : zvol '$dataset' deleted successfully");
return 1;
} else {
syslog("err", (caller(0))[3] . " : WARNING: could not delete zvol '$dataset' (HTTP $code) — manual cleanup may be needed");
return 0;
}
}
# Parse a blocksize string (e.g. "8k", "16k", "8192") to bytes.
sub freenas_parse_blocksize {
my ($val) = @_;
return 0 unless defined $val && $val ne '';
return $1 * 1024 if $val =~ /^(\d+)\s*[kK]$/;
return $1 * 1024 * 1024 if $val =~ /^(\d+)\s*[mM]$/;
return $val + 0;
}
# Returns the recommended ZFS volblocksize for this TrueNAS host based on its
# product type. TrueNAS SCALE ships a newer ZFS that requires >= 16k; CORE is
# fine with 8k. Returns undef if the API is unreachable so callers can fall
# back to the user-configured value or the ZFS default.
sub freenas_get_recommended_blocksize {
my ($scfg) = @_;
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
# freenas_api_check (not freenas_api_connect) must be used here because
# product_name is only parsed and cached by freenas_api_check.
# freenas_api_connect only sets up the HTTP client.
eval { freenas_api_check($scfg) };
if ($@) {
syslog("warn", (caller(0))[3] . " : unable to connect to TrueNAS API for blocksize detection: $@");
return;
}
my $product = $freenas_server_list->{$apihost}{product_name} // '';
if ($product =~ /SCALE/i) {
syslog("info", (caller(0))[3] . " : detected TrueNAS SCALE — recommending blocksize 16384");
return 16384;
}
syslog("info", (caller(0))[3] . " : detected '$product' — recommending blocksize 8192");
return 8192;
}
#
# Remove an extent by it's id
# http://api.freenas.org/resources/iscsi/index.html#delete-resource
# Parameters:
# - scfg
# - extent_id
#
sub freenas_iscsi_remove_extent {
my ($scfg, $extent_id) = @_;
syslog("info", (caller(0))[3] . " : called with (extent_id=$extent_id)");
freenas_api_call($scfg, 'DELETE', $freenas_api_methods->{'extent'}->{'resource'} . (($freenas_api_version eq "v2.0") ? "id/" : "") . "$extent_id/", $freenas_api_methods->{'extent'}->{'delete_body'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 204) {
syslog("info", (caller(0))[3] . "(extent_id=$extent_id) : successful");
return 1;
} else {
freenas_api_log_error();
return 0;
}
}
#
# Returns a list of all targets
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-target-
#
sub freenas_iscsi_get_target {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'target'}->{'resource'} . "?limit=0", $freenas_api_methods->{'target'}->{'get'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", (caller(0))[3] . " : successful");
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Returns a list of associated extents to targets
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-targettoextent-
#
sub freenas_iscsi_get_target_to_extent {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'targetextent'}->{'resource'} . "?limit=0", $freenas_api_methods->{'targetextent'}->{'get'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", (caller(0))[3] . " : successful");
# If 'iscsi_lunid' is undef then it is set to 'Auto' in FreeNAS
# which should be '0' in our eyes.
# This gave Proxmox 5.x and FreeNAS 11.1 a few issues.
foreach my $item (@$result) {
if (!defined($item->{$freenas_api_variables->{'lunid'}})) {
$item->{$freenas_api_variables->{'lunid'}} = 0;
syslog("info", (caller(0))[3] . " : change undef iscsi_lunid to 0");
}
}
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Associate a FreeNas extent to a FreeNas Target
# http://api.freenas.org/resources/iscsi/index.html#post--api-v1.0-services-iscsi-targettoextent-
# Parameters:
# - target config (scfg)
# - FreeNas Target ID
# - FreeNas Extent ID
# - Lun ID
#
sub freenas_iscsi_create_target_to_extent {
my ($scfg, $target_id, $extent_id, $lun_id) = @_;
syslog("info", (caller(0))[3] . " : called with (target_id=$target_id, extent_id=$extent_id, lun_id=$lun_id)");
my %tte_vars = ('$target_id' => $target_id, '$extent_id' => $extent_id, '$lun_id' => $lun_id);
my $post_body = {};
while ((my $key, my $value) = each %{$freenas_api_methods->{'targetextent'}->{'post_body'}}) {
$post_body->{$key} = exists $tte_vars{$value} ? $tte_vars{$value} : $value;
}
freenas_api_call($scfg, 'POST', $freenas_api_methods->{'targetextent'}->{'resource'}, $post_body);
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 201) {
my $result = decode_json($freenas_rest_connection->responseContent());
syslog("info", (caller(0))[3] . "(target_id=$target_id, extent_id=$extent_id, lun_id=$lun_id) : successful");
return $result;
} else {
freenas_api_log_error();
return;
}
}
#
# Remove a Target to extent by it's id
# http://api.freenas.org/resources/iscsi/index.html#delete--api-v1.0-services-iscsi-targettoextent-(int-id)-
# Parameters:
# - scfg
# - link_id
#
sub freenas_iscsi_remove_target_to_extent {
my ($scfg, $link_id) = @_;
syslog("info", (caller(0))[3] . " : called with (link_id=$link_id)");
if ($freenas_api_version eq "v2.0") {
syslog("info", (caller(0))[3] . "(link_id=$link_id) : V2.0 API's so NOT Needed...successful");
return 1;
}
freenas_api_call($scfg, 'DELETE', $freenas_api_methods->{'targetextent'}->{'resource'} . (($freenas_api_version eq "v2.0") ? "id/" : "") . "$link_id/", $freenas_api_methods->{'targetextent'}->{'delete_body'});
my $code = $freenas_rest_connection->responseCode();
if ($code == 200 || $code == 204) {
syslog("info", (caller(0))[3] . "(link_id=$link_id) : successful");
return 1;
} else {
freenas_api_log_error();
return 0;
}
}
#
# Returns all luns associated to the current target defined by $scfg->{target}
# This method returns an array reference like "freenas_iscsi_get_extent" do
# but with an additionnal hash entry "iscsi_lunid" retrieved from "freenas_iscsi_get_target_to_extent"
#
sub freenas_list_lu {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
my $targets = freenas_iscsi_get_target($scfg);
my $target_id = freenas_get_targetid($scfg);
my %lun_hash;
my $iscsi_lunid = undef;
if(defined($target_id)) {
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
my $extents = freenas_iscsi_get_extent($scfg);
foreach my $item (@$target2extents) {
if($item->{$freenas_api_variables->{'targetid'}} == $target_id) {
foreach my $node (@$extents) {
if($node->{'id'} == $item->{$freenas_api_variables->{'extentid'}}) {
if ($item->{$freenas_api_variables->{'lunid'}} =~ /(\d+)/) {
if (defined($node)) {
$node->{$freenas_api_variables->{'lunid'}} .= "$1";
$lun_hash{$node->{$freenas_api_variables->{'extentpath'}}} = $node;
}
last;
} else {
syslog("warn", (caller(0))[3] . " : iscsi_lunid did not pass tainted testing");
}
}
}
}
}
}
syslog("info", (caller(0))[3] . " : successful");
return \%lun_hash;
}
#
# Returns the first available "lunid" (in all targets namespaces)
#
sub freenas_get_first_available_lunid {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
my $target_id = freenas_get_targetid($scfg);
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
my @luns = ();
foreach my $item (@$target2extents) {
push(@luns, $item->{$freenas_api_variables->{'lunid'}}) if ($item->{$freenas_api_variables->{'targetid'}} == $target_id);
}
my @sorted_luns = sort {$a <=> $b} @luns;
my $lun_id = 0;
# find the first hole, if not, give the +1 of the last lun
foreach my $lun (@sorted_luns) {
last if $lun != $lun_id;
$lun_id = $lun_id + 1;
}
syslog("info", (caller(0))[3] . " : $lun_id");
return $lun_id;
}
#
# Returns the target id on FreeNas of the currently configured target of this PVE storage
#
sub freenas_get_targetid {
my ($scfg) = @_;
syslog("info", (caller(0))[3] . " : called");
my $targets = freenas_iscsi_get_target($scfg);
my $target_id = undef;
foreach my $target (@$targets) {
my $iqn = $freenas_global_config->{$freenas_api_variables->{'basename'}} . ':' . $target->{$freenas_api_variables->{'targetname'}};
if($iqn eq $scfg->{target}) {
$target_id = $target->{'id'};
last;
}
}
syslog("info", (caller(0))[3] . " : successful : $target_id");
return $target_id;
}
1;

View File

@ -1,140 +0,0 @@
--- ZFSPlugin.pm.orig 2018-10-03 11:02:46.000000000 -0400
+++ ZFSPlugin.pm 2018-10-28 21:27:44.794732848 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -32,7 +33,7 @@
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +41,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +65,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +165,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +192,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +237,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +273,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,10 +0,0 @@
--- ZFSPlugin-5.0-33.pm 2018-12-15 09:29:56.586101170 -0500
+++ ZFSPlugin.pm 2018-12-15 09:24:08.393984276 -0500
@@ -27,6 +27,7 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2018-11-27 07:23:15.000000000 -0500
+++ ZFSPlugin.pm 2018-12-15 09:24:08.393984276 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin-5.0-34.pm 2018-12-21 12:52:21.764194866 -0500
+++ ZFSPlugin.pm 2018-12-21 12:53:08.663665081 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-02-07 09:14:12.000000000 -0500
+++ ZFSPlugin.pm 2019-02-26 20:53:29.096598801 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-04-30 09:54:55.000000000 -0400
+++ ZFSPlugin.pm 2019-05-11 10:32:36.326734352 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-06-25 06:54:50.000000000 -0400
+++ ZFSPlugin.pm 2019-07-16 13:36:48.817831174 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-07-25 07:34:52.000000000 -0400
+++ ZFSPlugin.pm 2019-08-07 13:28:29.268383007 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-09-03 04:24:37.000000000 -0400
+++ ZFSPlugin.pm 2019-09-22 13:54:51.570048336 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-09-23 12:17:37.000000000 -0400
+++ ZFSPlugin.pm 2019-10-13 09:31:58.780554103 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -162,6 +168,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -180,6 +195,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -207,14 +240,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -239,6 +276,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2022-02-04 12:08:01.000000000 -0500
+++ ZFSPlugin.pm 2022-03-26 13:51:40.660068908 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -166,6 +172,15 @@
die "lun_number for guid $guid is not a number";
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -184,6 +199,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -211,14 +244,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -243,6 +280,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `/usr/sbin/multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,157 +0,0 @@
--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500
+++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -166,6 +172,15 @@
die "lun_number for guid $guid is not a number";
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -184,6 +199,32 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password (Deprecated)",
+ type => 'string',
+ },
+ truenas_secret => {
+ description => "TrueNAS API Secret",
+ type => 'string',
+ },
+ truenas_token_auth => {
+ description => "TrueNAS API Authentication with Token",
+ type => 'boolean',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -211,14 +252,20 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ truenas_secret => { optional => 1 },
+ truenas_token_auth => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -243,6 +290,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `/usr/sbin/multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,157 +0,0 @@
--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500
+++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -166,6 +172,15 @@
die "lun_number for guid $guid is not a number";
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -184,6 +199,32 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password (Deprecated)",
+ type => 'string',
+ },
+ truenas_secret => {
+ description => "TrueNAS API Secret",
+ type => 'string',
+ },
+ truenas_token_auth => {
+ description => "TrueNAS API Authentication with Token",
+ type => 'boolean',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -211,14 +252,20 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ truenas_secret => { optional => 1 },
+ truenas_token_auth => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -243,6 +290,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `/usr/sbin/multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,549 +0,0 @@
package REST::Client;
=head1 NAME
REST::Client - A simple client for interacting with RESTful http/https resources
=head1 SYNOPSIS
use REST::Client;
#The basic use case
my $client = REST::Client->new();
$client->GET('http://example.com/dir/file.xml');
print $client->responseContent();
#A host can be set for convienience
$client->setHost('http://example.com');
$client->PUT('/dir/file.xml', '<example>new content</example>');
if( $client->responseCode() eq '200' ){
print "Updated\n";
}
#custom request headers may be added
$client->addHeader('CustomHeader', 'Value');
#response headers may be gathered
print $client->responseHeader('ResponseHeader');
#X509 client authentication
$client->setCert('/path/to/ssl.crt');
$client->setKey('/path/to/ssl.key');
#add a CA to verify server certificates
$client->setCa('/path/to/ca.file');
#you may set a timeout on requests, in seconds
$client->setTimeout(10);
#options may be passed as well as set
$client = REST::Client->new({
host => 'https://example.com',
cert => '/path/to/ssl.crt',
key => '/path/to/ssl.key',
ca => '/path/to/ca.file',
timeout => 10,
});
$client->GET('/dir/file', {CustomHeader => 'Value'});
# Requests can be specificed directly as well
$client->request('GET', '/dir/file', 'request body content', {CustomHeader => 'Value'});
# Requests can optionally automatically follow redirects and auth, defaults to
# false
$client->setFollow(1);
#It is possible to access the L<LWP::UserAgent> object REST::Client is using to
#make requests, and set advanced options on it, for instance:
$client->getUseragent()->proxy(['http'], 'http://proxy.example.com/');
# request responses can be written directly to a file
$client->setContentFile( "FileName" );
# or call back method
$client->setContentFile( \&callback_method );
# see LWP::UserAgent for how to define callback methods
=head1 DESCRIPTION
REST::Client provides a simple way to interact with HTTP RESTful resources.
=cut
=head1 METHODS
=cut
use strict;
use warnings;
use 5.008_000;
use constant TRUE => 1;
use constant FALSE => 0;
our ($VERSION) = ('$Rev: 273 $' =~ /(\d+)/);
use URI;
use LWP::UserAgent;
use Carp qw(croak carp);
=head2 Construction and setup
=head3 new ( [%$config] )
Construct a new REST::Client. Takes an optional hash or hash reference or
config flags. Each config flag also has get/set accessors of the form
getHost/setHost, getUseragent/setUseragent, etc. These can be called on the
instantiated object to change or check values.
The config flags are:
=over 4
=item host
A default host that will be prepended to all requests. Allows you to just
specify the path when making requests.
The default is undef - you must include the host in your requests.
=item timeout
A timeout in seconds for requests made with the client. After the timeout the
client will return a 500.
The default is 5 minutes.
=item cert
The path to a X509 certificate file to be used for client authentication.
The default is to not use a certificate/key pair.
=item key
The path to a X509 key file to be used for client authentication.
The default is to not use a certificate/key pair.
=item ca
The path to a certificate authority file to be used to verify host
certificates.
The default is to not use a certificates authority.
=item pkcs12
The path to a PKCS12 certificate to be used for client authentication.
=item pkcs12password
The password for the PKCS12 certificate specified with 'pkcs12'.
=item follow
Boolean that determins whether REST::Client attempts to automatically follow
redirects/authentication.
The default is false.
=item useragent
An L<LWP::UserAgent> object, ready to make http requests.
REST::Client will provide a default for you if you do not set this.
=back
=cut
sub new {
my $class = shift;
my $config;
$class->_buildAccessors();
if(ref $_[0] eq 'HASH'){
$config = shift;
}elsif(scalar @_ && scalar @_ % 2 == 0){
$config = {@_};
}else{
$config = {};
}
my $self = bless({}, $class);
$self->{'_config'} = $config;
$self->_buildUseragent();
return $self;
}
=head3 addHeader ( $header_name, $value )
Add a custom header to any requests made by this client.
=cut
sub addHeader {
my $self = shift;
my $header = shift;
my $value = shift;
my $headers = $self->{'_headers'} || {};
$headers->{$header} = $value;
$self->{'_headers'} = $headers;
return;
}
=head3 buildQuery ( [...] )
A convienience wrapper around URI::query_form for building query strings from a
variety of data structures. See L<URI>
Returns a scalar query string for use in URLs.
=cut
sub buildQuery {
my $self = shift;
my $uri = URI->new();
$uri->query_form(@_);
return $uri->as_string();
}
=head2 Request Methods
Each of these methods makes an HTTP request, sets the internal state of the
object, and returns the object.
They can be combined with the response methods, such as:
print $client->GET('/search/?q=foobar')->responseContent();
=head3 GET ( $url, [$body_content, %$headers] )
Preform an HTTP GET to the resource specified. Takes an optional hashref of custom request headers.
=cut
sub GET {
my $self = shift;
return $self->request('GET', @_);
}
=head3 PUT ($url, [$body_content, %$headers] )
Preform an HTTP PUT to the resource specified. Takes an optional body content and hashref of custom request headers.
=cut
sub PUT {
my $self = shift;
return $self->request('PUT', @_);
}
=head3 PATCH ( $url, [$body_content, %$headers] )
Preform an HTTP PATCH to the resource specified. Takes an optional body content and hashref of custom request headers.
=cut
sub PATCH {
my $self = shift;
return $self->request('PATCH', @_);
}
=head3 POST ( $url, [$body_content, %$headers] )
Preform an HTTP POST to the resource specified. Takes an optional body content and hashref of custom request headers.
=cut
sub POST {
my $self = shift;
return $self->request('POST', @_);
}
=head3 DELETE ( $url, [$body_content, %$headers] )
Preform an HTTP DELETE to the resource specified. Takes an optional hashref of custom request headers.
=cut
sub DELETE {
my $self = shift;
return $self->request('DELETE', @_);
}
=head3 OPTIONS ( $url, [%$headers] )
Preform an HTTP OPTIONS to the resource specified. Takes an optional hashref of custom request headers.
=cut
sub OPTIONS {
my $self = shift;
my $url = shift;
my $headers = shift;
return $self->request('OPTIONS', $url, undef, $headers);
}
=head3 HEAD ( $url, [%$headers] )
Preform an HTTP HEAD to the resource specified. Takes an optional hashref of custom request headers.
=cut
sub HEAD {
my $self = shift;
my $url = shift;
my $headers = shift;
return $self->request('HEAD', $url, undef, $headers);
}
=head3 request ( $method, $url, [$body_content, %$headers] )
Issue a custom request, providing all possible values.
=cut
sub request {
my $self = shift;
my $method = shift;
my $url = shift;
my $content = shift;
my $headers = shift;
$self->{'_res'} = undef;
$self->_buildUseragent();
#error check
croak "REST::Client exception: First argument to request must be one of GET, PATCH, PUT, POST, DELETE, OPTIONS, HEAD" unless $method =~ /^(get|patch|put|post|delete|options|head)$/i;
croak "REST::Client exception: Must provide a url to $method" unless $url;
croak "REST::Client exception: headers must be presented as a hashref" if $headers && ref $headers ne 'HASH';
$url = $self->_prepareURL($url);
my $ua = $self->getUseragent();
if(defined $self->getTimeout()){
$ua->timeout($self->getTimeout);
}else{
$ua->timeout(300);
}
my $req = HTTP::Request->new( $method => $url );
#build headers
if(defined $content && length($content)){
$req->content($content);
$req->header('Content-Length', length($content));
}else{
$req->header('Content-Length', 0);
}
my $custom_headers = $self->{'_headers'} || {};
for my $header (keys %$custom_headers){
$req->header($header, $custom_headers->{$header});
}
for my $header (keys %$headers){
$req->header($header, $headers->{$header});
}
#prime LWP with ssl certfile if we have values
if($self->getCert){
carp "REST::Client exception: Certs defined but not using https" unless $url =~ /^https/;
croak "REST::Client exception: Cannot read cert and key file" unless -f $self->getCert && -f $self->getKey;
$ua->ssl_opts(SSL_cert_file => $self->getCert);
$ua->ssl_opts(SSL_key_file => $self->getKey);
}
#prime LWP with CA file if we have one
if(my $ca = $self->getCa){
croak "REST::Client exception: Cannot read CA file" unless -f $ca;
$ua->ssl_opts(SSL_ca_file => $ca);
}
#prime LWP with PKCS12 certificate if we have one
if($self->getPkcs12){
carp "REST::Client exception: PKCS12 cert defined but not using https" unless $url =~ /^https/;
croak "REST::Client exception: Cannot read PKCS12 cert" unless -f $self->getPkcs12;
$ENV{HTTPS_PKCS12_FILE} = $self->getPkcs12;
if($self->getPkcs12password){
$ENV{HTTPS_PKCS12_PASSWORD} = $self->getPkcs12password;
}
}
my $res = $self->getFollow ?
$ua->request( $req, $self->getContentFile ) :
$ua->simple_request( $req, $self->getContentFile );
$self->{_res} = $res;
return $self;
}
=head2 Response Methods
Use these methods to gather information about the last requset
performed.
=head3 responseCode ()
Return the HTTP response code of the last request
=cut
sub responseCode {
my $self = shift;
return $self->{_res}->code;
}
=head3 responseContent ()
Return the response body content of the last request
=cut
sub responseContent {
my $self = shift;
return $self->{_res}->content;
}
=head3 responseHeaders()
Returns a list of HTTP header names from the last response
=cut
sub responseHeaders {
my $self = shift;
return $self->{_res}->headers()->header_field_names();
}
=head3 responseHeader ( $header )
Return a HTTP header from the last response
=cut
sub responseHeader {
my $self = shift;
my $header = shift;
croak "REST::Client exception: no header provided to responseHeader" unless $header;
return $self->{_res}->header($header);
}
=head3 responseXpath ()
A convienience wrapper that returns a L<XML::LibXML> xpath context for the body content. Assumes the content is XML.
=cut
sub responseXpath {
my $self = shift;
require XML::LibXML;
my $xml= XML::LibXML->new();
$xml->load_ext_dtd(0);
if($self->responseHeader('Content-type') =~ /html/){
return XML::LibXML::XPathContext->new($xml->parse_html_string( $self->responseContent() ));
}else{
return XML::LibXML::XPathContext->new($xml->parse_string( $self->responseContent() ));
}
}
# Private methods
sub _prepareURL {
my $self = shift;
my $url = shift;
my $host = $self->getHost;
if($host){
$url = '/'.$url unless $url =~ /^\//;
$url = $host . $url;
}
unless($url =~ /^\w+:\/\//){
$url = ($self->getCert ? 'https://' : 'http://') . $url;
}
return $url;
}
sub _buildUseragent {
my $self = shift;
return if $self->getUseragent();
my $ua = LWP::UserAgent->new;
$ua->agent("REST::Client/$VERSION");
$self->setUseragent($ua);
return;
}
sub _buildAccessors {
my $self = shift;
return if $self->can('setHost');
my @attributes = qw(Host Key Cert Ca Timeout Follow Useragent Pkcs12 Pkcs12password ContentFile);
for my $attribute (@attributes){
my $set_method = "
sub {
my \$self = shift;
\$self->{'_config'}{lc('$attribute')} = shift;
return \$self->{'_config'}{lc('$attribute')};
}";
my $get_method = "
sub {
my \$self = shift;
return \$self->{'_config'}{lc('$attribute')};
}";
{
no strict 'refs';
*{'REST::Client::set'.$attribute} = eval $set_method ;
*{'REST::Client::get'.$attribute} = eval $get_method ;
}
}
return;
}
1;
=head1 TODO
Caching, content-type negotiation, readable handles for body content.
=head1 AUTHOR
Miles Crawford, E<lt>mcrawfor@cpan.orgE<gt>
=head1 COPYRIGHT
Copyright 2008 - 2010 by Miles Crawford
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut

View File

@ -1 +0,0 @@
/apidoc.js

View File

@ -1,66 +0,0 @@
--- apidoc.js.orig 2018-08-19 10:18:11.715767285 -0400
+++ apidoc.js.new 2018-08-19 10:17:09.364282178 -0400
@@ -27584,6 +27584,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"content" : {
"description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n",
"format" : "pve-storage-content-list",
@@ -27948,6 +27973,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"content" : {
"description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n",

View File

@ -1,79 +0,0 @@
--- apidoc-5.2-9.js 2018-11-10 14:28:44.022355849 -0500
+++ apidoc.js 2018-11-10 15:49:38.873720011 -0500
@@ -32448,6 +32448,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -32604,6 +32629,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -32810,6 +32841,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc-5.3-1.js 2018-12-21 12:46:55.459865478 -0500
+++ apidoc.js 2018-12-06 15:50:21.487987807 -0500
@@ -33477,6 +33477,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -33633,6 +33658,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -33840,6 +33871,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2019-02-20 06:20:05.000000000 -0500
+++ apidoc.js 2019-02-26 20:51:26.857204965 -0500
@@ -33709,6 +33709,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -33865,6 +33890,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -34071,6 +34102,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2019-04-08 02:02:09.000000000 -0400
+++ apidoc.js 2019-05-11 10:34:28.305764709 -0400
@@ -34385,6 +34385,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -34541,6 +34566,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -34748,6 +34779,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2019-07-15 15:45:00.000000000 -0400
+++ apidoc.js 2019-08-07 13:28:29.292382229 -0400
@@ -35001,6 +35001,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -35157,6 +35182,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -35362,6 +35393,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2021-11-15 10:07:34.000000000 -0500
+++ apidoc.js 2021-12-06 08:04:01.648822707 -0500
@@ -44064,6 +44064,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -44275,6 +44300,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -44547,6 +44578,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,91 +0,0 @@
--- apidoc.js.orig 2024-01-06 13:02:06.730512378 -0500
+++ apidoc.js 2024-01-06 13:02:55.349787105 -0500
@@ -50336,6 +50336,37 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access (Deprecated)",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "truenas_secret" : {
+ "description" : "TrueNAS Secret for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -50555,6 +50586,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -50854,6 +50891,37 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access (Deprecated)",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "truenas_secret" : {
+ "description" : "TrueNAS secret for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,91 +0,0 @@
--- apidoc.js.orig 2023-08-16 06:03:55.000000000 -0400
+++ apidoc.js 2023-12-26 14:45:47.202566775 -0500
@@ -47579,6 +47579,37 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access (Deprecated)",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "truenas_secret" : {
+ "description" : "TrueNAS Secret for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -47798,6 +47829,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -48097,6 +48134,37 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access (Deprecated)",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "truenas_secret" : {
+ "description" : "TrueNAS secret for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1 +0,0 @@
/pvemanagerlib.js

View File

@ -1,167 +0,0 @@
--- pvemanagerlib-5.2-10.js 2018-10-23 07:18:18.058603386 -0400
+++ pvemanagerlib.js 2018-10-23 15:29:52.573563883 -0400
@@ -5597,6 +5597,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -28583,6 +28584,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -28595,10 +28597,19 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ }
}
},
@@ -28616,6 +28627,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -28632,7 +28644,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -28646,7 +28658,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -28657,8 +28669,28 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: { hidden: '{!isFreeNAS}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ allowBlank: false,
+ fieldLabel: gettext('API Username'),
+ bind: { hidden: '{!isFreeNAS}' },
}
];
@@ -28689,7 +28721,7 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -28697,9 +28729,60 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ bind: { hidden: '{!isLIO}' },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API IPv4 Host'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ allowBlank: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,86 +0,0 @@
--- pvemanagerlib.js.orig 2018-07-30 23:13:28.045035059 -0400
+++ pvemanagerlib.js.new 2018-08-19 10:42:58.494724196 -0400
@@ -5379,6 +5379,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET']
]
@@ -27361,7 +27362,23 @@
value: '',
fieldLabel: gettext('Target group'),
allowBlank: true
- }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ checked: false,
+ uncheckedValue: 0,
+ hidden: me.iscsiprovider !== "freenas",
+ fieldLabel: gettext('FreeNAS API use SSL')
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'freenas_user',
+ value: '',
+ hidden: me.iscsiprovider !== "freenas",
+ fieldLabel: gettext('FreeNAS User'),
+ allowBlank: true
+ },
];
me.column2 = [
@@ -27370,7 +27387,26 @@
name: 'iscsiprovider',
value: 'comstar',
fieldLabel: gettext('iSCSI Provider'),
- allowBlank: false
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (value === "freenas") {
+ me.down('field[name=freenas_use_ssl]').setHidden(false);
+ me.down('field[name=freenas_apiv4_host]').setHidden(false);
+ me.down('field[name=freenas_user]').setHidden(false);
+ me.down('field[name=freenas_password]').setHidden(false);
+ } else {
+ me.down('field[name=freenas_use_ssl]').setHidden(true);
+ me.down('field[name=freenas_use_ssl]').setValue(false);
+ me.down('field[name=freenas_apiv4_host]').setHidden(true);
+ me.down('field[name=freenas_apiv4_host]').setValue(false);
+ me.down('field[name=freenas_user]').setHidden(true);
+ me.down('field[name=freenas_user]').setValue('');
+ me.down('field[name=freenas_password]').setHidden(true);
+ me.down('field[name=freenas_password]').setValue('');
+ }
+ }
+ }
},
{
xtype: 'proxmoxcheckbox',
@@ -27392,6 +27428,22 @@
value: '',
fieldLabel: gettext('Host group'),
allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'freenas_apiv4_host',
+ value: '',
+ hidden: me.iscsiprovider !== "freenas",
+ fieldLabel: gettext('API IPv4 Host'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'freenas_password',
+ value: '',
+ hidden: me.iscsiprovider !== "freenas",
+ fieldLabel: gettext('FreeNAS Password'),
+ allowBlank: true
}
];

View File

@ -1,102 +0,0 @@
--- pvemanagerlib.js.5.2-8.js 2018-08-30 20:10:52.957313992 -0400
+++ pvemanagerlib.js.new 2018-09-01 16:20:05.131908768 -0400
@@ -28578,6 +28578,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -28590,11 +28591,18 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
vm.set('isFreeNAS', newVal === 'freenas');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_password_field').setValue('');
+ }
}
},
@@ -28653,16 +28661,28 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: me.isCreate ? { hidden: '{!isComstar}' } : { hidden: '{!isComstar}' },
allowBlank: true
},
{
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: me.isCreate ? { hidden: '{!isFreeNAS}' } : { hidden: '{!isFreeNAS}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
value: '',
- fieldLabel: gettext('FreeNAS User'),
+ fieldLabel: gettext('API Username'),
bind: me.isCreate ? { hidden: '{!isFreeNAS}' } : { hidden: '{!isFreeNAS}' },
- allowBlank: true
+ allowBlank: me.isFreeNAS ? false : true
}
];
@@ -28693,7 +28713,7 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: me.isCreate ? { hidden: '{!isComstar}' } : { hidden: '{!isComstar}' },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -28701,9 +28721,27 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: me.isCreate ? { hidden: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ bind: me.isCreate ? { hidden: '{!isFreeNAS}' } : { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API IPv4 Host'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ value: '',
+ bind: me.isCreate ? { hidden: '{!isFreeNAS}' } : { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API Password'),
+ allowBlank: true
}
];

View File

@ -1,169 +0,0 @@
--- pvemanagerlib-5.2-9.js 2018-10-16 07:17:21.915593478 -0400
+++ pvemanagerlib.js 2018-10-21 21:52:20.781740237 -0400
@@ -5597,6 +5597,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -28574,6 +28575,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -28586,10 +28588,19 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ }
}
},
@@ -28607,6 +28618,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -28623,7 +28635,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -28637,7 +28649,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -28648,8 +28660,28 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: { hidden: '{!isFreeNAS}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ allowBlank: false,
+ fieldLabel: gettext('API Username'),
+ bind: { hidden: '{!isFreeNAS}' },
}
];
@@ -28680,7 +28712,7 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -28688,9 +28720,61 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: { hidden: '{!isLIO}' },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API IPv4 Host'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ allowBlank: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ allowBlank: false,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-02-20 13:40:43.000000000 -0500
+++ pvemanagerlib.js 2019-02-26 20:49:41.032327478 -0500
@@ -5920,6 +5920,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -30182,6 +30183,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -30194,10 +30196,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -30215,6 +30233,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -30231,7 +30250,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -30241,11 +30260,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -30256,8 +30275,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -30288,7 +30332,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -30296,9 +30342,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,172 +0,0 @@
--- pvemanagerlib-5.3-5.js 2018-12-21 12:34:35.727962756 -0500
+++ pvemanagerlib.js 2018-12-21 12:43:02.370462786 -0500
@@ -5854,6 +5854,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -29983,6 +29984,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -29995,10 +29997,19 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ }
}
},
@@ -30016,6 +30027,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -30032,7 +30044,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -30042,11 +30054,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -30057,8 +30069,28 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: { hidden: '{!isFreeNAS}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ allowBlank: false,
+ fieldLabel: gettext('API Username'),
+ bind: { hidden: '{!isFreeNAS}' },
}
];
@@ -30089,7 +30121,7 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: { hidden: '{!isComstar}' },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -30097,9 +30129,60 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ bind: { hidden: '{!isLIO}' },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API IPv4 Host'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ allowBlank: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: { hidden: '{!isFreeNAS}' },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,191 +0,0 @@
--- pvemanagerlib-5.3-6.js 2018-12-21 12:54:58.650421078 -0500
+++ pvemanagerlib.js 2019-02-01 13:52:17.906074280 -0500
@@ -5894,6 +5894,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -30085,6 +30086,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -30097,10 +30099,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -30118,6 +30136,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -30134,7 +30153,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -30144,11 +30163,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -30159,8 +30178,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -30191,7 +30235,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -30199,9 +30245,64 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-01-11 04:23:42.000000000 -0500
+++ pvemanagerlib.js 2019-02-05 07:30:18.168655803 -0500
@@ -5894,6 +5894,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -30085,6 +30086,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -30097,10 +30099,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -30118,6 +30136,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -30134,7 +30153,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -30144,11 +30163,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -30159,8 +30178,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -30191,7 +30235,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -30199,9 +30245,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-07-09 03:52:19.000000000 -0400
+++ pvemanagerlib.js 2019-07-16 13:59:37.658409977 -0400
@@ -6094,6 +6094,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -31700,6 +31701,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -31712,10 +31714,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -31733,6 +31751,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -31749,7 +31768,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -31759,11 +31778,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -31774,8 +31793,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -31806,7 +31850,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -31814,9 +31860,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-07-26 04:14:56.000000000 -0400
+++ pvemanagerlib.js 2019-08-05 16:02:38.484913553 -0400
@@ -6094,6 +6094,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -31722,6 +31723,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -31734,10 +31736,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -31755,6 +31773,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -31771,7 +31790,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -31781,11 +31800,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -31796,8 +31815,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -31828,7 +31872,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -31836,9 +31882,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-04-24 07:28:11.000000000 -0400
+++ pvemanagerlib.js 2019-05-11 10:33:38.130198999 -0400
@@ -6094,6 +6094,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -31483,6 +31484,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -31495,10 +31497,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -31516,6 +31534,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -31532,7 +31551,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -31542,11 +31561,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -31557,8 +31576,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -31589,7 +31633,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -31597,9 +31643,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-07-23 13:21:08.000000000 -0400
+++ pvemanagerlib.js 2019-08-07 13:28:29.304381840 -0400
@@ -6182,6 +6182,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -32977,6 +32978,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -32989,10 +32991,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -33010,6 +33028,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -33026,7 +33045,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -33036,11 +33055,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -33051,8 +33070,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -33083,7 +33127,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -33091,9 +33137,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2019-09-03 04:31:21.000000000 -0400
+++ pvemanagerlib.js 2019-09-22 13:54:51.602048049 -0400
@@ -6183,6 +6183,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
[ 'istgt', 'istgt'],
[ 'iet', 'IET'],
[ 'LIO', 'LIO']
@@ -32992,6 +32993,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true
}
},
@@ -33004,10 +33006,26 @@
}
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
}
},
@@ -33025,6 +33043,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -33041,7 +33060,7 @@
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -33051,11 +33070,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -33066,8 +33085,33 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL')
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ }
}
];
@@ -33098,7 +33142,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true
},
@@ -33106,9 +33152,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group')
+ bind: {
+ hidden: '{!isLIO}'
+ },
+ fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ }
}
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2021-03-09 02:22:47.000000000 -0500
+++ pvemanagerlib.js 2021-03-20 11:52:39.694828636 -0400
@@ -7849,6 +7849,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -46874,6 +46875,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true,
},
},
@@ -46886,10 +46888,26 @@
},
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
},
},
@@ -46907,6 +46925,7 @@
},
setValues: function diff(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -46923,7 +46942,7 @@
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -46933,11 +46952,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -46948,9 +46967,34 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ },
];
me.column2 = [
@@ -46980,7 +47024,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -46988,9 +47034,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}'
+ },
fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ },
},
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2021-05-27 08:28:35.000000000 -0400
+++ pvemanagerlib.js 2021-06-10 11:03:15.380175988 -0400
@@ -7900,6 +7900,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -47367,6 +47368,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true,
},
},
@@ -47379,10 +47381,26 @@
},
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
},
},
@@ -47400,6 +47418,7 @@
},
setValues: function(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -47416,7 +47435,7 @@
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -47426,11 +47445,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -47441,9 +47460,34 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ },
];
me.column2 = [
@@ -47473,7 +47517,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -47481,9 +47527,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}'
+ },
fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ },
},
];

View File

@ -1,189 +0,0 @@
--- pvemanagerlib.js.orig 2022-03-17 09:08:40.000000000 -0400
+++ pvemanagerlib.js 2022-04-03 08:54:10.229689187 -0400
@@ -8068,6 +8068,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS-API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -49636,6 +49637,7 @@
data: {
isLIO: false,
isComstar: true,
+ isFreeNAS: false,
hasWriteCacheOption: true,
},
},
@@ -49648,10 +49650,26 @@
},
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('freenas_password_field').setValue('');
+ me.lookupReference('freenas_password_field').allowBlank = true;
+ me.lookupReference('freenas_confirmpw_field').setValue('');
+ me.lookupReference('freenas_confirmpw_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('freenas_password_field').allowBlank = false;
+ me.lookupReference('freenas_confirmpw_field').allowBlank = false;
+ }
},
},
@@ -49669,6 +49687,7 @@
},
setValues: function(values) {
+ values.freenas_confirmpw = values.freenas_password;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -49685,7 +49704,7 @@
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -49695,11 +49714,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -49710,9 +49729,34 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ },
];
me.column2 = [
@@ -49742,7 +49786,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -49750,9 +49796,62 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}'
+ },
fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_password',
+ reference: 'freenas_password_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=freenas_confirmpw]').validate();
+ }
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_confirmpw',
+ reference: 'freenas_confirmpw_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm Password'),
+ validator: function(value) {
+ var pw = this.up().down('field[name=freenas_password]').getValue();
+ if (pw !== value) {
+ return "Passwords do not match!";
+ }
+ return true;
+ },
},
];

View File

@ -1,286 +0,0 @@
--- pvemanagerlib.js.orig 2024-01-06 14:50:39.890252726 -0500
+++ pvemanagerlib.js 2024-01-06 15:51:53.708260622 -0500
@@ -9228,6 +9228,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS/TrueNAS API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -58017,16 +58018,23 @@
me.callParent();
},
});
+
Ext.define('PVE.storage.ZFSInputPanel', {
extend: 'PVE.panel.StorageBase',
-
viewModel: {
- parent: null,
- data: {
- isLIO: false,
- isComstar: true,
- hasWriteCacheOption: true,
- },
+ parent: null,
+ data: {
+ isComstar: true,
+ isFreeNAS: false,
+ isLIO: false,
+ isToken: false,
+ hasWriteCacheOption: true,
+ },
+ formulas: {
+ hideUsername: function(get) {
+ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken')));
+ },
+ },
},
controller: {
@@ -58035,12 +58043,41 @@
'field[name=iscsiprovider]': {
change: 'changeISCSIProvider',
},
+ 'field[name=truenas_token_auth]': {
+ change: 'changeUsername',
+ },
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('truenas_token_auth_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('truenas_secret_field').setValue('');
+ me.lookupReference('truenas_secret_field').allowBlank = true;
+ me.lookupReference('truenas_confirm_secret_field').setValue('');
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('truenas_secret_field').allowBlank = false;
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = false;
+ }
+ },
+ changeUsername: function(f, newVal, oldVal) {
+ var me = this;
+ var vm = me.getViewModel();
+ vm.set('isToken', newVal);
+ me.lookupReference('freenas_user_field').allowBlank = newVal;
+ if (newVal) {
+ me.lookupReference('freenas_user_field').setValue('');
+ }
},
},
@@ -58053,18 +58090,68 @@
values.nowritecache = values.writecache ? 0 : 1;
delete values.writecache;
+ console.warn(values.freenas_password);
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
+ console.warn(values.truenas_secret);
return me.callParent([values]);
},
setValues: function(values) {
- values.writecache = values.nowritecache ? 0 : 1;
- this.callParent([values]);
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
+ values.truenas_confirm_secret = values.truenas_secret;
+ values.writecache = values.nowritecache ? 0 : 1;
+ this.callParent([values]);
},
initComponent: function() {
var me = this;
+ var tnsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_secret',
+ reference: 'truenas_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=truenas_confirm_secret]').validate();
+ }
+ },
+ });
+
+ var tnconfirmsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_confirm_secret',
+ reference: 'truenas_confirm_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm API Password'),
+ validator: function(value) {
+ var pw = me.up().down('field[name=truenas_secret]').getValue();
+ if (pw !== value) {
+ return "Secrets do not match!";
+ }
+ return true;
+ },
+ });
+
me.column1 = [
{
xtype: me.isCreate ? 'textfield' : 'displayfield',
@@ -58074,7 +58161,7 @@
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -58084,11 +58171,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -58099,9 +58186,60 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'truenas_token_auth',
+ reference: 'truenas_token_auth_field',
+ inputId: 'truenas_use_token_auth_field',
+ checked: false,
+ listeners: {
+ change: function(field, newValue) {
+ if (newValue === true) {
+ tnsecret.labelEl.update('API Token');
+ tnconfirmsecret.labelEl.update('Confirm API Token');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ } else {
+ tnsecret.labelEl.update('API Password');
+ tnconfirmsecret.labelEl.update('Confirm API Password');
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ }
+ },
+ },
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API Token Auth'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{hideUsername}'
+ },
+ },
];
me.column2 = [
@@ -58131,7 +58269,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -58139,15 +58279,32 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}'
+ },
fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
},
+ tnsecret,
+ tnconfirmsecret,
];
me.callParent();
},
});
+
Ext.define('PVE.storage.ZFSPoolSelector', {
extend: 'PVE.form.ComboBoxSetStoreNode',
alias: 'widget.pveZFSPoolSelector',

View File

@ -1,241 +0,0 @@
--- /tmp/pvemanagerlib-8.4.14-stock.js 2026-05-19 17:46:16.553917259 -0400
+++ /tmp/pvemanagerlib-8.4.14-patched-v2.js 2026-05-19 21:35:03.162563535 -0400
@@ -9745,6 +9745,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS/TrueNAS API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -62673,10 +62674,17 @@
viewModel: {
parent: null,
data: {
- isLIO: false,
isComstar: true,
+ isFreeNAS: false,
+ isLIO: false,
+ isToken: false,
hasWriteCacheOption: true,
},
+ formulas: {
+ hideUsername: function(get) {
+ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken')));
+ },
+ },
},
controller: {
@@ -62685,12 +62693,41 @@
'field[name=iscsiprovider]': {
change: 'changeISCSIProvider',
},
+ 'field[name=truenas_token_auth]': {
+ change: 'changeUsername',
+ },
},
changeISCSIProvider: function (f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('truenas_token_auth_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('truenas_secret_field').setValue('');
+ me.lookupReference('truenas_secret_field').allowBlank = true;
+ me.lookupReference('truenas_confirm_secret_field').setValue('');
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('truenas_secret_field').allowBlank = false;
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = false;
+ }
+ },
+ changeUsername: function (f, newVal, oldVal) {
+ var me = this;
+ var vm = me.getViewModel();
+ vm.set('isToken', newVal);
+ me.lookupReference('freenas_user_field').allowBlank = newVal;
+ if (newVal) {
+ me.lookupReference('freenas_user_field').setValue('');
+ }
},
},
@@ -62703,11 +62740,18 @@
values.nowritecache = values.writecache ? 0 : 1;
delete values.writecache;
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
return me.callParent([values]);
},
setValues: function (values) {
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
+ values.truenas_confirm_secret = values.truenas_secret;
values.writecache = values.nowritecache ? 0 : 1;
this.callParent([values]);
},
@@ -62715,6 +62759,47 @@
initComponent: function () {
var me = this;
+ var tnsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_secret',
+ reference: 'truenas_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}',
+ },
+ fieldLabel: gettext('API Password'),
+ change: function (f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=truenas_confirm_secret]').validate();
+ }
+ },
+ });
+
+ var tnconfirmsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_confirm_secret',
+ reference: 'truenas_confirm_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}',
+ },
+ fieldLabel: gettext('Confirm API Password'),
+ validator: function (value) {
+ var pw = me.up().down('field[name=truenas_secret]').getValue();
+ if (pw !== value) {
+ return 'Secrets do not match!';
+ }
+ return true;
+ },
+ });
+
me.column1 = [
{
xtype: me.isCreate ? 'textfield' : 'displayfield',
@@ -62749,9 +62834,60 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}',
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}',
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'truenas_token_auth',
+ reference: 'truenas_token_auth_field',
+ inputId: 'truenas_use_token_auth_field',
+ checked: false,
+ listeners: {
+ change: function (field, newValue) {
+ if (newValue === true) {
+ tnsecret.labelEl.update('API Token');
+ tnconfirmsecret.labelEl.update('Confirm API Token');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ } else {
+ tnsecret.labelEl.update('API Password');
+ tnconfirmsecret.labelEl.update('Confirm API Password');
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ }
+ },
+ },
+ bind: {
+ hidden: '{!isFreeNAS}',
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API Token Auth'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{hideUsername}',
+ },
+ },
];
me.column2 = [
@@ -62783,7 +62919,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}',
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -62791,10 +62929,26 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}',
+ },
+ allowBlank: true,
fieldLabel: gettext('Target portal group'),
},
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}',
+ },
+ fieldLabel: gettext('API IPv4 Host'),
+ },
+ tnsecret,
+ tnconfirmsecret,
];
me.callParent();

View File

@ -1,286 +0,0 @@
--- pvemanagerlib.js.orig 2024-01-06 14:50:39.890252726 -0500
+++ pvemanagerlib.js 2024-01-06 15:51:53.708260622 -0500
@@ -9228,6 +9228,7 @@
alias: ['widget.pveiScsiProviderSelector'],
comboItems: [
['comstar', 'Comstar'],
+ ['freenas', 'FreeNAS/TrueNAS API'],
['istgt', 'istgt'],
['iet', 'IET'],
['LIO', 'LIO'],
@@ -58017,16 +58018,23 @@
me.callParent();
},
});
+
Ext.define('PVE.storage.ZFSInputPanel', {
extend: 'PVE.panel.StorageBase',
-
viewModel: {
- parent: null,
- data: {
- isLIO: false,
- isComstar: true,
- hasWriteCacheOption: true,
- },
+ parent: null,
+ data: {
+ isComstar: true,
+ isFreeNAS: false,
+ isLIO: false,
+ isToken: false,
+ hasWriteCacheOption: true,
+ },
+ formulas: {
+ hideUsername: function(get) {
+ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken')));
+ },
+ },
},
controller: {
@@ -58035,12 +58043,41 @@
'field[name=iscsiprovider]': {
change: 'changeISCSIProvider',
},
+ 'field[name=truenas_token_auth]': {
+ change: 'changeUsername',
+ },
},
changeISCSIProvider: function(f, newVal, oldVal) {
+ var me = this;
var vm = this.getViewModel();
vm.set('isLIO', newVal === 'LIO');
vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ vm.set('isFreeNAS', newVal === 'freenas');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt');
+ if (newVal !== 'freenas') {
+ me.lookupReference('freenas_use_ssl_field').setValue(false);
+ me.lookupReference('truenas_token_auth_field').setValue(false);
+ me.lookupReference('freenas_apiv4_host_field').setValue('');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ me.lookupReference('truenas_secret_field').setValue('');
+ me.lookupReference('truenas_secret_field').allowBlank = true;
+ me.lookupReference('truenas_confirm_secret_field').setValue('');
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = true;
+ } else {
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ me.lookupReference('truenas_secret_field').allowBlank = false;
+ me.lookupReference('truenas_confirm_secret_field').allowBlank = false;
+ }
+ },
+ changeUsername: function(f, newVal, oldVal) {
+ var me = this;
+ var vm = me.getViewModel();
+ vm.set('isToken', newVal);
+ me.lookupReference('freenas_user_field').allowBlank = newVal;
+ if (newVal) {
+ me.lookupReference('freenas_user_field').setValue('');
+ }
},
},
@@ -58053,18 +58090,68 @@
values.nowritecache = values.writecache ? 0 : 1;
delete values.writecache;
+ console.warn(values.freenas_password);
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
+ console.warn(values.truenas_secret);
return me.callParent([values]);
},
setValues: function(values) {
- values.writecache = values.nowritecache ? 0 : 1;
- this.callParent([values]);
+ if (values.freenas_password) {
+ values.truenas_secret = values.freenas_password;
+ }
+ values.truenas_confirm_secret = values.truenas_secret;
+ values.writecache = values.nowritecache ? 0 : 1;
+ this.callParent([values]);
},
initComponent: function() {
var me = this;
+ var tnsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_secret',
+ reference: 'truenas_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API Password'),
+ change: function(f, value) {
+ if (f.rendered) {
+ f.up().down('field[name=truenas_confirm_secret]').validate();
+ }
+ },
+ });
+
+ var tnconfirmsecret = Ext.create('Ext.form.TextField', {
+ xtype: 'proxmoxtextfield',
+ name: 'truenas_confirm_secret',
+ reference: 'truenas_confirm_secret_field',
+ inputType: me.isCreate ? '' : 'password',
+ value: '',
+ editable: true,
+ submitValue: false,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('Confirm API Password'),
+ validator: function(value) {
+ var pw = me.up().down('field[name=truenas_secret]').getValue();
+ if (pw !== value) {
+ return "Secrets do not match!";
+ }
+ return true;
+ },
+ });
+
me.column1 = [
{
xtype: me.isCreate ? 'textfield' : 'displayfield',
@@ -58074,7 +58161,7 @@
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'pool',
value: '',
fieldLabel: gettext('Pool'),
@@ -58084,11 +58171,11 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'blocksize',
value: '4k',
- fieldLabel: gettext('Block Size'),
+ fieldLabel: gettext('ZFS Block Size'),
allowBlank: false,
},
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
+ xtype: 'textfield',
name: 'target',
value: '',
fieldLabel: gettext('Target'),
@@ -58099,9 +58186,60 @@
name: 'comstar_tg',
value: '',
fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
allowBlank: true,
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'freenas_use_ssl',
+ reference: 'freenas_use_ssl_field',
+ inputId: 'freenas_use_ssl_field',
+ checked: false,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API use SSL'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'truenas_token_auth',
+ reference: 'truenas_token_auth_field',
+ inputId: 'truenas_use_token_auth_field',
+ checked: false,
+ listeners: {
+ change: function(field, newValue) {
+ if (newValue === true) {
+ tnsecret.labelEl.update('API Token');
+ tnconfirmsecret.labelEl.update('Confirm API Token');
+ me.lookupReference('freenas_user_field').setValue('');
+ me.lookupReference('freenas_user_field').allowBlank = true;
+ } else {
+ tnsecret.labelEl.update('API Password');
+ tnconfirmsecret.labelEl.update('Confirm API Password');
+ me.lookupReference('freenas_user_field').allowBlank = false;
+ }
+ },
+ },
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ uncheckedValue: 0,
+ fieldLabel: gettext('API Token Auth'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'freenas_user',
+ reference: 'freenas_user_field',
+ inputId: 'freenas_user_field',
+ value: '',
+ fieldLabel: gettext('API Username'),
+ bind: {
+ hidden: '{hideUsername}'
+ },
+ },
];
me.column2 = [
@@ -58131,7 +58269,9 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'comstar_hg',
value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ bind: {
+ hidden: '{!isComstar}'
+ },
fieldLabel: gettext('Host group'),
allowBlank: true,
},
@@ -58139,15 +58279,32 @@
xtype: me.isCreate ? 'textfield' : 'displayfield',
name: 'lio_tpg',
value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
+ bind: {
+ hidden: '{!isLIO}'
+ },
fieldLabel: gettext('Target portal group'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'freenas_apiv4_host',
+ reference: 'freenas_apiv4_host_field',
+ value: '',
+ editable: true,
+ emptyText: Proxmox.Utils.noneText,
+ bind: {
+ hidden: '{!isFreeNAS}'
+ },
+ fieldLabel: gettext('API IPv4 Host'),
},
+ tnsecret,
+ tnconfirmsecret,
];
me.callParent();
},
});
+
Ext.define('PVE.storage.ZFSPoolSelector', {
extend: 'PVE.form.ComboBoxSetStoreNode',
alias: 'widget.pveZFSPoolSelector',

View File

@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Migrate the truenas-proxmox / freenas-proxmox apt source from Cloudsmith
# to the GitHub Pages apt repo.
#
# Run on each Proxmox node as root (or with sudo).
#
# What this script does:
# 1. Detects the currently installed version and which repo is configured
# 2. Removes any Cloudsmith source files and keyrings for this package
# 3. Imports the GitHub Pages GPG key
# 4. Adds the correct dist track (v3 for v3.x installs, main for v2.x)
# 5. Runs apt update and confirms the new repo is reachable
#
# Safe to re-run: it detects if you're already on GitHub Pages and exits early.
set -euo pipefail
GITHUB_PAGES_BASE="https://thegrandwazoo.github.io/freenas-proxmox"
KEYRING_PATH="/etc/apt/keyrings/truenas-proxmox.gpg"
SOURCES_FILE="/etc/apt/sources.list.d/truenas-proxmox.list"
CLOUDSMITH_URL_PATTERN="dl.cloudsmith.io.*ksatechnologies.*truenas-proxmox"
# ── Must run as root ──────────────────────────────────────────────────────────
if [[ "$EUID" -ne 0 ]]; then
echo "Error: run this script as root or with sudo." >&2
exit 1
fi
# ── Detect installed version ──────────────────────────────────────────────────
INSTALLED_PKG=""
INSTALLED_VER=""
for pkg in truenas-proxmox freenas-proxmox; do
ver="$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || true)"
if [[ -n "$ver" ]]; then
INSTALLED_PKG="$pkg"
INSTALLED_VER="$ver"
break
fi
done
if [[ -z "$INSTALLED_VER" ]]; then
echo "Neither truenas-proxmox nor freenas-proxmox is installed on this node."
echo "Use the install instructions in the README for a fresh install."
exit 0
fi
MAJOR="${INSTALLED_VER%%.*}"
echo "Detected: $INSTALLED_PKG $INSTALLED_VER (major version: $MAJOR)"
# ── Determine target dist track ───────────────────────────────────────────────
case "$MAJOR" in
2) DIST="main" ;;
3) DIST="v3" ;;
4) DIST="v4" ;;
*)
echo "Error: unrecognised major version '$MAJOR' — update this script." >&2
exit 1
;;
esac
# ── Check if already on GitHub Pages with the correct dist track ─────────────
if grep -qrF "$GITHUB_PAGES_BASE $DIST main" /etc/apt/sources.list.d/ 2>/dev/null; then
echo "Already configured correctly for GitHub Pages (dist: $DIST) — nothing to do."
echo "Current source:"
grep -rF "$GITHUB_PAGES_BASE" /etc/apt/sources.list.d/
exit 0
fi
# GitHub Pages is configured but with the wrong dist track — fix it
if grep -qr "$GITHUB_PAGES_BASE" /etc/apt/sources.list.d/ 2>/dev/null; then
echo "GitHub Pages is configured but with the wrong dist track — correcting..."
WRONG=$(grep -rl "$GITHUB_PAGES_BASE" /etc/apt/sources.list.d/)
echo " Removing: $WRONG"
echo "$WRONG" | xargs rm -f
fi
# ── Find and remove Cloudsmith sources ───────────────────────────────────────
echo ""
echo "==> Searching for Cloudsmith apt sources..."
CLOUDSMITH_FILES="$(grep -rl "$CLOUDSMITH_URL_PATTERN" /etc/apt/sources.list.d/ 2>/dev/null || true)"
if [[ -z "$CLOUDSMITH_FILES" ]]; then
echo " No Cloudsmith sources found for this package."
else
echo " Found:"
while IFS= read -r f; do echo " $f"; done <<< "$CLOUDSMITH_FILES"
echo " Removing..."
echo "$CLOUDSMITH_FILES" | xargs rm -f
echo " Done."
fi
# ── Remove Cloudsmith keyrings ────────────────────────────────────────────────
echo ""
echo "==> Searching for Cloudsmith keyrings..."
CLOUDSMITH_KEYRINGS="$(find /usr/share/keyrings /etc/apt/keyrings \
-name 'ksatechnologies-truenas-proxmox*.gpg' \
-o -name 'ksatechnologies-freenas-proxmox*.gpg' 2>/dev/null || true)"
if [[ -z "$CLOUDSMITH_KEYRINGS" ]]; then
echo " No Cloudsmith keyrings found."
else
echo " Found:"
while IFS= read -r f; do echo " $f"; done <<< "$CLOUDSMITH_KEYRINGS"
echo " Removing..."
echo "$CLOUDSMITH_KEYRINGS" | xargs rm -f
echo " Done."
fi
# ── Import GitHub Pages GPG key ───────────────────────────────────────────────
echo ""
echo "==> Importing GitHub Pages GPG key → $KEYRING_PATH"
mkdir -p /etc/apt/keyrings
curl -fsSL "${GITHUB_PAGES_BASE}/public.gpg.key" \
| gpg --dearmor \
| tee "$KEYRING_PATH" > /dev/null
echo " Done."
# ── Add new source ────────────────────────────────────────────────────────────
echo ""
echo "==> Adding GitHub Pages source (dist: $DIST) → $SOURCES_FILE"
echo "deb [signed-by=${KEYRING_PATH}] ${GITHUB_PAGES_BASE} ${DIST} main" \
> "$SOURCES_FILE"
cat "$SOURCES_FILE"
# ── apt update ───────────────────────────────────────────────────────────────
echo ""
echo "==> Running apt update..."
apt-get update 2>&1 \
| grep -E "^(Get|Hit|Err|Fetched|W:|E:)" || true
# Confirm the package is visible from the new repo
CANDIDATE="$(apt-cache policy "$INSTALLED_PKG" 2>/dev/null \
| awk '/Candidate:/{print $2}')"
if apt-cache policy "$INSTALLED_PKG" 2>/dev/null \
| grep -q "$GITHUB_PAGES_BASE"; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Migration complete."
echo " Package : $INSTALLED_PKG $INSTALLED_VER"
echo " Repo : $GITHUB_PAGES_BASE"
echo " Dist : $DIST"
echo " Candidate from new repo: $CANDIDATE"
echo ""
if [[ "$CANDIDATE" != "$INSTALLED_VER" ]]; then
echo " Note: a newer version ($CANDIDATE) is available."
echo " Run: apt-get install $INSTALLED_PKG"
else
echo " You are on the latest release in this track."
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
echo ""
echo "Warning: migration completed but $INSTALLED_PKG was not found in" >&2
echo " $GITHUB_PAGES_BASE" >&2
echo "Run 'apt-get update && apt-cache policy $INSTALLED_PKG' to investigate." >&2
exit 1
fi

View File

@ -0,0 +1,86 @@
#!/usr/bin/env bash
# One-time setup: generate the GPG key used to sign the GitHub Pages apt repo.
#
# Run this locally once. After running:
# 1. Commit public.gpg.key to the gh-pages branch (instructions printed below)
# 2. Add APT_SIGNING_KEY to GitHub Actions secrets
# 3. Add APT_SIGNING_KEY_PASSPHRASE to GitHub Actions secrets (empty string if no passphrase)
#
# Re-running is safe — it checks for an existing key first.
set -euo pipefail
REPO="TheGrandWazoo/freenas-proxmox"
KEY_NAME="truenas-proxmox"
KEY_EMAIL="packages@ksatechnologies.com"
KEY_COMMENT="truenas-proxmox apt repo signing key"
# ── Check for existing key ────────────────────────────────────────────────────
EXISTING="$(gpg --list-secret-keys --with-colons 2>/dev/null \
| awk -F: -v name="$KEY_NAME" '$1=="uid" && $10 ~ name {found=1} END {print found+0}')"
if [[ "$EXISTING" == "1" ]]; then
echo "Key '$KEY_NAME' already exists in your keyring — skipping generation."
KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \
| awk -F: '/^sec/{print $5; exit}')"
else
# ── Generate key ─────────────────────────────────────────────────────────────
echo "Generating GPG key for apt repo signing..."
read -r -s -p "Enter a passphrase (leave empty for no passphrase): " PASSPHRASE
echo
gpg --batch --gen-key <<EOF
%echo Generating truenas-proxmox apt signing key
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: ${KEY_NAME}
Name-Comment: ${KEY_COMMENT}
Name-Email: ${KEY_EMAIL}
Expire-Date: 0
$([ -n "$PASSPHRASE" ] && echo "Passphrase: ${PASSPHRASE}" || echo "%no-protection")
%commit
%echo Done
EOF
KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \
| awk -F: '/^sec/{print $5; exit}')"
echo "Generated key: $KEY_ID"
fi
# ── Export public key ─────────────────────────────────────────────────────────
echo
echo "==> Exporting public key..."
gpg --export --armor "$KEY_ID" > /tmp/truenas-proxmox-public.gpg.key
echo "Public key written to /tmp/truenas-proxmox-public.gpg.key"
# ── Export private key (base64) ───────────────────────────────────────────────
echo
echo "==> Exporting private key (base64) for GitHub Actions secret..."
PRIVATE_KEY_B64="$(gpg --export-secret-keys --armor "$KEY_ID" | base64 -w 0)"
# ── Instructions ─────────────────────────────────────────────────────────────
cat <<INSTRUCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXT STEPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Add APT_SIGNING_KEY to GitHub Actions secrets:
gh secret set APT_SIGNING_KEY --repo ${REPO} --body "${PRIVATE_KEY_B64}"
2. Add APT_SIGNING_KEY_PASSPHRASE to GitHub Actions secrets:
gh secret set APT_SIGNING_KEY_PASSPHRASE --repo ${REPO} --body "YOUR_PASSPHRASE"
(use empty string if you chose no passphrase:)
gh secret set APT_SIGNING_KEY_PASSPHRASE --repo ${REPO} --body ""
3. Commit public.gpg.key to the gh-pages branch:
git checkout gh-pages
cp /tmp/truenas-proxmox-public.gpg.key public.gpg.key
git add public.gpg.key
git commit -m "chore: add apt repo GPG public key"
git push origin gh-pages
git checkout release/3.x
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
INSTRUCTIONS

View File

@ -1,4 +0,0 @@
/ZFSPlugin.pm
/ZFSPoolPlugin.pm
/ZFSPlugin-new.pm
/ZFSPlugin-newver.patch

View File

@ -1,546 +0,0 @@
package PVE::Storage::LunCmd::FreeNAS;
use strict;
use warnings;
use Data::Dumper;
use PVE::SafeSyslog;
use IO::Socket::SSL;
use REST::Client;
use MIME::Base64;
use JSON;
# Max LUNS per target on the iSCSI server
my $MAX_LUNS = 255;
#
#
#
sub get_base {
return '/dev/zvol';
}
#
#
#
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
if(!defined($scfg->{'freenas_user'}) || !defined($scfg->{'freenas_password'})) {
die "Undefined freenas_user and/or freenas_password.";
}
syslog("info","FreeNAS::lun_command : $method(@params)");
if($method eq "create_lu") {
return run_create_lu($scfg, $timeout, $method, @params);
}
if($method eq "delete_lu") {
return run_delete_lu($scfg, $timeout, $method, @params);
}
if($method eq "import_lu") {
return run_create_lu($scfg, $timeout, $method, @params);
}
if($method eq "modify_lu") {
return run_modify_lu($scfg, $timeout, $method, @params);
}
if($method eq "add_view") {
return run_add_view($scfg, $timeout, $method, @params);
}
if($method eq "list_view") {
return run_list_view($scfg, $timeout, $method, @params);
}
if($method eq "list_extent") {
return run_list_extent($scfg, $timeout, $method, @params);
}
if($method eq "list_lu") {
return run_list_lu($scfg, $timeout, $method, "name", @params);
}
syslog("error","FreeNAS::lun_command : unknown method $method");
return undef;
}
#
#
#
sub run_add_view {
return '';
}
#
# a modify_lu occur by example on a zvol resize. we just need to destroy and recreate the lun with the same zvol.
# Be careful, the first param is the new size of the zvol, we must shift params
#
sub run_modify_lu {
my ($scfg, $timeout, $method, @params) = @_;
shift(@params);
run_delete_lu($scfg, $timeout, $method, @params);
return run_create_lu($scfg, $timeout, $method, @params);
}
#
#
#
sub run_list_view {
my ($scfg, $timeout, $method, @params) = @_;
return run_list_lu($scfg, $timeout, $method, "lun-id", @params);
}
#
#
#
sub run_list_lu {
my ($scfg, $timeout, $method, $result_value_type, @params) = @_;
my $object = $params[0];
my $result = undef;
my $luns = freenas_list_lu($scfg);
foreach my $lun (@$luns) {
if ($lun->{'iscsi_target_extent_path'} =~ /^$object$/) {
$result = $result_value_type eq "lun-id" ? $lun->{'iscsi_lunid'} : $lun->{'iscsi_target_extent_path'};
syslog("info","FreeNAS::list_lu($object):$result_value_type : lun found $result");
last;
}
}
if(!defined($result)) {
syslog("info","FreeNAS::list_lu($object):$result_value_type : lun not found");
}
return $result;
}
#
#
#
sub run_list_extent {
my ($scfg, $timeout, $method, @params) = @_;
my $object = $params[0];
my $result = undef;
my $luns = freenas_list_lu($scfg);
foreach my $lun (@$luns) {
if ($lun->{'iscsi_target_extent_path'} =~ /^$object$/) {
$result = $lun->{'iscsi_target_extent_naa'};
syslog("info","FreeNAS::list_extent($object): naa found $result");
last;
}
}
if (!defined($result)) {
syslog("info","FreeNAS::list_extent($object): naa not found");
}
return $result;
}
#
#
#
sub run_create_lu {
my ($scfg, $timeout, $method, @params) = @_;
my $lun_path = $params[0];
my $lun_id = freenas_get_first_available_lunid($scfg);
die "Maximum number of LUNs per target is $MAX_LUNS" if scalar $lun_id >= $MAX_LUNS;
die "$params[0]: LUN $lun_path exists" if defined(run_list_lu($scfg, $timeout, $method, "name", @params));
my $target_id = freenas_get_targetid($scfg);
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
# Create the extent
my $extent = freenas_iscsi_create_extent($scfg, $lun_path);
# Associate the new extent to the target
my $link = freenas_iscsi_create_target_to_extent($scfg, $target_id, $extent->{'id'}, $lun_id);
if (defined($link)) {
syslog("info","FreeNAS::create_lu(lun_path=$lun_path, lun_id=$lun_id) : sucessfull");
} else {
die "Unable to create lun $lun_path";
}
return "";
}
#
#
#
sub run_delete_lu {
my ($scfg, $timeout, $method, @params) = @_;
my $lun_path = $params[0];
my $luns = freenas_list_lu($scfg);
my $lun = undef;
my $link = undef;
foreach my $item (@$luns) {
if($item->{'iscsi_target_extent_path'} =~ /^$lun_path$/) {
$lun = $item;
last;
}
}
die "Unable to find the lun $lun_path for $scfg->{target}" if !defined($lun);
my $target_id = freenas_get_targetid($scfg);
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
# find the target to extent
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
foreach my $item (@$target2extents) {
if($item->{'iscsi_target'} == $target_id &&
$item->{'iscsi_lunid'} == $lun->{'iscsi_lunid'} &&
$item->{'iscsi_extent'} == $lun->{'id'}) {
$link = $item;
last;
}
}
die "Unable to find the link for the lun $lun_path for $scfg->{target}" if !defined($link);
# Remove the link
my $remove_link = freenas_iscsi_remove_target_to_extent($scfg, $link->{'id'});
# Remove the extent
my $remove_extent = freenas_iscsi_remove_extent($scfg, $lun->{'id'});
if($remove_link == 1 && $remove_extent == 1) {
syslog("info","FreeNAS::delete_lu(lun_path=$lun_path) : sucessfull");
} else {
die "Unable to delete lun $lun_path";
}
return "";
}
#
### FREENAS API CALLING ###
#
sub freenas_api_call {
my ($scfg, $method, $path, $data) = @_;
my $client = undef;
my $scheme = $scfg->{freenas_use_ssl} ? "https" : "http";
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
my $apiping = '/api/v1.0/system/version/';
$client = REST::Client->new();
$client->setHost($scheme . '://' . $apihost);
$client->addHeader('Content-Type' , 'application/json');
$client->addHeader('Authorization' , 'Basic ' . encode_base64($scfg->{freenas_user} . ':' . $scfg->{freenas_password}));
# If using SSL, don't verify SSL certs
if ($scfg->{freenas_use_ssl}) {
$client->getUseragent()->ssl_opts(verify_hostname => 0);
$client->getUseragent()->ssl_opts(SSL_verify_mode => SSL_VERIFY_NONE);
}
# Check if the APIs are accessable via the selected host and scheme
my $code = $client->request('GET', $apiping)->responseCode();
if ($code != 200) {
freenas_api_log_error($client, "freenas_api_call");
die "Unable to connect to the FreeNAS API service at '" . $apihost . "' using the '" . $scheme . "' protocol";
}
syslog("info","FreeNAS::api_call : setup : sucessfull");
if ($method eq 'GET') {
$client->GET($path);
}
if ($method eq 'DELETE') {
$client->DELETE($path);
}
if ($method eq 'POST') {
$client->POST($path, encode_json($data));
}
return $client
}
#
# Writes the Response and Content to SysLog
#
sub freenas_api_log_error {
my ($client, $method) = @_;
syslog("info","[ERROR]FreeNAS::API::" . $method . " : Response code: " . $client->responseCode());
syslog("info","[ERROR]FreeNAS::API::" . $method . " : Response content: " . $client->responseContent());
return 1;
}
#
#
#
sub freenas_iscsi_get_globalconfiguration {
my ($scfg) = @_;
my $client = freenas_api_call($scfg, 'GET', "/api/v1.0/services/iscsi/globalconfiguration/", undef);
my $code = $client->responseCode();
if ($code == 200) {
my $result = decode_json($client->responseContent());
syslog("info","FreeNAS::API::get_globalconfig : target_basename=" . $result->{'iscsi_basename'});
return $result;
} else {
freenas_api_log_error($client, "get_globalconfig");
return undef;
}
}
#
# Returns a list of all extents.
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-extent-
#
sub freenas_iscsi_get_extent {
my ($scfg) = @_;
my $client = freenas_api_call($scfg, 'GET', "/api/v1.0/services/iscsi/extent/?limit=0", undef);
my $code = $client->responseCode();
if ($code == 200) {
my $result = decode_json($client->responseContent());
syslog("info","FreeNAS::API::get_extent : sucessfull");
return $result;
} else {
freenas_api_log_error($client, "get_extent");
return undef;
}
}
#
# Create an extent on FreeNas
# http://api.freenas.org/resources/iscsi/index.html#create-resource
# Parameters:
# - target config (scfg)
# - lun_path
#
sub freenas_iscsi_create_extent {
my ($scfg, $lun_path) = @_;
my $name = $lun_path;
$name =~ s/^.*\///; # all from last /
$name = $scfg->{'pool'} . '/' . $name;
my $device = $lun_path;
$device =~ s/^\/dev\///; # strip /dev/
my $request = {
"iscsi_target_extent_type" => "Disk",
"iscsi_target_extent_name" => $name,
"iscsi_target_extent_disk" => $device,
};
my $client = freenas_api_call($scfg, 'POST', "/api/v1.0/services/iscsi/extent/", $request);
my $code = $client->responseCode();
if ($code == 201) {
my $result = decode_json($client->responseContent());
syslog("info", "FreeNAS::API::create_extent(lun_path=" . $result->{'iscsi_target_extent_path'} . ") : sucessfull");
return $result;
} else {
freenas_api_log_error($client, "create_extent");
return undef;
}
}
#
# Remove an extent by it's id
# http://api.freenas.org/resources/iscsi/index.html#delete-resource
# Parameters:
# - scfg
# - extent_id
#
sub freenas_iscsi_remove_extent {
my ($scfg, $extent_id) = @_;
my $client = freenas_api_call($scfg, 'DELETE', "/api/v1.0/services/iscsi/extent/$extent_id/", undef);
my $code = $client->responseCode();
if ($code == 204) {
syslog("info","FreeNAS::API::remove_extent(extent_id=$extent_id) : sucessfull");
return 1;
} else {
freenas_api_log_error($client, "remove_extent");
return 0;
}
}
#
# Returns a list of all targets
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-target-
#
sub freenas_iscsi_get_target {
my ($scfg) = @_;
my $client = freenas_api_call($scfg, 'GET', "/api/v1.0/services/iscsi/target/?limit=0", undef);
my $code = $client->responseCode();
if ($code == 200) {
my $result = decode_json($client->responseContent());
syslog("info","FreeNAS::API::get_target() : sucessfull");
return $result;
} else {
freenas_api_log_error($client, "get_target");
return undef;
}
}
#
# Returns a list of associated extents to targets
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-targettoextent-
#
sub freenas_iscsi_get_target_to_extent {
my ($scfg) = @_;
my $client = freenas_api_call($scfg, 'GET', "/api/v1.0/services/iscsi/targettoextent/?limit=0", undef);
my $code = $client->responseCode();
if ($code == 200) {
my $result = decode_json($client->responseContent());
syslog("info","FreeNAS::API::get_target_to_extent() : sucessfull");
# If 'iscsi_lunid' is undef then it is set to 'Auto' in FreeNAS
# which should be '0' in our eyes.
# This gave Proxmox 5.x and FreeNAS 11.1 a few issues.
foreach my $item (@$result) {
if (!defined($item->{'iscsi_lunid'})) {
$item->{'iscsi_lunid'} = 0;
syslog("info", "FreeNAS::API::get_target_to_extent() : change undef iscsi_lunid to 0");
}
}
return $result;
} else {
freenas_api_log_error($client, "get_target_to_extent");
return undef;
}
}
#
# Associate a FreeNas extent to a FreeNas Target
# http://api.freenas.org/resources/iscsi/index.html#post--api-v1.0-services-iscsi-targettoextent-
# Parameters:
# - target config (scfg)
# - FreeNas Target ID
# - FreeNas Extent ID
# - Lun ID
#
sub freenas_iscsi_create_target_to_extent {
my ($scfg, $target_id, $extent_id, $lun_id) = @_;
my $request = {
"iscsi_target" => $target_id,
"iscsi_extent" => $extent_id,
"iscsi_lunid" => $lun_id
};
my $client = freenas_api_call($scfg, 'POST', "/api/v1.0/services/iscsi/targettoextent/", $request);
my $code = $client->responseCode();
if ($code == 201) {
my $result = decode_json($client->responseContent());
syslog("info","FreeNAS::API::create_target_to_extent(target_id=$target_id, extent_id=$extent_id, lun_id=$lun_id) : sucessfull");
return $result;
} else {
freenas_api_log_error($client, "create_target_to_extent");
return undef;
}
}
#
# Remove a Target to extent by it's id
# http://api.freenas.org/resources/iscsi/index.html#delete--api-v1.0-services-iscsi-targettoextent-(int-id)-
# Parameters:
# - scfg
# - link_id
#
sub freenas_iscsi_remove_target_to_extent {
my ($scfg, $link_id) = @_;
my $client = freenas_api_call($scfg, 'DELETE', "/api/v1.0/services/iscsi/targettoextent/$link_id/", undef);
my $code = $client->responseCode();
if ($code == 204) {
syslog("info","FreeNAS::API::remove_target_to_extent(link_id=$link_id) : sucessfull");
return 1;
} else {
freenas_api_log_error($client, "remove_target_to_extent");
return 0;
}
}
#
# Returns all luns associated to the current target defined by $scfg->{target}
# This method returns an array reference like "freenas_iscsi_get_extent" do
# but with an additionnal hash entry "iscsi_lunid" retrieved from "freenas_iscsi_get_target_to_extent"
#
sub freenas_list_lu {
my ($scfg) = @_;
my $targets = freenas_iscsi_get_target($scfg);
my $target_id = freenas_get_targetid($scfg);
my @luns = ();
my $iscsi_lunid = undef;
if(defined($target_id)) {
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
my $extents = freenas_iscsi_get_extent($scfg);
foreach my $item (@$target2extents) {
if($item->{'iscsi_target'} == $target_id) {
foreach my $node (@$extents) {
if($node->{'id'} == $item->{'iscsi_extent'}) {
if ($item->{'iscsi_lunid'} =~ /(\d+)/) {
$iscsi_lunid = "$1";
} else {
syslog("info", "FreeNAS::API::freenas_list_lu : iscsi_lunid did not pass tainted testing");
next;
}
$node->{'iscsi_lunid'} .= $iscsi_lunid;
push(@luns , $node);
}
}
}
}
}
syslog("info", "FreeNAS::API::freenas_list_lu : sucessfull");
return \@luns;
}
#
# Returns the first available "lunid" (in all targets namespaces)
#
sub freenas_get_first_available_lunid {
my ($scfg) = @_;
my $target_id = freenas_get_targetid($scfg);
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
my @luns = ();
foreach my $item (@$target2extents) {
push(@luns, $item->{'iscsi_lunid'}) if ($item->{'iscsi_target'} == $target_id);
}
my @sorted_luns = sort {$a <=> $b} @luns;
my $lun_id = 0;
# find the first hole, if not, give the +1 of the last lun
foreach my $lun (@sorted_luns) {
last if $lun != $lun_id;
$lun_id = $lun_id + 1;
}
syslog("info", "FreeNAS::API::freenas_get_first_available_lunid : return $lun_id");
return $lun_id;
}
#
# Returns the target id on FreeNas of the currently configured target of this PVE storage
#
sub freenas_get_targetid {
my ($scfg) = @_;
my $global = freenas_iscsi_get_globalconfiguration($scfg);
my $targets = freenas_iscsi_get_target($scfg);
my $target_id = undef;
foreach my $target (@$targets) {
my $iqn = $global->{'iscsi_basename'} . ':' . $target->{'iscsi_target_name'};
if($iqn eq $scfg->{target}) {
$target_id = $target->{'id'};
last;
}
}
return $target_id;
}
1;

View File

@ -1,140 +0,0 @@
--- ZFSPlugin.pm.orig 2018-10-03 11:02:46.000000000 -0400
+++ ZFSPlugin.pm 2018-10-28 21:27:44.794732848 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -32,7 +33,7 @@
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +41,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +65,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +165,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +192,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +237,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +273,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,10 +0,0 @@
--- ZFSPlugin-5.0-33.pm 2018-12-15 09:29:56.586101170 -0500
+++ ZFSPlugin.pm 2018-12-15 09:24:08.393984276 -0500
@@ -27,6 +27,7 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2018-11-27 07:23:15.000000000 -0500
+++ ZFSPlugin.pm 2018-12-15 09:24:08.393984276 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin-5.0-34.pm 2018-12-21 12:52:21.764194866 -0500
+++ ZFSPlugin.pm 2018-12-21 12:53:08.663665081 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-02-07 09:14:12.000000000 -0500
+++ ZFSPlugin.pm 2019-02-26 20:53:29.096598801 -0500
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-04-30 09:54:55.000000000 -0400
+++ ZFSPlugin.pm 2019-05-11 10:32:36.326734352 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-06-25 06:54:50.000000000 -0400
+++ ZFSPlugin.pm 2019-07-16 13:36:48.817831174 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1,147 +0,0 @@
--- ZFSPlugin.pm.orig 2019-06-25 06:54:50.000000000 -0400
+++ ZFSPlugin.pm 2019-07-16 13:36:48.817831174 -0400
@@ -10,6 +10,7 @@
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::FreeNAS;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
@@ -26,13 +27,14 @@
modify_lu => 1,
add_view => 1,
list_view => 1,
+ list_extent => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
@@ -40,6 +42,8 @@
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ return PVE::Storage::LunCmd::FreeNAS::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -62,6 +66,8 @@
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+ } elsif ($scfg->{iscsiprovider} eq 'freenas') {
+ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -160,6 +166,15 @@
return $class->zfs_request($scfg, undef, 'list_view', $guid);
}
+# Part of the multipath enhancement
+sub zfs_get_wwid_number {
+ my ($class, $scfg, $guid) = @_;
+
+ die "could not find lun_number for guid $guid" if !$guid;
+
+ return $class->zfs_request($scfg, undef, 'list_extent', $guid);
+}
+
# Configuration
sub type {
@@ -178,6 +193,24 @@
description => "iscsi provider",
type => 'string',
},
+ # This is for FreeNAS iscsi and API intergration
+ # And some enhancements asked by the community
+ freenas_user => {
+ description => "FreeNAS API Username",
+ type => 'string',
+ },
+ freenas_password => {
+ description => "FreeNAS API Password",
+ type => 'string',
+ },
+ freenas_use_ssl => {
+ description => "FreeNAS API access via SSL",
+ type => 'boolean',
+ },
+ freenas_apiv4_host => {
+ description => "FreeNAS API Host",
+ type => 'string',
+ },
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
@@ -205,14 +238,18 @@
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
- target => { fixed => 1 },
- pool => { fixed => 1 },
+ target => { fixed => 0 },
+ pool => { fixed => 0 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
+ freenas_user => { optional => 1 },
+ freenas_password => { optional => 1 },
+ freenas_use_ssl => { optional => 1 },
+ freenas_apiv4_host => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
@@ -237,6 +274,40 @@
my $path = "iscsi://$portal/$target/$lun";
+ # Multipath enhancement
+ eval {
+ my $wwid = $class->zfs_get_wwid_number($scfg, $guid);
+# syslog(info,"JD: path get_lun_number guid $guid");
+
+ if ($wwid =~ /^([-\@\w.]+)$/) {
+ $wwid = $1; # $data now untainted
+ } else {
+ die "Bad data in '$wwid'"; # log this somewhere
+ }
+ my $wwid_end = substr $wwid, 16;
+
+ my $mapper = '';
+ sleep 3;
+ run_command("iscsiadm -m session --rescan");
+ sleep 3;
+ my $line = `multipath -ll | grep \"$wwid_end\"`;
+ my ($mapper_device) = split(' ', $line);
+ $mapper_device = "" unless $mapper_device;
+ $mapper .= $mapper_device;
+
+ if ($mapper =~ /^([-\@\w.]+)$/) {
+ $mapper = $1; # $data now untainted
+ } else {
+ $mapper = '';
+ }
+
+# syslog(info,"Multipath mapper found: $mapper\n");
+ if ($mapper ne "") {
+ $path = "/dev/mapper/$mapper";
+ sleep 5;
+ }
+ };
+
return ($path, $vmid, $vtype);
}

View File

@ -1 +0,0 @@
/apidoc.js

View File

@ -1,66 +0,0 @@
--- apidoc.js.orig 2018-08-19 10:18:11.715767285 -0400
+++ apidoc.js.new 2018-08-19 10:17:09.364282178 -0400
@@ -27584,6 +27584,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"content" : {
"description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n",
"format" : "pve-storage-content-list",
@@ -27948,6 +27973,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"content" : {
"description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n",

View File

@ -1,79 +0,0 @@
--- apidoc-5.2-9.js 2018-11-10 14:28:44.022355849 -0500
+++ apidoc.js 2018-11-10 15:49:38.873720011 -0500
@@ -32448,6 +32448,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -32604,6 +32629,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -32810,6 +32841,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc-5.3-1.js 2018-12-21 12:46:55.459865478 -0500
+++ apidoc.js 2018-12-06 15:50:21.487987807 -0500
@@ -33477,6 +33477,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -33633,6 +33658,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -33840,6 +33871,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2019-02-20 06:20:05.000000000 -0500
+++ apidoc.js 2019-02-26 20:51:26.857204965 -0500
@@ -33709,6 +33709,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -33865,6 +33890,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -34071,6 +34102,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

View File

@ -1,79 +0,0 @@
--- apidoc.js.orig 2019-04-08 02:02:09.000000000 -0400
+++ apidoc.js 2019-05-11 10:34:28.305764709 -0400
@@ -34385,6 +34385,31 @@
"type" : "string",
"typetext" : "<string>"
},
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"fuse" : {
"description" : "Mount CephFS through FUSE.",
"optional" : 1,
@@ -34541,6 +34566,12 @@
"type" : "boolean",
"typetext" : "<boolean>"
},
+ "target" : {
+ "description" : "iSCSI target.",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
"transport" : {
"description" : "Gluster transport: tcp or rdma",
"enum" : [
@@ -34748,6 +34779,31 @@
"optional" : 1,
"type" : "string",
"typetext" : "<string>"
+ },
+ "freenas_user" : {
+ "description" : "FreeNAS user for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_password" : {
+ "description" : "FreeNAS password for API access",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
+ },
+ "freenas_use_ssl" : {
+ "description" : "FreeNAS API access via SSL",
+ "optional" : 1,
+ "type" : "boolean",
+ "typetext" : "<boolean>"
+ },
+ "freenas_apiv4_host" : {
+ "description" : "FreeNAS API Host via IPv4",
+ "format" : "address",
+ "optional" : 1,
+ "type" : "string",
+ "typetext" : "<string>"
},
"fuse" : {
"description" : "Mount CephFS through FUSE.",

Some files were not shown because too many files have changed in this diff Show More