Important
Developer Preview Disclaimer
This repository is an early-stage developer preview and is not production-ready. While we are currently finalizing an open-source friendly license, all rights are reserved in the interim. We encourage you to explore the code, experiment with it, and share your feedback via issues or discussions. Use of this software is at your own risk and provided "as-is" without warranty.
A FastAPI service exposing exclusive, queue-brokered access to a real Netlab topology — drive it from a pytest11 plugin (Python), the bundled REST API (any stack), or both.
neops-remote-lab fronts a Netlab host with a small HTTP service and a pytest fixture. Every consumer asks for a session, waits in a FIFO queue, gets the lab, and tears it down when the last consumer walks away. Topologies are identified by their SHA-256 content hash, so byte-identical files share the running lab — and FRR, Nokia SR Linux, and Cisco IOL all work out of the box.
On PyPI · Docs · Worker SDK consumes it as a stable contract
For the Python client / pytest fixture (library install) — pick whichever your project uses:
uv add neops-remote-lab # uv (recommended)
poetry add neops-remote-lab # Poetry
pip install neops-remote-lab # pipFor the runnable server CLI (isolated install):
uv tool install neops-remote-lab # uv (recommended)
pipx install neops-remote-lab # pipxPicking between the two? Library install is what most consumers want — it gives you the pytest fixture and
RemoteLabClient. CLI install is for operators standing up the server itself.
export REMOTE_LAB_URL=http://lab.example.com:8000from neops_remote_lab.testing.fixture import remote_lab_fixture
demo = remote_lab_fixture("tests/topologies/demo.yml")def test_lab_has_two_devices(demo):
assert len(demo) == 2pytest -vThe fixture handles session creation, queue waiting, topology upload, heartbeat,
and teardown. The Worker SDK imports remote_lab_fixture directly as a
stable public API.
neops-remote-lab # binds 0.0.0.0:8000 by defaultcurl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/healthz
# 204 = alivenetlab must be on PATH — the launcher refuses to start otherwise. For a fresh
host, see Netlab host setup;
for a systemd-managed deployment, see the
Operator runbook.
sequenceDiagram
participant ClientA as Client A
participant ClientB as Client B
participant Server
ClientA->>Server: POST /session
Server-->>ClientA: 201 session_id (ACTIVE)
ClientB->>Server: POST /session
Server-->>ClientB: 201 session_id (WAITING, position=1)
ClientA->>Server: POST /lab (topology.yml)
Note over Server: acquire lab (netlab up)
Server-->>ClientA: 200 lab acquired
loop heartbeat (until release)
ClientA->>Server: POST /session/heartbeat
Server-->>ClientA: 204
end
ClientB->>Server: GET /session/{id}
Server-->>ClientB: status=WAITING, position=1
ClientA->>Server: DELETE /session/{id}
Note over Server: tear down lab, promote next in queue
Server-->>ClientA: 204
ClientB->>Server: GET /session/{id}
Server-->>ClientB: status=ACTIVE, position=0
The full state machine, heartbeat cadence, and timeout semantics live on Session queue; SHA-256-keyed reuse semantics on Lab lifecycle.
- One lab per host, enforced. Cross-process
filelock+ reference counting; two clients can't trample each other's topology. Topology identity is the SHA-256 of file content, so byte-identical files share a running lab. - Stable Python contract.
remote_lab_fixtureis the public surface the Worker SDK imports directly. - Driveable from any HTTP stack. pytest is convenient; cURL works; Go works; whatever you already have works.
- Operator-friendly. Single-instance
systemdunit, structured logs,/healthzand/debug/health, automatic recovery from stale Netlab instances on startup.
| Variable | Purpose | Default |
|---|---|---|
REMOTE_LAB_URL |
Base URL for client + fixtures | — (required) |
REMOTE_LAB_REQUEST_TIMEOUT |
Per-HTTP-request timeout (seconds) | 30 |
REMOTE_LAB_SESSION_TIMEOUT |
Client-side session-queue wait limit (seconds) | 600 |
REMOTE_LAB_ACQUISITION_TIMEOUT |
Max wait for lab acquisition (seconds) | 600 |
Timeout-coordination guidance + RemoteLabClient constructor overrides:
Client config.
The service ships without HTTP authentication — the only access boundary on
/lab/* is the X-Session-ID of an active session. Treat it as internal-trust;
deploy behind a network enclosure. The recommended path is self-hosted
Headscale + Headplane
because that's what the project's reference deployment uses, but any equivalent
enclosure works (managed Tailscale, plain WireGuard, an internal VLAN with IP
allowlists, mTLS at a reverse proxy). Walkthroughs and alternatives:
Headscale: quick setup
and its
Other approaches
section.
| Symptom | Likely cause + fix |
|---|---|
Server exits with Another Remote Lab Manager instance is already running. |
Stale single-instance filelock from a crashed prior run. See Stale-lock recovery. |
Address already in use on port 8000 |
Prior server didn't exit cleanly, or another service holds the port. lsof -i :8000 and kill, or start with --port. |
POST /lab returns 423 Locked |
Caller's session isn't ACTIVE, or a different topology owns the host. GET /session/{id} to check; release / force-destroy if stuck. |
Full table: Debugging.
The full docs are at docs.neops.io/neops-remote-lab. Common entry points:
- Use from Python — pytest fixtures,
RemoteLabClient, client config. - Run the service — install,
systemd, security model, REST contract, debugging. - Deploy & Operate — Netlab host setup, VPN paths, vendor walkthroughs (FRR / SR Linux / IOL).
- Concepts — architecture, session queue, lab lifecycle, topology format.
- Cookbook — runnable end-to-end recipes.
Interactive OpenAPI UI is at http://<host>:8000/docs once the server is running.
make test # 39 tests; uses a stubbed LabManager (no Netlab needed)Branch from develop. make check runs lint, typecheck, audit, and tests. Full
contributor guide: Contributing.
See the Developer Preview Disclaimer at the top of this file. A formal open-source license will be applied once finalized.