API Endpoints¶
This page is generated at build time from the route handlers in
packages/allocator/src/lablink_allocator_service/routes/ — the docstring on
each handler is the documentation you read here. To change it, edit the
docstring.
Authentication¶
LabLink uses four gates. All bearer credentials go in
Authorization: Bearer <token>.
| Gate | Credential | Guards |
|---|---|---|
| HTTP Basic | app.admin_user / app.admin_password from config.yaml |
/admin/* pages and the operator JSON APIs |
| Client secret | A per-client secret minted when that client registers, stored as an argon2 hash | client → allocator telemetry (/api/heartbeat, /api/vm-status, …) |
| Register token | One deployment-wide bootstrap token, also stored hashed | POST /api/v1/clients/register only |
| Signed cookie | lablink_session, minted by /api/request_vm |
GET /desktop |
A handful of endpoints are deliberately unauthenticated: / and
/api/request_vm (participant-facing), /api/health and
/api/unassigned_vms_count (health/monitoring), and /internal/* (reachable
only from the allocator's own nginx, never exposed publicly).
Secrets are per-client, so a leaked one compromises a single machine. The register token is the only deployment-wide credential, and it can do nothing but register.
Participant Endpoints¶
What a workshop participant touches: the landing page and the seat-claim endpoint. The participant supplies nothing but an email address.
GET /¶
Authentication: None
Landing page.
Renders index.html, the email form a participant submits to claim a
seat.
POST /api/request_vm¶
Authentication: None — this is the only unauthenticated state-changing endpoint in the allocator.
Claim a seat for a participant and redirect them to their desktop.
Request Body: application/x-www-form-urlencoded
email(string, required): The participant's email address.
What it does:
- Idempotent rejoin. If this email already owns a running seat, it keeps that seat rather than consuming a second one.
- Atomic claim. Otherwise
assign_vmclaims a free seat withSELECT … FOR UPDATE SKIP LOCKED, so concurrent requesters cannot collide on one VM. An empty pool raises and returns503withno_seats.html. - Per-session prep. Mints a
session_idandbrowser_token, then rotates the KasmVNC password on the assigned client through that client's local agent. The claim has already committed by this point, so a rotation failure is compensated for, not rolled back: the seat is released and the VM flaggedUnhealthy. - Cookie + redirect. Signs a
lablink_sessioncookie bound to thesession_idand redirects toGET /desktop.
Success Response:
- Code:
303 See Other→/desktop, with thelablink_sessioncookie set.
Error Response:
- Code:
503 Service Unavailable—no_seats.htmlwhen the pool is empty, orrotation_failed.htmlwhen the assigned client could not be reached. On a rotation failure the seat is released and the VM flaggedUnhealthyso the participant isn't wedged on a dead machine. - Code:
200 OK—index.htmlre-rendered with an error ifemailis missing.
The participant supplies nothing but an email address.
GET /api/unassigned_vms_count¶
Authentication: None (health/monitoring).
Get the number of available (unassigned) VMs.
Returns the current count of VMs that are running and not yet assigned to a user.
Success Response:
- Code:
200 OK - Content:
Client Usage: Not used by the client service. Intended for external monitoring or UI components on the allocator to display the number of available VMs.
Participant Desktop¶
GET /desktop¶
Authentication: The signed lablink_session cookie.
The participant desktop.
Renders the noVNC viewer page. Reads the cookie minted by
POST /api/request_vm, looks up the assigned VM by session_id, and
configures the viewer from the persisted browser_ws_url and
browser_credential. If the cookie is missing, invalid, or the bound VM
is no longer running, it redirects to / so the participant can submit
their email again.
Internal Proxy Authorization¶
auth_request subrequest targets for the allocator's own nginx. Never routed publicly.
GET|POST /internal/proxy_auth¶
Authentication: None — an auth_request subrequest target for the allocator's own nginx, never routed publicly.
nginx auth_request gate for the desktop WebSocket proxy.
nginx calls this before proxying desktop bytes, to check that the
requesting session is entitled to the client it is asking for: the
signed lablink_session cookie must resolve to the VM whose
browser_token appears in the original URI.
GET|POST /internal/tunnel_auth¶
Authentication: None
nginx auth_request gate for the reverse-tunnel WebSocket upgrade.
The path prefix identifies which client is attaching; the bearer token is what authenticates it. Both must agree, so a client cannot attach under another client's prefix even holding a valid secret of its own.
This check is the ONLY binding between a secret and an identity. tunnel_manager's restrictions are keyed by path prefix, so they grant whatever alias the presented path claims -- they constrain a client to one alias, they do not prove who the client is. Do not describe them as a second authentication layer.
Health¶
GET /api/health¶
Authentication: None — this is what load balancers, lablink deploy and lablink status poll.
Readiness check.
Returns a structured readiness report rather than a bare 200. The
checks map always covers database, scheduler and reboot_service,
and gains a tailscale entry when the configured connectivity needs
one, or a tunnel entry for reverse-tunnel deployments.
Success Response:
- Code:
200 OKwhen every gating check isok - Content:
Error Response:
- Code:
503 Service Unavailablewith"status": "starting"when a gating check fails.
A dead client tunnel does not make the allocator unready: unattached client tunnels are reported but deliberately don't gate readiness — a registering client is unattached until its tunnel comes up. The allocator's own dependencies (tunnel server, database) do gate.
GET /api/health/connections¶
Authentication: Admin (HTTP Basic)
Report PostgreSQL connection usage.
Reports usage against the server's configured maximum, for checking headroom before scaling a deployment up.
active_connections counts client backends only — the things
max_connections bounds — excluding the background-worker rows
pg_stat_activity also carries. It includes the connection serving this
request. max_connections is read from PostgreSQL, so it tracks
whatever is actually configured.
idle_in_transaction distinguishes a leaked pooled connection from
ordinary load; a non-zero value that doesn't fall back to zero is worth
investigating. It counts idle in transaction (aborted) too — an
aborted-but-open transaction pins locks just like a live one.
The same numbers appear on the /admin panel, which escalates to a
banner above 90%.
Success Response:
- Code:
200 OK - Content:
level is ok, warning above 80% utilization, or critical above
90%. High utilization still answers 200 — busy is not broken.
Error Response:
- Code:
503 Service Unavailablewith{"status": "unavailable"}when the numbers can't be read — the database isn't initialized yet, or the query failed (including a pool with no free connections). Never means the pool is merely busy.
Client VM Telemetry¶
Client VMs report upward through these; they are never told about an assignment. The allocator pushes assignments the other way, by calling the client's local agent (see POST /api/request_vm and Database). All telemetry writes require that client's own secret as a bearer token — see Authentication.
POST /api/update_inuse_status¶
Authentication: Client secret (Bearer)
Update the in-use status of a VM.
Called by the client VM to indicate whether a user is actively using it.
Request Body: application/json
Success Response:
- Code:
200 OK - Content:
{"message": "In-use status updated successfully."}
Error Response:
- Code:
400 Bad Requestifhostnameorstatusis missing. - Code:
500 Internal Server Erroron failure.
Client Usage: Called by the update_inuse_status service, which is
started by start.sh and runs continuously. The service monitors for the
presence of the research software process (e.g., sleap). When the
process starts, it sends status: true; when it stops, status: false.
This lets the allocator know if a user is actively using the VM.
POST /api/gpu_health¶
Authentication: Client secret (Bearer)
Update the GPU health status of a VM.
Request Body: application/json
Success Response:
- Code:
200 OK - Content:
{"message": "GPU health status updated successfully."}
Error Response:
- Code:
400 Bad Requestifhostnameorgpu_statusis missing. - Code:
500 Internal Server Erroron failure.
Client Usage: Called by the check_gpu service, which is started by
start.sh and runs continuously. The service periodically runs
nvidia-smi, determines the GPU status (Healthy, Unhealthy, or
N/A), and reports whenever the status changes. The value lands in the
Healthy column — the auto-reboot service decides what to do about an
unhealthy VM.
POST /api/heartbeat¶
Authentication: Client secret (resolved from the vm_id field).
Record a client-VM liveness heartbeat.
Periodic liveness ping. Lets the allocator detect silent failures — a dead container, a broken network, a hung host, an OOM, or an out-of-band VM termination — that no other endpoint would reveal. The body also carries cheap health signals for early warning.
Request Body: application/json
Client Usage: Sent by the client's heartbeat service every
HEARTBEAT_INTERVAL_SECONDS. A client that stops heartbeating for longer
than the staleness window becomes eligible for automatic recovery.
POST /api/vm-status¶
Authentication: Client secret (Bearer)
Update the overall status of a VM.
Called by the client VM during its startup sequence to report its
current status (initializing, running, error, rebooting).
Request Body: application/json
Success Response:
- Code:
200 OK - Content:
{"message": "VM status updated successfully."}
Error Response:
- Code:
400 Bad Requestifhostnameorstatusis missing. - Code:
500 Internal Server Erroron failure.
Client Usage: Not called from the packages/client code. It is
called by the user_data.sh script during the client VM's initial boot
sequence (cloud-init), which reports initializing, running, and
error statuses so the allocator can track the VM's progress before the
client service container has started. An error trap in user_data.sh
automatically sends an error status if any command fails, which can
then trigger the auto-reboot service.
GET /api/vm-status¶
Authentication: Admin (HTTP Basic)
Get the status of all VMs.
Returns a JSON object mapping each VM hostname to its current status. Used by the admin dashboard.
Success Response:
- Code:
200 OK - Content:
Error Response:
- Code:
404 Not Foundif no VMs are found in the database. - Code:
500 Internal Server Erroron failure.
POST /api/vm-logs/<hostname>¶
Authentication: Client secret (Bearer)
Receive and store logs pushed from a VM.
Receives batched log lines pushed from a VM by the log_shipper.sh
script, which tails the VM's cloud-init output and the client
container's Docker logs.
URL Parameters:
hostname(string, required): The hostname of the VM.
Request Body: application/json
{
"log_group": "/aws/ec2/lablink",
"log_stream": "lablink-vm-prod-1",
"messages": ["log line 1", "log line 2"]
}
Success Response:
- Code:
200 OK - Content:
{"message": "VM logs posted successfully."}
Error Response:
- Code:
400 Bad Requestif required fields are missing. - Code:
404 Not Foundif the VM does not exist. - Code:
500 Internal Server Erroron failure.
Client Usage: Not called directly from packages/client code. The
log_shipper.sh script, installed on each VM by the OpenTofu user-data
script, tails the VM's cloud-init output log and the client
container's Docker logs, batches new lines, and POSTs them here. ANSI
escapes are stripped and each log type is capped at 1MB.
GET /api/vm-logs/<hostname>¶
Authentication: Admin (HTTP Basic)
Get the stored logs for a specific VM.
Used by the admin log viewer page.
URL Parameters:
hostname(string, required): The hostname of the VM.
Query Parameters:
errors_only(boolean, optional, defaultfalse): Whentrue, bothcloud_init_logsanddocker_logsare reduced to their error lines — lines carrying an uppercase level token (ERROR,CRITICAL,FATAL, plus cloudflared'sERR/FTLand postgres'PANIC), together with any traceback that follows them. Matching is case-sensitive so the client startup script's--retry-all-errorsandstatus=errorlines are not treated as errors, and a short denylist suppresses client reporting-path failures that do not affect the VM's usability (GPU-health and in-use-status report failures, and session-metrics push failures). A stream with no error lines becomes"", notnull.
Success Response:
- Code:
200 OK - Content:
Error Response:
- Code:
404 Not Foundif the VM is not found. - Code:
503 Service Unavailableif the logs are not yet available because the VM's log shipper has not started reporting yet. - Code:
500 Internal Server Erroron failure.
POST /api/vm-metrics/<hostname>¶
Authentication: Client secret (Bearer)
Receive and store startup metrics from a VM.
Called by the client VM to post timing metrics for cloud-init and
container startup.
URL Parameters:
hostname(string, required): The hostname of the VM reporting metrics.
Request Body: application/json
Success Response:
- Code:
200 OK - Content:
{"message": "VM metrics posted successfully."}
Error Response:
- Code:
404 Not Foundif the VM does not exist. - Code:
500 Internal Server Erroron failure.
Client Usage: user_data.sh posts cloud-init timings during boot,
and the client container's start.sh entrypoint posts its own start/end
times at the end of its startup sequence. This helps in monitoring
startup duration.
Provisioning and Operations¶
Creating and destroying the VM pool. Long-running OpenTofu work runs asynchronously: launch/destroy enqueue a job in the operations table and the operations endpoints expose its progress.
POST /api/launch¶
Authentication: Admin (HTTP Basic)
Launch new client VMs.
Takes a number of VMs to create and enqueues an async apply operation;
the background worker generates an OpenTofu variables file and runs
tofu apply to provision the new instances. Poll GET /api/operations
for progress. Rejected with 405 when the provider cannot provision
hosts (manual).
Request Body: application/x-www-form-urlencoded
num_vms(integer, required): The number of new VMs to launch.
Success Response:
- Code:
202 Acceptedwith{"job_id": 7, "status": "queued"}for JSON clients; browsers get302→/admin/instances?job=<id>.
Error Response:
- Code:
409 Conflictwhen another operation is already in progress; the body carries the in-flightjob_id. - Code:
400 Bad Requestfor a missing or non-positivenum_vms.
POST /destroy¶
Authentication: Admin (HTTP Basic)
Destroy all client VMs.
Enqueues an async destroy operation: the background worker runs
tofu destroy to terminate all client EC2 instances and associated
resources, then clears all records from the vms table. This is a
destructive action. Driven by lablink client destroy, and by
lablink destroy as its first teardown step. Open session-metrics rows
are sealed (best-effort) before teardown so final sessions keep a
duration.
Request Body: None
Success Response:
- Code:
202 Acceptedwith{"job_id": 8, "status": "queued"}for JSON clients; browsers get302→/admin/instances?job=<id>.
Error Response:
- Code:
409 Conflictwhen another operation is already in progress. - Code:
405 Method Not Allowedwhen the provider cannot destroy hosts (manual).
GET /api/operations¶
Authentication: Admin (HTTP Basic)
List recent operations.
Returns recent operations with their op_type, status, timestamps,
and resources_completed / resources_total progress counters.
?status=in_progress returns just the currently running operation, if
any.
GET /api/operations/<int:operation_id>¶
Authentication: Admin (HTTP Basic)
Get one operation.
Returns one operation, including its captured output and error.
This is what the CLI polls while client launch runs.
Error Response:
- Code:
404 Not Foundif the operation does not exist.
Client Registration API¶
Used by bring-your-own (BYO) client machines to enrol themselves under the manual provider. AWS-provisioned clients also register, with credentials supplied by OpenTofu.
Registration is what the CLI's lablink client register drives — see Bring-Your-Own Clients for the operator-facing walkthrough, and Configuration for the manual.* settings it depends on.
POST /api/v1/clients/register¶
Authentication: Register token (the one deployment-wide bearer credential).
Register a client machine and mint its credentials.
Request Body: application/json
| Field | Required | Description |
|---|---|---|
hostname |
yes | The client's self-declared hostname, which becomes its client_id. Must start with a letter or digit and contain only letters, digits, dots, dashes and underscores (max 253 chars). |
machine_identity |
yes | Stable machine identifier, unique across the deployment. Re-registering the same identity replaces the old row rather than adding a seat. |
provider |
no | Defaults to aws. BYO clients send manual. |
provider_metadata |
no | Shape must match the deployment's configured manual.connectivity — lan_ip for lan_direct, overlay_hostname for mesh_overlay, reverse_tunnel: true for reverse_tunnel. A mismatch is rejected here rather than failing opaquely at assignment time. |
endpoint_url, gpu_present, gpu_model |
no | Reported by the client; all auto-detected by the CLI. |
Success Response:
- Code:
200 OK - Content: the client's credentials and the configuration it needs to start:
{
"client_id": "gpu-box-3",
"client_secret": "…",
"agent_token": "…",
"allocator_url": "https://lab.example.org",
"connectivity": "lan_direct",
"client_image": "ghcr.io/talmolab/lablink-client-base-image:latest",
"repository": "https://github.com/talmolab/sleap-tutorial-data.git",
"subject_software": "sleap",
"register_token": "…",
"startup_script_b64": "",
"startup_on_error": "continue",
"startup_max_attempts": 3,
"startup_base_delay_seconds": 30,
"startup_success_check_b64": "",
"monitoring": { "enabled": false }
}
client_secret is returned once and stored only as an argon2 hash.
Reverse-tunnel registrations additionally receive tunnel_url,
tunnel_path_prefix and tunnel_bind_addr, all minted by the
allocator.
Error Response:
- Code:
400 Bad Request— missing/invalidhostnameormachine_identity, or aprovider_metadatashape that doesn't match the configured connectivity. - Code:
401 Unauthorized— bad or missing register token.
GET /api/v1/clients/<client_id>/status¶
Authentication: Bearer client_secret (that client's own secret).
Get one client's status.
Lets a registered client confirm the allocator still knows about it.
DELETE /api/v1/clients/<client_id>¶
Authentication: Bearer client_secret (the secret minted at register time).
Best-effort caller-driven deregistration.
Hard-deletes the row, even when useremail is set — the BYO
operator is voluntarily withdrawing the box, and the student's
session is already broken because the local container is going
away.
GET /api/v1/clients¶
Authentication: admin HTTP Basic — same gate as /admin/instances.
List registered clients for operator status views.
Returns only operator-safe columns (no secrets, no log blobs).
POST /api/overlay-hostname¶
Authentication: Client secret (Bearer)
Record the overlay hostname Tailscale actually assigned this client.
The name captured at registration is only what the client asked for.
tailscale up --hostname=X exits 0 even when an existing (possibly
offline) node already holds X, in which case Tailscale appends a numeric
suffix instead. The allocator dials the recorded name, so an
unreconciled rename sends every call to a dead node and the client is
marked Unhealthy forever (lablink#404).
Deliberately its own endpoint rather than a field on /api/vm-status:
that handler and the client's send_status are shared with the AWS
path, where a lost status POST is unrecoverable (see start.sh's own
comment and assign_vm's status='running' requirement). Nothing here runs
for an AWS or lan_direct client — start.sh only calls it inside its
TAILSCALE_AUTHKEY gate.
Lives in this module rather than vm_telemetry because this is where the
overlay_hostname contract is already enforced (see register_client's
expects_overlay check). Flat /api/ prefix to match the sibling
client-VM writes (/api/vm-status, /api/gpu_health,
/api/heartbeat) and to stay out of /api/v1/clients/<client_id>'s
path space.
Scheduled Destruction API¶
Lets an operator schedule tear-down ahead of time — useful for capping the cost of a workshop deployment. Schedules are persisted in the scheduled_destructions table.
POST /api/schedule-destruction¶
Authentication: Admin (HTTP Basic)
Create a new scheduled destruction.
Request JSON: { "schedule_name": "Friday Tutorial End", "destruction_time": "2025-12-05T17:30:00Z", "recurrence_rule": null // or "FREQ=WEEKLY;BYDAY=FR;BYHOUR=17;BYMINUTE=30" }
Returns: Response: JSON with schedule_id and success status, or error with status code.
GET /api/schedule-destruction/<int:schedule_id>¶
Authentication: Admin (HTTP Basic)
Get details of a scheduled destruction.
GET /api/schedule-destruction¶
Authentication: Admin (HTTP Basic)
List all scheduled destructions.
Query parameters: status (optional): Filter by status (scheduled, executing, completed, failed, cancelled)
Returns: Response: JSON with list of schedules, or error with status code.
DELETE /api/schedule-destruction/<int:schedule_id>¶
Authentication: Admin (HTTP Basic)
Cancel a scheduled destruction.
Session Metrics API¶
Populated only when monitoring.enabled is true.
POST /api/session-metrics/<hostname>¶
Authentication: Client secret (Bearer)
Report session metrics.
The client's monitoring sampler posts its accumulated per-session counters (time-in-software, GPU activity, training progress).
The 200 response echoes session_started_at — the VM's
authoritative assignment time (ISO-8601, null while the seat is
free) — which the client writes to its session anchor, healing an
anchor lost to a container restart mid-session.
Error Response:
- Code:
400 Bad Requestifcountersis missing. - Code:
404 Not Foundif the VM does not exist. - Code:
409 Conflictif the session's row is already sealed.
GET /admin/session-metrics¶
Authentication: Admin (HTTP Basic)
Render the cohort summary + per-VM table for Tier 1 monitoring.
GET /api/session-metrics/summary¶
Authentication: Admin (HTTP Basic)
Get the cohort summary.
Returns the aggregate view model — participation funnel plus cohort
totals. Both the admin Session Metrics page and lablink stats
render this same payload, so the two can never disagree.
GET /api/export-metrics¶
Authentication: Admin (HTTP Basic)
Export metrics.
Per-VM metrics as CSV or JSON (controlled by ?format=). Backs
lablink export-metrics --client.
Allocator Logs¶
GET /api/allocator-logs¶
Authentication: Admin (HTTP Basic)
Get the allocator's own logs.
Returns a redacted tail (last 2000 lines) of the allocator's own
container output, read from the file start.sh writes at
/var/log/lablink/allocator.log. Backs the /admin/allocator-logs
page. Values of PASSWORD/TOKEN/SECRET/KEY assignments are masked
before the response leaves the process.
Query Parameters:
errors_only(boolean, optional, defaultfalse): As onGET /api/vm-logs/<hostname>— reducesdocker_logsto its error lines.errorstill reports only a genuinely missing log file; a log with no error lines returnsdocker_logs: ""witherror: null.
Success Response:
- Code:
200 OK - Content:
The response mirrors /api/vm-logs/<hostname>'s keys so the shared logs
template's JavaScript works unchanged. cloud_init_logs is always
null: the allocator host's cloud-init output lives outside the
container and is out of scope. When no log file exists, docker_logs is
null and error explains why — the response is still 200.
GET /admin/allocator-logs¶
Authentication: Admin (HTTP Basic)
Render the shared logs page pointed at this blueprint's API.
Admin Session Actions¶
Per-VM operator actions on the Instances page: preview a desktop, join a participant's session, release a seat, clear a health lockout.
GET /admin/instances/<hostname>/peek¶
Authentication: Admin (HTTP Basic)
View (read-only) a VM already assigned to a participant, without touching useremail or rotating credentials — opens a second WS viewer onto the same live KasmVNC session.
POST /admin/instances/<hostname>/connect¶
Authentication: Admin (HTTP Basic)
Connect (full control) to a VM not currently assigned to anyone, for admin troubleshooting. Reserves the VM out of the assignable pool (AdminReservedAt) and mints a real session via the same prepare_browser_session path /api/request_vm uses — but never sets useremail.
POST /admin/instances/<hostname>/release¶
Authentication: Admin (HTTP Basic)
End an admin troubleshooting session, returning the VM to the assignable pool. Posted to by both the dashboard row's Release button and the /desktop wrapper page's Release form.
POST /admin/instances/<hostname>/clear-unhealthy¶
Authentication: Admin (HTTP Basic)
Clear an allocator-set Unhealthy flag by hand.
A rotation timeout marks a client Unhealthy and assign_vm skips those rows, so the pool can read as empty while the client is perfectly fine. The client cannot clear it itself: check_gpu only reports on change, and on a GPU-less box it posts 'N/A' once and then exits its loop entirely. A successful Admin Connect clears it automatically (see admin_connect_vm); this is the escape hatch for when the box is known good but connecting to it isn't wanted. Before either existed, raw SQL was the only way out (lablink#404).
Admin Pages¶
Operator HTML pages, all behind HTTP Basic Auth. They are walked through in the Workshop Guide, and /admin/byo-onboarding in Bring-Your-Own Clients.
GET /admin/create¶
Authentication: Admin (HTTP Basic)
Render the VM creation page (drives POST /api/launch).
GET /admin¶
Authentication: Admin (HTTP Basic)
Render the admin panel: entry links, pool counts, connection usage.
GET /admin/byo-onboarding¶
Authentication: Admin (HTTP Basic)
Render the ready-to-copy lablink client register command for BYO clients.
The register token rotates on each allocator restart, so this page is dynamic — re-render to get the current token. Behind admin Basic auth (same gate as the rest of /admin); no new privilege boundary.
The rendered flags depend on the configured connectivity, because
register_client rejects a shape that doesn't match it with a 400 (see
its expects_overlay/expects_tunnel checks) — a single lan_direct
command would be guaranteed-broken copy-paste on the other two modes.
Read off client_connectivity.name rather than
cfg.manual.connectivity so this page and that check can never disagree.
GET /admin/instances¶
Authentication: Admin (HTTP Basic)
Render the instances table with per-VM session actions.
GET /admin/instances/fragment¶
Authentication: Admin (HTTP Basic)
Instances table as an HTML fragment, for the page's auto-refresh.
GET /admin/instances/delete¶
Authentication: Admin (HTTP Basic)
Render the destroy-confirmation page (drives POST /destroy).
GET /admin/logs/<hostname>¶
Authentication: Admin (HTTP Basic)
Get the logs for a specific VM.
GET /admin/scheduled-destruction¶
Authentication: Admin (HTTP Basic)
Render scheduled destruction management page.