reservations

The reservations package gates every live-lab subcommand on whether the effective user actually holds the resources the selected lab needs. It is pluggable: the check itself is fixed, but the “who has what reserved?” query is answered by a ReservationBackend implementation — shipped ones, or your own class selected by dotted path in .otto/settings.toml.

For narrative setup, configuration, and writing a custom backend, see the user guide.

Package summary

Lab reservation / scheduler-check subsystem.

See otto.reservations.protocol for the backend contract and docs/guide/reservations.md for the end-user and implementer docs.

The backend contract

Third-party backends implement the ReservationBackend Protocol. The contract is deliberately small — three read-only methods, no write methods of any kind. Otto never mutates scheduler state.

Protocol contract for pluggable lab-reservation backends.

A reservation backend answers two questions for otto:

  • “What resources does user X currently have reserved?”

  • “Who, if anyone, currently holds resource Y?”

Otto is strictly a consumer — it never creates, modifies, or releases reservations. The scheduler (Jira, a booking tool, a shared JSON file, anything) remains authoritative.

Implementers

Third-party backends implement the ReservationBackend protocol and are selected in the repo’s .otto/settings.toml:

[reservations]
backend = "mypkg.mybackend:MyBackend"
url = "https://scheduler.example.com"

[reservations.mypkg-mybackend]
api_key_env = "SCHEDULER_API_KEY"

The url key and any [reservations.<backend>] sub-table are passed to the backend’s __init__ as keyword arguments. url is optional on both sides: implementers may accept and use it, or hardcode their own endpoint — whichever fits the deployment.

All failure modes that prevent answering a query (network down, database unreachable, credentials rejected, file corrupt) must be raised as otto.reservations.check.ReservationBackendError so the CLI can translate them into a fail-closed startup error with a clear hint about the --skip-reservation-check escape hatch.

class otto.reservations.protocol.ReservationBackend(*args, **kwargs)

Bases: Protocol

Read-only view over a reservation scheduler.

get_reserved_resources(username)

Return the set of resource identifiers currently reserved by username.

Return type:

set[str]

Parameters

usernamestr

The reservation-system identity to query. Case sensitivity and any other normalization rules are the backend’s responsibility; otto passes the username through unchanged.

Returns

set[str]

Resource identifiers the user currently holds. Empty set if the user has no active reservations. Resource strings must match byte-for-byte the values in RemoteHost.resources and Lab.resources — any necessary normalization is the backend’s job.

Raises

otto.reservations.check.ReservationBackendError

On any failure that prevents a definitive answer (network error, file I/O error, DB error, credential rejection, malformed data).

who_reserved(resource)

Return the username currently holding resource.

Used for error messages when a reservation check fails (e.g. "rack3-psu is held by alice") so the caller knows who to talk to.

Return type:

str | None

Parameters

resourcestr

Resource identifier to look up.

Returns

str | None

The holder’s username, or None if no one currently holds the resource.

Raises

otto.reservations.check.ReservationBackendError

On any failure that prevents a definitive answer.

backend_name()

Return a short human-readable identifier for this backend.

Used in diagnostic output and error messages (e.g. "json", "my-team-jira"). Should be stable across runs.

Return type:

str

Exceptions

Two exceptions classify the two failure modes a caller cares about: the user doesn’t hold something versus we couldn’t ask. They are surfaced differently in the CLI — see Skip-flag hint policy below.

exception otto.reservations.check.ReservationBackendError

Bases: Exception

Raised by backends when a query cannot be answered.

Network outages, DB errors, malformed data files, and authentication failures all surface as this exception so the CLI can translate them into a single fail-closed startup error.

exception otto.reservations.check.MissingReservationError

Bases: Exception

Raised when the effective user does not hold every required resource.

The message lists the missing resources and their current holders. It does not mention --skip-reservation-check — that suggestion belongs only in the backend-failure path, never on a legitimate contention failure (or the option gets abused).

The check

Lab reservation check logic and exceptions.

The check_reservations() function is the heart of the subsystem: given a lab, a username, and a backend, it raises MissingReservationError if the user does not hold every resource the lab needs. The error message lists missing resources and their current holders (via ReservationBackend.who_reserved()) but deliberately does NOT advertise --skip-reservation-check — that flag is surfaced only when the backend itself is unreachable, where proceeding requires it.

gate() is the subcommand-facing entry point that wires the check into the CLI: it reads state from the configmodule, honors the top-level skip flag, emits the bold-red skip warning when used, and otherwise runs the check.

exception otto.reservations.check.ReservationBackendError

Bases: Exception

Raised by backends when a query cannot be answered.

Network outages, DB errors, malformed data files, and authentication failures all surface as this exception so the CLI can translate them into a single fail-closed startup error.

exception otto.reservations.check.MissingReservationError

Bases: Exception

Raised when the effective user does not hold every required resource.

The message lists the missing resources and their current holders. It does not mention --skip-reservation-check — that suggestion belongs only in the backend-failure path, never on a legitimate contention failure (or the option gets abused).

otto.reservations.check.required_resources(lab)

Return every resource identifier the lab needs.

The union of the lab’s own resources set and each host’s resources set. Any of these resources that are not held by the effective user will cause check_reservations() to raise.

Return type:

set[str]

otto.reservations.check.check_reservations(lab, username, backend)

Raise MissingReservationError if username does not cover lab.

Return type:

None

Parameters

labLab

The lab about to be used.

usernamestr

The reservation-system identity to check against.

backendReservationBackend

The configured reservation backend.

Raises

MissingReservationError

If any required resource is not held by username.

ReservationBackendError

If the backend cannot answer the query (network, file, DB failure).

otto.reservations.check.gate(cm)

