Output and scripting
Every command produces the same information in several shapes, and -o picks
one. A command never formats its own output, so -o json means the same thing
everywhere.
Formats
-o | Use |
|---|---|
table | The default. Human readable, aligned, never truncated. |
wide | The same plus the columns left out for width. |
json | Structured, indented. |
yaml | The same as YAML. |
nodeset | Just the node set, folded. |
name | One host name per line. |
jsonpath=… | A field or a template. |
jq=… | A jq program, embedded; no jq binary needed. |
$ clusterctl slurm node list --state drain -o nodeset
exe[0007,0042,0511]
$ clusterctl node list -o json | jq '.[] | select(.rack == "R02") | .name'
$ clusterctl bmc status -n '@rack:R02' -o jsonpath='{.[*].state}'
On On Off On
$ clusterctl exec -n '@compute' -o jq='.[] | select(.exitCode != 0) | .target.name' -- true
A jsonpath or jq expression is checked when the command line is read. A
mistake in it exits 2 before anything has run, rather than after a command has
changed something and has only its result left to print.
JSONPath
The JSONPath templates take the part of the kubectl syntax a command line
needs. Text outside braces is printed as it is; inside braces a path is written
as $.a.b, .a.b, ['a']["b"], [0], [-1], [*] or [1:3]. Several
values are joined with a space. A field name after a dot may hold letters,
digits, _ and -; any other name goes in brackets and quotes, as
['@odata.id'].
range, end, quoted literals such as {"\n"}, filters, recursive descent
and functions are rejected with exit code 2 rather than matching nothing. Use
jq for them:
$ clusterctl exec -n '@compute' -o jq='.[] | "\(.target.name) \(.exitCode)"' -- true
jq
A jq program runs inside clusterctl and stops when the command is
interrupted. It cannot read the environment: $ENV and env are empty.
A string result prints without quotes, as jq -r would print it.
JSON and YAML
Numbers print as they are: an exit code of 0 is 0, not 0.0, and a large
PID keeps every digit, in YAML and in jsonpath and jq too. The YAML leaves
a string unquoted only when no YAML reader could take it for anything else, so
yes, .inf, 2026-09-24 and 10.0.0.1 come out quoted, and a control
character is written as an escape.
The result of a remote command, as exec, copy and provision print it,
looks like this; error appears only when the command failed:
{
"target": {"name": "exe0003", "host": "exe0003.hpc.example.org", "user": "alice_adm"},
"exitCode": 3,
"stdout": "failed\n",
"error": "exe0003 (exe0003.hpc.example.org): command exited 3"
}A value in a table may come from a node, a BMC or a Slurm user, so it is
escaped before it is printed: a newline, carriage return or tab shows as \n,
\r or \t, and any other control character as an escape such as \x1b.
So do a byte that is not UTF-8, the Unicode controls that change the direction
text is shown in, such as \u202e, and the line and paragraph separators
\u2028 and \u2029. A value cannot start a row of its own, show itself in
another order than it has or move the cursor over what is already on the
screen. The json and yaml formats replace a byte that is not UTF-8 with
U+FFFD. The yaml format escapes every other one of these characters in its
own syntax. The json format escapes the C0 control characters and the line
and paragraph separators, but leaves DEL, the C1 control characters and the
direction controls in a string as they are, as JSON allows; a program that
shows a string from it on a terminal has to escape it. jsonpath and jq
print a selected string as it is, like jq -r.
Progress and errors go to stderr
Everything a program would parse goes to standard output; notes, progress and prompts go to standard error. Redirecting one does not lose the other.
An error message often quotes what a node, a BMC or a group source said, so it is escaped the same way before it is printed, except that newlines and tabs are kept: some messages are several lines on purpose, such as the list of problems clusterctl found in a configuration file.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Everything succeeded |
| 1 | clusterctl worked, at least one target failed |
| 2 | The command line or the configuration was rejected |
| 3 | A host could not be reached or authenticated with |
| 130 | Interrupted, or a confirmation was declined |
The difference between 1 and 3 is what lets a script tell “the node said no” from “the node was not there”:
clusterctl exec -n '@compute' -- systemctl is-active slurmd
case $? in
0) echo "all healthy" ;;
1) echo "some nodes are unhealthy" ;;
3) echo "some nodes are unreachable" ;;
*) echo "clusterctl could not run" ; exit 2 ;;
esacRead $? straight after clusterctl. After if ! clusterctl …; then it holds
the status of the !, which is always 0 inside the then. Under set -e,
keep the status with rc=0; clusterctl … || rc=$? and switch on $rc.
Scripting safely
#!/usr/bin/env bash
set -euo pipefail
export CLUSTERCTL_CONTEXT=cluster1
# -y confirms in advance. Without a terminal and without -y, a destructive
# command refuses rather than proceeding unasked.
clusterctl slurm node drain "maintenance $(date -I)" -n '@rack:R02' -y
# Wait for the jobs to drain.
while [ "$(clusterctl slurm job list -n '@rack:R02' -o json | jq length)" -gt 0 ]; do
sleep 60
done
clusterctl bmc power soft -n '@rack:R02' -yPut --dry-run in front of it first. It prints what would happen and changes
nothing; the lookups it needs, such as the Slurm job check, still run.
Overriding configuration for one command
$ clusterctl --set fanout.max=4 exec -n '@compute' -- uptime
$ clusterctl --set ssh.connectTimeout=30s login install
$ CLUSTERCTL_FANOUT=4 clusterctl exec -n '@compute' -- uptime
--set takes a dotted path into the merged configuration and wins over
everything else. clusterctl config view --show-sources lists the paths.