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>
9.2 KiB
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 8000web: 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) :smartctletpvs/lvsnécessitent des droits élevés - Tolérant aux erreurs : un disque ou une commande qui échoue ne bloque pas l'inventaire des autres
- Si
smartctlest absent →smart.status = "unavailable" - Si
pvs/lvssont 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é
{
"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 :
- Upsert dans
machines(hostname, ip, last_seen) - Pour chaque disque du payload :
- Upsert dans
disks(serial comme PK) — met à jourlast_host,last_seen,status - Si
last_host≠first_host→ le mouvement est détecté (visible via l'API) - Insert dans
snapshots(historique immuable)
- Upsert dans
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/localapp.jsx— composant principal React (chargé Babel)
Structure de page prévue :
- Colonne gauche :
TreeNavdes machines, avec nombre de disques et statut global - Zone principale : cartes disques par machine
StatusLed(ok/warn/err) + modèle + serialBatteryGaugeespace par partition/LV- Détail partitions + LVM dans un
Popup
- Indicateur visuel de disque déplacé (serial apparu sur nouvelle machine)
Docker Compose
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
# 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 :
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