otto
otto — Our Trusty Testing Orchestrator — is a framework for deploying products to remote hosts for testing and validation. It provides a CLI and a Python API for running commands on remote systems, transferring files, executing test suites, and monitoring host metrics in real time.
Who is otto for?
Otto is a general-purpose tool for developers and testers who need to interact with one or more remote machines as part of their workflow — deploying builds, validating firmware, running integration tests, or collecting performance data.
Two ways to use otto
- CLI users — interact with otto through the
otto run,otto test,otto monitor, andotto covcommands. - API builders — import otto's Python packages to build higher-level automation on top of hosts, suites, and the monitor.
Installation
Otto requires Python 3.10 or later. Install the latest release from PyPI:
pip install otto-sh
The distribution is named otto-sh; the CLI command it installs is otto.
For development installs, building from a wheel, GitHub-release artifacts, and
air-gapped installation, see docs/getting-started.md.
Key concepts
Hosts
A Host represents a machine otto can talk to. UnixHost connects over SSH or Telnet; EmbeddedHost (and its concrete ZephyrHost) drives a firmware/RTOS target over a serial console; LocalHost runs commands on the local machine with no network; DockerContainerHost targets a container. All extend a common BaseHost interface (run, oneshot, send/expect, and — on the networked hosts — put/get).
run executes a command on a host's persistent shell session (state like the
working directory and environment variables are preserved between calls).
oneshot runs each call independently of the persistent shell and of other
concurrent oneshot calls, making it safe to fan out via asyncio.gather().
Labs
Hosts can be reached through intermediate hops — SSH jump hosts that otto
tunnels through automatically. Hops can be chained for multi-hop paths
(otto -> hop1 -> hop2 -> target). All file transfer protocols (SCP, SFTP,
FTP, netcat) work through hops. Embedded hosts use their own console/tftp
transfer backends instead (see the embedded-hosts guide). Set the hop field
in a host's JSON definition or use --hop on the CLI.
A Lab is a JSON file that describes a set of hosts and their topology.
Otto loads labs at startup (via --lab or the OTTO_LAB environment
variable) and makes every host available to instructions, test suites, and
the monitor. Multiple labs can be merged by combining their names with +
(--lab lab_a+lab_b).
[
{
"ip": "192.168.1.1",
"ne": "router1",
"osType": "unix",
"term": "ssh",
"creds": [{ "login": "admin", "password": "secret" }]
},
{
"ip": "192.168.1.2",
"ne": "switch1",
"term": "telnet",
"creds": [{ "login": "admin", "password": "secret" }]
}
]
Repos and settings
Otto discovers your project through a .otto/settings.toml file at the
repository root. This file tells otto where to find your Python libraries,
test suites, run instructions, and lab data:
name = "my_project"
version = "1.0.0"
labs = ["${sutDir}/../lab_data"]
libs = ["${sutDir}/pylib"]
tests = ["${sutDir}/tests"]
init = ["my_instructions"]
${sutDir} is replaced with the repository root at load time. The init list
names Python modules that otto imports at startup — this is where you
register your instructions and shared options.
Instructions (otto run)
An instruction is an async function decorated with @instruction() that
becomes a subcommand of otto run. Instructions have full access to the
lab's hosts and can accept their own CLI options via Typer annotations:
import logging
from otto import all_hosts
from otto.cli.run import instruction
logger = logging.getLogger("otto")
@instruction()
async def deploy(
debug: Annotated[bool, typer.Option("--field/--debug")] = False,
):
for host in all_hosts():
await host.run(["echo deploying", "make install"])
logger.info("Done")
otto -l my_lab run deploy --debug
Test suites (otto test)
A suite is a class that extends OttoSuite and is registered with the
@register_suite() decorator. Each suite becomes a subcommand of otto test.
Suites can define their own Options dataclass whose fields appear as CLI
flags:
from dataclasses import dataclass
from typing import Annotated
import typer
from otto.suite import OttoSuite, register_suite
@dataclass
class _Options:
firmware: Annotated[str, typer.Option(help="Firmware version.")] = "latest"
@register_suite()
class TestDevice(OttoSuite[_Options]):
Options = _Options
async def test_device_reachable(self, suite_options: _Options) -> None:
self.logger.info(f"firmware={suite_options.firmware}")
assert True
otto -l my_lab test TestDevice --firmware 2.1
otto test --iterations 10 --threshold 95 TestDevice
Suites support pytest markers (timeout, retry, parametrize,
integration), non-fatal assertions via self.expect(), per-test artifact
directories, and built-in monitoring.
Both suites and instructions accept an options dataclass. For flags that are
repo-wide (device type, lab environment, etc.), define a single RepoOptions
dataclass in your pylib and inherit it from both sides.
Monitor (otto monitor)
The monitor collects live performance metrics (CPU, memory, disk, network) from one or more hosts and serves an interactive web dashboard:
otto -l my_lab monitor # all hosts, default 5 s interval
otto monitor host1,host2 --interval 2.0 # specific hosts, faster polling
otto monitor --db metrics.db # persist data for later viewing
otto monitor --file metrics.db # replay saved data
Monitoring can also be started from within a test suite using
await self.startMonitor(hosts=...) and await self.stopMonitor().
Coverage (otto cov)
Otto retrieves gcov code-coverage data from the systems under test and renders multi-tier HTML reports — e2e, unit, and manual coverage merged into a single per-line view:
otto -l my_lab test TestDevice --cov # collect coverage during a test run
otto cov report # render the multi-tier HTML report
This works for GCC- and clang-built products on Unix hosts (.gcda
counters fetched over the network, cross-toolchains supported) — and for
embedded RTOS targets, where otto pulls coverage over the serial console
from an instrumented LLEXT extension. See
docs/guide/coverage.md and its per-build-type
subpages (GCC, clang, embedded).
Quick-start example
-
Set the environment — point otto at your repo and lab:
export OTTO_SUT_DIRS=/path/to/my_project otto --lab my_lab --list-hosts # verify hosts are visible
-
Run an instruction:
otto -l my_lab run deploy --debug
-
Run a test suite:
otto -l my_lab test TestDevice --firmware 2.1
-
Monitor hosts:
otto -l my_lab monitor
Documentation
Hosted documentation: otto-sh.readthedocs.io.
The same content lives under docs/ and can be built locally with make docs
— the generated HTML is written to docs/_build/html/. Key entry points:
docs/getting-started.md— installation and first stepsdocs/guide/— detailed guides for each CLI commanddocs/guide/setup/lab-config.md— full lab/host schemadocs/guide/coverage.md— coverage collection & reports (GCC, clang, embedded)docs/guide/hosts/embedded.md— embedded (firmware/RTOS) hostsdocs/guide/hosts/os-profiles.md— OS profiles & custom host classesdocs/library/— using otto as a Python library + recipesdocs/api/— full API reference for all otto packages
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file otto_sh-0.8.2.tar.gz.
File metadata
- Download URL: otto_sh-0.8.2.tar.gz
- Upload date:
- Size: 4.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d127bfbbd1b563c155cfe9b0169460425ef6f9cab2385f9aa4c63d580d919b5
|
|
| MD5 |
7c6e92501fa204310391d6b2096a491a
|
|
| BLAKE2b-256 |
1d2379a477ed33876d7f5b6ffef8479dceac53a28701cc6f133bc56a9b6aadab
|
Provenance
The following attestation bundles were made for otto_sh-0.8.2.tar.gz:
Publisher:
release.yml on ludachrish3/otto-sh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
otto_sh-0.8.2.tar.gz -
Subject digest:
7d127bfbbd1b563c155cfe9b0169460425ef6f9cab2385f9aa4c63d580d919b5 - Sigstore transparency entry: 2332784938
- Sigstore integration time:
-
Permalink:
ludachrish3/otto-sh@546f5b2ea0be8b983a364508a5aa793edddd2b8b -
Branch / Tag:
refs/tags/v0.8.2 - Owner: https://github.com/ludachrish3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@546f5b2ea0be8b983a364508a5aa793edddd2b8b -
Trigger Event:
push
-
Statement type:
File details
Details for the file otto_sh-0.8.2-py3-none-any.whl.
File metadata
- Download URL: otto_sh-0.8.2-py3-none-any.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32f04367e7bfb3fd0dee599a72d999832791a42305cac1ee8f71358b5571367e
|
|
| MD5 |
6a1ff99deb284099478676dc8915e504
|
|
| BLAKE2b-256 |
3f0aca38417ff4fef2ad045e592ff0a2b5540d20364b73cb4f8409ffda31420b
|
Provenance
The following attestation bundles were made for otto_sh-0.8.2-py3-none-any.whl:
Publisher:
release.yml on ludachrish3/otto-sh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
otto_sh-0.8.2-py3-none-any.whl -
Subject digest:
32f04367e7bfb3fd0dee599a72d999832791a42305cac1ee8f71358b5571367e - Sigstore transparency entry: 2332784957
- Sigstore integration time:
-
Permalink:
ludachrish3/otto-sh@546f5b2ea0be8b983a364508a5aa793edddd2b8b -
Branch / Tag:
refs/tags/v0.8.2 - Owner: https://github.com/ludachrish3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@546f5b2ea0be8b983a364508a5aa793edddd2b8b -
Trigger Event:
push
-
Statement type: