README.md
Ref: Size: 5.8 KiB History
# eitri
A control plane for running virtual machines on your own hardware.
eitri turns a pool of machines into a small VM cloud. You describe the guests
you want; each host runs an agent that makes reality match that description and
reports back. Guests boot as real VMs under UEFI, own their own kernel, and get
a sticky IP: on Linux under [cloud-hypervisor](https://www.cloudhypervisor.org/)
on a per-host bridge, on macOS under Apple's Virtualization.framework via
[vfkit](https://github.com/crc-org/vfkit).
## How it works
eitri is built around a single desired-state loop, the same shape as a kubelet:
```
eitri-server ──Snapshot──▶ eitri-agent ──▶ guests (the host's VMM)
(control plane) (one per host)
▲ │
└───────────Report───────────┘ (also the heartbeat)
```
- **The control plane (`eitri-server`)** holds the desired fleet—which VMs
should exist, on which host, with what resources—and streams it to each host
over a persistent QUIC connection.
- **The agent (`eitri-agent`)** reconciles: it gives every VM its own worker
goroutine that creates, converges, or tears down that one guest, so a slow
operation on one VM never stalls the others or the host's heartbeat. It reports
the actual state back on the same stream; that report doubles as the heartbeat,
and the control plane marks a host offline after ~30s of silence. Each agent
reports its binary version and can be upgraded per host from the fleet
console, which also signals when a newer eitri release is available.
- **State is desired-state, not RPC.** The loop is level-triggered: a failed step
is retried on the next tick, and a host that reconnects re-derives everything
from persisted records plus what it observes on the box.
The agent owns everything host-local: resource admission (vCPU / memory / disk /
address are admitted through one serialized gate), addressing (on Linux an
embedded DHCP server hands each VM a sticky, deterministic address and reserves
it at create; on macOS the OS's own NAT assigns it and the agent reads the
lease), and a content-addressed image cache (each base image is downloaded and
decoded to raw once, then reflink-copied per guest).
## Components
| Binary | Role |
| --- | --- |
| `eitri-server` | Control plane: HTTP API, the MCP endpoint at `/mcp`, QUIC sync stream, and the SSH-CA jump gate. |
| `eitri-agent` | Host agent: enrolls a host, reconciles its VMs, drives the host's VMM. |
| `eitri` | Client CLI: signs an ephemeral cert with a tenant CA and reaches a guest through the gate. |
`eitri-shape` (regenerate the architecture graph) rounds out the binaries.
## Access model
eitri is multi-tenant. A **fleet** of hosts is partitioned into **tenants**, each
its own isolated namespace with its own SSH user CA—eitri holds no tenant user
signing key. You reach a guest by name:
```
eitri ssh <vm-name>
```
`eitri ssh` self-signs a short-lived certificate with your tenant's user CA and
jumps through the server's gate, which authorizes the connection against the
tenant derived from the signing CA. Guest host certificates are namespaced the
same way, so names never collide across tenants.
## Getting started
Build everything:
```sh
make build # binaries into ./bin
```
**Enrolling a real host.** The server mints a single-paste join blob; on the host,
`eitri-agent join <blob>` enrolls it (posting to `/api/v1/enroll`), pins the
server certificate from the blob, and persists its identity. From then on the
agent runs the reconcile + sync loop against the fleet. Run it under systemd
with `scripts/eitri-agent.service`—`Restart=on-failure` revives a crashed
agent, and its `KillMode=process` keeps running VMs alive across agent stops.
Guests boot from cloud images (the default is Ubuntu resolute) under UEFI, so
the guest owns its kernel and any disk-only image boots unmodified. On Linux
that is the `CLOUDHV.fd` firmware shipped to each host; on macOS it is the
framework's own EFI bootloader, and each guest keeps its NVRAM beside its disk.
## Documentation
[docs/](docs/README.md) is indexed by what you're trying to do—understand it
([architecture](docs/architecture.md), [ethos](docs/ethos.md)), run a fleet
([cert rotation](docs/cert-rotation.md), [revocation](docs/credential-revocation.md)),
use a fleet ([connecting](docs/connecting.md), [mcp](docs/mcp.md)), or see why
it's built this way ([decisions](docs/decisions.md)). What ships next is in
[ROADMAP.md](ROADMAP.md).
## Repository layout
```
cmd/ entrypoints (eitri-server, eitri-agent, eitri, tooling)
internal/
agent/ reconcile loop, VMM drivers (cloudhv, vfkit), DHCP, image cache, netenv
server/ API, QUIC sync service, SSH gate/CA, store, registry, hub
transport/ QUIC transport shared by both sides
pb/ generated protobuf (proto/eitri/v1)
web/ SvelteKit fleet console, embedded into eitri-server
proto/ the wire contract
docs/shape.* the generated, explorable architecture graph
```
## Development
`make ci` is the gate—everything a change must pass before it lands:
```sh
make ci
```
It runs `go vet`, the build for linux and darwin, the architecture fitness
functions, `golangci-lint`, `gofmt`, the test suite under `-race`, per-package
coverage floors, the drift checks (`go mod tidy`, protobuf, API contract,
shape graph), the deadcode sweep, a site render, and the console's tests and
typecheck. The `.githooks/pre-push` hook—installed by `make hooks`—runs it and
blocks a red push to `main`.
Common loops:
```sh
make test # go test -race
make deploy # roll HEAD to the fleet (local server + remote agents) — runs a real-VM boot-gate
make shape # regenerate docs/shape.{json,html} after a package change
```