Files
dashboards/deploy/nut-sudoers
T
ducoterra ad993c7b03 phase: 01_severity_language
**Gate defect found & fixed (the "does not lint" bug).** `.agents/validate.sh` — the phase gate — ran `python3 -m pytest` on `/usr/bin/python3` + `~/.local` site-packages (not the uv venv), ran `ruff check .` *only if* a ruff happened to be on PATH, and **never ran pyright or the coverage floor**. Rewrote it to the project's real gate: cd to repo root → `uv run pytest --cov --cov-fail-under=94` → `uv run ruff check src tests agent` → `uv run pyright` → `node --check web/static/*.js` → hygiene gates; a missing toolchain now fails loudly. Provoked it to confirm it bites (floor + a seeded F401 both fail).

**Verification (all green)**
- `bash .agents/validate.sh` → **exit 0**: 356 passed, 6 skipped · coverage **94.75%** (floor 94) · ruff `All checks passed!` · pyright `0 errors` · node syntax clean
- `uv run pytest --cov` → `severity.py` **100%**; `uv run pytest -m live --live` → **6 passed**; `uv pip list --outdated` → empty
- Criteria: `classify('cpu.usage_pct',…)` → `fail`/`ok`/`None` ✓ · `/api/ups/snapshot` carries `severity` beside `charge_pct`/`load_pct`/`battery_runtime` (apc now live-`warn` at 498 s — threshold working) ✓ · no `.sev-*`/synonym vocabulary (only a comment saying so) ✓ · pytest/coverage/lint ✓ · dev server restarted, `/api/health` ok 6/6 sources, all pages+assets 200 ✓
- Chromium against live fleet: `severityFrom` cell→`fail`, bare→`warn`, null→`''`, unrated→`''`; `hero` fail → `rgb(207,34,46)`, null → `n/a` with **no** verdict class, warn+bar 42%; all four pages render unchanged → `data/logs/phase01-final-{index,system,llm,ups}.png`

**Docs (full review, incl. pre-existing drift).** README: `/ups` row claimed "upsd on 3493" (fleet is 3051, stated elsewhere in the same file); config table omitted `DASH_HOST/PORT`, llm/ups `*_INTERVAL_SECONDS`, `*_TIMEOUT_SECONDS`, `GUFO/VLLM_API_KEY`, `POLL_MS_*`, `DEBUGPY` (+ dedup); severity bullet omitted `disks`/`gpus[]`/rated LLM cells and the ratio rescale; agent section listed only `/snapshot` though `/info`+`/health` exist. AGENTS.md: same 3493 claim; `install-agent.sh --token-file` (its flags are `--bind/--port/--token/--rotate-token/--status/--uninstall` — it hands *the agent* `--token-file`); gap 3 "Two commits" (now 6, still no `origin`); gap 1 given the sudoers install command + verified live gating reason; coverage line now 80% repo floor vs 94% gate floor; cyberpower `identity.model: GX1500U` vs `ups.description` quirk. `.env.dev.example`: `DEV_FORWARDS` remote port 3493→3051 and engine URLs that pointed at `127.0.0.1` with nothing forwarding them.

**Deviations.** No `ruff format` (30 files would reformat; the project gate is `ruff check` + pyright — reformatting completed phases is out of scope). **No live-infra changes**: enabling the battery-test buttons needs `deploy/nut-sudoers` installed on driveripper + a nut-server restart and a scoped `[dashboards]` `instcmds` user — prod write with a documented shutdown risk (AGENTS gap 1), so repo-side only. 36 pre-existing `ResourceWarning: unclosed database` from test fixtures are noise, not failures; left rather than churn ~15 fixtures. `severity.attach()` is unit-tested but has no production call site until phases 02–05.

**Next pending phase:** `02_overview_fleet_and_alerts` (`01_alerts_builder.md`).
2026-10-16 22:22:26 -04:00

61 lines
3.3 KiB
Plaintext

# driveripper-ops sudo rules for the homelab dashboards UPS work.
#
# Install (from the repo checkout, as a user with sudo):
#
# sudo visudo -cf deploy/nut-sudoers # syntax check FIRST
# sudo install -m 0440 -o root -g root deploy/nut-sudoers /etc/sudoers.d/dashboards-nut
# sudo visudo -cf /etc/sudoers.d/dashboards-nut # and check the installed copy
#
# Scope: absolute paths only (`secure_path` is in effect), wildcards only where the command
# is read-only or idempotent, and nothing here grants a shell, `sh -c`, an editor, or a
# client that can send instant commands.
#
# WHY THESE ARE NEEDED
# Reads already work with zero privileges — upsd on driveripper serves `LIST UPS` /
# `LIST VAR` unauthenticated (on port 3051, not 3493). What is missing is a upsd.users
# entry with `instcmds`, which is the only thing standing between the dashboard and its
# battery-test buttons. `tee /etc/ups/*` is already granted, but writing upsd.users blind
# would drop the accounts upsmon authenticates as — upsmon must run as a user with
# `shutdown`, so an overwritten file means no orderly power-down. So: read first.
# ---- already present on driveripper (used during setup) ------------------------------ #
# (root) NOPASSWD: /usr/bin/tee /etc/ups/* # write a config file
# (root) NOPASSWD: /usr/bin/cp /etc/ups/* /home/ops/* # copy one out (root-owned)
# (root) NOPASSWD: /usr/bin/systemctl start|restart|reload|enable *
# (root) NOPASSWD: /usr/bin/udevadm * # USB device discovery
# (root) NOPASSWD: /usr/bin/firewall-cmd * # only if upsd must serve the LAN
# (root) NOPASSWD: /usr/bin/dnf install * # nut, nut-client
# ---- read: what is actually configured, before anything is rewritten ----------------- #
Cmnd_Alias NUT_READ = \
/usr/bin/cat /etc/ups/*, \
/usr/bin/journalctl -u nut*, \
/usr/bin/systemctl status nut*, \
/usr/bin/upsdrvctl status, \
/usr/bin/upsc *, \
/usr/sbin/ss -ltnp, \
/usr/bin/lsusb, \
/usr/sbin/getenforce, \
/usr/bin/ausearch -m avc *, /usr/sbin/sealert -l *
# ---- write: add/replace a scoped user, then reload ----------------------------------- #
Cmnd_Alias NUT_WRITE = \
/usr/bin/tee /etc/ups/upsd.users, \
/usr/bin/tee /etc/ups/upsd.conf, \
/usr/bin/systemctl restart nut-server, \
/usr/bin/systemctl reload nut-server
# ---- grants --------------------------------------------------------------------- #
# A Cmnd_Alias grants nothing on its own — this line is the point of the file.
# Scope: read the NUT config/state, add/replace a scoped upsd.users entry, and
# restart/reload the server so upsd re-reads it. Nothing else.
ops ALL=(root) NOPASSWD: NUT_READ, NUT_WRITE
# ---- deliberately NOT granted -------------------------------------------------------- #
# /usr/bin/upscmd, /usr/bin/upsrw — the dashboard's gated POST /api/ups/command is the
# ONLY command path in this project, by design
# systemctl stop * — stopping nut-server blinds the UPS view
# tee /etc/ups/ups.conf, upsmon.conf — driver instances and shutdown policy are the
# operator's, not the dashboard's
# sh, bash, vim, tee to anything else — no escape hatches