Adding the new rewrite
This commit is contained in:
1 parent
4aca9f08d1
commit
9b48b01d1d
62 files changed
+4510
-1406
No files matched your search
@@ -1,148 +1,124 @@
|
||||
# APC UPS Dashboard
|
||||
|
||||
A lightweight FastAPI + Redis based dashboard for multiple APC UPS devices using the `apcaccess` CLI (apcupsd Network Information Server mode). Shows real-time metrics, lightweight charts, and maintains 7 days of historical snapshots.
|
||||
A production-ready FastAPI + Redis dashboard for monitoring multiple APC UPS devices via `apcupsd` NIS. Single-admin auth (argon2 + session cookies), CSRF, rate limiting, security headers, SSE live updates, 7-day history, connection-health tracking, battery degradation trends, HTML email alerts with batching and silent-hours, and Prometheus metrics.
|
||||
|
||||
## Features
|
||||
- Multiple UPS managed dynamically (stored in Redis; add/update/delete via API/UI)
|
||||
- Polling via `apcaccess` CLI against apcupsd NIS (default port 3551)
|
||||
- Connection test: TCP port reachability (no protocol parsing)
|
||||
- Real-time dashboard (Server Sent Events) updating key metrics
|
||||
- 7-day retention of snapshots in Redis lists
|
||||
- Simple Chart.js load percentage sparkline
|
||||
- Docker & docker-compose deployment (image bundles apcupsd + apcaccess)
|
||||
- SMTP alerting (high load, low battery %, on battery, low runtime) with cooldown
|
||||
|
||||
## Configuration
|
||||
**Monitoring**
|
||||
- Multiple UPS managed dynamically via UI/REST (stored in Redis)
|
||||
- Real-time dashboard with SSE, fleet-overview panel, per-UPS state indicators
|
||||
- 7-day snapshot history, per-minute watts averages, daily energy + cost
|
||||
- Connection-health tracker — COMMLOST detection after 3× interval, recovery INFO alerts
|
||||
- Battery-health sampling (1/min) with 7-day trend + decline %
|
||||
- CSV export (history/events/energy), event log, alert management + ack
|
||||
|
||||
Configuration is persisted in Redis (key `ups:config:json`). Use the web UI or the REST API to manage UPS entries and SMTP settings. A legacy `config/ups.yaml` (or path set via `UPS_CONFIG_PATH`) is imported once on first startup if Redis has no configuration; after migration the file is no longer written or read.
|
||||
**Alerts (email-only)**
|
||||
- Severity taxonomy: CRITICAL (ONBATT/COMMLOST/RUNTIME_LOW/BCHARGE_LOW), WARNING (LOAD_HIGH/REPLACEBATT/SELFTEST_FAIL/TEMP_HIGH/XFER_BURST/VOLT_DEV), INFO (REACHABLE/LINE_RESTORED)
|
||||
- Coalesced HTML emails (one per UPS per poll cycle), severity-colored
|
||||
- 30-min per-message cooldown, silent-hours deferral (CRITICAL always delivered), retries via tenacity
|
||||
- Test-email button and optional daily summary
|
||||
|
||||
UPS fields:
|
||||
- name (unique)
|
||||
- host (apcupsd server hostname / IP)
|
||||
- port (default 3551)
|
||||
- interval_seconds (polling interval)
|
||||
- Optional alert thresholds: alert_loadpct_high, alert_bcharge_low, alert_on_battery, alert_runtime_low_minutes
|
||||
**Security & ops**
|
||||
- Single-admin auth (argon2 password, signed session cookie, double-submit CSRF)
|
||||
- SSRF host validation (rejects loopback/link-local; private IPs gated by `ALLOW_PRIVATE_IPS`)
|
||||
- SMTP password **only** from env — never persisted to Redis
|
||||
- Subprocess timeout on `apcaccess` (10s), rate-limiting on auth/config, security headers + CSP
|
||||
- Non-root container (UID 10001), pinned `python:3.12.7-slim` multi-stage build
|
||||
- `/healthz`, `/readyz`, `/metrics` (Prometheus), JSON structured logs with request-ID correlation
|
||||
- GitHub Actions pipeline runs ruff + pytest before building/publishing the image
|
||||
|
||||
SMTP fields (optional): host, port, username, password (or env `SMTP_PASSWORD`), use_tls, use_ssl, from_addr, to_addrs[], subject_prefix.
|
||||
## Quick start (docker-compose)
|
||||
|
||||
Environment secret: set `SMTP_PASSWORD` instead of storing cleartext.
|
||||
|
||||
Alert suppression: identical alert per UPS suppressed for 30 minutes (cooldown).
|
||||
|
||||
### apcupsd Server Requirements
|
||||
Your remote APC UPS hosts must be running `apcupsd` with the Network Information Server (NIS) enabled. Typical steps (on Linux):
|
||||
|
||||
1. Install apcupsd (example for Debian/Ubuntu):
|
||||
```bash
|
||||
sudo apt-get install apcupsd
|
||||
```
|
||||
2. Edit `/etc/apcupsd/apcupsd.conf` and confirm at least:
|
||||
```
|
||||
UPSTYPE usb # or 'net' / 'snmp' depending on your setup
|
||||
DEVICE # usually blank for USB
|
||||
NISIP 0.0.0.0 # listen on all interfaces (restrict in firewalled env)
|
||||
NISPORT 3551 # must match the configured port (default 3551)
|
||||
NETSERVER on # enable network server
|
||||
# Optional: restrict access (recommended)
|
||||
ACCESS 192.168.1.0/24 # Only allow your monitoring subnet (supported on some builds)
|
||||
```
|
||||
3. Restart service:
|
||||
```bash
|
||||
sudo systemctl restart apcupsd
|
||||
```
|
||||
4. Test locally:
|
||||
```bash
|
||||
apcaccess status
|
||||
nc -vz <server-ip> 3551
|
||||
```
|
||||
|
||||
If you receive connectivity errors in logs:
|
||||
- Verify `NETSERVER on` is set.
|
||||
- Check host firewall (e.g., `ufw allow 3551/tcp`).
|
||||
- Confirm the port is correct and reachable from the container network.
|
||||
- Test manually:
|
||||
```bash
|
||||
nc -vz <server-ip> 3551
|
||||
apcaccess -h <server-ip>:3551 status
|
||||
```
|
||||
|
||||
### Connection Testing Logic
|
||||
Simplified: only a raw TCP connect test. If the port is reachable it's reported as success. Polling uses the `apcaccess` CLI for data collection.
|
||||
|
||||
## Run (docker-compose)
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Required in production — edit .env:
|
||||
# SESSION_SECRET=<run: python -c "import secrets; print(secrets.token_urlsafe(48))">
|
||||
# SMTP_PASSWORD=<your SMTP password, if using alerts>
|
||||
docker compose up --build
|
||||
```
|
||||
Visit http://localhost:8000
|
||||
|
||||
### Persistence
|
||||
Open http://localhost:10280 — the app redirects to `/setup` where you create the admin account on first boot. Subsequent visits require login.
|
||||
|
||||
Configuration and historical metrics live in Redis. The provided `docker-compose.yml` now mounts a named volume (`redis-data`) at `/data` inside the Redis container and enables Append Only File (AOF) with `--appendonly yes`.
|
||||
## Environment variables
|
||||
|
||||
Data will persist across `docker compose down` / `up` cycles as long as you do NOT remove the volume. To explicitly remove all persisted configuration and history you must prune the volume:
|
||||
| Var | Required | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `SESSION_SECRET` | yes (prod) | ephemeral | 32+ random bytes, signs session cookies |
|
||||
| `ADMIN_USERNAME` | no | `admin` | |
|
||||
| `ADMIN_PASSWORD_HASH` | no | unset | Skip setup wizard by pre-seeding an argon2 hash |
|
||||
| `REDIS_URL` | no | `redis://redis:6379/0` | Supports `redis://:pw@host:port/db` |
|
||||
| `SMTP_PASSWORD` | no | unset | Read only from env, never stored |
|
||||
| `ALLOW_PRIVATE_IPS` | no | `true` | Set false to block RFC1918 UPS hosts |
|
||||
| `TRUST_PROXY` | no | `false` | Enable Secure cookies + HSTS when behind HTTPS proxy |
|
||||
| `LOG_LEVEL` | no | `INFO` | DEBUG/INFO/WARNING/ERROR |
|
||||
| `SESSION_MAX_AGE_SECONDS` | no | `1209600` | 14 days |
|
||||
| `RATE_LIMIT_ENABLED` | no | `true` | Disable in tests |
|
||||
| `TZ` | no | `UTC` | |
|
||||
|
||||
Generate values:
|
||||
```bash
|
||||
# SESSION_SECRET
|
||||
python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
# ADMIN_PASSWORD_HASH (optional pre-seed)
|
||||
python -c "from passlib.hash import argon2; print(argon2.hash('mysecret'))"
|
||||
```
|
||||
|
||||
## apcupsd server requirements
|
||||
|
||||
Your remote APC UPS hosts must run `apcupsd` with the Network Information Server (NIS) enabled:
|
||||
|
||||
1. Install: `sudo apt-get install apcupsd`
|
||||
2. Edit `/etc/apcupsd/apcupsd.conf`:
|
||||
```
|
||||
UPSTYPE usb
|
||||
NISIP 0.0.0.0
|
||||
NISPORT 3551
|
||||
NETSERVER on
|
||||
```
|
||||
3. Restart: `sudo systemctl restart apcupsd`
|
||||
4. Test: `apcaccess status` and `nc -vz <server-ip> 3551`
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker volume rm apcupsd-client_redis-data # volume name may be prefixed by folder/project
|
||||
python3.12 -m venv .venv && . .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests (uses fakeredis)
|
||||
pytest tests/
|
||||
|
||||
# With coverage
|
||||
coverage run -m pytest tests/ && coverage report
|
||||
|
||||
# Lint
|
||||
ruff check .
|
||||
|
||||
# Local dev server (needs apcaccess binary + REDIS_URL env)
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
If you had been losing configuration previously, ensure you pulled the updated compose file and that the `redis` service includes:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"]
|
||||
```
|
||||
|
||||
You can inspect Redis persistence files locally by running:
|
||||
## Persistence
|
||||
|
||||
Config and history live in Redis via an AOF-backed `redis-data` volume. To wipe state:
|
||||
```bash
|
||||
docker compose exec redis ls -lh /data
|
||||
docker compose down && docker volume rm apcupsd-client_redis-data
|
||||
```
|
||||
|
||||
If you want to enforce periodic RDB snapshots as well, you can leave default save settings (remove the `--save ""` override). The current configuration uses AOF every second for a balance of durability and write performance.
|
||||
## API (summary)
|
||||
|
||||
## Data Storage
|
||||
Redis stores:
|
||||
- Latest snapshot hash: `ups:snap:<name>`
|
||||
- History list (JSON {ts,data}): `ups:hist:<name>`
|
||||
- Per-minute watts averages: `ups:watts:permin:<name>`
|
||||
- Energy (watt-seconds) daily totals: `ups:energy:<name>:YYYYMMDD`
|
||||
- Events list: `ups:event:list:<name>`
|
||||
- Recent alerts: `ups:alerts:recent:<name>`
|
||||
- Voltage deviation samples: `ups:volt:dev:samples:<name>`
|
||||
|
||||
A pruning task runs hourly removing entries older than 7 days.
|
||||
|
||||
## Extending
|
||||
- Add more charts: query `/api/ups/<name>/history`
|
||||
- Add gauges: integrate a JS gauge lib in `dashboard.html`
|
||||
- Alerts: create background task checking thresholds
|
||||
| Path | Auth | Notes |
|
||||
|---|---|---|
|
||||
| `/` (dashboard), `/config`, `/events`, `/alerts`, `/settings` | session | Jinja pages |
|
||||
| `/login`, `/setup`, `/logout` | open/session | |
|
||||
| `/healthz`, `/readyz`, `/metrics` | open | Prom metrics on `/metrics` |
|
||||
| `GET /api/stream` | session | SSE snapshots |
|
||||
| `GET /api/ups`, `/api/ups/{name}/{status\|history\|events\|energy\|health\|battery_health}` | session | |
|
||||
| `GET /api/ups/fleet/overview` | session | Aggregate fleet summary |
|
||||
| `GET /api/ups/{name}/export?format=csv&kind=history\|events\|energy&since_days=N` | session | Streaming CSV |
|
||||
| `GET /api/events`, `/api/alerts`, `/api/alerts/active` | session | |
|
||||
| `POST /api/alerts/{id}/ack` | session+CSRF | |
|
||||
| `GET/POST/PUT/DELETE /api/config/{ups,smtp,ui}/...` | session (+CSRF for writes) | Rate-limited |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Security & Hardening Notes
|
||||
| Area | Current | Recommendation |
|
||||
|------|---------|---------------|
|
||||
| Authentication | None (open dashboard) | Add reverse proxy auth or FastAPI auth if exposed beyond LAN |
|
||||
| Config Storage | Redis (JSON) | Protect Redis with auth / network policy |
|
||||
| Network to apcupsd | Plain TCP | Use network segmentation / firewall; protocol has no encryption |
|
||||
| Redis | No auth configured | Enable AUTH / TLS if crossing trust boundaries |
|
||||
| Input Validation | Pydantic for config schema | Add stricter hostname/IP validation if multi-tenant |
|
||||
| Dependency Versions | Pinned | Review periodically for CVEs |
|
||||
| Logging | Polling errors logged | Avoid logging secrets; sanitize future additions |
|
||||
|
||||
### Additional Notes
|
||||
- Legacy YAML migration (one-time) to Redis; file no longer updated afterward.
|
||||
- Dynamic poller reconciles tasks on config change (no restart needed).
|
||||
- Alert cooldown prevents email flood.
|
||||
- AOF-based Redis persistence keeps configuration across container rebuilds.
|
||||
|
||||
### Suggested Future Enhancements
|
||||
- Optional Basic Auth / OIDC for web UI.
|
||||
- Rate limiting on config mutation endpoints.
|
||||
- CSRF protection if cookies/session auth added later.
|
||||
- Health endpoint (`/healthz`) returning Redis + config status.
|
||||
- Structured logging (JSON) for production observability.
|
||||
Reference in new issue
Block a user