Skip to content

Recipes

Worked examples for things people actually do with a TrueNAS box. Each one is complete enough to paste and adapt.

Connect once, reuse

A client opens its websocket lazily and reconnects if the connection drops, so build one and keep it. As a context manager it also closes the transports it opened:

from pytruenas import TrueNASClient

with TrueNASClient("nas.example.com", "1-<api-key>") as nas:
    print(nas.ping())                    # "pong"
    print(nas.me()["pw_name"])           # the authenticated session's user
    print(nas.api.system.info()["version"])

Note

me() returns the session user (auth.me, keyed pw_name), while api.user.query() returns account records (keyed username). They are different shapes — an easy mix-up.

Find things: query and its helpers

query takes middleware filters directly, but the helpers cover the common shapes with less ceremony:

# Every dataset on a pool
nas.api.pool.dataset.query([["pool", "=", "tank"]])

# Exactly one, or None
root = nas.api.user._get(username="root")
root["username"], root["uid"]

# One field off each row
names = [u["username"] for u in nas.api.user.query([], {"select": ["username"]})]

# Does it exist?
if nas.api.sharing.smb._get(name="media") is None:
    ...

Create or update, idempotently

_upsert picks the selector fields, looks for a match, and creates or updates accordingly — so a script can run twice without erroring:

nas.api.user._upsert(
    "username",
    username="svc-backup",
    full_name="Backup service",
    group_create=True,
    home="/nonexistent",
)

# Multiple selector fields, as a tuple
nas.api.sharing.nfs._upsert(("path", "enabled"), path="/mnt/tank/media", enabled=True)

Watch for changes

Subscriptions are bounded queues fed by the live connection. Consume with the iterator, a callback, or both:

sub = nas.subscribe("alert.list")
for event in sub.events(timeout=30):
    print(event.msg, event.fields.get("klass"))
    if event.msg == "removed":
        break
sub.unsubscribe()

A subscription is bound to the connection it was made on and does not survive a reconnect — the events() iterator ending is that signal, so re-subscribe there rather than assuming a permanent stream.

Run a command

result = nas.run("zpool status tank", capture_output="stdout", encoding="utf-8")
print(result.stdout)

# Non-zero is an error unless you say otherwise
nas.run("zpool scrub tank", check=False).returncode

Check first if the target might not have a command channel at all:

if "run" in nas.capabilities:
    nas.run("uptime")
else:
    print("no SSH and no web shell — API only")

Upload and download

The side channels move file content through HTTP(S) rather than the websocket:

# Push a config file into a middleware method that takes an upload
nas.upload(open("cert.pem", "rb").read(), "certificate.create", {"name": "web"})

# Pull a debug bundle
data = nas.download("system.debug", filename="debug.tgz")
open("debug.tgz", "wb").write(data)

Note

A client connected over the local unix socket cannot use download(): the side channel resolves to https://localhost and trips the appliance's self-signed certificate. Connect over wss:// for that.

Several hosts at once

Nothing special is needed — a client per host, and threads if you want them in parallel:

from concurrent.futures import ThreadPoolExecutor

HOSTS = ["nas1.example.com", "nas2.example.com", "nas3.example.com"]

def version(host):
    with TrueNASClient(host, api_key) as nas:
        return host, nas.api.system.info()["version"]

with ThreadPoolExecutor() as pool:
    for host, ver in pool.map(version, HOSTS):
        print(f"{host}: {ver}")

The CLI does this for you — see CLI for --parallel and target range expansion.

Provision SSH for yourself

install_sshcreds() generates (or reuses) a keypair in keychaincredential, installs the public half on root's authorized_keys, and wires the private half into this client's SSH leg — so a client that had only the web shell gains real SSH:

nas = TrueNASClient("nas.example.com", api_key)
nas.capabilities            # {"path", "run"} -- run via the web shell

nas.install_sshcreds()
nas.config.ssh              # now an SshConfig with client_keys set
nas.last_selection          # subsequent run() calls report 'ssh'

Fail loudly on a typo

Unknown credential names and unknown provider names raise rather than being ignored, which turns a silent misconfiguration into an immediate error:

TrueNASClient("wss://nas", passwrd="s3cret")   # ValueError: unknown credential argument
TrueNASClient("wss://nas", executor=["shh"])   # ValueError: unknown executor provider