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:
ProtocolRead-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.resourcesandLab.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
Noneif no one currently holds the resource.
Raises¶
- otto.reservations.check.ReservationBackendError
On any failure that prevents a definitive answer.
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:
ExceptionRaised 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:
ExceptionRaised 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:
ExceptionRaised 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:
ExceptionRaised 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
resourcesset and each host’sresourcesset. Any of these resources that are not held by the effective user will causecheck_reservations()to raise.- Return type:¶
set[str]
- otto.reservations.check.check_reservations(lab, username, backend)¶
Raise
MissingReservationErrorifusernamedoes not coverlab.- 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 fromcov report, which is offline.Behavior: :rtype:
NoneIf
cmisNone(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_checkis 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:
objectEffective 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:¶
Parameters¶
- as_userstr | None
The value of the top-level
--as-userTyper option.Noneor an empty string means the option was not supplied.
Returns¶
- ResolvedIdentity
(username, source)—source="--as-user"when the flag was used, otherwisesource="$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:
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.
Tests — unit and integration tests construct fixture JSON files and point a
JsonReservationBackendat them.
File format (version: 1):
{
"version": 1,
"reservations": [
{"user": "alice", "resources": ["rack3-psu"], "expires": "2026-05-01T00:00:00Z"},
{"user": "bob", "resources": ["rack4-psu"]}
]
}
reservationsis a list so one user may appear multiple times (useful when merging reservations from multiple sources).expiresis optional; past-dated entries are silently ignored.
-
class otto.reservations.json_backend.JsonReservationBackend(url=
None, *, path)¶ Bases:
objectRead 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 passurl=urluniformly to any backend.- pathPath
Location of the reservation file on disk. Required.
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:
objectAlways returns “no reservations known” — the check is a no-op.
Backend factory¶
- otto.reservations.build_backend(settings, repo_dir)¶
Construct a reservation backend from a parsed
[reservations]section.- Return type:¶
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 asurl=...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
pathsetting when it is relative.
Returns¶
- ReservationBackend
A ready-to-query backend instance.
Raises¶
- ValueError
If
backendnames 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:
A class that satisfies
ReservationBackend. The Protocol is@runtime_checkable, so stockisinstancechecks work, but Protocol satisfaction is structural — no explicit inheritance is required (and none is recommended).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.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.