# 🧠 Fester **A distributed, DAG-driven build execution system with real-time scheduling, thermal/load awareness, cache-aware execution, and deterministic replay/debugging.** [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) [![Python: 3.12+](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://www.python.org/) [![FastAPI](https://img.shields.io/badge/FastAPI-0.128+-green.svg)](https://fastapi.tiangolo.com/) --- ## πŸ“Έ Screenshots | Dashboard | Live DAG | Replay | |-----------|----------|--------| | ![Dashboard](docs/screenshots/01_dashboard.png) | ![Live DAG](docs/screenshots/02_live_dag.png) | ![Replay](docs/screenshots/03_replay.png) | | Sessions | Metrics | Cause Graph | |----------|---------|-------------| | ![Sessions](docs/screenshots/04_sessions.png) | ![Metrics](docs/screenshots/05_metrics.png) | ![Cause](docs/screenshots/06_cause.png) | | Timeline | Debugger | |----------|----------| | ![Timeline](docs/screenshots/07_timeline.png) | ![Debugger](docs/screenshots/08_debugger.png) | --- ## 🎯 What Fester Does Fester turns a cluster of machines into a single, observable build brain: - **DAG-driven execution** β€” every build is compiled into a directed acyclic graph of actions, with dependencies honored and parallel branches run concurrently - **Smart scheduler** β€” picks the best node for each action based on CPU load, temperature, policy constraints, and historical instability - **Real-time observability** β€” every event (node state, schedule decision, task lifecycle, cache hit/miss, failure) streams live to the UI via WebSocket - **Deterministic replay** β€” every build session is journaled to SQLite; scrub back through the timeline to see exactly what happened, when, and why - **Failure autopsy** β€” when an action fails, trace its dependency chain backward, see the last scheduler decision, and compute the forward blast radius - **Cause graph** β€” a post-hoc reasoning layer that builds a causal graph from system events, so you can ask "why did this node make this decision?" - **Cache layer** β€” MinIO distributed cache + optional Btrfs CoW reflinks + QCOW2 workspace snapshots ## πŸ—οΈ Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Fester Cluster β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Backend │◄──►│ EventBus │◄──►│ PipelineEngine β”‚ β”‚ β”‚ β”‚ (FastAPI) β”‚ β”‚ (singleton)β”‚ β”‚ (DAG executor) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ WebSocket β”‚ β”‚ Timeline β”‚ β”‚ Scheduler β”‚ β”‚ β”‚ β”‚ β”‚ Stream β”‚ β”‚ Store β”‚ β”‚ (weighted/thermalβ”‚ β”‚ β”‚ β”‚ β”‚ (/ws, etc) β”‚ β”‚ (SQLite) β”‚ β”‚ /cache-aware) β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Node Registry β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (probe + drift)β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ UI (8 pages) β”‚ β”‚ β”‚ β”‚ β”‚ Dashboard β”‚ Live DAG β”‚ Replay β”‚ Sessions β”‚ Metrics β”‚ β”‚ β”‚ β”‚ β”‚ Cause Graph β”‚ Timeline β”‚ Debugger β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ β”‚ MinIO Cacheβ”‚ β”‚ Btrfs CAS β”‚ β”‚ QCOW2 Snap β”‚ β”‚ tmux β”‚β”‚ β”‚ β”‚ β”‚ (optional) β”‚ β”‚ (optional)β”‚ β”‚ (optional) β”‚ β”‚ runtimeβ”‚β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–² β”‚ β”‚ HTTP :8787/status (probe) β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Cluster Nodes β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ β”‚ x99-v3 β”‚ β”‚ x99-v4 β”‚ β”‚ rpi-1 β”‚ β”‚ ryzen-1β”‚β”‚ β”‚ β”‚ β”‚(x86_64)β”‚ β”‚(x86_64)β”‚ β”‚(arm64) β”‚ β”‚(x86_64)β”‚β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ``` ## πŸ“ Project Layout ``` fester/ β”œβ”€β”€ backend/ # Python backend (FastAPI) β”‚ β”œβ”€β”€ main.py # App entrypoint + 71 routes β”‚ β”œβ”€β”€ api/ # REST routers + WebSocket hub β”‚ β”‚ β”œβ”€β”€ build.py # /api/build β”‚ β”‚ β”œβ”€β”€ nodes.py # /api/nodes (list, policy, probe) β”‚ β”‚ β”œβ”€β”€ replay.py # /replay/start, /events β”‚ β”‚ β”œβ”€β”€ autopsy.py # /autopsy/{sid}/{action} β”‚ β”‚ β”œβ”€β”€ timeline.py # /timeline/{node}, /rewind/{i} β”‚ β”‚ β”œβ”€β”€ debugger.py # /debugger/{pause,resume,step} β”‚ β”‚ β”œβ”€β”€ pipeline_control.py # /api/pipeline/{state,retry,...} β”‚ β”‚ β”œβ”€β”€ metrics.py # /metrics (Prometheus) + /api/metrics/json β”‚ β”‚ └── cause.py # /api/cause/{explain,trace,events} β”‚ β”œβ”€β”€ pipeline/ # Build execution β”‚ β”‚ β”œβ”€β”€ engine.py # DAG executor with pause/step/critical-path β”‚ β”‚ └── runner.py # BuildRunner (lifecycle + persistence) β”‚ β”œβ”€β”€ scheduler/ # Node selection β”‚ β”‚ └── optimizer.py # choose_best_node (weighted scoring) β”‚ β”œβ”€β”€ analysis/ # Reasoning layer β”‚ β”‚ β”œβ”€β”€ cause_graph.py # Causal graph from events β”‚ β”‚ β”œβ”€β”€ timeline_store.py # SQLite-backed event journal β”‚ β”‚ β”œβ”€β”€ failure_autopsy.py # Backward dependency tracing β”‚ β”‚ └── failure_propagation.py # Forward blast radius β”‚ β”œβ”€β”€ events/ # Event infrastructure β”‚ β”‚ β”œβ”€β”€ bus.py # Singleton EventBus β”‚ β”‚ └── schema.py # FesterEvent dataclass + EventType enum β”‚ β”œβ”€β”€ nodes/ # Node management β”‚ β”‚ β”œβ”€β”€ probe.py # HTTP agent probing (async + sync) β”‚ β”‚ β”œβ”€β”€ roles.py # NodeRole dataclass β”‚ β”‚ └── state_model.py # NodeStateRegistry β”‚ β”œβ”€β”€ storage/ # Storage layer β”‚ β”‚ β”œβ”€β”€ btrfs_cas.py # CoW reflink snapshots β”‚ β”‚ β”œβ”€β”€ qcow2_freeze.py # QCOW2 workspace freezing β”‚ β”‚ β”œβ”€β”€ tmpfs.py # tmpfs workspace acceleration β”‚ β”‚ └── sqlite_db.py # Persistent storage (builds, sessions, events) β”‚ β”œβ”€β”€ integrations/ # External tool integrations β”‚ β”‚ β”œβ”€β”€ tmux.py # Detached tmux action runtime β”‚ β”‚ β”œβ”€β”€ mosh.py # mosh/ssh shell command builder β”‚ β”‚ β”œβ”€β”€ forgejo.py # Forgejo push webhook handler β”‚ β”‚ β”œβ”€β”€ lxc.py # LXC container execution β”‚ β”‚ └── libvirt.py # libvirt VM execution β”‚ β”œβ”€β”€ executor/ # Runtime router (host/lxc/libvirt/tmux) β”‚ β”œβ”€β”€ cache/ # MinIO distributed cache β”‚ β”œβ”€β”€ graph/ # DAG construction + critical path β”‚ β”œβ”€β”€ metrics/ # Prometheus exporter + observability hub β”‚ β”œβ”€β”€ policy/ # Policy engine (rules + overrides) β”‚ └── targets/ # Build target catalog (Gentoo, Buildroot, ...) β”œβ”€β”€ ui/ # Frontend (vanilla JS, no build step) β”‚ β”œβ”€β”€ app.js # Shared shell (WS, toasts, topbar, health) β”‚ β”œβ”€β”€ live_dag.html # Real-time DAG with layered layout β”‚ β”œβ”€β”€ replay.html # Timeline scrubber + autopsy β”‚ β”œβ”€β”€ sessions.html # Build history + tmux output viewer β”‚ β”œβ”€β”€ metrics.html # Live charts + per-node cards β”‚ β”œβ”€β”€ cause.html # Cause graph + blast radius β”‚ β”œβ”€β”€ timeline.html # Per-node event drill-down β”‚ └── debugger.html # Step-through execution control β”œβ”€β”€ cli/ # CLI tool (35+ subcommands) β”‚ └── fester.py β”œβ”€β”€ cockpit/ # Cockpit module (optional) β”œβ”€β”€ docs/ # Documentation + screenshots β”œβ”€β”€ scripts/ # Helper scripts (mock agent, syntax checker) β”œβ”€β”€ pyproject.toml # Python packaging β”œβ”€β”€ bootstrap.sh # One-shot dependency installer β”œβ”€β”€ run.sh # Start the backend └── config.yaml # Cluster configuration ``` ## πŸš€ Quick Start ```bash # 1. Clone git clone https://git.dcos.net/dcosnet/fester.git cd fester # 2. Bootstrap (creates venv, installs deps, sets up config) ./bootstrap.sh # 3. Run ./run.sh # 4. Open the UI open http://localhost:8080 ``` See **[quickstart.md](quickstart.md)** for the full 5-minute walkthrough. ## πŸ› οΈ Prerequisites - **Python 3.12+** - **pip** (for dependency installation) - **Optional** (features activate automatically when installed): - `tmux` β€” for the tmux action runtime (live output viewing) - `mosh` + `ssh` β€” for the "Shell into node" UI button - `qemu-utils` β€” for QCOW2 workspace snapshots - `rsync` β€” for workspace freeze operations - `btrfs-progs` β€” for CoW reflink snapshots (requires btrfs filesystem) - `minio` server β€” for distributed cache (otherwise falls back to local) ## πŸ“Š Tech Stack | Layer | Technology | |-------|------------| | Backend | Python 3.12, FastAPI 0.128, uvicorn, pydantic 2 | | Realtime | WebSockets (3 channels: `/ws`, `/ws-targets`, `/ws-debugger`) | | Storage | SQLite (WAL mode) for builds/sessions/events; MinIO for distributed cache | | Frontend | Vanilla HTML/CSS/JS (no build step, no framework, ~12KB total) | | Observability | Prometheus exposition + Grafana dashboard configs | | CLI | Python argparse (35+ subcommands) | | Integrations | tmux, mosh, LXC, libvirt, Forgejo, distcc, ccache | ## πŸŽ›οΈ Configuration Fester reads from `config.yaml` (or `FESTER_CONFIG` env var) at startup: ```yaml master: name: fester-master role: control nodes: - name: x99-v3 host: 192.168.1.10 max_jobs: 24 - name: x99-v4 host: 192.168.1.11 max_jobs: 30 projects: - name: linux-tool repo: https://forgejo.local/linux-tool.git targets: debian: "make clean && make debian" arch: "make clean && make arch" ``` ### Environment Variables | Variable | Default | Purpose | |----------|---------|---------| | `FESTER_CONFIG` | `config.yaml` | Path to YAML config | | `FESTER_DB_PATH` | `/var/lib/fester/fester.db` (with fallbacks) | SQLite database location | | `FESTER_NO_DRIFT` | unset | Set to `1` to disable synthetic drift when agents unreachable | | `FESTER_AUTOBUILD` | unset | Set to `1` to auto-trigger a build every 60s | | `FESTER_ROLE_DB` | `/etc/fester/node_roles.json` | Node role overrides | | `FESTER_CACHE_DIR` | `/var/lib/fester/cache` | Local cache directory | | `FESTER_STORAGE_CONFIG` | `/etc/fester/storage.json` | Storage layer config | | `FESTER_API` | `http://localhost:8080` | CLI: backend URL | | `FESTER_WS` | `ws://localhost:8080/ws` | CLI: WebSocket URL | ## πŸ”Œ API Surface 71 routes total. Key endpoints: | Method | Path | Purpose | |--------|------|---------| | `POST` | `/api/build` | Kick off a build | | `GET` | `/api/builds` | Build history | | `POST` | `/api/builds/{id}/cancel` | Cancel a running build | | `GET` | `/api/nodes` | List cluster nodes (with live metrics) | | `POST` | `/api/nodes/{name}/policy` | Set node policy (preferred/avoid/neutral) | | `POST` | `/api/nodes/{name}/probe` | Manually probe a node's agent | | `POST` | `/api/nodes/{name}/shell` | Build mosh/ssh shell command | | `GET` | `/api/metrics/json` | JSON metrics snapshot | | `GET` | `/metrics` | Prometheus exposition | | `GET` | `/api/cause/explain/{node}` | Causal chain for a node | | `GET` | `/api/propagation/{action}` | Blast radius if action fails | | `POST` | `/replay/start` | Start a replay session | | `GET` | `/replay/events` | Full event journal | | `GET` | `/autopsy/{sid}/{action}` | Failure autopsy | | `GET` | `/api/storage/status` | Storage capabilities | | `POST` | `/api/storage/freeze` | Freeze workdir into qcow2 | | `GET` | `/api/actions/active` | List active tmux sessions | | `WS` | `/ws` | Main event stream | | `WS` | `/ws-debugger` | Debugger control channel | | `WS` | `/ws-targets` | Target toggle notifications | Full route list: `curl http://localhost:8080/openapi.json | jq '.paths | keys[]'` ## πŸ–₯️ CLI ```bash # Build + watch fester build --cmd "make -j$(nproc)" --dir /home/user/linux --watch # List builds fester builds # Node management fester node list fester node set-policy x99-v3 preferred fester node probe x99-v3 # Cause analysis fester cause explain x99-v3 build_kernel fester blast build_kernel # Live event stream fester stream # Health check fester health ``` Run `fester --help` for the full list of 35+ subcommands. ## πŸ“š Documentation - **[quickstart.md](quickstart.md)** β€” 5-minute getting started guide - **[CHEATSHEET.md](CHEATSHEET.md)** β€” Operator survival guide - **[CONTRIBUTING.md](CONTRIBUTING.md)** β€” How to contribute - **[CHANGELOG.md](CHANGELOG.md)** β€” Version history - **[docs/screenshots/](docs/screenshots/)** β€” UI screenshots ## 🐳 Docker ```bash docker build -t fester . docker run -p 8080:8080 -v fester-data:/var/lib/fester fester ``` See **[Dockerfile](Dockerfile)** for details. ## πŸ” Security Fester is designed to run behind your existing network controls (NAT, firewalls like OPNsense, etc.). There is **no built-in auth** β€” assume the network is trusted. If you need to expose it publicly, put it behind a reverse proxy with auth (nginx + OAuth2 Proxy, Traefik + Authelia, etc.). ## ⚠️ License GNU Affero General Public License v3.0 β€” see **[LICENSE](LICENSE)**. Software is provided "AS IS", without warranty of any kind. This is a high-concurrency distributed execution system β€” review the safety notes in the license before production use.