Skip to main content

cloudalone

cloudalone is a small cloud you run on a single Linux box. It speaks the Hetzner Cloud API, so the official hcloud CLI and the Hetzner client libraries work against it unchanged: point them at your own machine instead of api.hetzner.cloud, and hcloud server create boots a real VM.

Inside, it's a small REST API backed by SQLite, a fixed catalog of server types and images, and libvirt/QEMU for the actual VMs. Every VM gets its own public IPv6 address. It's a single process on one host: no clustering or message broker.

Status

It's a working prototype.

The core loop works on a real host: hcloud server create boots a Debian VM, cloud-init brings up its IPv6, and list, describe, delete, and the power actions behave the way the CLI expects. Responses are checked against the real hcloud Python client, and the request and polling behaviour against the Go CLI. See Limitations for what's missing.

How it works

cloudalone is a Litestar app on SQLite (SQLAlchemy async via Advanced Alchemy), served by uvicorn. VMs run on libvirt and QEMU/KVM and are configured with cloud-init.

The Actions model. The Hetzner API is asynchronous: a call that changes something returns an action, and the client polls that action until it succeeds or fails. cloudalone works the same way. A create request writes the server and action rows, returns 201 straight away, and runs the blocking libvirt work in the background; the CLI polls GET /v1/actions just as it would against Hetzner.

IPv6. Every VM gets a real, public IPv6 address. cloudalone takes one IPv6 prefix that is routed to the host, gives the br-cloud bridge the first address, turns on forwarding, and hands each VM its own slice of the prefix through cloud-init. That's ordinary L3 routing, no NDP proxying. The only provider-specific part is obtaining the routed prefix. On a Scaleway Dedibox you order a /48 and a DHCPv6-PD client keeps a /56 routed to the box (the deploy sets this up); with a provider that routes you a /64 directly, or your own static route, you just point ipv6_prefix at it. Guests are IPv6-only.

Backends. The default stub backend does no real virtualization, so you can run and test the whole thing (API, actions, CLI) on a Mac. The libvirt backend is the real one and needs a Linux host with KVM. libvirt-python is only installed on that host, so Mac development doesn't pull it in.

Requirements

  • To develop: Python 3.12+ and uv, on macOS or Linux. The stub backend means no VMs are involved.
  • To run VMs: a Linux host with KVM, libvirt, QEMU, and cloud-image-utils, plus an IPv6 prefix routed to it. The deploy installs the packages.

Quickstart

Run the API against the stub backend and poke it with curl:

uv sync
CLOUDALONE_BACKEND=stub CLOUDALONE_TOKEN=dev-token uv run cloudalone serve &
curl -s -H "Authorization: Bearer dev-token" http://localhost:8000/v1/server_types | python3 -m json.tool

To drive the real hcloud client through a full create → poll → delete against the stub:

uv run python scripts/conformance.py    # prints CONFORMANCE OK

The CLI

cloudalone is both the server and a small admin tool:

cloudalone init                  # set up /home/cloudalone and write a config with a fresh token
cloudalone migrate               # create or upgrade the database schema
cloudalone token                 # rotate the bootstrap token in config.toml
cloudalone token create ci       # create a named API token (printed once)
cloudalone token list            # list named tokens
cloudalone token revoke ci       # revoke a named token
cloudalone image list            # list catalog images and which are downloaded
cloudalone image pull debian-13  # download a base image (or `all`)
cloudalone dns                   # print the VM name -> IPv6 mapping (hosts/dnsmasq format)
cloudalone serve                 # run the API

The catalog is fixed: server types small, medium, large, and images debian-13 and ubuntu-24.04.

Dashboard

A read-only web dashboard lives at /dashboard: servers, per-server action history, ssh-keys, and the catalog. Log in with a valid API token; it's server-rendered HTML with no build step.

Deploying to a real host

The deploy is a single pyinfra run that turns a bare Linux box into a running service. It installs the virtualization stack, creates the cloudalone user, sets up the br-cloud bridge and IPv6 forwarding (and, on Dedibox, the DHCPv6-PD client that keeps the prefix routed), syncs the code, writes the config and token, pulls the Debian image, and installs the systemd unit. Re-running it upgrades in place; it's idempotent.

  1. Edit deploy/inventory.py: your host, the routed ipv6_prefix, and optionally api_host (for TLS) and dhcpv6_duid (for Dedibox).

  2. Run it:

    uv run pyinfra deploy/inventory.py deploy/deploy.py
    

Before pointing it at a real box, you can run the whole deploy against a throwaway container:

uv run pytest tests/c_e2e/test_deploy_docker.py    # deploys into a systemd container and checks the API serves

TLS

