Host Database

Otto builds its lab — the set of hosts a command can touch — from a host source. By default that source is the hosts.json files under your labs directories, but the source is a pluggable backend: point otto at a CMDB, an inventory API, or any system of record by implementing one small interface.

Note

Choosing a host source is a one-time, team-level decision — part of setting otto up for your team. See the Team setup checklist in Repository Setup.

Otto is strictly a consumer of host data. It reads hosts; it never writes back to your source of record.

The interface

A host source implements the LabRepository protocol — two read-only methods:

load_lab(name, preferences=None) -> Lab

Build and return the named lab. Raises LabNotFoundError if the name is unknown.

list_labs() -> list[str]

The lab names this source can provide.

Configuration is supplied at construction time, so a backend is built once and then queried.

Quick start: the built-in JSON source

The default backend is "json": it reads hosts.json from each directory in your labs setting. No [lab] block is required — a repo with just labs = [...] already uses it:

name = "my_project"
version = "1.0.0"

labs = ["${sut_dir}/lab_data"]

Writing it out explicitly is equivalent:

[lab]
backend = "json"

The per-host hosts.json schema — every field, and how labs merge — lives in Lab Configuration.

Selecting a different source

[lab] backend selects any registered backend by name. Register your backend from an init module (one of the modules listed in init = [...]), then name it in settings:

# my_lab_source.py  (listed in init = [...])
from otto.storage import register_lab_repository
from my_company.cmdb import CmdbLabRepository

register_lab_repository("cmdb", CmdbLabRepository)
[lab]
backend = "cmdb"

[lab.cmdb]
url = "https://cmdb.example.com"

Otto constructs the backend as CmdbLabRepository(repo_dir=<repo root>, url="https://cmdb.example.com") — the [lab.<name>] sub-table becomes keyword arguments, plus repo_dir for resolving any relative paths. Selecting an unregistered name raises LabRepositoryError, listing the registered names.

Note

This is the same named-registry mechanism otto uses everywhere else (register_term_backend, register_reservation_backend, register_host_class). An init module always imports before the lab is loaded, so the name is registered by the time settings select it.

Writing a custom backend

A backend is any class satisfying the two-method protocol. Otto ships a small, dependency-free reference implementation — otto.examples.lab_repository.ExampleLabRepository — that you can copy from src/otto/examples/lab_repository.py as a starting point. It holds a mapping of lab name to host dicts and builds real hosts with create_host_from_dict so each becomes a RemoteHost keyed by its id — which is what the rest of otto expects.

The shipped sample works out of the box and demonstrates the contract:

>>> from otto.examples.lab_repository import ExampleLabRepository
>>> repo = ExampleLabRepository()
>>> repo.list_labs()
['east', 'west']
>>> lab = repo.load_lab("east")
>>> lab.name
'east'
>>> sorted(lab.hosts)
['router1']

Loading an unknown lab raises the contract’s error — never a bare KeyError or None:

>>> from otto.storage import LabNotFoundError
>>> try:
...     repo.load_lab("does-not-exist")
... except LabNotFoundError:
...     print("not found")
not found

Error contract

A backend signals trouble through two exceptions (from otto.storage):

LabNotFoundError

load_lab was asked for a name the backend does not know. Raise this — never return None or raise a bare KeyError.

LabRepositoryError

Any other failure (I/O, network, parse, credentials) that prevents a definitive answer. LabNotFoundError is a subclass, so callers can catch the base.

Verify your backend

Otto ships a conformance helper that checks a backend against the full contract and reports every violation at once (it raises a single AssertionError listing each failed rule). The shipped sample conforms:

>>> from otto.testing import assert_lab_repository_conforms
>>> from otto.examples.lab_repository import ExampleLabRepository
>>> assert_lab_repository_conforms(
...     ExampleLabRepository(), expected_labs=["east", "west"]
... )

Call it from your own test suite, passing expected_labs=[...] to also assert specific labs are present and loadable against your known fixtures:

from otto.testing import assert_lab_repository_conforms
from my_lab_source import CmdbLabRepository

def test_cmdb_conforms():
    assert_lab_repository_conforms(CmdbLabRepository(repo_dir="."))

Troubleshooting

"Unknown lab repository backend '...'"

[lab] backend names a backend that was never registered. Check the name, and confirm the init module that calls register_lab_repository(...) is listed in init = [...].

LabNotFoundError: Lab '...' not found

The backend has no lab by that name. Check --lab / OTTO_LAB against list_labs().