f9f805cd8b
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>
295 lines
9.2 KiB
Markdown
295 lines
9.2 KiB
Markdown
# 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
|