Skip to content

CLI

The pytruenas console script (built on duho) runs a command against one or more targets.

pytruenas --help
pytruenas <command> [command-options] [command-positionals] [TARGET ...]

Targets are the trailing positional arguments of the command — a command's own positionals (like query's namespace) come first, and any hosts after them are the targets.

Targets

Each target may be comma-separated and supports [A-Z]/[0-9] range expansion. With no target given, the command runs against localhost (the local middleware socket).

pytruenas query user 'nas[1-3].example.com'
pytruenas query user nas1 nas2 --parallel 2
pytruenas query user nas1,nas2

--parallel N runs several targets concurrently, each with its own connected client and its own log prefix.

Credentials and TLS

Credentials come from the target string (wss://root:pw@nas, wss://nas with 1-<api-key> as the password). A target that carries none falls back to $PYTRUENAS_CREDENTIALS, then to credentials: in the config file — either a connection string or a mapping of keyword arguments:

export PYTRUENAS_CREDENTIALS='1-<64-char-api-key>'
pytruenas call system.version nas.example.com

A password in a target string must percent-encode /, ?, #, @, [ and ] (%2F, %3F, %23, %40, %5B, %5D): unencoded, they change where the host starts, and a target that does not parse is refused with that advice.

TLS certificates are verified the way the library verifies them (the operating system's trust). A lab box with a self-signed certificate needs an explicit opt-out, or a CA bundle:

pytruenas call -k system.version nas                          # or --insecure
pytruenas call --ca-bundle /etc/pki/lab-ca.pem system.version nas
PYTRUENAS_CA_BUNDLE=/etc/pki/lab-ca.pem pytruenas call system.version nas

-k also skips the SSH host-key check, as verify=False does in the library. $PYTRUENAS_INSECURE and the config file's insecure / ca_bundle set the default for a whole site; a flag on the command line wins over the environment, and the environment over the file.

Commands

query — read a namespace

pytruenas query user nas.example.com
pytruenas query user -f username=root nas.example.com     # -f/--filter, repeatable
pytruenas query pool.dataset nas.example.com

query issues <namespace>.query, so it works on queryable namespaces (user, pool.dataset, …), not on plain methods like system.info.

help — what does this method take?

pytruenas help user.create nas.example.com    # one method, in full
pytruenas help user nas.example.com           # a namespace's methods
pytruenas help all nas.example.com            # every namespace

Prints every field with its type, whether it is required, its default, the values an enum allows, and whether the method is a job (a job returns an id, not a result) — read from the appliance's own API definition, so it matches the version in front of you rather than the docs. --json emits the definition slice instead of prose.

NAME is required and comes first, like query's namespace — hence all for the index, rather than omitting it. (Omitting it cannot work: argparse would claim the only remaining word, leaving no target.)

The schema is fetched from the API one namespace at a time, which needs no shell access and no special role — it works with a plain API key on an account that cannot run commands at all. Answers are cached per host, version and namespace. --refresh-api re-fetches, needed only after upgrading an appliance in place.

--dump instead reads the whole middlewared --dump-api definition. That needs command access and weighs ~24 MB, so use it only for --api-version, to describe a version older than the one running.

call — invoke any method

pytruenas call system.info nas.example.com
pytruenas call core.ping nas.example.com

call invokes any middleware method by its dotted name (unlike query, which only covers <namespace>.query). Parameters are JSON values via -p/--param (repeatable), in any position relative to the method and the targets.

For anything that writes, pass fields after a literal -- instead of hand-written JSON:

pytruenas call user.create nas.example.com -- \
    --username=svc --full_name='Svc account' --group_create=true

pytruenas call user.update -p 1 nas.example.com -- --full_name='New name'

--field=value and bare field=value are the same thing there, and each value is typed from the method's schema: booleans, integers, null for a nullable field, arrays from a comma list or a repeated flag, and nested objects from dotted names (--options.acl=true). Two escape hatches: --name:json=<json> for a literal JSON value, and --name=@path to read one from a file — which is what you want for a key or a certificate.

An unknown field name or a bad enum value is an error that names the alternatives, raised before anything is called, so a typo cannot reach the appliance as a half-filled payload.

Note

The -- separator is required rather than decorative: the valid field names are only known once the method name is parsed, so bare top-level --flags would collide with the global options — a middleware field named config or parallel would be unreachable. Targets go before the separator, as they do for deploy.

--no-schema skips the definition entirely (values are typed the way -p types them, and no name is checked).

See Provisioning for worked examples.

dump-api — dump the API definition

pytruenas dump-api nas.example.com > api.json

The dump is built and compressed on the target, then fetched over SFTP when the host has an SSH leg and in verified chunks through the command channel otherwise. It is ~24 MB on 26.0 and takes about 75 s to produce, so it is cached per host and API version under $PYTRUENAS_CACHE and shared with help and generate-typings.

It holds the API version the appliance runs. The appliance serves older ones too — eight on 26.0, five times the bytes — and the dump is pruned to the current one on the target before anything is transferred; --all-versions keeps them all.

generate-typings — build .pyi stubs

See Generating typings.

Config file

A config file supplies these settings, and only these; any other key is ignored (targets and options always come from the command line). It is TOML unless it says otherwise:

credentials = "1-<64-char-api-key>"   # or a table: {username = "root", password = "..."}
ca_bundle = "/etc/pki/lab-ca.pem"     # or: insecure = true
cmdspath = ["mycommands"]             # extra command sources, relative to this file

The suffix picks the format (.toml, .yaml/.yml, .json), and a .cfg file is the format its #!<format> first line names (#!yaml, #!json, also #!/usr/bin/env yaml; the line is not part of the document), else TOML. So a pytruenas.cfg can be YAML (with the yaml extra) by starting it with a #!yaml line:

#!yaml
credentials: 1-<64-char-api-key>
ca_bundle: /etc/pki/lab-ca.pem
  • credentials — used for a target that carries none, after $PYTRUENAS_CREDENTIALS.
  • insecure / ca_bundle — the TLS default, below -k/--ca-bundle and $PYTRUENAS_INSECURE/$PYTRUENAS_CA_BUNDLE.
  • cmdspath — directories or packages to load commands from.

The file is --config FILE (-c), else $PYTRUENAS_CONFIG, else pytruenas.cfg in your configuration directory (%APPDATA% on Windows, $XDG_CONFIG_HOME or ~/.config elsewhere); the current directory is never searched. The default file may be absent; a file you named must exist, and a file that does not parse stops the run with exit status 2 before any target.

Your own commands

A command source is a directory of command modules (or RunPath step directories), or an importable package of them. Sources add up, and a later one's command replaces a same-named earlier one:

  1. the built-ins;
  2. $PYTRUENAS_PATH, separated by os.pathsep (:, or ; on Windows);
  3. each --cmdspath SOURCE, repeated for more than one;
  4. the config file's cmdspath.
pytruenas --cmdspath ./ops --cmdspath C:\work\more-commands mytask nas1

A source that is neither a directory nor an importable package is skipped with a warning.

MCP

Since duho 0.6.0, setting PYTRUENAS_MCP=stdio makes pytruenas serve MCP over stdio instead of running its normal CLI dispatch, exposing call, query, help, dump-api, generate-typings and your RunPath commands as MCP tools for an agent to call directly — with no extra wiring on pytruenas's side. This is duho's own trigger (checked in both duho.main and duho.app, so it applies here too), keyed off the app's Env prefix (PYTRUENAS_).

deploy is not a tool: installing software on an appliance is for a person to choose. call is, and it reaches every middleware method — including ones that change the target — so give an agent credentials scoped to what it should be able to do. A command module of your own leaves the tool list with a module-level _mcp_ = False.

Each tool takes the command's own arguments plus targets, the hosts it runs on — the same trailing positionals as on the command line, so with none it runs against localhost:

{"name": "pytruenas.query", "arguments": {"namespace": "user", "targets": ["nas1"]}}
PYTRUENAS_MCP=stdio pytruenas   # serves MCP tools over stdio
pytruenas --help                # unaffected: the env var is unset here