Skip to content

Architecture

Layout

src/cloudalone/
├── app.py               # Litestar app factory, auth guard, error handler
├── models.py            # SQLAlchemy models (Server, Action, SSHKey)
├── controllers/         # Litestar controllers (hcloud-compatible REST; one module each)
├── schemas.py           # hcloud wire-format builders (the external contract)
├── db.py                # SQLAlchemy async config, session_maker for the worker
├── worker.py            # background action runner (create/delete/reboot VMs)
├── backend.py           # VM backend Protocol + stub
├── libvirt_backend.py   # real backend (Linux-only, libvirt + QEMU/KVM)
├── catalog.py           # server types, images, datacenters, locations
├── net.py               # IPv6 allocation from the routed prefix
├── config.py            # runtime config: env var > TOML > default
├── cli.py               # the `cloudalone` CLI (init, token, image, serve)
└── __init__.py          # main() entry point

Principles

  • Functional core / imperative shell. Side effects live at the edges (DB, libvirt, the network); pure logic (schemas, net, the cloud-init/XML renderers) is kept side-effect-free and unit-tested on any platform. Prefer dataclasses/attrs over raw dicts for internal state.
  • The backend is a Protocol. backend.py defines the seam; StubBackend and libvirt_backend implement it. The whole API + Actions model runs against the stub, so only real VM boot needs a Linux host.
  • The worker runs outside request scope. worker.py executes the blocking libvirt work off the event loop via asyncio.to_thread, using db.session_maker directly (not Litestar DI). Don't add DB calls there without understanding this.

Request → action flow

controllers/ write the Server + Action rows and return 201 immediately with a BackgroundTask that calls worker.run_action. The worker allocates the IPv6, drives the backend, and flips the action's status. Recovery on startup marks orphaned running actions as error and reconciles each server against the backend's live state.

Gotchas

  • Litestar 3.0: use NamedDependency[AsyncSession] for db_session, FromPath[T] / FromQuery[T] for path/query params. Import from advanced_alchemy.extensions.litestar.
  • Test isolation: use flush(), not commit(), in DB tests; refresh objects after flush.
  • domain_type: _domain_xml is a pure renderer. The kvm→qemu fallback for hosts without /dev/kvm is resolved once in config.DOMAIN_TYPE, outside the renderer.

Stack notes

Litestar (ASGI) + SQLAlchemy Async + Advanced Alchemy on SQLite (aiosqlite). No DI container (Dishka not adopted). The libvirt bits are behind the optional libvirt extra so macOS dev never needs libvirt-python.