Set api_host in the inventory and the deploy also installs Caddy as a TLS reverse proxy in front of cloudalone (https://<api_host>/v1, automatic Let's Encrypt), binding the app to localhost behind it. Point an A record at the box and open ports 80 and 443 before deploying, so Caddy can complete the certificate challenge.

Using the hcloud CLI

Point the official CLI at your box and use it normally:

export HCLOUD_TOKEN=$(ssh root@<host> grep token /home/cloudalone/config.toml | cut -d'"' -f2)
export HCLOUD_ENDPOINT=https://api.example.com/v1     # or http://[<host-ipv6>]:8000/v1

hcloud ssh-key create --name me --public-key-from-file ~/.ssh/id_ed25519.pub
hcloud server create --name t1 --type small --image debian-13 --ssh-key me
hcloud server list
ssh -6 root@$(hcloud server ip -6 t1)   # your key is installed for root
hcloud server delete t1

You need -6 on the ssh line (or AddressFamily inet6 in ~/.ssh/config), because the address is a bare IPv6 literal.

Configuration

Settings are read from, in order of precedence: an environment variable, the TOML config file, then a built-in default. The config path is CLOUDALONE_CONFIG (default /home/cloudalone/config.toml), and cloudalone init writes one for you.

Setting Env var Default
Bearer token CLOUDALONE_TOKEN dev-token
Backend CLOUDALONE_BACKEND stub
Routed IPv6 prefix for VMs CLOUDALONE_IPV6_SLICE 2001:bc8:1200:19:c10d::/80 (dev placeholder; the deploy sets the real routed prefix)
Base directory CLOUDALONE_HOME /home/cloudalone
SQLite path CLOUDALONE_DB cloudalone.sqlite
Host bridge CLOUDALONE_BRIDGE br-cloud
DNS zone for VM names CLOUDALONE_DNS_ZONE vms.local
libvirt URI CLOUDALONE_LIBVIRT_URI qemu:///system
Bind host / port CLOUDALONE_HOST / CLOUDALONE_PORT ::1 / 8000

See config.toml.example for the full file.

Testing

Tests are split into three tiers; uv run pytest runs the fast ones.

  • tests/a_unit/: fast and isolated (renderers, config, pure logic). Runs anywhere.
  • tests/b_integration/: the real libvirt backend. Each test skips unless what it needs is present (libvirtd, qemu-img, cloud-localds, /dev/kvm), so on a Mac or in CI they skip rather than fail.
  • tests/c_e2e/: the API through Litestar's test client on the stub backend, plus two Docker tests: one runs the suite inside a Linux container, the other runs the full deploy.

make test, make lint, and make test-cov are the usual entry points. CI runs on GitHub Actions and SourceHut.

Project layout

src/cloudalone/
  app.py             Litestar app, bearer-auth guard, hcloud error envelope
  controllers/       hcloud-compatible REST controllers (one module each)
  schemas.py         hcloud wire-format builders (the external contract)
  catalog.py         server types, images, locations, datacenters
  models.py          SQLAlchemy models (Server, Action, SSHKey, Token)
  worker.py          background action runner (the Actions model)
  backend.py         VM backend Protocol + stub
  libvirt_backend.py real backend: QEMU/KVM + cloud-init (Linux-only)
  net.py             IPv6 allocation from the routed prefix
  dns.py             VM DNS naming (<name>.<zone>)
  dashboard.py       server-rendered HTML for the read-only dashboard
  config.py          runtime config (env > TOML > default)
  cli.py             the cloudalone CLI
migrations/          alembic schema migrations
deploy/              pyinfra deploy + the Docker deploy test
scripts/             conformance check, IPv6 probe, host setup

Limitations

cloudalone implements the core of the hcloud API and stops there. The main gaps:

  • No networks, volumes, floating IPs, load balancers, or firewalls. Guests are IPv6-only.
  • Flat tokens, no projects. Multiple named tokens work, but there's no project scoping of servers or keys.
  • DNS needs a resolver. Each server has a name (<name>.<zone>) and cloudalone dns emits the mapping, but you still have to point a resolver at it (dnsmasq or /etc/hosts) and delegate the zone before ssh <name> resolves. If the box already runs a resolver on port 53 (systemd-resolved, a stock BIND), pin dnsmasq's bind first; see the deploy guide.

On a Dedibox specifically, you have to order the /48 block and enable "Activate IPv6 SLAAC" in the console before the routed prefix works.

License

Apache 2.0. © Abilian SAS.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cloudalone-0.2.0.tar.gz (34.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cloudalone-0.2.0-py3-none-any.whl (43.6 kB view details)

Uploaded Python 3

File details

Details for the file cloudalone-0.2.0.tar.gz.

File metadata

  • Download URL: cloudalone-0.2.0.tar.gz
  • Upload date:
  • Size: 34.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cloudalone-0.2.0.tar.gz
Algorithm Hash digest
SHA256 59684c78ed9f43df84f9765b0d95cef5a9e8e7150eed9b7ed1f0b16d14f8942a
MD5 0001a090016aa69dfc454603869c7514
BLAKE2b-256 3fbf0cdfb0a2cc03727dc4dfe94433f080e23cb619ad73284d115685e635adb3

See more details on using hashes here.

File details

Details for the file cloudalone-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: cloudalone-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 43.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cloudalone-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 913a21ec6bea93ccb62fa9fc2e7707606d4054263cbfce51eab2874fba8fa737
MD5 9c39b2851c49545799cd4c87c0578db2
BLAKE2b-256 bc48d3f883b8dbd0157f1aeb6a1ff3dd60dd54e81dc29fcbe749dbbc3749c1b5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page