Monitors and administers one or more TrueNAS SCALE instances from the PegaProx panel (TrueCommand-style), over the JSON-RPC 2.0 WebSocket API — REST v2.0 is deprecated in 25.10 and removed in TrueNAS 26, so this plugin never uses it.
v0.9.0 — enough surface to run TrueNAS day-to-day without opening its
own UI: pools/disks health, datasets, snapshots, SMB/NFS shares (iSCSI is
read-only), replication, apps/VMs, services, data-protection posture
(cloudsync/rsync/certificates) and CPU/memory/network telemetry. Every
write (datasets/zvols, snapshots, services, VMs/apps, SMB/NFS shares) goes
through the same dry-run → typed-confirmation → execute → verify → audit
path — see WRITE_OPS in src/routes/api.py. 352 tests, all built and
verified against fakes/mocks first; every real RPC schema was confirmed
live against a real instance (core.get_methods) before being coded, and
every write phase's first real call was reviewed by the operator before
shipping. See CHANGELOG.md for the full phase-by-phase history (F0
transport/config → F1 read-only monitoring → F2 datasets/snapshots writes
→ F3 fleet overview → F4a-c services/shares → F5 VMs/apps lifecycle → F6
data-protection posture → telemetry charts).
Full detail for every entry below is in CHANGELOG.md; this is the map.
src/subsystems/fleet.py fans out across every configured instance
concurrently (ThreadPoolExecutor) and aggregates health + recent
non-auth audit events into one dashboard — the plugin's entry tab.
src/subsystems/services.py. F4b's control ops needed a SERVICE_WRITE
privilege grant on the RW service account before the RPCs became visible —
same pattern repeated for F5's VM_WRITE/APPS_WRITE.
src/subsystems/shares.py. Typed-confirmation delete guard adapted for
opaque integer share ids (the caller supplies expected_name/
expected_path instead of the id being self-referential like a dataset
path). iSCSI stays read-only — see "Known gaps" below.
src/subsystems/apps_vms.py: start/stop/restart for VMs, start/stop/
redeploy for Apps (app.restart doesn't exist on this TrueNAS
version — redeploy pulls fresh images, a heavier op, never mislabeled as
a plain restart).
src/subsystems/data_protection.py — Cloudsync/Rsync tasks + certificate
expiry, read-only. Security fix baked in: this is the one subsystem
that does NOT do this codebase's usual attrs-passthrough, because the raw
cloudsync.query/certificate.query records embed cleartext secrets
(a cloud provider's API key, a certificate's private key PEM). Every
function returns an explicit field allow-list instead; tests assert the
secret strings are ABSENT from the output, not just that safe fields are
present.
src/subsystems/telemetry.py, backed by reporting.get_data. No
charting library (CT119 has no internet access to fetch one) — hand-rolled
SVG <polyline> sparklines, server-side downsampled to ≤120 points.
- Write path (
src/subsystems/datasets.py,snapshots.py):create/update/delete(datasets) andcreate/delete(snapshots), each split into a purebuild_<op>_envelope(...)(noconn— returns(method, params)or raises) and a real op that calls that SAME builder.POST writes/dry-run/POST writes/execute(both admin-gated) share this registry (WRITE_OPSinsrc/routes/api.py) so a dry-run preview can never describe a different call than what actually runs. - Typed confirmation on delete:
confirm_namemust match the resource's full name/id exactly, checked inside the builder — before any envelope exists, let alone before any TrueNAS call. ConnectionManager.get_rw_connection(): separately cached from the read-only client — a write never upgrades the shared read connection's privilege._resolve_writable_instance:readonly is False+api_key_rwpresent, both required before anything else happens.- Post-write verify + audit, every outcome (
ok/pending/verify_failed/error) logged, never auto-retried. - See
CHANGELOG.md[0.3.0]for the full write-flow detail and the sync-vs-async design decision.
src/core/subsystem.py— theSubsystemcontract (list/read/health,write()read-only by default) every module insrc/subsystems/implements.src/subsystems/{system,pools,datasets,snapshots,shares,replication, apps_vms}.py— one module per TrueNAS concept, each wrapping the exact JSON-RPC methods from brief §4.2. See CHANGELOG.md[0.2.0]for the full per-module method list and the pools temperature-exclusion safety correction (brief §4.3/§9).- 7 new read routes in
src/routes/api.py,instance_idas a query param (see the module docstring for why — the confirmed plugin routing mechanism doesn't support URL path parameters). TrueNASWSClient.is_authenticated— tracks the CURRENT socket's live session, distinct from "an api_key is remembered for relogin".src/ui/plugin.html— every tab fetches and renders real data now.
src/core/ws_client.py— a generic, reusable JSON-RPC 2.0 client over a persistent WebSocket: request/response framing, concurrentidhandling, typed errors, lazy-connect, and automatic reconnection (exponential backoff + jitter) that re-logs-in and re-subscribes after a drop.src/core/conn_manager.py— one client per configured instance, lazy-connect, multi-instance from day one.config.example.json/config.json(chmod 600, never committed) — multi-tenant instance list (client_id+ host/port/TLS/API keys/readonly) and the polling budget.src/routes/api.py+src/routes/config_store.py— theui,config,config/saveandinstances/testroutes, with masked-key round-tripping.src/ui/plugin.html— the UI shell: instance selector grouped by client, placeholder tabs, and a functional Settings tab.
This plugin will eventually manage TrueNAS instances belonging to
different clients in the same panel (e.g. acme, globex, initech,
...), not just the operator's own infrastructure. Every instance
in config.json carries a free-form client_id field from F0 onward — the
Settings UI and the instance selector group by it. F0 does not yet call
PegaProx's check_cluster_access for per-client scoping — that mapping
(client_id ↔ Proxmox cluster) lands in F1+, once more than one real client
is on-boarded. Until then, every write in this plugin (config, instance
test) is gated on the admin role, same as the rest of the plugin's RBAC.
PegaProx's PERMISSIONS table is fixed — a plugin cannot register new
assignable permissions. This plugin reuses:
| Action | Permission |
|---|---|
ui (the tab itself) |
storage.view |
system, pools, datasets, snapshots, shares, replication, apps_vms (F1 reads) |
storage.view |
config, config/save, instances/test (touch API keys) |
admin role only |
admin always passes any has_permission check automatically.
Each entry in instances[]:
| Field | Meaning |
|---|---|
id |
Stable identifier, unique within this plugin's config |
name |
Display name |
client_id |
Free-form tenant namespace (idkmanager, acme, ...) |
host / port |
TrueNAS UI host and port |
use_tls |
Must be true whenever an API key is set — TrueNAS auto-revokes a key used over plain HTTP. config/save rejects the combination. |
verify_tls |
Whether to verify the appliance's TLS certificate (usually self-signed → false) |
api_key_ro / api_key_rw |
Service-account keys (svc-pegaprox-ro/svc-pegaprox-rw); masked as *** on every GET config, round-tripped on save |
readonly |
Server-evaluated kill-switch — stays effective even if api_key_rw is set |
poll.{fast_s,slow_s,cold_s} — the polling budget from the brief §4.3;
unused in F0 (no subsystem polls anything yet), validated and persisted for
F1 to consume.
Connects the WebSocket and calls auth.login_with_api_key — nothing else
(no subsystem call). Used from the Settings tab's "Probar conexión" button,
works against either a saved instance (id) or an unsaved draft from the
form, and never persists anything to config.json. (F0 called this "the
only real TrueNAS interaction allowed" — F1 adds seven read-only ones on
top, see above.)
- Reconnect trigger, not a background poller. The client's automatic
reconnection only runs after the WebSocket reader thread observes an
unexpected disconnect (a
recv()failure) — there is no separate keepalive/health-check thread pinging the socket. This satisfies "backoff- jitter reconnection" without adding a second timer to reason about in F0; F1's job-tracking loop can add liveness probing if TrueNAS's own socket timeout proves too silent in practice.
- Relogin/resubscribe only on recovery, not on the very first connect.
An earlier draft called
_relogin_and_resubscribe()unconditionally after every successfulconnect(). That recursed:subscribe()registers its callback before issuing thecore.subscribecall, so the first-ever connect (triggered lazily by that samesubscribe()) would re-entercall()for the same subscription while the outer call was still in-flight. Fixed by only running relogin/resubscribe from the_background_reconnectpath (a genuine post-drop recovery), never from plain lazy-connect. log_audit(..., cluster=client_id)not used as written in brief §3.1. Without a live PegaProx host to confirmlog_audit's real keyword arguments, passing an unverifiedcluster=kwarg risked a runtimeTypeErrorin production.client_idis instead folded into thedetailsstring of every audit call — same information, no dependency on an unconfirmed signature. Revisit oncepegaprox/utils/audit.pyis readable from this workspace.instances/testaccepts drafts, not just saved instances, so the Settings "Probar conexión" button works before hitting Save (per the UI flow described in brief §6/§7) — it resolves a maskedapi_key_roback to the stored value only when anidmatching a saved instance is supplied.
instance_idas a query param, not a URL path segment. The brief's/<instance_id>/<subsystem>phrasing reads like a URL template, but the only routing mechanism confirmed in production (pegaprox.api.plugins.register_plugin_route, verified againstpegaprox-plugin-wake-on-lan) maps one FIXED path string per handler — wake-on-lan's own dynamic routes already use query params for exactly this reason. Followed the proven pattern rather than assume PegaProx's catch-all supports path parameters it hasn't been observed to support.shares.list()/apps_vms.list()return a dict, not a flat list. Both wrap multiple distinct TrueNAS collections (SMB/NFS/3× iSCSI; apps/VMs) that the UI's own tab layout (brief §6) treats as separate groups — flattening them would just force the caller to re-split what was artificially joined.- No
virt.instance.*shim for VMs. The brief flags 25.04→25.10 moved VMs between Incus and libvirt namespaces.vm.queryresponds (with[]) on the real.64(25.10.1) today, so no shim is implemented — adding one now would be speculative code for a namespace not in use on the only instance this plugin talks to. Add it if/when a future instance provesvm.queryerrors andvirt.instance.queryanswers instead.
- iSCSI CRUD. An iSCSI "share" is a 3-way join (target + extent + targetextent) — meaningfully bigger than SMB/NFS create/update/delete, which already covers the "share a folder without opening TrueNAS" ask. iSCSI stays read-only.
- No job poller for the (rare) async writes — a create/delete that comes
back as a TrueNAS job is reported as
pendingwith a re-check path, not actively polled to completion. - No
check_cluster_accessper-client RBAC — admin-only gate until a second real client is on-boarded (all instances so far belong to the operator's ownidkmanagertenant, not a second real client). - Two instances configured today, both
idkmanager:datos-64(general SMB/NFS/iSCSI storage) andnvmeof-253(the NVMe-oF pool backing VM block storage, added 2026-07-21 —svc-pegaprox-ro/READONLY_ADMIN, read-only). A second real client tenant still requires explicit operator confirmation in a separate session, same review discipline as every write phase above. smart.test.results(SMART data) was dropped, not deferred — this TrueNAS version has nosmart.*method namespace at all.
pip install -r requirements-dev.txt
pytest -q
tests/conftest.py stubs flask and pegaprox.* at the module level so
__init__.py imports standalone in CI without a live PegaProx host. core/
tests (tests/unit/) never touch a real socket — a FakeTransport drives
send/recv through an in-memory queue.
install.sh copies __init__.py, manifest.json and src/ into
/opt/PegaProx/plugins/truenas, seeds an empty config.json (instances are
added afterwards from the Settings tab, never shipped in the repo), and
installs a systemd timer (truenas-maintenance.sh) that restores the plugin
from a cache outside /opt/PegaProx if a PegaProx upgrade ever wipes it.
uninstall.sh removes the plugin, its guard timer, and its config
(including any saved API keys).
Deployment is NOT performed by this repository's automation. Running
install.sh against production (CT119/pve1) requires explicit operator
confirmation in a separate session — see brief §0.5.