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.pydefines the seam;StubBackendandlibvirt_backendimplement 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.pyexecutes the blocking libvirt work off the event loop viaasyncio.to_thread, usingdb.session_makerdirectly (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]fordb_session,FromPath[T]/FromQuery[T]for path/query params. Import fromadvanced_alchemy.extensions.litestar. - Test isolation: use
flush(), notcommit(), in DB tests; refresh objects after flush. domain_type:_domain_xmlis a pure renderer. Thekvm→qemufallback for hosts without/dev/kvmis resolved once inconfig.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.