Migrate TrueNAS to supported JSON-RPC transport

This commit is contained in:
rcourtman
2026-07-24 00:07:19 +01:00
parent 0c17922055
commit b81ba7dd06
19 changed files with 2145 additions and 371 deletions
@@ -615,8 +615,13 @@ claim, broad same-shape TrueNAS iteration is not default work above this
declared floor.
1. Architecture boundary:
TrueNAS is API-first. The unified agent may augment a TrueNAS system later,
but it is not required for bootstrap or baseline support. TrueNAS must
TrueNAS is API-first. SCALE 25.04 and later uses the supported versioned
JSON-RPC 2.0 WebSocket API at `/api/current`; REST is a connection-local,
version-gated compatibility boundary only for SCALE releases before 25.04
and TrueNAS CORE/FreeNAS. Authentication, permission, TLS, protocol, and
method errors on the current endpoint fail closed and never downgrade to
REST. The unified agent may augment a TrueNAS system later, but it is not
required for bootstrap or baseline support. TrueNAS must
project into the canonical `agent`, `vm`, `app-container`, `storage`,
`physical-disk`, and recovery contracts. TrueNAS app rows may carry a
TrueNAS-native app facet sourced from `app.query` / `active_workloads`, and
@@ -627,7 +632,10 @@ declared floor.
Supported now through the shared platform-connections flow and
`/api/truenas/connections`. Operators can add, test, edit, retest, and
delete stored TrueNAS connections without re-entering masked secrets on
ordinary edits. `truenas_disabled` is only an explicit server opt-out, not
ordinary edits. Current API-key setup includes the key owner's username for
the supported `auth.login_ex` flow; username-less stored keys remain an
upgrade bridge only while the appliance still exposes its deprecated login
method. `truenas_disabled` is only an explicit server opt-out, not
the baseline state. Out of scope for this floor: a separate TrueNAS-first
wizard, agent-required bootstrap, or disabled-by-default launch posture.
3. Infrastructure visibility:
@@ -5359,7 +5359,13 @@ workspace can render real API-backed status and handoff context without
inventing a settings-local shadow fetch path. `frontend-modern/src/api/truenas.ts`
owns the browser normalizer for that observed summary and must preserve those
native inventory facets instead of collapsing them into a generic app or
storage count. Zero-value legacy
storage count. The same redacted list payload carries an optional secret-free
`transport` object with `mode`, `endpoint`, `tls`, `connected`,
`authMechanism`, `applianceVersion`, `legacyReason`, `reconnects`,
`lastError`, and `lastConnectedAt`. Settings must identify current
`jsonrpc-websocket`, version-gated `legacy-rest`, and not-yet-negotiated states
without exposing a credential or rebuilding transport state in the browser.
Zero-value legacy
`pollIntervalSeconds` config must normalize back to the canonical 60-second
default at this same boundary instead of leaking ambiguous `0` values to the
frontend.
@@ -222,6 +222,7 @@ node-local Agent evidence.
45. `pkg/agents/kubernetes/report.go`
46. `internal/monitoring/temperature.go`
47. `internal/truenas/client.go`
47a. `internal/truenas/transport.go`
48. `internal/truenas/disk_health.go`
49. `internal/truenas/provider.go`
50. `internal/models/ceph_cluster_identity.go`
@@ -434,8 +435,9 @@ node-local Agent evidence.
hard-coded cluster name.
16. Add or change TrueNAS supplemental inventory only through the native
TrueNAS provider path and unified-resource projection. TrueNAS apps are
API-owned application records: `app.query` is the preferred live inventory
source, with legacy REST allowed only as a compatibility fallback. The
API-owned application records: `app.query` is the live inventory source on
the negotiated current transport, with legacy REST allowed only for a
connection proven to run a release that predates the versioned API. The
provider may preserve Docker-compatible runtime metadata for shared
container tooling, but it must also publish the native app identity, state,
version, update availability, workload containers, ports, images, volumes,
@@ -463,10 +465,31 @@ node-local Agent evidence.
`internal/truenas/contract_test.go`. The canonical-ID migration semantics
for rows minted under the retired hostname-keyed derivation live in the
unified-resources contract (record-declared succession, item 27).
TrueNAS storage and alert inventory follow native query methods first:
Every live TrueNAS client owns one explicit, immutable transport decision.
SCALE 25.04 and later must use JSON-RPC 2.0 over a TLS WebSocket at
`/api/current`; authentication, authorization, TLS, protocol, or method
failures on that endpoint are authoritative and must never wake the
deprecated REST bridge. Only an unsupported-endpoint WebSocket handshake
may trigger a REST `/system/info` version probe, and REST may then be
selected only for recognized SCALE releases before 25.04 or TrueNAS
CORE/FreeNAS. Unknown or current versions fail closed. The decision and
persistent socket belong to one configured client, so reconnects or
legacy negotiation for one appliance cannot alter another appliance.
Current API-key authentication uses `auth.login_ex` with the key owner's
username and `API_KEY_PLAIN`; password authentication uses
`PASSWORD_PLAIN`. Username-less stored API keys may use the deprecated
login method only as an upgrade bridge, with explicit remediation when a
release removes that method. Read calls may reconnect with bounded backoff
and replay once after a transport failure. Mutating app calls must never
replay after dispatch because their outcome is ambiguous. Event reads must
retain the `core.subscribe` ID, call `core.unsubscribe` before reusing the
session, and discard a socket after a terminal stream read deadline.
Connection summaries expose only secret-free transport mode, endpoint,
TLS, authentication mechanism, appliance version, reconnect count, and
last-error timing diagnostics.
TrueNAS storage and alert inventory follow the negotiated transport:
pools use `pool.query`, datasets use `pool.dataset.query`, disks use
`disk.query`, and alerts use `alert.list`, with legacy REST allowed only
as compatibility fallback. Inventory readers must only consume fields the
`disk.query`, and alerts use `alert.list`. Inventory readers must only consume fields the
API actually serves on every supported TrueNAS line (CORE 13 REST-only
included): `pool.dataset.query` carries no `mounted` field, so a listed
dataset counts as mounted unless `locked` or an explicit value says
@@ -474,8 +497,9 @@ node-local Agent evidence.
`extra.pools` join cannot cross the REST bridge, so per-disk pool
membership and ZFS member state derive from the vdev topology that
`pool.query` attaches unconditionally; and `disk.temperatures` is a
parameterized method the REST bridge only serves as POST, tried after
native JSON-RPC reporting. Missing disk telemetry is reported as
parameterized method the legacy REST bridge only serves as POST, while
current releases use native JSON-RPC reporting without per-method REST
fallback. Missing disk telemetry is reported as
unknown, never as a failure signal. Regression coverage:
`internal/truenas/client_api_shapes_test.go`. Unhealthy pool state
from `pool.query` must emit a provider-native `zfs_pool_state` incident on
@@ -501,9 +525,11 @@ node-local Agent evidence.
`sharing.nfs.query` publishes native `TrueNASData.Share` on canonical
`network-share` resources parented to the owning dataset or pool when the
API/path supplies that evidence. TrueNAS protection inventory follows the
same native-query rule: ZFS snapshots prefer `zfs.resource.snapshot.query`
with older `pool.snapshot.query`/REST compatibility fallback, and
replication tasks prefer `replication.query`.
same native-query rule: current connections prefer
`zfs.resource.snapshot.query`, with `pool.snapshot.query` allowed only as a
same-transport method-name compatibility path; version-gated legacy
connections use REST. Replication tasks use `replication.query` on current
connections.
TrueNAS system services are also native appliance inventory: `service.query`
is the preferred source for service name, boot enablement, runtime state,
and process IDs. Pulse must publish that data through `TrueNASData.Services`
@@ -1682,9 +1708,9 @@ machine ID, capacity, and poll-health path instead of failing connection tests
or background refreshes during JSON decoding.
That same monitoring boundary now also owns live TrueNAS disk temperatures.
`internal/truenas/client.go` and `internal/truenas/provider.go` must ingest
`disk.temperatures` from the TrueNAS API, fall back to modern
`reporting.get_data` `disktemp` when the dedicated endpoint is unavailable, and
project those readings into the canonical physical-disk model and risk path
legacy `disk.temperatures` from the REST API or `reporting.get_data` `disktemp`
from the current JSON-RPC transport, and project those readings into the
canonical physical-disk model and risk path
instead of leaving temperature telemetry agent-only or adding a TrueNAS-local
presentation shim.
That same monitoring boundary also owns SMART-backed TrueNAS disk risk
@@ -1701,10 +1727,11 @@ null, empty, missing, unknown, or unavailable SMART telemetry to canonical
native failure states such as `FAULTED`, `FAILED`, `OFFLINE`, `REMOVED`, and
`UNAVAIL` must continue to produce canonical disk-health risk.
That same boundary owns boot-pool and replication-target storage posture.
`internal/truenas/client.go` must collect `boot.get_state` with the legacy REST
bridge as compatibility fallback, merge it only within the current configured
connection, and use its vdev leaves to enrich boot-disk pool membership and
native ZFS state. `internal/monitoring/truenas_poller.go` must correlate
`internal/truenas/client.go` must collect `boot.get_state` through the
connection's negotiated transport, use REST only on a version-gated legacy
connection, merge it only within the current configured connection, and use
its vdev leaves to enrich boot-disk pool membership and native ZFS state.
`internal/monitoring/truenas_poller.go` must correlate
`replication.query` intent across providers within the same organization using
local/PULL ownership or a unique configured/observed target-host match for
remote PUSH tasks. The resulting `SET`/`REQUIRE` receive-side read-only posture
@@ -5255,6 +5255,7 @@
"internal/truenas/disk_health.go",
"internal/truenas/fixtures.go",
"internal/truenas/provider.go",
"internal/truenas/transport.go",
"internal/truenas/types.go"
],
"allow_same_subsystem_tests": false,
@@ -5264,7 +5265,8 @@
"internal/monitoring/monitor_polling_test.go",
"internal/truenas/client_test.go",
"internal/truenas/contract_test.go",
"internal/truenas/provider_test.go"
"internal/truenas/provider_test.go",
"internal/truenas/transport_test.go"
]
},
{