Run the reservation check for the current invocation, if applicable.

Called from each live-lab subcommand callback (run, test, host, monitor) after the configmodule has been populated by the top-level Typer callback. Not called from cov report, which is offline.

Behavior: :rtype: None

  • If cm is None (the configmodule singleton was never set up — e.g. a unit test invoking a subcommand app directly), the gate is a no-op.

  • If cm.skip_reservation_check is set, logs a bold-red WARNING and returns without querying the backend.

  • Otherwise, calls check_reservations(). Lets the raised exception propagate so Typer renders it with the normal error path.

Skip-flag hint policy

Only ReservationBackendError surfaces a suggestion to pass --skip-reservation-check / -R — because with a broken backend the user has no other way to proceed. MissingReservationError deliberately does not mention the flag, since offering it on every contention failure trains users to reach for the bypass instead of fixing the underlying reservation.

Identity resolution

Effective-user resolution for the reservation check.

Precedence: --as-user USERNAME > getpass.getuser().

There is deliberately no persistent config or environment-variable source. A CLI flag is always visible on the command line that uses it; a sticky env var in shell rc would be invisible and could cause a user to operate under someone else’s identity for weeks without realizing. When --as-user IS used, the top-level Typer callback prints a bold-magenta banner so the override is impossible to miss.

class otto.reservations.identity.ResolvedIdentity(username, source)

Bases: object

Effective reservation identity for this otto invocation.

Attributes

usernamestr

The username passed to the backend.

sourceLiteral[”–as-user”, “$USER”]

Where the username came from, used for diagnostic output.

username : --is-rst--:py:class:`str`
source : --is-rst--:py:data:`~typing.Literal`\ \[``'--as-user'``, ``'$USER'``]
otto.reservations.identity.resolve_username(as_user)

Resolve the effective reservation identity.

Return type:

ResolvedIdentity

Parameters

as_userstr | None

The value of the top-level --as-user Typer option. None or an empty string means the option was not supplied.

Returns

ResolvedIdentity

(username, source)source="--as-user" when the flag was used, otherwise source="$USER".

Bundled backends

JSON backend

Reference implementation and test double — also a perfectly usable production backend for small teams that don’t have a scheduler yet. See the user guide for the file format.

JSON-file reservation backend — reference implementation and test double.

Intended for two audiences:

  1. Small teams with no scheduler who just want to hand-edit a JSON file checked into the repo (or kept on a shared volume) that lists who holds which resources.

  2. Tests — unit and integration tests construct fixture JSON files and point a JsonReservationBackend at them.

File format (version: 1):

{
  "version": 1,
  "reservations": [
    {"user": "alice", "resources": ["rack3-psu"], "expires": "2026-05-01T00:00:00Z"},
    {"user": "bob",   "resources": ["rack4-psu"]}
  ]
}
  • reservations is a list so one user may appear multiple times (useful when merging reservations from multiple sources).

  • expires is optional; past-dated entries are silently ignored.

class otto.reservations.json_backend.JsonReservationBackend(url=None, *, path)

Bases: object

Read reservations from a JSON file on disk.

Parameters

urlstr | None

Accepted and ignored. Kept in the signature so the factory (otto.reservations.build_backend()) can pass url=url uniformly to any backend.

pathPath

Location of the reservation file on disk. Required.

backend_name()
Return type:

str

get_reserved_resources(username)
Return type:

set[str]

who_reserved(resource)
Return type:

str | None

Null backend

Default when no [reservations] section is configured, or when backend = "none" is set. check_reservations() recognizes this type and becomes a no-op.

Null reservation backend used when no scheduler is configured.

Selected by setting backend = "none" in the repo’s [reservations] TOML section. otto.reservations.check.check_reservations() recognizes this type and becomes a no-op, so teams that haven’t set up a scheduler yet aren’t blocked.

class otto.reservations.null_backend.NullReservationBackend

Bases: object

Always returns “no reservations known” — the check is a no-op.

get_reserved_resources(username)
Return type:

set[str]

who_reserved(resource)
Return type:

str | None

backend_name()
Return type:

str

Backend factory

otto.reservations.build_backend(settings, repo_dir)

Construct a reservation backend from a parsed [reservations] section.

Return type:

ReservationBackend

Parameters

settingsdict[str, Any]

The [reservations] sub-dict parsed from .otto/settings.toml. Expected keys:

  • backend"json", "none", or a dotted path "pkg.module:ClassName" for third-party implementations. Defaults to "none" when absent.

  • url — optional string, forwarded as url=... to the backend constructor when present.

  • <backend-name> — optional nested table with backend-specific keyword arguments (e.g. [reservations.json] path = "...").

repo_dirPath

The SUT repo root. Used only to expand the JSON backend’s path setting when it is relative.

Returns

ReservationBackend

A ready-to-query backend instance.

Raises

ValueError

If backend names an unknown backend or a malformed dotted path.

ReservationBackendError

If a third-party backend’s construction fails for backend reasons (network, bad credentials, etc.).

Extension points for implementers

A custom backend needs three pieces:

  1. A class that satisfies ReservationBackend. The Protocol is @runtime_checkable, so stock isinstance checks work, but Protocol satisfaction is structural — no explicit inheritance is required (and none is recommended).

  2. An import path otto can reach. Either add the containing directory to libs = [...] in .otto/settings.toml, or install the module as a package into the same Python environment as otto.

  3. A ``[reservations]`` entry with backend = "pkg.mod:ClassName".

The factory calls the class as Class(url=url, **kwargs_from_settings) when url is set in settings, otherwise Class(**kwargs_from_settings). Accept or omit url as fits your deployment.

See the user guide for a worked example with request handling, credential loading, and package layout.