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-bundleand$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:
- the built-ins;
$PYTRUENAS_PATH, separated byos.pathsep(:, or;on Windows);- each
--cmdspath SOURCE, repeated for more than one; - 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