otto-sh 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- otto_sh-0.1.0/LICENSE +21 -0
- otto_sh-0.1.0/PKG-INFO +229 -0
- otto_sh-0.1.0/README.md +189 -0
- otto_sh-0.1.0/pyproject.toml +164 -0
- otto_sh-0.1.0/src/otto/__init__.py +6 -0
- otto_sh-0.1.0/src/otto/__main__.py +3 -0
- otto_sh-0.1.0/src/otto/cli/__init__.py +4 -0
- otto_sh-0.1.0/src/otto/cli/banner.py +10 -0
- otto_sh-0.1.0/src/otto/cli/callbacks.py +14 -0
- otto_sh-0.1.0/src/otto/cli/cov.py +174 -0
- otto_sh-0.1.0/src/otto/cli/host.py +189 -0
- otto_sh-0.1.0/src/otto/cli/login.py +0 -0
- otto_sh-0.1.0/src/otto/cli/main.py +440 -0
- otto_sh-0.1.0/src/otto/cli/monitor.py +169 -0
- otto_sh-0.1.0/src/otto/cli/reservation.py +78 -0
- otto_sh-0.1.0/src/otto/cli/run.py +166 -0
- otto_sh-0.1.0/src/otto/cli/test.py +583 -0
- otto_sh-0.1.0/src/otto/configmodule/__init__.py +111 -0
- otto_sh-0.1.0/src/otto/configmodule/completion_cache.py +519 -0
- otto_sh-0.1.0/src/otto/configmodule/completion_stubs.py +95 -0
- otto_sh-0.1.0/src/otto/configmodule/configmodule.py +248 -0
- otto_sh-0.1.0/src/otto/configmodule/env.py +115 -0
- otto_sh-0.1.0/src/otto/configmodule/lab.py +114 -0
- otto_sh-0.1.0/src/otto/configmodule/repo.py +503 -0
- otto_sh-0.1.0/src/otto/configmodule/version.py +43 -0
- otto_sh-0.1.0/src/otto/console.py +5 -0
- otto_sh-0.1.0/src/otto/coverage/__init__.py +18 -0
- otto_sh-0.1.0/src/otto/coverage/correlator/__init__.py +5 -0
- otto_sh-0.1.0/src/otto/coverage/correlator/lcov_loader.py +115 -0
- otto_sh-0.1.0/src/otto/coverage/correlator/merger.py +177 -0
- otto_sh-0.1.0/src/otto/coverage/correlator/paths.py +187 -0
- otto_sh-0.1.0/src/otto/coverage/fetcher/__init__.py +3 -0
- otto_sh-0.1.0/src/otto/coverage/fetcher/remote.py +152 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/__init__.py +3 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/html_renderer.py +375 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/static/report.css +291 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/static/report.js +53 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/templates/file.html +95 -0
- otto_sh-0.1.0/src/otto/coverage/renderer/templates/index.html +85 -0
- otto_sh-0.1.0/src/otto/coverage/reporter.py +373 -0
- otto_sh-0.1.0/src/otto/coverage/store/__init__.py +3 -0
- otto_sh-0.1.0/src/otto/coverage/store/model.py +363 -0
- otto_sh-0.1.0/src/otto/host/__init__.py +23 -0
- otto_sh-0.1.0/src/otto/host/connections.py +301 -0
- otto_sh-0.1.0/src/otto/host/host.py +502 -0
- otto_sh-0.1.0/src/otto/host/interact.py +544 -0
- otto_sh-0.1.0/src/otto/host/localHost.py +207 -0
- otto_sh-0.1.0/src/otto/host/options.py +414 -0
- otto_sh-0.1.0/src/otto/host/remoteHost.py +678 -0
- otto_sh-0.1.0/src/otto/host/repeat.py +161 -0
- otto_sh-0.1.0/src/otto/host/session.py +849 -0
- otto_sh-0.1.0/src/otto/host/telnet.py +206 -0
- otto_sh-0.1.0/src/otto/host/toolchain.py +74 -0
- otto_sh-0.1.0/src/otto/host/toolchain_discovery.py +182 -0
- otto_sh-0.1.0/src/otto/host/transfer.py +1225 -0
- otto_sh-0.1.0/src/otto/host/transport.py +87 -0
- otto_sh-0.1.0/src/otto/logger/__init__.py +9 -0
- otto_sh-0.1.0/src/otto/logger/formatters.py +102 -0
- otto_sh-0.1.0/src/otto/logger/levels.py +9 -0
- otto_sh-0.1.0/src/otto/logger/logger.py +240 -0
- otto_sh-0.1.0/src/otto/monitor/__init__.py +32 -0
- otto_sh-0.1.0/src/otto/monitor/collector.py +686 -0
- otto_sh-0.1.0/src/otto/monitor/events.py +61 -0
- otto_sh-0.1.0/src/otto/monitor/parsers.py +347 -0
- otto_sh-0.1.0/src/otto/monitor/server.py +284 -0
- otto_sh-0.1.0/src/otto/monitor/static/dashboard.css +373 -0
- otto_sh-0.1.0/src/otto/monitor/static/dashboard.html +80 -0
- otto_sh-0.1.0/src/otto/monitor/static/dashboard.js +786 -0
- otto_sh-0.1.0/src/otto/monitor/static/plotly.min.js +8 -0
- otto_sh-0.1.0/src/otto/params.py +37 -0
- otto_sh-0.1.0/src/otto/reservations/__init__.py +133 -0
- otto_sh-0.1.0/src/otto/reservations/check.py +154 -0
- otto_sh-0.1.0/src/otto/reservations/identity.py +56 -0
- otto_sh-0.1.0/src/otto/reservations/json_backend.py +167 -0
- otto_sh-0.1.0/src/otto/reservations/null_backend.py +24 -0
- otto_sh-0.1.0/src/otto/reservations/protocol.py +110 -0
- otto_sh-0.1.0/src/otto/storage/__init__.py +14 -0
- otto_sh-0.1.0/src/otto/storage/factory.py +173 -0
- otto_sh-0.1.0/src/otto/storage/json_repository.py +201 -0
- otto_sh-0.1.0/src/otto/storage/protocol.py +74 -0
- otto_sh-0.1.0/src/otto/suite/__init__.py +7 -0
- otto_sh-0.1.0/src/otto/suite/expect.py +149 -0
- otto_sh-0.1.0/src/otto/suite/plugin.py +251 -0
- otto_sh-0.1.0/src/otto/suite/register.py +135 -0
- otto_sh-0.1.0/src/otto/suite/suite.py +441 -0
- otto_sh-0.1.0/src/otto/suite/timeout.py +55 -0
- otto_sh-0.1.0/src/otto/utils.py +131 -0
- otto_sh-0.1.0/src/otto/version.py +7 -0
otto_sh-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Collins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
otto_sh-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: otto-sh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Our Trusty Testing Orchestrator - framework and CLI for deploying, testing, and monitoring software across remote hosts.
|
|
5
|
+
Keywords: testing,automation,orchestration,ssh,telnet,asyncio,cli,remote-execution
|
|
6
|
+
Author: Chris Collins
|
|
7
|
+
Author-email: Chris Collins <chriscoll93@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Requires-Dist: aioftp>=0.27.2
|
|
21
|
+
Requires-Dist: aiosqlite>=0.21.0
|
|
22
|
+
Requires-Dist: asyncssh>=2.22.0
|
|
23
|
+
Requires-Dist: fastapi>=0.135.1
|
|
24
|
+
Requires-Dist: jinja2>=3.1.0
|
|
25
|
+
Requires-Dist: pytest>=9.0.1
|
|
26
|
+
Requires-Dist: pytest-asyncio>=1.3.0
|
|
27
|
+
Requires-Dist: rich>=14.3.3
|
|
28
|
+
Requires-Dist: sse-starlette>=3.3.3
|
|
29
|
+
Requires-Dist: telnetlib3>=4.0.1
|
|
30
|
+
Requires-Dist: tomli>=2.4.0
|
|
31
|
+
Requires-Dist: typer>=0.24.0
|
|
32
|
+
Requires-Dist: uvicorn>=0.42.0
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Project-URL: Homepage, https://github.com/ludachrish3/otto-sh
|
|
35
|
+
Project-URL: Source, https://github.com/ludachrish3/otto-sh
|
|
36
|
+
Project-URL: Issues, https://github.com/ludachrish3/otto-sh/issues
|
|
37
|
+
Project-URL: Documentation, https://otto-sh.readthedocs.io
|
|
38
|
+
Project-URL: Changelog, https://github.com/ludachrish3/otto-sh/blob/main/CHANGELOG.md
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# otto
|
|
42
|
+
|
|
43
|
+
**otto** — Our Trusty Testing Orchestrator — is a framework for deploying
|
|
44
|
+
products to remote hosts for testing and validation. It provides a CLI and a
|
|
45
|
+
Python API for running commands on remote systems, transferring files,
|
|
46
|
+
executing test suites, and monitoring host metrics in real time.
|
|
47
|
+
|
|
48
|
+
## Who is otto for?
|
|
49
|
+
|
|
50
|
+
Otto is a general-purpose tool for developers and testers who need to interact
|
|
51
|
+
with one or more remote machines as part of their workflow — deploying builds,
|
|
52
|
+
validating firmware, running integration tests, or collecting performance
|
|
53
|
+
data.
|
|
54
|
+
|
|
55
|
+
## Two ways to use otto
|
|
56
|
+
|
|
57
|
+
- **CLI users** — interact with otto through the `otto run`, `otto test`, and
|
|
58
|
+
`otto monitor` commands.
|
|
59
|
+
- **API builders** — import otto's Python packages to build higher-level
|
|
60
|
+
automation on top of hosts, suites, and the monitor.
|
|
61
|
+
|
|
62
|
+
## Key concepts
|
|
63
|
+
|
|
64
|
+
### Hosts
|
|
65
|
+
|
|
66
|
+
A **Host** represents a machine otto can talk to. `RemoteHost` connects over
|
|
67
|
+
SSH or Telnet; `LocalHost` runs commands on the local machine without any
|
|
68
|
+
network connection.
|
|
69
|
+
|
|
70
|
+
Both expose the same core interface — `run`, `oneshot`, `send` / `expect`, and
|
|
71
|
+
file-transfer methods (`put`, `get`).
|
|
72
|
+
|
|
73
|
+
`run` executes a command on a host's persistent shell session (state like the
|
|
74
|
+
working directory and environment variables are preserved between calls).
|
|
75
|
+
`oneshot` runs each call independently of the persistent shell and of other
|
|
76
|
+
concurrent `oneshot` calls, making it safe to fan out via `asyncio.gather()`.
|
|
77
|
+
|
|
78
|
+
### Labs
|
|
79
|
+
|
|
80
|
+
Hosts can be reached through intermediate **hops** — SSH jump hosts that otto
|
|
81
|
+
tunnels through automatically. Hops can be chained for multi-hop paths
|
|
82
|
+
(`otto -> hop1 -> hop2 -> target`). All file transfer protocols (SCP, SFTP,
|
|
83
|
+
FTP, netcat) work through hops. Set the `hop` field in a host's JSON
|
|
84
|
+
definition or use `--hop` on the CLI.
|
|
85
|
+
|
|
86
|
+
A **Lab** is a JSON file that describes a set of hosts and their topology.
|
|
87
|
+
Otto loads labs at startup (via `--lab` or the `OTTO_LAB` environment
|
|
88
|
+
variable) and makes every host available to instructions, test suites, and
|
|
89
|
+
the monitor. Multiple labs can be merged by passing several names.
|
|
90
|
+
|
|
91
|
+
### Repos and settings
|
|
92
|
+
|
|
93
|
+
Otto discovers your project through a `.otto/settings.toml` file at the
|
|
94
|
+
repository root. This file tells otto where to find your Python libraries,
|
|
95
|
+
test suites, run instructions, and lab data:
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
name = "my_project"
|
|
99
|
+
version = "1.0.0"
|
|
100
|
+
|
|
101
|
+
labs = ["${sutDir}/../lab_data"]
|
|
102
|
+
libs = ["${sutDir}/pylib"]
|
|
103
|
+
tests = ["${sutDir}/tests"]
|
|
104
|
+
init = ["my_instructions"]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`${sutDir}` is replaced with the repository root at load time. The `init` list
|
|
108
|
+
names Python modules that otto imports at startup — this is where you
|
|
109
|
+
register your instructions and shared options.
|
|
110
|
+
|
|
111
|
+
### Instructions (`otto run`)
|
|
112
|
+
|
|
113
|
+
An **instruction** is an async function decorated with `@instruction()` that
|
|
114
|
+
becomes a subcommand of `otto run`. Instructions have full access to the
|
|
115
|
+
lab's hosts and can accept their own CLI options via Typer annotations:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from otto.cli.run import instruction
|
|
119
|
+
from otto.configmodule.configmodule import all_hosts
|
|
120
|
+
from otto.logger import getOttoLogger
|
|
121
|
+
|
|
122
|
+
logger = getOttoLogger()
|
|
123
|
+
|
|
124
|
+
@instruction()
|
|
125
|
+
async def deploy(
|
|
126
|
+
debug: Annotated[bool, typer.Option("--field/--debug")] = False,
|
|
127
|
+
):
|
|
128
|
+
for host in all_hosts():
|
|
129
|
+
await host.run(["echo deploying", "make install"])
|
|
130
|
+
logger.info("Done")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
otto -l my_lab run deploy --debug
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Test suites (`otto test`)
|
|
138
|
+
|
|
139
|
+
A **suite** is a class that extends `OttoSuite` and is registered with the
|
|
140
|
+
`@register_suite()` decorator. Each suite becomes a subcommand of `otto test`.
|
|
141
|
+
Suites can define their own `Options` dataclass whose fields appear as CLI
|
|
142
|
+
flags:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from dataclasses import dataclass
|
|
146
|
+
from typing import Annotated
|
|
147
|
+
|
|
148
|
+
import typer
|
|
149
|
+
from otto.suite import OttoSuite, register_suite
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class _Options:
|
|
153
|
+
firmware: Annotated[str, typer.Option(help="Firmware version.")] = "latest"
|
|
154
|
+
|
|
155
|
+
@register_suite()
|
|
156
|
+
class TestDevice(OttoSuite[_Options]):
|
|
157
|
+
Options = _Options
|
|
158
|
+
|
|
159
|
+
async def test_device_reachable(self, suite_options: _Options) -> None:
|
|
160
|
+
self.logger.info(f"firmware={suite_options.firmware}")
|
|
161
|
+
assert True
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
otto -l my_lab test TestDevice --firmware 2.1
|
|
166
|
+
otto test --iterations 10 --threshold 95 TestDevice
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Suites support pytest markers (`timeout`, `retry`, `parametrize`,
|
|
170
|
+
`integration`), non-fatal assertions via `self.expect()`, per-test artifact
|
|
171
|
+
directories, and built-in monitoring.
|
|
172
|
+
|
|
173
|
+
Both suites and instructions accept an options dataclass. For flags that are
|
|
174
|
+
repo-wide (device type, lab environment, etc.), define a single `RepoOptions`
|
|
175
|
+
dataclass in your pylib and inherit it from both sides.
|
|
176
|
+
|
|
177
|
+
### Monitor (`otto monitor`)
|
|
178
|
+
|
|
179
|
+
The monitor collects live performance metrics (CPU, memory, disk, network)
|
|
180
|
+
from one or more hosts and serves an interactive web dashboard:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
otto -l my_lab monitor # all hosts, default 5 s interval
|
|
184
|
+
otto monitor host1,host2 --interval 2.0 # specific hosts, faster polling
|
|
185
|
+
otto monitor --db metrics.db # persist data for later viewing
|
|
186
|
+
otto monitor --file metrics.db # replay saved data
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Monitoring can also be started from within a test suite using
|
|
190
|
+
`await self.startMonitor(hosts=...)` and `await self.stopMonitor()`.
|
|
191
|
+
|
|
192
|
+
## Quick-start example
|
|
193
|
+
|
|
194
|
+
1. **Set the environment** — point otto at your repo and lab:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
export OTTO_SUT_DIRS=/path/to/my_project
|
|
198
|
+
otto --lab my_lab --list-hosts # verify hosts are visible
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
2. **Run an instruction:**
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
otto -l my_lab run deploy --debug
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. **Run a test suite:**
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
otto -l my_lab test TestDevice --firmware 2.1
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
4. **Monitor hosts:**
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
otto -l my_lab monitor
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
Hosted documentation: [otto-sh.readthedocs.io](https://otto-sh.readthedocs.io).
|
|
222
|
+
|
|
223
|
+
The same content lives under `docs/` and can be built locally with `make docs`
|
|
224
|
+
— the generated HTML is written to `docs/_build/html/`. Key entry points:
|
|
225
|
+
|
|
226
|
+
- `docs/getting-started.md` — installation and first steps
|
|
227
|
+
- `docs/guide/` — detailed guides for each CLI command
|
|
228
|
+
- `docs/cookbook/` — recipes for common asyncio patterns
|
|
229
|
+
- `docs/api/` — full API reference for all otto packages
|
otto_sh-0.1.0/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# otto
|
|
2
|
+
|
|
3
|
+
**otto** — Our Trusty Testing Orchestrator — is a framework for deploying
|
|
4
|
+
products to remote hosts for testing and validation. It provides a CLI and a
|
|
5
|
+
Python API for running commands on remote systems, transferring files,
|
|
6
|
+
executing test suites, and monitoring host metrics in real time.
|
|
7
|
+
|
|
8
|
+
## Who is otto for?
|
|
9
|
+
|
|
10
|
+
Otto is a general-purpose tool for developers and testers who need to interact
|
|
11
|
+
with one or more remote machines as part of their workflow — deploying builds,
|
|
12
|
+
validating firmware, running integration tests, or collecting performance
|
|
13
|
+
data.
|
|
14
|
+
|
|
15
|
+
## Two ways to use otto
|
|
16
|
+
|
|
17
|
+
- **CLI users** — interact with otto through the `otto run`, `otto test`, and
|
|
18
|
+
`otto monitor` commands.
|
|
19
|
+
- **API builders** — import otto's Python packages to build higher-level
|
|
20
|
+
automation on top of hosts, suites, and the monitor.
|
|
21
|
+
|
|
22
|
+
## Key concepts
|
|
23
|
+
|
|
24
|
+
### Hosts
|
|
25
|
+
|
|
26
|
+
A **Host** represents a machine otto can talk to. `RemoteHost` connects over
|
|
27
|
+
SSH or Telnet; `LocalHost` runs commands on the local machine without any
|
|
28
|
+
network connection.
|
|
29
|
+
|
|
30
|
+
Both expose the same core interface — `run`, `oneshot`, `send` / `expect`, and
|
|
31
|
+
file-transfer methods (`put`, `get`).
|
|
32
|
+
|
|
33
|
+
`run` executes a command on a host's persistent shell session (state like the
|
|
34
|
+
working directory and environment variables are preserved between calls).
|
|
35
|
+
`oneshot` runs each call independently of the persistent shell and of other
|
|
36
|
+
concurrent `oneshot` calls, making it safe to fan out via `asyncio.gather()`.
|
|
37
|
+
|
|
38
|
+
### Labs
|
|
39
|
+
|
|
40
|
+
Hosts can be reached through intermediate **hops** — SSH jump hosts that otto
|
|
41
|
+
tunnels through automatically. Hops can be chained for multi-hop paths
|
|
42
|
+
(`otto -> hop1 -> hop2 -> target`). All file transfer protocols (SCP, SFTP,
|
|
43
|
+
FTP, netcat) work through hops. Set the `hop` field in a host's JSON
|
|
44
|
+
definition or use `--hop` on the CLI.
|
|
45
|
+
|
|
46
|
+
A **Lab** is a JSON file that describes a set of hosts and their topology.
|
|
47
|
+
Otto loads labs at startup (via `--lab` or the `OTTO_LAB` environment
|
|
48
|
+
variable) and makes every host available to instructions, test suites, and
|
|
49
|
+
the monitor. Multiple labs can be merged by passing several names.
|
|
50
|
+
|
|
51
|
+
### Repos and settings
|
|
52
|
+
|
|
53
|
+
Otto discovers your project through a `.otto/settings.toml` file at the
|
|
54
|
+
repository root. This file tells otto where to find your Python libraries,
|
|
55
|
+
test suites, run instructions, and lab data:
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
name = "my_project"
|
|
59
|
+
version = "1.0.0"
|
|
60
|
+
|
|
61
|
+
labs = ["${sutDir}/../lab_data"]
|
|
62
|
+
libs = ["${sutDir}/pylib"]
|
|
63
|
+
tests = ["${sutDir}/tests"]
|
|
64
|
+
init = ["my_instructions"]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`${sutDir}` is replaced with the repository root at load time. The `init` list
|
|
68
|
+
names Python modules that otto imports at startup — this is where you
|
|
69
|
+
register your instructions and shared options.
|
|
70
|
+
|
|
71
|
+
### Instructions (`otto run`)
|
|
72
|
+
|
|
73
|
+
An **instruction** is an async function decorated with `@instruction()` that
|
|
74
|
+
becomes a subcommand of `otto run`. Instructions have full access to the
|
|
75
|
+
lab's hosts and can accept their own CLI options via Typer annotations:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from otto.cli.run import instruction
|
|
79
|
+
from otto.configmodule.configmodule import all_hosts
|
|
80
|
+
from otto.logger import getOttoLogger
|
|
81
|
+
|
|
82
|
+
logger = getOttoLogger()
|
|
83
|
+
|
|
84
|
+
@instruction()
|
|
85
|
+
async def deploy(
|
|
86
|
+
debug: Annotated[bool, typer.Option("--field/--debug")] = False,
|
|
87
|
+
):
|
|
88
|
+
for host in all_hosts():
|
|
89
|
+
await host.run(["echo deploying", "make install"])
|
|
90
|
+
logger.info("Done")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
otto -l my_lab run deploy --debug
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Test suites (`otto test`)
|
|
98
|
+
|
|
99
|
+
A **suite** is a class that extends `OttoSuite` and is registered with the
|
|
100
|
+
`@register_suite()` decorator. Each suite becomes a subcommand of `otto test`.
|
|
101
|
+
Suites can define their own `Options` dataclass whose fields appear as CLI
|
|
102
|
+
flags:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from dataclasses import dataclass
|
|
106
|
+
from typing import Annotated
|
|
107
|
+
|
|
108
|
+
import typer
|
|
109
|
+
from otto.suite import OttoSuite, register_suite
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class _Options:
|
|
113
|
+
firmware: Annotated[str, typer.Option(help="Firmware version.")] = "latest"
|
|
114
|
+
|
|
115
|
+
@register_suite()
|
|
116
|
+
class TestDevice(OttoSuite[_Options]):
|
|
117
|
+
Options = _Options
|
|
118
|
+
|
|
119
|
+
async def test_device_reachable(self, suite_options: _Options) -> None:
|
|
120
|
+
self.logger.info(f"firmware={suite_options.firmware}")
|
|
121
|
+
assert True
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
otto -l my_lab test TestDevice --firmware 2.1
|
|
126
|
+
otto test --iterations 10 --threshold 95 TestDevice
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Suites support pytest markers (`timeout`, `retry`, `parametrize`,
|
|
130
|
+
`integration`), non-fatal assertions via `self.expect()`, per-test artifact
|
|
131
|
+
directories, and built-in monitoring.
|
|
132
|
+
|
|
133
|
+
Both suites and instructions accept an options dataclass. For flags that are
|
|
134
|
+
repo-wide (device type, lab environment, etc.), define a single `RepoOptions`
|
|
135
|
+
dataclass in your pylib and inherit it from both sides.
|
|
136
|
+
|
|
137
|
+
### Monitor (`otto monitor`)
|
|
138
|
+
|
|
139
|
+
The monitor collects live performance metrics (CPU, memory, disk, network)
|
|
140
|
+
from one or more hosts and serves an interactive web dashboard:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
otto -l my_lab monitor # all hosts, default 5 s interval
|
|
144
|
+
otto monitor host1,host2 --interval 2.0 # specific hosts, faster polling
|
|
145
|
+
otto monitor --db metrics.db # persist data for later viewing
|
|
146
|
+
otto monitor --file metrics.db # replay saved data
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Monitoring can also be started from within a test suite using
|
|
150
|
+
`await self.startMonitor(hosts=...)` and `await self.stopMonitor()`.
|
|
151
|
+
|
|
152
|
+
## Quick-start example
|
|
153
|
+
|
|
154
|
+
1. **Set the environment** — point otto at your repo and lab:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export OTTO_SUT_DIRS=/path/to/my_project
|
|
158
|
+
otto --lab my_lab --list-hosts # verify hosts are visible
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
2. **Run an instruction:**
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
otto -l my_lab run deploy --debug
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
3. **Run a test suite:**
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
otto -l my_lab test TestDevice --firmware 2.1
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
4. **Monitor hosts:**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
otto -l my_lab monitor
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
Hosted documentation: [otto-sh.readthedocs.io](https://otto-sh.readthedocs.io).
|
|
182
|
+
|
|
183
|
+
The same content lives under `docs/` and can be built locally with `make docs`
|
|
184
|
+
— the generated HTML is written to `docs/_build/html/`. Key entry points:
|
|
185
|
+
|
|
186
|
+
- `docs/getting-started.md` — installation and first steps
|
|
187
|
+
- `docs/guide/` — detailed guides for each CLI command
|
|
188
|
+
- `docs/cookbook/` — recipes for common asyncio patterns
|
|
189
|
+
- `docs/api/` — full API reference for all otto packages
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "otto-sh"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Our Trusty Testing Orchestrator - framework and CLI for deploying, testing, and monitoring software across remote hosts."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Chris Collins", email = "chriscoll93@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
keywords = [
|
|
13
|
+
"testing",
|
|
14
|
+
"automation",
|
|
15
|
+
"orchestration",
|
|
16
|
+
"ssh",
|
|
17
|
+
"telnet",
|
|
18
|
+
"asyncio",
|
|
19
|
+
"cli",
|
|
20
|
+
"remote-execution",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Environment :: Console",
|
|
25
|
+
"Framework :: AsyncIO",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: POSIX :: Linux",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Topic :: Software Development :: Testing",
|
|
32
|
+
"Topic :: System :: Systems Administration",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"aioftp>=0.27.2",
|
|
36
|
+
"aiosqlite>=0.21.0",
|
|
37
|
+
"asyncssh>=2.22.0",
|
|
38
|
+
"fastapi>=0.135.1",
|
|
39
|
+
"jinja2>=3.1.0",
|
|
40
|
+
# pytest + pytest-asyncio are runtime deps because otto imports user
|
|
41
|
+
# test files at runtime (orchestration introspection), and those files
|
|
42
|
+
# commonly `import pytest` / use `pytest_asyncio` markers.
|
|
43
|
+
"pytest>=9.0.1",
|
|
44
|
+
"pytest-asyncio>=1.3.0",
|
|
45
|
+
"rich>=14.3.3",
|
|
46
|
+
"sse-starlette>=3.3.3",
|
|
47
|
+
"telnetlib3>=4.0.1",
|
|
48
|
+
"tomli>=2.4.0",
|
|
49
|
+
"typer>=0.24.0",
|
|
50
|
+
"uvicorn>=0.42.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
otto = "otto:app"
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://github.com/ludachrish3/otto-sh"
|
|
58
|
+
Source = "https://github.com/ludachrish3/otto-sh"
|
|
59
|
+
Issues = "https://github.com/ludachrish3/otto-sh/issues"
|
|
60
|
+
Documentation = "https://otto-sh.readthedocs.io"
|
|
61
|
+
Changelog = "https://github.com/ludachrish3/otto-sh/blob/main/CHANGELOG.md"
|
|
62
|
+
|
|
63
|
+
[build-system]
|
|
64
|
+
requires = [
|
|
65
|
+
"uv_build>=0.10.0",
|
|
66
|
+
"uv_build<0.11.0",
|
|
67
|
+
]
|
|
68
|
+
build-backend = "uv_build"
|
|
69
|
+
|
|
70
|
+
# Dist name is `otto-sh` (for PyPI); the import package stays `otto`, so the
|
|
71
|
+
# build backend needs to be pointed at src/otto/ explicitly.
|
|
72
|
+
[tool.uv.build-backend]
|
|
73
|
+
module-name = "otto"
|
|
74
|
+
|
|
75
|
+
[dependency-groups]
|
|
76
|
+
dev = [
|
|
77
|
+
"bump-my-version>=1.3.0",
|
|
78
|
+
"sphinx-immaterial",
|
|
79
|
+
"myst-parser>=4.0.1",
|
|
80
|
+
"py-spy>=0.4.1",
|
|
81
|
+
"pyinstrument>=5.1.2",
|
|
82
|
+
"pytest-cov>=7.0.0",
|
|
83
|
+
"pytest-xdist>=3.8.0",
|
|
84
|
+
"ruff>=0.14.7",
|
|
85
|
+
"sphinx>=8.0",
|
|
86
|
+
"sphinx-autodoc-typehints>=2.0",
|
|
87
|
+
"ty==0.0.31",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.pyright]
|
|
91
|
+
typeCheckingMode = "strict"
|
|
92
|
+
ignore = [
|
|
93
|
+
#"tests", # pytest does not play nice with type checking
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# ty is being trialled alongside pyright; see todo/ty_vs_pylance_eval.md.
|
|
97
|
+
# Pinned hard because ty uses 0.0.x versioning with breaking changes between
|
|
98
|
+
# any two releases.
|
|
99
|
+
[tool.ty.environment]
|
|
100
|
+
python-version = "3.10"
|
|
101
|
+
root = ["./src"]
|
|
102
|
+
|
|
103
|
+
[tool.ty.src]
|
|
104
|
+
include = ["src"]
|
|
105
|
+
exclude = ["tests", "typings", "docs", "build", "dist"]
|
|
106
|
+
|
|
107
|
+
# All rules at error — there is no pyright-style --strict preset, so we
|
|
108
|
+
# escalate everything uniformly. Rules that turn out to be too noisy should
|
|
109
|
+
# be demoted individually here (not silenced globally) so the decision is
|
|
110
|
+
# visible in diff review.
|
|
111
|
+
[tool.ty.rules]
|
|
112
|
+
all = "error"
|
|
113
|
+
|
|
114
|
+
[tool.pytest.ini_options]
|
|
115
|
+
testpaths = [
|
|
116
|
+
"tests/unit",
|
|
117
|
+
"tests/integration",
|
|
118
|
+
]
|
|
119
|
+
pythonpath = ["src", "."]
|
|
120
|
+
log_cli = true
|
|
121
|
+
log_level = "INFO"
|
|
122
|
+
log_format = "%(asctime)s [%(levelname)s] %(message)s"
|
|
123
|
+
log_cli_date_format = "%Y-%m-%d %H:%M:%S.%f"
|
|
124
|
+
addopts = "--doctest-modules --cov=otto --cov-context=test --cov-report term-missing --cov-report html -n auto --dist loadgroup"
|
|
125
|
+
doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"]
|
|
126
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
127
|
+
asyncio_mode = "strict"
|
|
128
|
+
markers = [
|
|
129
|
+
"integration: requires running Vagrant VMs (deselect with -m 'not integration')",
|
|
130
|
+
"hops: multi-hop integration tests requiring 3 Vagrant VMs (deselect with -m 'not hops')",
|
|
131
|
+
"timeout(seconds): fail the test if it exceeds the given number of seconds",
|
|
132
|
+
"retry(n): retry the test up to n times on failure before reporting it as failed",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
[tool.bumpversion]
|
|
136
|
+
current_version = "0.1.0"
|
|
137
|
+
commit = true
|
|
138
|
+
tag = true
|
|
139
|
+
tag_name = "v{new_version}"
|
|
140
|
+
tag_message = "Release v{new_version}"
|
|
141
|
+
message = "chore(release): bump version {current_version} -> {new_version}"
|
|
142
|
+
|
|
143
|
+
[[tool.bumpversion.files]]
|
|
144
|
+
filename = "pyproject.toml"
|
|
145
|
+
search = 'version = "{current_version}"'
|
|
146
|
+
replace = 'version = "{new_version}"'
|
|
147
|
+
|
|
148
|
+
[[tool.bumpversion.files]]
|
|
149
|
+
filename = "CHANGELOG.md"
|
|
150
|
+
search = "## [Unreleased]"
|
|
151
|
+
replace = "## [Unreleased]\n\n## [{new_version}] - {now:%Y-%m-%d}"
|
|
152
|
+
|
|
153
|
+
[[tool.bumpversion.files]]
|
|
154
|
+
filename = "CHANGELOG.md"
|
|
155
|
+
search = "[Unreleased]: https://github.com/ludachrish3/otto-sh/compare/v{current_version}...HEAD"
|
|
156
|
+
replace = "[Unreleased]: https://github.com/ludachrish3/otto-sh/compare/v{new_version}...HEAD\n[{new_version}]: https://github.com/ludachrish3/otto-sh/compare/v{current_version}...v{new_version}"
|
|
157
|
+
|
|
158
|
+
# Keep the lock entry for otto-sh in sync. Without this, every `uv run`
|
|
159
|
+
# after a bump detects pyproject ↔ lock drift and re-syncs the lock,
|
|
160
|
+
# dirtying the tree and blocking the next `make release`.
|
|
161
|
+
[[tool.bumpversion.files]]
|
|
162
|
+
filename = "uv.lock"
|
|
163
|
+
search = "name = \"otto-sh\"\nversion = \"{current_version}\""
|
|
164
|
+
replace = "name = \"otto-sh\"\nversion = \"{new_version}\""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from rich.text import (
|
|
2
|
+
Text,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
banner = Text(style="bold green", no_wrap=True)
|
|
6
|
+
banner.append(r' __ __ ' '\n')
|
|
7
|
+
banner.append(r' ____ / /_/ /_____ ' '\n')
|
|
8
|
+
banner.append(r' / __ \/ __/ __/ __ \ ' '\n')
|
|
9
|
+
banner.append(r' / /_/ / /_/ /_/ /_/ / ' '\n')
|
|
10
|
+
banner.append(r' \____/\__/\__/\____/ ' '\n')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared Typer option callbacks used across multiple CLI subapps."""
|
|
2
|
+
|
|
3
|
+
from ..configmodule import getConfigModule
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_hosts_callback(value: bool) -> None:
|
|
7
|
+
"""Print all host IDs from the current lab and exit."""
|
|
8
|
+
if not value:
|
|
9
|
+
return
|
|
10
|
+
lab = getConfigModule().lab
|
|
11
|
+
print()
|
|
12
|
+
for host in lab.hosts:
|
|
13
|
+
print(f'\u2022 {host}')
|
|
14
|
+
print()
|