Skip to content

How it works

Stack

  • Litestar (ASGI) for the REST API, served by uvicorn.
  • SQLAlchemy async via Advanced Alchemy on SQLite (aiosqlite) for state.
  • libvirt + QEMU/KVM for the VMs, seeded with cloud-init.

The hcloud Python client is a dependency only because it's the most convenient oracle for the wire format (the conformance test drives it); the server itself never imports it.

The Actions model

hcloud is asynchronous: every mutating call returns an action that the client polls until it reaches success or error. cloudalone mirrors this exactly:

  1. A request (e.g. POST /v1/servers) writes the Server + Action rows and returns 201 immediately.
  2. A background task runs the blocking libvirt work off the event loop (asyncio.to_thread) and flips the action's status.
  3. The client polls GET /v1/actions?id=…, the same as it would against Hetzner.

This keeps the API responsive while VMs boot, and it's why hcloud server create returns instantly and then "waits" for the action.

POST /v1/servers ──▶ write Server + Action (201) ──▶ background task
                                                        ├─ allocate IPv6
                                                        ├─ create libvirt domain
                                                        └─ action.status = success
GET /v1/actions?id=… ◀── client polls until success/error

Backends

The VM work lives behind a small Backend Protocol with two implementations:

Backend Selected by Does
stub CLOUDALONE_BACKEND=stub (default) No virtualization; records state only. Runs on any OS, used for dev and tests.
libvirt CLOUDALONE_BACKEND=libvirt Real VMs via QEMU/KVM. Linux host only; behind the optional libvirt extra.

Because the backend is a Protocol, the entire API + Actions model is exercised on macOS/CI with the stub; only the actual boot needs a Linux host.

Design

cloudalone follows functional core / imperative shell: pure logic (wire-format builders, address allocation, cloud-init/XML renderers) is separated from the side-effecting edges (DB, libvirt, the network). The worker runs outside the request scope, so it uses its own session_maker rather than Litestar's dependency injection.

See the Developer guide for the code layout.