Skip to content

Storage

The Storage module (id storage, AGPL-3.0-only) integrates TrueNAS SCALE and CORE appliances into FreeSDN as Fabric participants. Unlike every other module, it declares no standalone HTTP routes and no database tables. Its sole job is to own the storage Fabric surface: a health-read operation, a staged blob-write operation, and six health-transition events that the rest of the platform can wire to notifications and automation.

Live storage reads are served through the controller-scoped endpoint at GET /api/v1/controllers/{controller_id}/storage. The module itself only becomes visible when you author a Fabric Connection.


CapabilityMechanism
ZFS pool health rollupstorage.health Fabric read - aggregates across one or all org TrueNAS appliances
Active alert aggregationAll non-dismissed appliance alerts, classified by severity
Disk temperaturesPer-disk SMART polling; max_temp_c surfaced at the appliance level
Scrub statusscrub flattened per pool (function, state, errors, percentage, finished timestamp)
Pool redundancyDerived from vdev topology: RAIDZ1/2/3, Mirror, Stripe
Staged blob writesstorage.store_blob Fabric write - stages a file upload for operator sign-off
Health-transition eventsSix storage.* events emitted on state changes for Fabric wiring

The TrueNAS adapter uses WebSocket JSON-RPC over TLS. There is no plaintext path.

On startup the adapter attempts the WebSocket JSON-RPC transport (/api/current, port 443). It falls back to REST (/api/v2.0) only if the endpoint is unreachable - which typically means a pre-25.04 CORE box. An authentication error on a reachable WS endpoint is surfaced directly; the adapter does not mask it with a doomed REST retry.

All TrueNAS appliances ship self-signed certificates. The adapter defaults to verify_ssl=False (check_hostname off, CERT_NONE) so health polling works out of the box. If you have replaced the appliance certificate with one from a trusted CA, set verify_ssl=True when registering the controller - but do not set it against a self-signed cert: the TLS handshake will fail.


TrueNAS uses an API key stored in FreeSDN’s Controller.password field. It is Fernet-encrypted at rest and decrypted per-request.

In TrueNAS: System → API Keys → Add. The key needs read access for health monitoring. Write access (the filesystem.put job) is required only if you plan to use storage.store_blob.

On SCALE 25.x the default root account has login disabled. Create the API key under truenas_admin (or another login-enabled account). An API key tied to an OTP/2FA account cannot authenticate without an OTP token - use a non-MFA service account.

Open Settings → Controllers → Add Controller and select vendor TrueNAS. Paste the API key into the Password / API Key field. The host must be the appliance hostname or IP - FreeSDN constructs wss://{host}:443/api/current internally.

Alternatively via the API:

POST /api/v1/controllers
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "truenas-s4",
"controller_type": "truenas",
"host": "192.168.1.100",
"port": 443,
"username": "truenas_admin",
"password": "<truenas-api-key>",
"site_id": "<site-uuid>"
}

username is the TrueNAS account that owns the API key - typically truenas_admin on SCALE 25.x (where root login is disabled by default). The ControllerCreate schema validates that username is present for all non-cloud, non-Proxmox controller types; omitting it returns a 422 "Local mode requires username" error.

port is required - omitting it returns a 422 validation error. Use 443 for the standard TrueNAS HTTPS listener. If your appliance listens on a non-standard TLS port, pass that port number instead. If you supply port 80 or 0, the WS client internally maps it to 443 to prevent a plaintext connection, but you should supply the real port explicitly.

After saving, trigger a test from the controller list or call:

POST /api/v1/controllers/{controller_id}/test

A successful probe returns {"success": true, "message": "Connection successful", "status": "connected", "details": {"latency_ms": 42, "mode": "local"}}. An authentication failure returns a 502 with a hint - not a 401 - so that a revoked appliance key does not log the FreeSDN user out. (The transport field - "ws" or "rest" - is present in the storage inventory response at GET /controllers/{id}/storage, not in the test response.)


GET /api/v1/controllers/{controller_id}/storage
Authorization: Bearer <token>

One round-trip aggregates system info, pools, disks, datasets, snapshots, alerts, disk temperatures, services, and data-protection tasks. The response shape:

{
"controller_id": "...",
"name": "truenas-s4",
"host": "192.168.1.100",
"transport": "ws",
"system": {
"version": "TrueNAS-SCALE-24.10.2",
"hostname": "truenas",
"product": "TRUENAS-MINI-R",
"serial": "A1B2C3D4",
"physmem": 137438953472,
"uptime_seconds": 1209600,
"timezone": "America/New_York"
},
"health": {
"status": "ok",
"pool_count": 3,
"alert_count": 1,
"critical_alert_count": 0
},
"alerts": [...],
"services": [...],
"data_protection": {
"snapshot_tasks": [...],
"replication": [...],
"cloudsync": [...]
},
"pools": [...],
"disks": [...],
"datasets": [...],
"snapshot_count": 42
}

The health.status field reflects the worst condition across all pools and alerts:

ConditionStatus
All pools ONLINE, no critical alertsok
Any pool DEGRADED or UNAVAILwarning
Any pool FAULTED, OFFLINE, or REMOVEDerror
A CRITICAL or ALERT active alert present (even if all pools are ONLINE)error
A WARNING or ERROR active alert present (even if all pools are ONLINE)warning

Each pool in the response includes:

FieldDescription
nameZFS pool name
statusONLINE / DEGRADED / FAULTED / OFFLINE / REMOVED / UNAVAIL
usage_percentUsed percentage
redundancyDerived: {type, vdevs, width} (RAIDZ1/2/3, MIRROR, STRIPE)
scrub{function, state, errors, percentage, finished_at_ms}

redundancy is derived by walking the pool’s vdev topology. A pool with no data vdevs (e.g. a newly created pool with only special vdevs) reports UNKNOWN ({"type": "UNKNOWN", "vdevs": 0, "width": 0}).

Each disk in the response includes:

FieldDescription
nameDevice name (e.g. sda)
typeHDD / SSD / NVMe
sizeCapacity in bytes
serialDrive serial number
temperature_cCurrent SMART temperature
read_errorsZFS read error count from pool topology
write_errorsZFS write error count from pool topology
checksum_errorsZFS checksum error count from pool topology
poolName of the pool this disk belongs to (empty if a hot spare or unassigned)
vdev_typevdev topology type for the disk slot (RAIDZ1 / RAIDZ2 / RAIDZ3 / MIRROR / DISK; null if unassigned)

ZFS read/write/checksum error counts are resolved by walking the pool topology tree. Temperature is merged from the SMART feed.


The Storage module registers two Fabric operations in the tier-tagged catalog at GET /api/v1/fabric/catalog.

Aggregates ZFS pool health, alert counts, and disk temperatures across the org’s TrueNAS appliances. Executes inline - no staging, no operator sign-off.

Permission required: storage.view

Input parameters:

ParameterRequiredDescription
controller_idNoUUID of a specific TrueNAS controller. Omit to query all org appliances.

Response structure:

{
"status": "ok",
"count": 2,
"appliances": [
{
"controller_id": "...",
"name": "truenas-s4",
"status": "ok",
"pools": 3,
"degraded_pools": [],
"over_capacity_pools": [],
"alerts": 1,
"critical_alerts": 0,
"max_temp_c": 42.5
},
{
"controller_id": "...",
"name": "truenas-offsite",
"status": "unreachable",
"error": "Connection refused"
}
]
}

status is the worst-case across all queried appliances. A per-appliance connection failure is recorded as status: "unreachable" with an error message - it does not raise and does not prevent the other appliances from being queried. The top-level status escalates to error when any appliance is unreachable.

The operation is fail-closed for tenancy: only TrueNAS controllers belonging to your organization are ever queried, regardless of which controller_id you supply.

Stage a file upload to a TrueNAS dataset. The blob bytes travel via ctx.input_artifact (for example, a camera snapshot forwarded by the Camera module or a Fabric Connection step). The operation is always staged - actual upload runs only after an operator applies the pending change through the dual-gate.

Permission required: storage.write

Feature flag: storage.store_blob

Input parameters:

ParameterRequiredMax lengthDescription
controller_idYes-UUID of the target TrueNAS controller
dataset_pathYes512 charsDestination directory under /mnt, e.g. /mnt/tank/freesdn
filenameYes255 charsTarget filename (no path separators or ..)
site_idNo-UUID for site-scoping (informational; does not filter the appliance)

Accepted content types: image/jpeg, generic binary blob (MEDIA_BLOB).

Path validation (applied at two layers): the dataset_path must start with /mnt/, contain no .. segments, and each segment must match ^[A-Za-z0-9][A-Za-z0-9._-]*$. The filename must contain no /, \, or ... The adapter re-validates both as a backstop.

Upload mechanism: once applied, the executor decodes the blob from the artifact store, re-verifies its SHA-256, and sends it to TrueNAS via a multipart HTTPS POST to /_upload on the same TLS listener. The Authorization: Bearer <api_key> header is sent over TLS only - same revoke rule as the WS transport. Upload timeout is 300 seconds. The adapter then polls core.get_jobs at 1-second intervals until the filesystem.put job reaches SUCCESS, FAILED, or ABORTED (120-second overall timeout).


The storage.poll_health monitoring task emits six events on state transitions, not on every poll. Wiring these to notification or automation actions is safe - they will not fire repeatedly while a pool stays degraded.

Event typeFires when
storage.pool.degradedA ZFS pool transitions to DEGRADED, FAULTED, OFFLINE, REMOVED, or UNAVAIL
storage.pool.healthyA previously non-ONLINE pool returns to ONLINE
storage.capacity.warningA pool crosses the 85% capacity threshold
storage.alert.criticalA new CRITICAL or ERROR alert appears on the appliance
storage.appliance.unreachableThe WS connection to the appliance fails
storage.appliance.onlineThe appliance becomes reachable again after an unreachable period

All events carry:

