Getting started¶
Two parts. The first gets you to a finding table in a few minutes with nothing but the tool: a synthetic lab ships inside it. The second points the harness at your own lab, one source at a time. Nothing in either part changes anything on your network — discovery is read-only, and execution stays off until you turn a specific kind of change on yourself.
Part 1 — a finding table, with no lab at all¶
Requirements: Python 3.12 or newer on Linux or macOS, and uv or pipx.
1. Install¶
uv tool install homelab-helper # from PyPI
# or: pipx install homelab-helper
helper --install-completion # bash / zsh / fish, optional
If you installed a release candidate with --prerelease allow, reinstall
rather than upgrade: uv tool install --reinstall homelab-helper. An upgrade
keeps allowing pre-releases for every dependency too, and that is how one lab
ended up with an httpx 1.0 development build that has no AsyncClient.
To work from a checkout instead, uv sync --all-extras --group dev and prefix
every command below with uv run.
2. Initialize¶
State lives in a per-user directory, never in the working directory: the
database under ~/.local/share/homelab-helper/ and your credentials under
~/.config/homelab-helper/.env (XDG variables are honoured; set
HOMELAB_HELPER_HOME to put both somewhere else).
helper db init
running alembic upgrade head...
...
syncing probe entry points...
inserted: 12, updated: 0
seeding trust domains...
created: 8 (idempotent)
done
3. Replay the bundled lab¶
The harness ships a three-host synthetic lab — hosts and the raw observations a
probe would have returned from them — and a small library of assertions about
what those hosts should look like. discover replay loads it, reconciles
each host exactly as live discovery would, and runs the assertions.
helper discover replay
replayed: 3 host(s), 22 observation(s), 8 assertion(s) loaded / 8 run
Run helper audit to see the resulting findings.
4. Look at what it found¶
helper findings list
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ fingerprint ┃ sev ┃ kind ┃ status ┃ title ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ a44de43ef8c38f89 │ medium │ config-drift │ open │ Assertion failed: lab-c.memory_at_least_4gb │
│ 6d1307ebb3baf6da │ low │ config-drift │ open │ Assertion failed: lab-b.cpu_supports_avx2 │
│ 008d1e49f4aee444 │ medium │ config-drift │ open │ Assertion failed: lab-a.cpu_cores_at_least_2 │
│ 574bb9fbe593dc80 │ low │ inventory-gap │ open │ Storage device /dev/sdb has no stable identity │
│ 19314d5fb2cb3653 │ medium │ storage-provenance-delta │ open │ Storage device /dev/sda shares forged WWN 5000000000000099 │
│ 97d8fbc42e4c1126 │ low │ inventory-gap │ open │ DIMM in DIMM_A1 has no serial │
│ e02424bd3eb39acc │ low │ inventory-gap │ open │ Network interface eth0 has no MAC │
│ ... │ │ │ │ │
└──────────────────┴────────┴──────────────────────────┴────────┴─────────────────────────────────────────────────────────┘
13 finding(s)
Thirteen findings, three kinds, and each one is a real thing the harness does:
config-drift— an assertion about a host failed.lab-awas declared to need at least 2 cores and 4 GiB; it has 1 and 2. The one amd64-only assertion on the arm64 host was skipped, not failed — assertions carry an architecture scope.inventory-gap— a part the probe saw but cannot track: a disk with no WWN or serial, a NIC with no MAC, a DIMM with no serial. The harness tracks physical parts by identity so it can tell you a drive moved hosts; a part with no identity is a gap it tells you about rather than guesses at.storage-provenance-delta— three distinct USB drives on three hosts all report the same WWN, because cheap enclosures forge one. The harness refuses to merge them and says why.
helper audit rolls the same findings up by severity and status. Run
helper discover replay again: nothing is duplicated, and a finding whose
condition has gone away resolves itself. That lifecycle — stable fingerprint,
reopen on recurrence, resolve only when the category was actually observed —
is what makes the finding table something you can act on rather than re-read.
The lab is a plain YAML file. helper discover replay asymmetric loads a
second bundled one, a three-node cluster with one slow uplink, for
helper bottlenecks to find; pass a path to replay a fixture of your own.
Part 2 — your own lab¶
1. Write the config file¶
helper config init # writes a commented .env template to ~/.config/homelab-helper/
helper config # what the harness will actually talk to, and from where
Uncomment what you use. Every variable is prefixed HOMELAB_HELPER_, and each
source only needs its variables when you run that source's discover verb. A
secret-valued variable may hold a reference instead of a literal —
file:~/secrets.yaml#proxmox, file:~/secrets.sops.yaml#netbox (decrypted
with sops), an age-encrypted file, or keyring:homelab-helper/hass — and
helper config reports set via file / keyring / env without ever
printing the value.
| Source | Variables (after HOMELAB_HELPER_) |
|---|---|
| Proxmox | PROXMOX_URL, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET, PROXMOX_VERIFY_SSL |
| Kubernetes | KUBECONFIG, KUBE_CONTEXT (falls back to the ambient kubeconfig) |
| UniFi | UNIFI_URL, UNIFI_API_KEY, UNIFI_SITE, UNIFI_VERIFY_SSL; UNIFI_CONTROLLERS=main,remote for more than one gateway |
| MikroTik | MIKROTIK_URL, MIKROTIK_USERNAME, MIKROTIK_PASSWORD (a read-only user with the rest-api policy), MIKROTIK_NAME |
| Cloudflare | CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE or CLOUDFLARE_ZONE_ID |
| Argo CD | ARGOCD_URL, ARGOCD_API_TOKEN, ARGOCD_VERIFY_SSL |
| OpenMediaVault | OMV_URL, OMV_USERNAME, OMV_PASSWORD, OMV_VERIFY_SSL |
| Home Assistant | HASS_URL, HASS_TOKEN (a long-lived token; a non-admin user is enough) |
| print-warden | PRINTWARDEN_URL, PRINTWARDEN_VERIFY_SSL (its open /healthz; no credentials) |
| JetKVM | JETKVM_URL, JETKVM_PASSWORD (its local password; omit in no-password mode), JETKVM_VERIFY_SSL, JETKVM_ATTACHED_TO (the harness hostname it is plugged into), JETKVM_ATTACHED_PORT (default 22) — see Console evidence |
| NetBox | NETBOX_URL, NETBOX_TOKEN, NETBOX_VERIFY_SSL — the inventory you own; see NetBox custom fields |
| Chat | LLM_PRIVACY (strict-local / prefer-local / open), OLLAMA_URL, OLLAMA_MODEL; BYOK: ANTHROPIC_API_KEY, OPENAI_API_KEY |
Console evidence from a JetKVM¶
A JetKVM on a machine's HDMI and USB lets the harness see
that machine's screen. helper discover jetkvm --persist reads the device,
records it as a jetkvm service with the host you declared in JETKVM_ATTACHED_TO,
and tries a TCP connect to that host. When the host does not answer, a
host-unreachable finding opens and carries a console snapshot (a PNG under the
data directory, the newest five kept per host), so a kernel panic, a stuck boot
menu or a BIOS prompt is on the finding rather than behind a trip to the rack.
The finding resolves once the host answers again; a run that could not check the
host leaves it alone. helper discover jetkvm --snapshot console.png and the MCP
console_snapshot tool take a frame on demand.
Snapshots join the device's WebRTC stream as a viewer, which needs the optional
extra: uv tool install 'homelab-helper[jetkvm]'. Without it everything else
works and the finding says no snapshot was taken.
The harness only watches the console. There is no keyboard, mouse, power or virtual-media control, and there will not be one: typing into a console can run anything on the machine, which is unbounded remote code execution, and the trust gradient only authorizes node actions whose effect is fixed in code.
2. Probe a host over SSH¶
Hosts you deep-probe need SSH with key auth. The SMART and DIMM probes run
smartctl and dmidecode under sudo -n, so give the probe user passwordless
sudo for those two commands — or accept that disks and DIMMs report without
identity, which the finding table will tell you about.
helper discover host <hostname> --ssh-user <user> --ssh-key ~/.ssh/id_ed25519
helper host show <hostname>
Twelve probes run: identity, CPU, memory and DIMMs, network interfaces,
storage, SMART, PCI, GPUs, services, and a Talos variant for nodes that have
no SSH. helper probes list shows them; a probe you write yourself
(how) joins the list through a Python entry point.
3. Read the management planes¶
helper discover proxmox --persist # clusters, nodes, guests
helper discover k8s --persist # nodes, workloads
helper discover unifi --persist # static DNS, clients
helper discover omv --persist # filesystems, disks, shares; stray exports
helper discover printwarden --persist # 3D printers: offline, Klipper down, failing, maintenance due
helper discover jetkvm --persist # the KVM, and its attached host: unreachable → finding + console
helper audit
Each verb is a read; --persist writes what it read into the harness
database, and --dry-run previews that. A persisted Proxmox run also drops
guests that no longer exist in Proxmox and names them in its output. It keeps
them, and says why, when the cluster reports itself non-quorate or the listing
comes back empty. From here the planners have something to reason about:
helper plan placement immich # where would this workload go, and why not elsewhere
helper plan rebalance # three candidate plans with their trade-offs
helper host intent bmax0 --no-new-guests -r "runs hot" # a host the plans must not fill
helper bottlenecks # known patterns, with mitigations derived from your facts
helper plan surplus # capacity nothing is using, and what it could do
helper chat "what's wrong with my lab?"
4. Keep it running¶
helper daemon run --once # discovery → playbooks → listener, one pass; cron-friendly
helper daemon run # the same on cadences, until Ctrl-C
With no --sources, the daemon discovers from every source helper config
reports configured. Nothing it does executes anything: playbooks turn findings
into proposals, and the listener asks you about them only for the kinds of
change you have granted — which is the subject of
the trust gradient.
5. Before trusting it with a real change¶
Run the live validation runbook. The test suite is all mocks; the runbook is where the acceptance criteria meet your hardware, one read-only criterion at a time, and then — against a guest created to be destroyed — the first real execution.