chore: init projet — spec design + design system
Spec complète dans docs/superpowers/specs/2026-05-28-inventaire-hdd-design.md : architecture 2 conteneurs Docker (FastAPI + nginx), script Python stdlib only, SQLite avec serial comme clé de vérité, API ingest + dashboard + agents IA. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# Inventaire HDD — Design Spec
|
||||
|
||||
**Date** : 2026-05-28
|
||||
**Projet** : mes_hdd
|
||||
**Statut** : approuvé
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
Outil de monitoring des disques physiques (HDD/SSD/NVMe) pour un parc de 10–15 machines Proxmox, Debian et Ubuntu. Certaines machines sont des VMs avec disques en PCI passthrough (4–5 machines).
|
||||
|
||||
Le numéro de série est la **source de vérité unique** d'un disque : si un disque migre entre machines, si il tombe en panne ou est retiré, l'historique est conservé.
|
||||
|
||||
Le script est lancé **manuellement** par l'administrateur quand la configuration change (pas de cron, pas de daemon).
|
||||
|
||||
---
|
||||
|
||||
## Architecture globale
|
||||
|
||||
```
|
||||
[Machine cliente — Proxmox / Debian / Ubuntu]
|
||||
python3 inventaire.py (stdlib uniquement, pas de venv)
|
||||
└── HTTP POST http://10.0.0.50:8088/api/ingest
|
||||
│
|
||||
[nginx:alpine — port 8088]
|
||||
├── /api/* → proxy → [FastAPI :8000]
|
||||
└── / → static (HTML + design system)
|
||||
│
|
||||
SQLite (volume Docker nommé)
|
||||
```
|
||||
|
||||
**2 conteneurs Docker** via `docker-compose.yml` :
|
||||
- `api` : FastAPI (Python), port interne 8000
|
||||
- `web` : nginx:alpine, port exposé **8088**, sert les fichiers statiques et proxifie `/api/*`
|
||||
|
||||
SQLite dans un volume nommé (`mes_hdd_db`).
|
||||
|
||||
---
|
||||
|
||||
## Script client — `inventaire.py`
|
||||
|
||||
### Contraintes
|
||||
|
||||
- Python 3 uniquement, **stdlib exclusively** (`subprocess`, `json`, `urllib.request`)
|
||||
- Aucun `pip install`, aucun venv, aucune dépendance externe
|
||||
- Fonctionne sur Proxmox, Debian, Ubuntu
|
||||
- **Doit être exécuté en root** (ou via `sudo`) : `smartctl` et `pvs`/`lvs` nécessitent des droits élevés
|
||||
- Tolérant aux erreurs : un disque ou une commande qui échoue ne bloque pas l'inventaire des autres
|
||||
- Si `smartctl` est absent → `smart.status = "unavailable"`
|
||||
- Si `pvs`/`lvs` sont absents ou LVM non installé → partitions LVM ignorées, `lvm: null`
|
||||
|
||||
### Commandes utilisées
|
||||
|
||||
| Commande | Usage |
|
||||
|---|---|
|
||||
| `lsblk -J -o NAME,TYPE,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,ROTA` | Structure des disques et partitions |
|
||||
| `smartctl -H -A -i /dev/sdX` | Santé SMART + attributs |
|
||||
| `df -B1` | Espace utilisé/libre par point de montage |
|
||||
| `pvs --noheadings --reportformat json` | Volumes physiques LVM |
|
||||
| `vgs --noheadings --reportformat json` | Groupes de volumes LVM |
|
||||
| `lvs --noheadings --reportformat json -o lv_name,vg_name,lv_size,lv_path,lv_dm_path` | Volumes logiques LVM |
|
||||
| `udevadm info --query=property --name=/dev/sdX` | Bus (sata/usb/nvme) |
|
||||
| `find /dev/disk/by-id -type l` | Lien by-id (sans partitions) |
|
||||
| `hostname`, `ip route get 1.1.1.1` | Identité de la machine |
|
||||
|
||||
### Payload JSON envoyé
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "pve1",
|
||||
"ip": "10.0.0.10",
|
||||
"collected_at": "2026-05-28T15:30:00+02:00",
|
||||
"disks": [
|
||||
{
|
||||
"device": "sda",
|
||||
"path": "/dev/sda",
|
||||
"by_id": "ata-ST1000LM024_W1234567",
|
||||
"model": "ST1000LM024 HN-M101MBB",
|
||||
"serial": "W1234567",
|
||||
"type": "HDD",
|
||||
"capacity_bytes": 1000204886016,
|
||||
"capacity_human": "1.0 To",
|
||||
"bus": "sata",
|
||||
"smart": {
|
||||
"status": "ok",
|
||||
"label": "Bon état",
|
||||
"detail": "2 847h d'utilisation · 38°C · aucun secteur défectueux",
|
||||
"temperature_c": 38,
|
||||
"power_on_hours": 2847,
|
||||
"reallocated_sectors": 0,
|
||||
"pending_sectors": 0,
|
||||
"uncorrectable_sectors": 0
|
||||
},
|
||||
"partitions": [
|
||||
{
|
||||
"name": "sda1",
|
||||
"fstype": "ext4",
|
||||
"size_bytes": 536870912000,
|
||||
"size_human": "500 Go",
|
||||
"used_bytes": 128849018880,
|
||||
"used_human": "120 Go",
|
||||
"free_bytes": 408021991120,
|
||||
"free_human": "380 Go",
|
||||
"used_percent": 24,
|
||||
"mountpoint": "/",
|
||||
"lvm": null
|
||||
},
|
||||
{
|
||||
"name": "sda2",
|
||||
"fstype": "LVM2_member",
|
||||
"size_bytes": 536870912000,
|
||||
"size_human": "500 Go",
|
||||
"mountpoint": null,
|
||||
"lvm": {
|
||||
"vg_name": "vg_data",
|
||||
"logical_volumes": [
|
||||
{
|
||||
"lv_name": "lv_home",
|
||||
"size_human": "300 Go",
|
||||
"used_human": "50 Go",
|
||||
"free_human": "250 Go",
|
||||
"used_percent": 17,
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/home"
|
||||
},
|
||||
{
|
||||
"lv_name": "lv_swap",
|
||||
"size_human": "8 Go",
|
||||
"fstype": "swap",
|
||||
"mountpoint": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### SMART — labels en français
|
||||
|
||||
| Cas | status | label | Exemple de detail |
|
||||
|---|---|---|---|
|
||||
| Test global PASSED, pas d'attribut dégradé | `ok` | Bon état | "2 847h · 38°C · aucun secteur défectueux" |
|
||||
| PASSED mais attributs FAILING_NOW / In_the_past | `warn` | Attention | "3 secteurs réalloués · disque à surveiller" |
|
||||
| Test global FAILED | `fail` | Défaillance probable | "Prévoir le remplacement du disque" |
|
||||
| smartctl absent ou inaccessible | `unavailable` | SMART indisponible | "smartctl non disponible ou accès refusé" |
|
||||
|
||||
Les champs numériques (`temperature_c`, `power_on_hours`, `reallocated_sectors`, `pending_sectors`, `uncorrectable_sectors`) sont inclus quand disponibles, `null` sinon.
|
||||
|
||||
---
|
||||
|
||||
## Backend — FastAPI
|
||||
|
||||
### Routes
|
||||
|
||||
| Méthode | Route | Description |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/ingest` | Reçoit le payload du script client |
|
||||
| `GET` | `/api/disks` | Liste tous les disques (dernière observation par serial) |
|
||||
| `GET` | `/api/disks/{serial}` | Historique complet d'un disque |
|
||||
| `GET` | `/api/machines` | Liste des machines avec last_seen |
|
||||
| `GET` | `/api/ai/summary` | Synthèse structurée pour agents IA |
|
||||
| `GET` | `/api/ai/at-risk` | Disques avec status warn ou fail |
|
||||
|
||||
### Logique d'ingest
|
||||
|
||||
À chaque POST `/api/ingest` :
|
||||
1. Upsert dans `machines` (hostname, ip, last_seen)
|
||||
2. Pour chaque disque du payload :
|
||||
- Upsert dans `disks` (serial comme PK) — met à jour `last_host`, `last_seen`, `status`
|
||||
- Si `last_host` ≠ `first_host` → le mouvement est détecté (visible via l'API)
|
||||
- Insert dans `snapshots` (historique immuable)
|
||||
|
||||
---
|
||||
|
||||
## Base de données — SQLite
|
||||
|
||||
### Table `disks`
|
||||
|
||||
| Colonne | Type | Description |
|
||||
|---|---|---|
|
||||
| `serial` | TEXT PK | Numéro de série (source de vérité) |
|
||||
| `model` | TEXT | Modèle du disque |
|
||||
| `type` | TEXT | HDD / SSD / NVMe / inconnu |
|
||||
| `capacity_bytes` | INTEGER | Capacité en octets |
|
||||
| `capacity_human` | TEXT | Capacité lisible |
|
||||
| `first_seen_host` | TEXT | Première machine observée |
|
||||
| `first_seen_at` | TEXT | Date première observation |
|
||||
| `last_seen_host` | TEXT | Dernière machine observée |
|
||||
| `last_seen_at` | TEXT | Date dernière observation |
|
||||
| `smart_status` | TEXT | Dernier status SMART |
|
||||
|
||||
### Table `snapshots`
|
||||
|
||||
| Colonne | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK AUTOINCREMENT | |
|
||||
| `serial` | TEXT FK → disks | |
|
||||
| `hostname` | TEXT | Machine au moment de la collecte |
|
||||
| `device` | TEXT | ex: sda, nvme0n1 |
|
||||
| `smart_status` | TEXT | ok / warn / fail / unavailable |
|
||||
| `smart_label` | TEXT | Label français |
|
||||
| `smart_detail` | TEXT | Détail lisible |
|
||||
| `smart_raw_json` | TEXT | Attributs bruts (JSON) |
|
||||
| `partitions_json` | TEXT | Partitions + LVM (JSON) |
|
||||
| `collected_at` | TEXT | ISO 8601 |
|
||||
|
||||
### Table `machines`
|
||||
|
||||
| Colonne | Type | Description |
|
||||
|---|---|---|
|
||||
| `hostname` | TEXT PK | |
|
||||
| `ip` | TEXT | |
|
||||
| `last_seen` | TEXT | ISO 8601 |
|
||||
|
||||
---
|
||||
|
||||
## Frontend (étape 2)
|
||||
|
||||
Design system **Gruvbox Seventies** — React 18 via CDN + Babel standalone (pas de build step).
|
||||
|
||||
Fichiers statiques servis par nginx, localisés dans `frontend/` :
|
||||
- `index.html` — point d'entrée, charge tokens.css + ui-kit.jsx via CDN/local
|
||||
- `app.jsx` — composant principal React (chargé Babel)
|
||||
|
||||
Structure de page prévue :
|
||||
- Colonne gauche : `TreeNav` des machines, avec nombre de disques et statut global
|
||||
- Zone principale : cartes disques par machine
|
||||
- `StatusLed` (ok/warn/err) + modèle + serial
|
||||
- `BatteryGauge` espace par partition/LV
|
||||
- Détail partitions + LVM dans un `Popup`
|
||||
- Indicateur visuel de disque déplacé (serial apparu sur nouvelle machine)
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
build: ./api
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mes_hdd_db:/data
|
||||
environment:
|
||||
DB_PATH: /data/mes_hdd.db
|
||||
expose:
|
||||
- "8000"
|
||||
|
||||
web:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8088:80"
|
||||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
volumes:
|
||||
mes_hdd_db:
|
||||
```
|
||||
|
||||
nginx proxifie `/api/*` vers `api:8000`, sert `/` depuis les fichiers statiques.
|
||||
|
||||
---
|
||||
|
||||
## Lancement du script client
|
||||
|
||||
```bash
|
||||
# Télécharger et exécuter (depuis Gitea ou autre dépôt)
|
||||
curl -fsSL http://<gitea>/mes_hdd/raw/main/inventaire.py | python3
|
||||
|
||||
# Ou localement
|
||||
python3 inventaire.py
|
||||
```
|
||||
|
||||
Aucun argument requis. L'URL de l'API est configurable via variable d'environnement :
|
||||
```bash
|
||||
MES_HDD_API=http://10.0.0.50:8088 python3 inventaire.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ce qui est hors scope (étape 2 ou plus tard)
|
||||
|
||||
- Authentification sur l'API
|
||||
- Alertes automatiques (email, Telegram) en cas de SMART dégradé
|
||||
- Scraping périodique automatique (cron distant)
|
||||
- Interface mobile
|
||||
Reference in New Issue
Block a user