RemoteLabClient Reference
The lower-level HTTP client the pytest fixture wraps. Use it directly from scripts, notebooks, or any non-pytest context.
RemoteLabClient is the Python interface to the Remote Lab Manager
HTTP API. Use it directly when you need to drive a lab from a script,
a notebook, or any non-pytest context.
Inside tests, use remote_lab_fixture
instead — the fixture wraps this client, adds lifecycle hooks, and is
the stable contract consumed by the
Worker SDK.
Import
Constructor
RemoteLabClient(
base_url: str,
request_timeout: int = 30,
session_timeout: int = 600,
lab_acquisition_timeout: int = 600,
)
Arguments
| Name | Type | Default | Description |
|---|---|---|---|
base_url |
str |
– | Base URL of the Remote Lab Manager (e.g. http://lab.internal:8000). If falsy, falls back to the REMOTE_LAB_URL environment variable. |
request_timeout |
int |
30 |
Per-HTTP-request timeout in seconds for short operations (create session, release, heartbeat). |
session_timeout |
int |
600 |
Max seconds the constructor will wait for the session to reach ACTIVE state in the FIFO queue. |
lab_acquisition_timeout |
int |
600 |
Max seconds for a single acquire() POST. Netlab up can take minutes on cold caches, so this is deliberately long. |
Raises
ValueErrorwhenbase_urlis falsy ANDREMOTE_LAB_URLis unset.TimeoutErrorwhen the session does not become ACTIVE withinsession_timeoutseconds.
Side effects at construction time
The constructor is not lazy. In order:
- An HTTP session is created with a retry adapter for 429/5xx responses.
- A new server session is created via
POST /sessionand the returnedsession_idis stored on theX-Session-IDheader of all subsequent requests. - The constructor blocks, polling
GET /session/{id}every 5 seconds until the session reaches ACTIVE (orsession_timeoutexpires).
If the server is unreachable, the constructor will retry (see Retry behavior)
and ultimately raise the final requests exception.
Session lifecycle
The client follows a strict lifecycle. All /lab/* calls are rejected by the
server with 423 Locked unless the session is ACTIVE, which the constructor
guarantees on return.
%%{init: {'theme': 'neutral'}}%%
stateDiagram-v2
[*] --> WAITING: __init__
WAITING --> ACTIVE: promoted
ACTIVE --> HOLDING_LAB: acquire()
HOLDING_LAB --> ACTIVE: release()
ACTIVE --> [*]: close()
HOLDING_LAB --> [*]: close() (forces teardown)
ACTIVE --> ACTIVE: heartbeat (implicit)
Client states; the constructor blocks until ACTIVE; acquire/release flip into HOLDING_LAB.
The session is kept alive by server-side timers: if no request touches the
session for 300 seconds the server marks it stale, tears down any held lab,
and promotes the next waiter. Call acquire/release/destroy to reset the
heartbeat clock; there is no dedicated heartbeat method on the client —
neops-remote-lab server supports POST /session/heartbeat, but the Python
client does not expose it, and the pytest fixture does not need it because
the test’s own acquire/release traffic keeps the session warm.
Public methods
acquire(topology, reuse) -> list[DeviceInfoDto]
Upload a topology and block until the server has a running lab for you.
| Argument | Type | Description |
|---|---|---|
topology |
pathlib.Path |
Path to a Netlab .yml file. The client opens the file and posts it as multipart form data. |
reuse |
bool |
When True, increments the reference count on an already-running lab with the same SHA-256 content identity; when False, refuses to start if another lab is already up. |
Returns a list of DeviceInfoDto describing the running devices. Each DTO
carries .name (from Netlab) and .raw — the full netlab inspect dictionary
for that node.
Raises the underlying requests.exceptions.RequestException on transport
errors and the standard HTTPError subclasses on non-retriable 4xx responses.
423 polling. If the server returns 423 Locked (another session holds
the host), acquire() sleeps 5 seconds and retries without user intervention.
There is no retry cap — the loop is bounded only by lab_acquisition_timeout
applied to each individual POST.
reuse and SHA identity
Topologies are identified by the SHA-256 of their file content, not by
filename. Two files with different names but identical bytes share one
lab when reuse=True; two files with the same name but different content
are separate labs. If you edit a topology file between tests, you are
booting a new lab.
release() -> None
Decrement the server-side reference count on the held lab. When the count reaches zero the server tears the lab down.
release() is tolerant of races — a 404 Not Found response (meaning the
lab was already torn down by another path) is treated as success and
logged at INFO without re-raising.
release() is best-effort — it never raises
The request is wrapped in a try: ... except Exception as e: _log.error(...)
block, so any HTTPError that raise_for_status() would have raised
(5xx, unexpected 4xx, transport failure) is swallowed and logged at
ERROR, not propagated. If you need to detect a failed release,
grep the logs for Failed to release lab: — there is no exception
to catch.
destroy() follows the same swallow-and-log pattern, so do not design
retry or alerting logic around exceptions from it either.
destroy(force=True) -> None
Force teardown of the held lab regardless of reference count.
The server returns 202 Accepted when it starts an async teardown or 204 No
Content when there was nothing to do; both are treated as success.
destroy bypasses reuse semantics
If another active session is also using a reuse=True lab, destroy()
will still tear it down and those other sessions will start seeing
423 Locked or acquire errors. Use release() unless you have a specific
reason — a stuck lab, a test runner wind-down — to force teardown.
close() -> None
End the session. This is the last call you make on a client instance.
close() is idempotent: after the first call the internal session_id
is cleared, so repeated calls log a debug line and return without contacting
the server.
Network errors during close are logged as warnings but not raised — the working assumption is that if the server is unreachable, your session will be reaped by the server’s stale-session sweeper anyway.
session_id: str
Read-only attribute. Populated by the constructor to the server-assigned
UUID; cleared to "" by close(). Send it as the X-Session-ID header on
any manual requests calls you make outside the client (for example, if you
want to hit an endpoint the client does not wrap).
Context manager support
RemoteLabClient does not implement __enter__ / __exit__. Wrap it in
your own try/finally or a small contextmanager:
Using this wrapper makes a script resilient to exceptions between acquire and release:
| scripts/collect_config.py | |
|---|---|
Retry behavior
The underlying requests.Session is configured with a urllib3.util.Retry
adapter. The policy is: three retries total, exponential backoff with
backoff_factor=1, and the retry list covers 429, 500, 502, 503, 504. The
retry applies to the HEAD, GET, OPTIONS, DELETE methods only — POST is
not retried because it is not idempotent on this API.
Implication: acquire() (which POSTs) has its own explicit polling loop for
423 Locked — the Retry adapter does not help there. Transient 5xx on
acquire will surface as an exception on the first attempt.
Timeouts vs retries
Each HTTP call still respects request_timeout (or
lab_acquisition_timeout for /lab). The Retry adapter does not extend
those — it issues a fresh request per retry, each subject to the normal
timeout.
End-to-end example
Run with:
Configuration
Each constructor argument falls back to a corresponding environment variable:
| Constructor arg | Falls back to |
|---|---|
base_url |
REMOTE_LAB_URL |
request_timeout |
REMOTE_LAB_REQUEST_TIMEOUT |
session_timeout |
REMOTE_LAB_SESSION_TIMEOUT |
lab_acquisition_timeout |
REMOTE_LAB_ACQUISITION_TIMEOUT |
Constructor kwargs win when set
Instantiating RemoteLabClient directly? The kwargs you pass override the environment variables. The env vars only take precedence through the pytest fixture path. See Client config for the full reference.
See also
- Pytest fixtures — the preferred interface for test code.
- Client config — environment variables that drive the constructor’s defaults via the fixture.
- Session queue — the FIFO model that
_wait_for_active_sessionpolls. - Lab lifecycle — reference counting, SHA identity, reuse semantics.
- REST API — every endpoint the client wraps, plus a few it doesn’t.