docs: add README operator guide
First operator-facing README. Covers quick start (build, configure, first-user login), the multi-drive batch workflow with concrete time estimates, the four drive-lock states with their confirm tokens, notable settings, daily report / notifications, ops cookbook (logs, user CLI, backups, /health probe, DB reset), and an honest "known gaps" list. Cross-references CLAUDE.md (architecture + rationale) and SPEC.md (per-version feature reference) for deeper docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d4c0770b9e
commit
c589e3c8e5
1 changed files with 242 additions and 0 deletions
242
README.md
Normal file
242
README.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# TrueNAS Burn-In Dashboard
|
||||
|
||||
Web dashboard for running disciplined burn-in tests on TrueNAS drives.
|
||||
Sits next to the NAS, not on it — orchestrates `smartctl`, `badblocks`, and
|
||||
`nvme-cli` over SSH and tracks every job in SQLite.
|
||||
|
||||
Inspired by the community `disk-burnin.sh` script (Spearfoot et al.) but
|
||||
adds: concurrent burn-ins, pool-membership safety locks, login + audit,
|
||||
live progress UI, daily email reports, and resumable state.
|
||||
|
||||
## Stack
|
||||
|
||||
FastAPI + HTMX (SSE) + asyncssh + SQLite, in one Docker container. No
|
||||
external services beyond your TrueNAS host. Templates and static assets
|
||||
are bind-mounted; Python source is baked into the image.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Configure
|
||||
cp .env.example .env
|
||||
# edit SSH_HOST / SSH_USER / SSH_KEY (see .env.example) and, optionally,
|
||||
# INITIAL_ADMIN_USERNAME / INITIAL_ADMIN_PASSWORD for first-run setup.
|
||||
|
||||
# 2. Build + run
|
||||
docker compose up -d --build
|
||||
|
||||
# 3. Open the dashboard
|
||||
open http://localhost:8084 # or your host's IP
|
||||
|
||||
# 4. First time: the login page renders a "Create initial admin" form.
|
||||
# Pick a username + password (>= 8 chars). Done.
|
||||
```
|
||||
|
||||
If you set `INITIAL_ADMIN_*` env vars *and* the users table is empty, that
|
||||
account is created on startup automatically. After that the env vars are
|
||||
ignored — change passwords from the UI ("Change password" header link) or
|
||||
the CLI (`docker exec -it truenas-burnin python -m app.auth_cli reset
|
||||
<username>`).
|
||||
|
||||
---
|
||||
|
||||
## Burning in many drives at once
|
||||
|
||||
The dashboard runs **up to `max_parallel_burnins`** burn-ins concurrently
|
||||
(configurable in Settings, default 4) and queues the rest. Submitting 14
|
||||
drives doesn't take 14 separate clicks — you submit once and the queue
|
||||
drains automatically as slots free up.
|
||||
|
||||
### The workflow
|
||||
|
||||
1. **Select all idle drives** — click the checkbox in the table header
|
||||
(next to "DRIVE"). It auto-checks every drive that's currently
|
||||
selectable: idle, no active SMART test, not pool-locked. Pool-locked
|
||||
drives are intentionally excluded; if you really want to burn one of
|
||||
them in, unlock it individually first (see [Drive locks](#drive-locks)
|
||||
below).
|
||||
2. **Click the Burn-In button** in the batch action bar that slides up
|
||||
from the bottom — it shows the count of selected drives.
|
||||
3. **In the batch modal**: pick the stages to run (Short SMART, Long
|
||||
SMART, Surface Validate — drag to reorder), confirm your operator
|
||||
name, and click Start.
|
||||
4. **All selected drives are queued** in one POST. Up to
|
||||
`max_parallel_burnins` enter `running`; the rest sit `queued`. As each
|
||||
running job finishes, the next queued job picks up the freed slot
|
||||
automatically — no operator action between batches.
|
||||
5. The toast shows e.g. "12 burn-in(s) queued, 0 skipped, 0 pool-locked."
|
||||
|
||||
### Time estimate
|
||||
|
||||
| Drive size | Profile | Per-drive runtime (default block size) |
|
||||
|-----------|-------------|----------------------------------------|
|
||||
| 250 GB SSD | Short + Long SMART + Surface | ~1 hour |
|
||||
| 14 TB HDD | Short + Long SMART + Surface | ~24 hours |
|
||||
| 14 TB HDD | Short + Long SMART (no surface) | ~6–8 hours |
|
||||
|
||||
For 12× 14 TB drives at default 4-parallel: roughly **3–4 days** end-to-end.
|
||||
Bumping `surface_validate_block_size` from 4096 to 8192 in Settings cuts
|
||||
runtime roughly in half at ~2× RAM cost — matches the upstream
|
||||
`disk-burnin.sh` recommendation.
|
||||
|
||||
### Watch out
|
||||
|
||||
- **Stuck-job timeout** — `stuck_job_hours` (default 24) marks any job
|
||||
past that threshold as `unknown` and kills the remote process. If
|
||||
you're burning in 14 TB drives with default block size, raise this to
|
||||
**48** in Settings before starting, or you'll get false positives near
|
||||
the end of surface_validate.
|
||||
- **Thermal gate** — if drives currently under burn-in hit the
|
||||
temperature warning threshold, new jobs wait up to 3 minutes before
|
||||
acquiring a slot. Increase `temp_warn_c` if your chassis runs hot but
|
||||
is otherwise fine.
|
||||
|
||||
### Cancelling
|
||||
|
||||
Click the red ✕ next to a running job. The orchestrator:
|
||||
1. Marks the job `cancelled` in the DB.
|
||||
2. Issues `kill -9 <remote_pid>` over a fresh SSH session (the badblocks
|
||||
PID is captured at launch via `sh -c 'echo PID:$$; exec ...'`).
|
||||
3. Cancels the asyncio task, releasing the semaphore slot for the next
|
||||
queued job.
|
||||
|
||||
Cancellations are durable — restart the container and queued jobs resume,
|
||||
cancelled jobs stay cancelled.
|
||||
|
||||
---
|
||||
|
||||
## Drive locks
|
||||
|
||||
To prevent destroying live data, the dashboard refuses to start
|
||||
destructive burn-in on drives ZFS or the kernel reports as in-use.
|
||||
Detected lock states (with the typed-confirmation token required to
|
||||
override):
|
||||
|
||||
| State | Detected via | Confirm token |
|
||||
|---------------|---------------------------|------------------------------|
|
||||
| Active pool | `zpool list -vHP` | the pool name (e.g. `tank`) |
|
||||
| Boot pool | pool name = `boot-pool` | `DESTROY BOOT POOL` |
|
||||
| Exported ZFS | `lsblk` `zfs_member` partitions not in any active pool | `DESTROY EXPORTED POOL` |
|
||||
| Mounted FS | `findmnt -no SOURCE` | `DESTROY MOUNTED FILESYSTEM` |
|
||||
|
||||
Detection runs every poll cycle (~12 s). On any SSH or parser failure the
|
||||
poller fails *closed*: previously-locked drives stay locked, previously-
|
||||
unlocked drives stay unlocked, until detection recovers.
|
||||
|
||||
Unlock is in-memory only with a 10-minute TTL — bound to the
|
||||
`(pool_name, pool_role)` observed at unlock time. If a subsequent poll
|
||||
reclassifies the drive (e.g. `(exported)` → `tank` because someone
|
||||
imported the pool), the grant is invalidated automatically.
|
||||
|
||||
Every unlock writes an audit event and surfaces in the next daily report
|
||||
in a red banner.
|
||||
|
||||
---
|
||||
|
||||
## Settings highlights
|
||||
|
||||
All settings live under `/settings` (header link). Key knobs:
|
||||
|
||||
- **`max_parallel_burnins`** (default 4) — semaphore cap. Restart container
|
||||
for changes to take effect.
|
||||
- **`surface_validate_block_size` / `_block_buffer` / `_passes`** —
|
||||
badblocks `-b` / `-c` / `-p`. Defaults preserve original behaviour;
|
||||
tune for speed vs paranoia.
|
||||
- **`stuck_job_hours`** (default 24) — raise for big drives.
|
||||
- **`temp_warn_c` / `temp_crit_c`** — thermal gating thresholds.
|
||||
- **`bad_block_threshold`** (default 0) — number of bad blocks
|
||||
surface_validate tolerates before failing the stage.
|
||||
- **`retention_log_days`** (default 35) — when to NULL out
|
||||
`burnin_stages.log_text`. Nightly job at 03:00 local.
|
||||
- **`retention_backup_keep`** (default 14) — how many nightly DB
|
||||
snapshots to keep in `/data/backups/`.
|
||||
|
||||
---
|
||||
|
||||
## Notifications
|
||||
|
||||
- **Daily SMTP report** at `smtp_report_hour` (default 08:00 local) with
|
||||
drive-level summary, failed-health banner, and a red banner listing
|
||||
every pool-drive unlock from the last 24 h.
|
||||
- **Per-job email alerts** on pass/fail (configurable).
|
||||
- **Webhook URL** posts JSON on every job state change.
|
||||
|
||||
Configure SMTP in Settings → Email. Includes a "Test SMTP" button.
|
||||
|
||||
---
|
||||
|
||||
## Operations
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
docker logs -f truenas-burnin
|
||||
# JSON-structured. Filter with jq:
|
||||
docker logs truenas-burnin 2>&1 | jq -rR 'fromjson? | "\(.ts) \(.level) \(.msg)"'
|
||||
```
|
||||
|
||||
### User management
|
||||
|
||||
```bash
|
||||
docker exec -it truenas-burnin python -m app.auth_cli list
|
||||
docker exec -it truenas-burnin python -m app.auth_cli add <username>
|
||||
docker exec -it truenas-burnin python -m app.auth_cli reset <username>
|
||||
```
|
||||
|
||||
Passwords are read from a TTY prompt; never accept them on the command
|
||||
line.
|
||||
|
||||
### Backups
|
||||
|
||||
Automated nightly to `/data/backups/app-YYYY-MM-DD.db` (online
|
||||
`sqlite3.backup`, doesn't lock writers). To restore:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
cp data/backups/app-2026-05-01.db data/app.db
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Health probe
|
||||
|
||||
`/health` is unauthenticated and returns 200 only when DB, poller, and
|
||||
SSH (when configured) all check green; 503 otherwise. Use it for
|
||||
container/orchestrator health checks.
|
||||
|
||||
```bash
|
||||
curl -sf http://localhost:8084/health | jq
|
||||
```
|
||||
|
||||
### Resetting the DB
|
||||
|
||||
If you need to start over:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
sudo rm -f data/app.db data/session_secret
|
||||
# keep data/settings_overrides.json if you want to preserve UI settings
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `CLAUDE.md` — full architecture, file map, deploy workflow, and the
|
||||
rationale behind every non-obvious design decision.
|
||||
- `SPEC.md` — canonical feature reference per version.
|
||||
- `tests/` — `python -m unittest discover tests/` (44 tests, stdlib-only).
|
||||
|
||||
---
|
||||
|
||||
## Known gaps / not-yet-built
|
||||
|
||||
- No multi-user RBAC — every user is effectively admin.
|
||||
- No per-drive SMART attribute trend graphs (snapshots only).
|
||||
- No scheduled burn-ins — jobs run immediately when queued.
|
||||
- No CSRF tokens on state-changing endpoints (relies on
|
||||
`SameSite=Strict` session cookie).
|
||||
|
||||
PRs welcome.
|
||||
Loading…
Add table
Reference in a new issue