No description
  • Go 88.2%
  • TypeScript 4%
  • Shell 3.7%
  • Svelte 2.9%
  • Makefile 0.6%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-30 14:36:57 +01:00
.githooks build: a push to main brings a line about what slowed it down 2026-08-22 17:12:02 +01:00
cmd Add CLI VM listing, volume management, and port exposure 2026-09-30 11:55:10 +01:00
deploy/server backup: a job that moved registries needs the credential to match 2026-09-05 20:02:35 +01:00
docs Merge released cloud-init UI into main before CLI publication 2026-09-30 14:34:19 +01:00
internal test: wait for sync clients to stop during cleanup 2026-09-30 14:36:57 +01:00
proto/eitri/v1 volumes: durable block storage a VM attaches at create 2026-09-05 18:43:27 +01:00
scripts Add CLI VM listing, volume management, and port exposure 2026-09-30 11:55:10 +01:00
site site: the image takes its fixes from the distro, not the base tag 2026-09-05 19:05:19 +01:00
web web: add cloud-init SSH key builder 2026-09-30 12:10:43 +01:00
.gitignore feat(deploy): a release is one pipeline run twice, at stg then prod 2026-08-08 15:02:59 +01:00
.golangci.yml wire: what the rename argued, the tree now enforces 2026-08-23 06:47:13 +01:00
AGENTS.md volumes: provision on selected hosts and manage through the console (#132) 2026-09-15 12:34:37 +00:00
go.mod deps: x/crypto v0.56.0, and the gate names the option it accepts 2026-09-05 18:43:27 +01:00
go.sum deps: x/crypto v0.56.0, and the gate names the option it accepts 2026-09-05 18:43:27 +01:00
Makefile wire: what the rename argued, the tree now enforces 2026-08-23 06:47:13 +01:00
README.md Add CLI VM listing, volume management, and port exposure 2026-09-30 11:55:10 +01:00
RETRO.md test: wait for sync clients to stop during cleanup 2026-09-30 14:36:57 +01:00
ROADMAP.md docs: the roadmap stops listing what already shipped 2026-08-18 15:46:49 +01:00

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 on a per-host bridge, on macOS under Apple's Virtualization.framework via 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: creates and lists VMs, manages volumes and published ports, and reaches guests through the SSH 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:

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/ is indexed by what you're trying to do—understand it (architecture, ethos), run a fleet (cert rotation, revocation), use a fleet (connecting, mcp), or see why it's built this way (decisions). What ships next is in 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:

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:

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