Thanks for considering a contribution! This project is small and focused — keeping it that way is a feature, so please read this before opening anything substantial.
- Bug fixes — particularly around new hardware (chassis profiles, GPU models, drive types) that the agent silently mis-handles.
- New chassis profiles (
profiles/<model>.env). These need IPMI sensor names + the chassis' fan-floor behavior. See "Adding a chassis profile" below. - New sensor backends in
internal/sensors/. If you have a non-NVIDIA GPU or a non-IPMI fan-control path (e.g. Supermicro'sraw 0x30 0x70 0x66 …), a clean implementation behind the existing interfaces is welcome. - Improvements to the control math in
internal/control/— must come with unit tests and a clear before/after. - Docs improvements.
- External Go dependencies. The binary is intentionally
zero-dependency (no
go.sumshipped, vendor-free build). Anything that adds animport "github.com/..."needs a strong justification. - Lookup tables / piecewise fan curves. The controller is dynamic
and derived-from-constants by design — temp ranges → continuous math
→ fan speed. No hardcoded
"if temp > 80 then fan = 60"rules. - Replacing the IPMI/smartctl/nvidia-smi subprocess approach with a library binding. The subprocess pattern is what makes the image trivially portable. Native bindings would pull in C deps, kill CGO-disabled builds, and explode the image size.
- Features that only make sense for a specific cloud or paid service. This project is hardware-and-Prometheus-shaped; please keep it that way.
You need Go 1.23+ and Docker. No other tooling.
git clone https://github.com/MattJackson/host-agent
cd host-agent
go test ./... # unit + e2e (no Docker needed)
docker build -t host-agent:dev . # full image buildMost logic lives in internal/control/ (pure functions) and is exhaustively
unit-testable without any I/O. Subprocess sensors are mocked via the
runner interface — see internal/runner/fake.go and the testdata
under internal/sensors/testdata/.
gofmt -s(run by CI; PRs failing it are blocked).- No external Go deps. Standard library only.
- Comments should explain why (a non-obvious constraint, a workaround for vendor behavior, an invariant). Don't restate what the code does.
- Keep functions small and pure where possible. The "main loop" lives in
internal/controller/; everything it calls is testable in isolation.
-
Determine the
dmidecodeproduct slug for your chassis:sudo dmidecode -s system-product-name # → "PowerEdge R730xd" → slug "r730xd"The slug is what
detect_modelproduces:lowercase(strip("PowerEdge "), then replace non-[a-z0-9] with _, then strip trailing _). -
Add
profiles/<slug>.env. Minimum content isMIN_FAN:: "${MIN_FAN:=15}" # the lowest PWM the BMC actually respects. # Below this it either clamps to a safety RPM # (Dell R7xx series) or obeys literally and # stalls fans (Dell R4xx series). Test before # picking a value.
-
(Optional) Add IPMI sensor mappings so the dashboard's per-CPU/inlet/exhaust panels resolve. See
profiles/r730xd.env:: "${SENSOR_CPU1_NAME:=Temp}" : "${SENSOR_CPU1_ID:=26}" : "${SENSOR_CPU2_NAME:=Temp}" : "${SENSOR_CPU2_ID:=27}" : "${SENSOR_INLET_NAME:=Inlet Temp}" : "${SENSOR_EXHAUST_NAME:=Exhaust Temp}"
The IDs come from
ipmi-sensors --output-sensor-stateon the actual hardware. Run it once and grep for the temp sensors. -
Test on real hardware: deploy a
host-agent:devbuild, watchdocker logs host-agentfor the[fan-controller] chassis=<slug>line and confirm the right profile loaded. Then watch one full 24-hour EWMA settling cycle before opening the PR. -
PR description should include:
- chassis model + iDRAC/BMC firmware version
dmidecode -s system-product-nameoutput- the fan-floor behavior you observed (which PWM values do what)
- a screenshot or paste of
/var/lib/host-agent/state/metrics.promafter the controller has been running for at least a few hours.
- One change per PR. Don't bundle a profile addition with a controller refactor.
- Reference any related issue in the description.
- Make sure
go test ./...passes. - CI runs
gofmt -s -landgo vet ./...; PRs failing either are blocked. - Commit messages: imperative mood, ~70 char subject, blank line, body if needed. No "Co-Authored-By: AI" trailers — keep authorship clean.
See SECURITY.md for vulnerability reports (please don't file public issues for those).
For functional bugs, open a GitHub issue with:
- which install method (compose,
docker run, Unraid, install.sh) - chassis (
dmidecode -s system-product-name) + which profile loaded (visible in container logs as[fan-controller] chassis=…) docker logs host-agent --tail 200output around the bad behavior- contents of
/var/lib/host-agent/state/metrics.prom - expected vs actual behavior