Using otto as a library¶
otto is not limited to the otto CLI. You can use it directly in your own
async Python scripts — for example, one-off automation, CI tooling, or
integration scripts that operate on lab hosts without needing test suites or
instructions.
Recommended: open_context()¶
open_context() is the single entry point for library use. It loads a lab,
installs the active context, enters the host lifecycle scope, yields the
context, and tears everything down on exit — even if your code raises.
import asyncio
import otto
async def main():
async with otto.open_context(lab="mylab", search_paths=[...]) as ctx:
results = await ctx.run_on_all_hosts("uname -a")
for host_id, result in results.items():
print(host_id, result)
# every host opened in the block is closed here, deterministically
asyncio.run(main())
Inside the block the context is the active one, so the zero-argument accessors
work without passing ctx around:
async with otto.open_context(lab="mylab") as ctx:
# explicit path
for host in ctx.all_hosts():
await host.run("uptime")
# or the zero-argument bare accessors — same result
for host in otto.all_hosts():
await host.run("uptime")
open_context accepts:
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
— |
A |
|
|
|
Log commands without executing them |
|
|
|
Stream command output to the otto logger |
|
|
|
Paths to search for lab definitions |
Bring-your-own-CLI: lower-level primitives¶
otto’s own CLI uses these three steps internally — open_context is just them
packaged across the callback/subcommand boundary:
Build an
OttoContextwith the chosen lab and runtime flags.Install it as the active context with
set_context(), which returns a reset token.Enter
ctx.scopeas an async context manager; on exit it closes any still-connected hosts, thenreset_context(token)restores the prior state.
from otto.context import OttoContext, reset_context, set_context
from otto.configmodule import load_lab
lab = load_lab("mylab", search_paths=[...])
ctx = OttoContext(lab=lab, dry_run=False)
token = set_context(ctx)
try:
async with ctx.scope:
# your work here
...
finally:
reset_context(token)
This is exactly what open_context does under the hood. Use this form when
you need fine-grained control — for instance, when a framework drives the
event loop and you cannot use async with at the top level.
Host lifetimes¶
There are three patterns for managing individual host connections inside an
open_context block. All three are safe — the scope provides the backstop.
(a) Tight scoping with async with:
async with otto.open_context(lab="mylab") as ctx:
async with ctx.get_host("router1") as host:
await host.run("show version")
# host.close() was called here; connection is gone
(b) Pass the host around; let the scope close it:
async with otto.open_context(lab="mylab") as ctx:
host = ctx.get_host("router1")
await configure(host) # pass it wherever you like
# scope.close() sweeps host when the block exits
(c) Explicit await host.close():
async with otto.open_context(lab="mylab") as ctx:
host = ctx.get_host("router1")
await host.run("reboot")
await host.close() # early close — idempotent; scope sweep is a no-op
close() is idempotent: calling it multiple times is safe.
FD-model caveat¶
A host you construct directly (e.g. UnixHost(...)) outside any context
has no scope backstop — it is yours to close, exactly like an explicitly-opened
file descriptor. Use async with, await h.close(), or register it manually
with ctx.scope.register(h) inside an active context.
Reservation checks are a CLI concern — open_context does not gate on them.
If your script needs to verify reservations before running, call
otto.reservations.check_reservations(...) explicitly before entering the
block.