graphing the topology of my homelab with a custom DSL
- Python 95.8%
- Vim Script 2.1%
- Just 1.6%
- Shell 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs | ||
| src/homelab_graph | ||
| syntax | ||
| tests | ||
| .coverage | ||
| .gitignore | ||
| .python-version | ||
| AGENTS.md | ||
| homelab-graph-wrapper.sh | ||
| Justfile | ||
| pyproject.toml | ||
| README.md | ||
| topology.homelab | ||
| uv.lock | ||
homelab-graph
A declarative DSL for describing homelab infrastructure as a dependency graph, with real-time file watching, linting, and embedded graph queries.
Open-WebUI runs on Kubernetes in namespace ai
Traefik proxies to Open-WebUI on port 8080 via https
Kubernetes is powered by etherium
Features
.homelabfiles — YAML frontmatter for entity metadata, plain English for relationships- 28 verb phrases — compute, network, storage, identity, backup, hardware, observability
- 19 modifier types — ports, VLANs, protocols, paths, modes, schedules, and more
- Lark EBNF parser — grammar-driven, order-independent modifiers, precise error reporting
- NetworkX + SQLite — embedded graph store, no external dependencies
- Real-time watcher — watchdog with debounce, lint-gated sync
- Graph queries — blast radius, dependency chains, SPOF detection, backup audits, 6 new operational queries
- CLI — unified
homelab-graphcommand with 6 subcommands - Vim syntax — auto-generated from grammar
- 146 tests — comprehensive coverage
Quick Start
# Install
git clone https://github.com/colin/graph-homelab
cd graph-homelab
just setup
# Parse a topology file
homelab-graph parse topology.homelab
# Lint for errors
homelab-graph lint topology.homelab
# Watch for changes (auto-syncs to graph)
homelab-graph watch
# Query the graph
homelab-graph query --blast-radius netherite
homelab-graph query --chain kubernetes
homelab-graph query --spofs
homelab-graph query --unbacked
homelab-graph query --on-node netherite
homelab-graph query --shares TrueNAS
homelab-graph query --backup-target PBS
homelab-graph query --notification-channel NTFY
homelab-graph query --namespace ai
homelab-graph query --affected-by "netherite disk full"
# Generate man pages
just man-pages
File Format
.homelab files
---
# Entity declarations (YAML frontmatter)
hosts:
netherite: { ip: 10.0.0.10, os: debian, cores: 16 }
etherium: { ip: 10.0.0.11, os: proxmox, cores: 32 }
services:
Open-WebUI: { repo: ghcr.io/open-webui/open-webui }
Authentik: { version: 2024.4 }
Traefik: { internal: false }
Prometheus: { port: 9090 }
storage:
TrueNAS: { pool: tank, capacity: 40TB }
PBS: { type: proxmox-backup }
---
# Relationships (plain English)
Open-WebUI runs on Kubernetes in namespace ai
Authentik runs on netherite
Kubernetes is powered by etherium
Traefik proxies to Open-WebUI on port 8080 via https
Plex mounts TrueNAS at /mnt/media as ro
netherite is plugged into USW-Lite-16 on port 8 on vlan 50
Kubernetes scraped by Prometheus interval 15s
Entity names
Spaces become dashes in the graph:
Open WebUI → Open-WebUI
my service → my-service
Modifiers
Modifiers are prepositional phrases that attach to relationships:
| Pattern | Property | Example |
|---|---|---|
on port <int> |
port |
on port 8080 |
on vlan <int> |
vlan |
on vlan 50 |
on outlet <int> |
outlet |
on outlet 2 |
at u <int> |
rack_unit |
at u 12 |
via <word> / over <word> |
protocol |
via https, over ipsec |
at <path> |
path |
at /mnt/media |
as <mode> |
mode |
as ro, as rw |
in namespace <word> |
namespace |
in namespace ai |
on interface <word> |
interface |
on interface eth0 |
using driver <word> |
driver |
using driver local |
scoped to <word> |
scope |
scoped to email |
with role <word> |
role |
with role admin |
encrypted with <word> |
encryption |
encrypted with aes256 |
channel <word> |
channel |
channel alerts |
with severity <word> |
severity |
with severity high |
interval <int>s |
interval_sec |
interval 15s |
retaining <int> |
retention_copies |
retaining 7 |
every <schedule> |
schedule |
every daily, every 0_2_*_*_* |
Modifiers can appear in any order:
Traefik proxies to App on port 443 via https
Traefik proxies to App via https on port 443
# Both parse identically
Verbs
| Category | Phrases | Canonical |
|---|---|---|
| Compute | runs on, hosted on, clusters with, orchestrated by |
RUNS_ON, CLUSTERS_WITH, ORCHESTRATED_BY |
| Network | proxies to, routes to, tunnels through, resolves through, listens on |
PROXIES_TO, TUNNELS_THROUGH, RESOLVES_THROUGH, LISTENS_ON |
| Storage | mounts, reads from, persists to, shares via |
MOUNTS, PERSISTS_TO, SHARES_VIA |
| Identity | logs in with, authenticates via, syncs users from, fetches secrets from |
AUTHENTICATES_VIA, SYNCS_USERS_FROM, FETCHES_SECRETS_FROM |
| Backup | backs up to, replicates to |
BACKS_UP_TO, REPLICATES_TO |
| Hardware | powered by, plugged into, located in, mounted in |
POWERED_BY, PLUGGED_INTO, LOCATED_IN |
| Observability | monitored by, scraped by, notifies via |
MONITORED_BY, NOTIFIES_VIA |
CLI
$ homelab-graph --help
Usage: homelab-graph [OPTIONS] COMMAND [ARGS]...
Commands:
complete Print shell completion script for the given shell.
grammar Print the EBNF grammar for .homelab files.
lint Lint a .homelab file for semantic errors.
parse Parse a .homelab file and print the parsed structure.
query Query the homelab graph for insights.
version Print the package version.
vim-syntax Generate Vim syntax highlighting for .homelab files.
watch Watch a .homelab file for changes and auto-sync to the graph.
Subcommands
grammar — Print the EBNF grammar
homelab-graph grammar
Useful for AI agents to understand all valid syntax.
complete — Print shell completion script
homelab-graph complete zsh # bash, zsh, fish, powershell
Or generate all completions with just completions.
parse — Print parsed entities and triplets
homelab-graph parse topology.homelab
homelab-graph parse topology.homelab --json # JSON output
lint — Validate topology file
homelab-graph lint topology.homelab
homelab-graph lint --strict topology.homelab # undeclared entities = errors
watch — Auto-reload on file changes
homelab-graph watch
homelab-graph watch -f topology.homelab -d homelab_graph.sqlite --debounce 0.5 --strict
query — Graph queries
homelab-graph query --blast-radius netherite
homelab-graph query --chain kubernetes
homelab-graph query --spofs
homelab-graph query --unbacked
homelab-graph query --on-node netherite
homelab-graph query --shares TrueNAS
homelab-graph query --backup-target PBS
homelab-graph query --notification-channel NTFY
homelab-graph query --namespace ai
homelab-graph query --affected-by "netherite disk full"
homelab-graph query --blast-radius X --json # JSON output
New query flags
| Flag | Purpose | Example |
|---|---|---|
--on-node <host> |
Services running on a host | --on-node netherite |
--shares <resource> |
Services sharing a resource | --shares TrueNAS |
--backup-target <target> |
Services backing up to a target | --backup-target PBS |
--notification-channel <ch> |
Services alerting via a channel | --notification-channel NTFY |
--namespace <name> |
Services in a K8s namespace | --namespace ai |
--affected-by <problem> |
Map problem → impacted services | --affected-by "disk full" |
vim-syntax — Generate Vim/Neovim syntax file
homelab-graph vim-syntax
homelab-graph vim-syntax ~/.config/nvim/syntax/homelab.vim
version — Print version
homelab-graph version
Architecture
topology.homelab
│
▼
┌─────────────────┐
│ Lark Parser │ EBNF grammar → Triplet AST
└─────────────────┘
│
▼
┌─────────────────┐
│ Linter │ Port/VLAN ranges, self-refs, typo detection
└─────────────────┘
│
▼
┌─────────────────┐
│ NetworkX + │ MultiDiGraph persisted to SQLite
│ SQLite │
└─────────────────┘
│
▼
┌─────────────────┐
│ homelab-graph │ CLI with query, blast radius, SPOF detection
└─────────────────┘
Development
just setup # Install all dependencies
just check # lint + ty + test
just lint # ruff check + format
just lint-fix # auto-fix ruff issues
just format # ruff format
just test # pytest
just test-cov # pytest with coverage
just build # uv build
just man-pages # generate man pages
just clean # remove build artifacts
Schema
Nodes
(:Component {
id: STRING, # slugified name
name: STRING, # display name
kind: STRING, # host, service, storage, component
metadata: STRING # JSON object
})
Relationships
| Edge Type | Description | Properties |
|---|---|---|
[:RUNS_ON] |
Service runs on host | — |
[:CLUSTERS_WITH] |
Nodes in cluster | — |
[:ORCHESTRATED_BY] |
Workload → control plane | role, namespace |
[:PROXIES_TO] |
Reverse proxy → backend | port, protocol |
[:TUNNELS_THROUGH] |
Host → VPN/overlay | interface |
[:RESOLVES_THROUGH] |
Subnet → DNS server | port |
[:LISTENS_ON] |
Service → port/VLAN | port, vlan, interface |
[:MOUNTS] |
Service → storage | path, mode, driver |
[:PERSISTS_TO] |
Service → database | driver |
[:SHARES_VIA] |
Storage → protocol | — |
[:BACKS_UP_TO] |
Service → backup target | schedule, retention_copies, encryption |
[:REPLICATES_TO] |
Primary → secondary | schedule, encryption |
[:AUTHENTICATES_VIA] |
Service → IdP | protocol, scope |
[:SYNCS_USERS_FROM] |
IdP → directory | — |
[:FETCHES_SECRETS_FROM] |
Service → secret store | role |
[:POWERED_BY] |
Host → UPS/PDU | outlet |
[:PLUGGED_INTO] |
Host → switch | port, vlan |
[:LOCATED_IN] |
Host → rack | rack_unit |
[:MONITORED_BY] |
Host → monitor | interval_sec |
[:NOTIFIES_VIA] |
Monitor → channel | channel, severity |
Lint Rules
| Check | Level | Detail |
|---|---|---|
| Port range | ERROR | 1 ≤ port ≤ 65535 |
| VLAN range | ERROR | 1 ≤ vlan ≤ 4094 |
| Outlet range | ERROR | 1 ≤ outlet ≤ 255 |
| Mount mode | ERROR | must be ro or rw |
| Self-reference | ERROR | subject ≠ target |
| Undeclared entity | WARNING | with fuzzy difflib suggestion |
| Undeclared entity (strict) | ERROR | same as above |
Vim/Neovim Setup
# Generate syntax file
homelab-graph vim-syntax ~/.config/nvim/syntax/homelab.vim
# Create ftdetect directory
mkdir -p ~/.config/nvim/ftdetect
# Add filetype detection
echo 'autocmd BufRead,BufNewFile *.homelab set filetype=homelab' \
>> ~/.config/nvim/ftdetect/homelab.vim
Shell Completion
# Generate and source zsh completion
eval "$(homelab-graph complete zsh)"
# Or generate a script for your shell config
homelab-graph complete zsh > ~/.local/share/zsh/site-functions/_homelab-graph
homelab-graph complete bash > ~/.local/share/bash-completion/completions/homelab-graph
homelab-graph complete fish > ~/.config/fish/completions/homelab-graph.fish
# Generate all at once
just completions
License
MIT