Host configuration (lab.json)¶
Persistent per-host connection tuning, declared in lab.json. (For
per-invocation overrides, see Connection control; for the custom netcat backend,
see Netcat transfers.)
Connection options¶
Every host can be configured with a dedicated options object per network
protocol. The default-constructed options reproduce otto’s historical
defaults exactly, so existing lab.json entries keep working without
changes. To tune a protocol, add the matching *_options object to
the host entry:
Object |
Protocol |
|---|---|
|
SSH sessions |
|
Telnet sessions |
|
SFTP transfers |
|
SCP transfers |
|
FTP transfers (aioftp) |
|
see Netcat transfers |
The same six tables are recognized in four places, layered from least to most specific:
Hardcoded defaults in
otto.host.options— what you get when no*_optionsis supplied anywhere.Per-host
*_optionsinlab.json— the lab’s own values for a single host.Product
[host_preferences]in.otto/settings.toml— applied to every host whose id matches the selector regex. Product values win overlab.json. See Product host preferences.CLI
--term/--transfer— final word, applied at invocation time.
Merging is per key between layers (1)–(3). A host that sets only
port in lab.json still inherits connect_timeout from the
product preference, and so on down to the dataclass default. The
fully resolved options are baked into the UnixHost at construction
time.
For one-off tuning at the call site (e.g. a single test wants a
different port), pass an *_options= keyword to get_host() /
all_hosts(). See Per-call overrides on get_host() / all_hosts() in the library recipes.
SSH¶
Set non-standard port, enable strict host-key checking, and tune the connect timeout:
{
"ip": "10.10.200.12",
"element": "target",
"creds": [{ "login": "admin", "password": "secret" }],
"ssh_options": {
"port": 2222,
"known_hosts": "/home/user/.ssh/known_hosts",
"connect_timeout": 5.0,
"keepalive_interval": 30
}
}
Anything supported by asyncssh.connect() but not surfaced as a
curated field is reachable via extra, which is forwarded verbatim:
{
"ssh_options": {
"extra": {
"config": ["/etc/ssh/otto_ssh_config"],
"proxy_command": "corkscrew proxy 8080 %h %p"
}
}
}
Port forwarding¶
Structured forwards are declarative and applied right after the
connection opens. Each list element maps straight to an
asyncssh.SSHClientConnection.forward_*_port call:
{
"ssh_options": {
"local_forwards": [
{"listen_host": "localhost", "listen_port": 8080,
"dest_host": "web.internal", "dest_port": 80}
],
"remote_forwards": [
{"listen_host": "", "listen_port": 9000,
"dest_host": "localhost", "dest_port": 22}
],
"socks_forwards": [
{"listen_host": "localhost", "listen_port": 1080}
]
}
}
For forwards that aren’t expressible in JSON (UNIX-socket forwards,
X11, custom subsystems), build the SshOptions in Python and supply
a post_connect async hook — see the
connection options recipe.
Telnet¶
{
"telnet_options": {
"port": 2323,
"cols": 200,
"rows": 50,
"echo_negotiation_timeout": 1.0
}
}
Set auto_window_resize to true for interactive telnet sessions
to have otto install a SIGWINCH handler that sends NAWS updates on
every local terminal resize — remote TUIs (vi, top, less)
then reflow like they do under SSH. It defaults to off so that
automated runs produce deterministic output.
Embedded / UART-backed consoles — four extra fields matter when the
telnet endpoint is a QEMU -serial telnet: bridge rather than a Unix
telnetd (TelnetOptions):
write_chunk_size(default0) — split each command write into chunks of at most this many bytes.0sends the whole payload in one call (correct for a host-terminated shell). Set a positive value (e.g.64) for a UART-backed RTOS shell that overruns its console RX FIFO on a multi-KBllext load_hexline.write_chunk_delay(default0.0) — seconds to pause between chunks; ignored whenwrite_chunk_sizeis0.login(defaulttrue) — setfalsefor a bare-metal / RTOS shell with no login step; otherwise otto waits for alogin:prompt that never arrives and hangs the connection.login_prompt(default":"byte) — byte delimiter that ends the login / password prompts. The default matcheslogin:,Username:,Password:, etc.single_client_console(defaultfalse) — settruewhen the endpoint is a single-client console (e.g. Zephyrshell_telnet). Otto registers the transport so the embedded teardown can force-release the slot if a timed-out test left it half-open. Leavefalsefor ordinary multi-session telnetd.
SFTP, SCP, FTP, Netcat¶
{
"sftp_options": { "env": { "LANG": "C" } },
"scp_options": { "block_size": 65536, "preserve": true },
"ftp_options": { "port": 2121, "ssl": true, "socket_timeout": 30 }
}
SftpOptions, ScpOptions, and FtpOptions each carry an extra
dict that is forwarded verbatim to the underlying library (asyncssh,
aioftp) for any option not surfaced as a curated field. NcOptions
has no extra — all netcat knobs are curated fields.
Notable SFTP fields (SftpOptions): env
sets remote environment variables; send_env forwards named local
variables to the remote SFTP process.
Notable SCP fields (ScpOptions): recurse
(default true) controls directory recursion — set it false to
transfer a single file without descending; preserve carries mtime/atime/mode.
Notable FTP fields (FtpOptions): beyond
port, ssl, and socket_timeout, the impactful knobs are
connection_timeout (handshake), path_timeout (list/stat),
read_speed_limit / write_speed_limit (bytes/sec caps, null =
unlimited), and passive_commands (default ["epsv", "pasv"]).
Netcat has additional options and auto-detection strategies — see Netcat transfers.
Shell history¶
otto drives a Unix host through a persistent interactive shell, so without intervention every command it runs is appended to that host’s shell history — burying a human’s own history under automation traffic on a shared lab box.
By default otto suppresses this: on each shell it opens it neutralizes
HISTFILE before running anything, so nothing it does reaches
~/.bash_history. Set shell_history to true on a host where otto’s
commands should stay visible in the shell’s own history (e.g. an audited
box where the history file is the record of what touched it):
{
"ip": "10.10.200.12",
"element": "target",
"creds": [{ "login": "admin", "password": "secret" }],
"shell_history": true
}
Being a plain host field, it can also be defaulted for a whole class of hosts
from an [os_profiles.<name>] block in .otto/settings.toml, with a
per-host lab.json value overriding the profile. It is not accepted in
[host_preferences], which takes only the menu-style capabilities
(term / transfer / impairer).
What suppression covers, and what it deliberately doesn’t:
Path |
Suppressed? |
|---|---|
SSH and telnet sessions ( |
yes |
Shells entered via a login proxy — |
yes; |
|
not needed — an exec channel has no PTY, and a non-interactive shell keeps no history at all |
Local host commands |
not needed — non-interactive |
|
no, deliberately — see the caveat below |
Embedded / Zephyr targets |
not applicable — their shell history is a RAM ring buffer, never a file |
Note
otto login is excluded because it hands you a real shell, and silently
losing up-arrow recall would be worse than the noise. The trade-off is not
free: if that login goes through a login proxy (--as-user), otto’s own
__OTTO_…_RECOVER__ resync probe is written into the elevated shell and so
appears in its history. The two cannot both be had — suppressing the probe
means suppressing your history for the whole session.
Suppression is best-effort and silent by design: every part of it is guarded, so a shell that refuses all of it keeps working, merely unsuppressed. The guards are load-bearing rather than decorative — POSIX makes both an error in a special builtin and a failed variable assignment abort the line, either of which would strand the readiness probe that shares it and take the host offline. Do not simplify them away.
Notably otto neutralizes HISTFILE rather than clearing HISTSIZE —
setting HISTSIZE=0 would make bash write its emptied history list over
the history file at exit, destroying the user’s real history.
SNMP monitoring block¶
A host that exposes metrics over SNMP rather than a shell carries an snmp
block (SnmpOptions) instead of (or alongside) the
*_options transport objects. The full field reference and a worked example
are in otto monitor — see the Configuring the snmp block in lab.json
section.
Per-host toolchain¶
Each host can specify a toolchain that tells otto which gcov and
lcov binaries to use for coverage report generation. This is
essential when hosts run products built with different cross-compilers.
Add an optional toolchain object to the host entry in lab.json:
{
"ip": "10.10.200.12",
"element": "target",
"board": "arm-board",
"creds": [{ "login": "admin", "password": "secret" }],
"toolchain": {
"sysroot": "/opt/arm-toolchain"
}
}
Tool paths (gcov, lcov) are resolved relative to the sysroot.
The defaults are usr/bin/gcov and usr/bin/lcov, so setting just
sysroot is sufficient when the toolchain follows the standard layout.
For non-standard layouts, override individual paths. A host whose
product is built with clang --coverage points gcov at an
llvm-cov binary — otto substitutes the one-word llvm-cov gcov
wrapper that lcov requires at capture time:
{
"toolchain": {
"sysroot": "/usr/lib/llvm-18",
"gcov": "bin/llvm-cov",
"lcov": "/usr/bin/lcov"
}
}
When no toolchain is specified, otto uses the system-installed tools
(/usr/bin/gcov, /usr/bin/lcov). Otto can also auto-discover
the toolchain from .gcno files produced during compilation – see the
coverage guide for details.