Payload fieldDescription
controller_idUUID of the TrueNAS controller
controller_nameHuman-readable controller name
poolZFS pool name (where applicable; empty for appliance-level events)
capacity_pctPool used percentage at time of event
critical_alertsCount of active CRITICAL/ERROR alerts on the appliance

Wire storage.pool.degraded to a multi-channel notification via Fabric Connections:

{
"name": "ZFS pool degraded alert",
"source": { "event_type": "storage.pool.degraded" },
"steps": [
{
"operation_id": "fabric.notify",
"params": {
"message": "ZFS pool {{pool}} on {{controller_name}} is degraded. Critical alerts: {{critical_alerts}}",
"channels": ["email", "slack"]
}
}
]
}

Example: camera snapshot to TrueNAS (staged)

Section titled “Example: camera snapshot to TrueNAS (staged)”

Use a Fabric Connection to stage camera snapshots to TrueNAS storage. Because storage.store_blob is staged, an operator must sign off each time:

{
"name": "Camera snapshot to NAS (staged)",
"source": { "event_type": "cameras.snapshot.captured" },
"steps": [
{
"operation_id": "storage.store_blob",
"params": {
"controller_id": "<truenas-controller-uuid>",
"dataset_path": "/mnt/tank/freesdn/snapshots",
"filename": "{{camera_name}}_{{timestamp}}.jpg"
}
}
]
}

See Fabric for the full Connection authoring guide.


The storage module’s get_router() returns an empty APIRouter(). There is no /api/v1/storage/ prefix. This is intentional: the module exists to own the Fabric surface, not to duplicate the controller-scoped endpoint hierarchy.

All storage REST traffic flows through:

GET /api/v1/controllers/{controller_id}/storage - requires controller:read

There are no POST, PUT, PATCH, or DELETE routes in the storage module. Configuration changes (adding, editing, or removing a TrueNAS controller) go through the standard Controllers API.


Permission codeScopeRequired for
storage.viewReadView storage health, pools, alerts, disk temperatures via Fabric storage.health
storage.writeUpdate (staged, sign-off required)Stage blob uploads to a TrueNAS dataset via storage.store_blob

Reading storage data directly via GET /controllers/{id}/storage uses the controller:read permission, not storage.view. The storage module permissions apply to Fabric operation invocations.

Applying a staged storage.store_blob change requires the network:write permission. No additional role minimum (site_admin or above) is enforced - the permission gate alone governs access.


Connection and authentication errors from TrueNAS are mapped to 502 (not 401) deliberately. A revoked or expired API key on the appliance must not cause FreeSDN to log out the FreeSDN user. The response body carries an actionable error hint:

ConditionHTTP statusHint
WS auth failure (bad key, account disabled)502Advises re-generating the TrueNAS API key
WS connection refused or timeout502Advises checking host/port/firewall
API key used over plaintext (key revoked by appliance)502Advises checking TLS configuration
Other502Generic storage controller communication error

No database tables. The storage module has no Alembic migrations and adds no PostgreSQL schema. Pool, alert, and temperature data is fetched live from the appliance on each request. There is no cached state to go stale.

SCALE 25.10/26.0 removed REST v2.0. The /api/v2.0 REST interface returns 404 on modern SCALE. If you are running current SCALE, the WS transport is the only working path. The adapter selects it automatically.

API keys auto-revoke over ws://. TrueNAS records the revocation reason in its audit log. If an appliance API key stops working, check the appliance audit log before generating a new key - a middleware terminating TLS before TrueNAS may be the root cause.

store_blob requires WS transport. The filesystem.put job is a WS JSON-RPC write. Appliances that only expose the legacy REST interface cannot accept blob writes.

store_blob is always staged. There is no force=true path that applies the write inline. The dual-gate - ADAPTER_READ_ONLY=false in the environment AND an operator applying the pending change - is unconditional for writes.

Dataset path must start with /mnt/. TrueNAS datasets always live under /mnt. Paths not starting with /mnt/ are rejected before the staged change is created.

storage.store_blob re-verifies SHA-256 before upload. If the artifact store returns bytes that do not match the stored hash, the apply step aborts. This is not a user-facing error in normal operation - it would indicate storage corruption in FreeSDN’s artifact layer.

Capacity threshold is not UI-configurable. The 85% warning threshold is defined in storage/health.py. The health read and the emitted events both derive from summarize_health(), so catalog reads and event payloads always agree.

StorageLocationsPage is a different feature. The frontend page at /storage-locations manages backup storage destinations (e.g. S3, SFTP targets used by the Backup module). It is not the TrueNAS storage area.


  • Fabric - Author Connections to wire storage events to notifications and automation steps.
  • Configuration Backup - Use the Backup module to create portable .fsdn config snapshots (separate from TrueNAS file-system backup).
  • Compute / Hypervisor - Manage Proxmox VE VMs and containers alongside your TrueNAS appliances from a single interface.
  • Adapters: Supported Vendors - Full adapter maturity matrix showing TrueNAS depth, write surface, and known limitations.
  • Controllers API - Register, edit, test, and remove controllers including TrueNAS appliances.