Files
mes_hdd/docs/superpowers/specs/2026-05-28-inventaire-hdd-design.md
Gilles Soulier ee0dc2461c docs: API complète + évolutivité schéma Alembic
- Routes read séparées dashboard vs agents IA
- Endpoints /api/ai/* (summary, at-risk, moved-disks, backup-needed)
- Stratégie migrations Alembic (alembic upgrade head au démarrage)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:58:24 +02:00

13 KiB
Raw Permalink Blame History

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 1015 machines Proxmox, Debian et Ubuntu. Certaines machines sont des VMs avec disques en PCI passthrough (45 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,UUID 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
du -sb /home/* 2>/dev/null Taille par utilisateur sous /home

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",
          "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "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": "/",
          "home_users": null,
          "lvm": null
        },
        {
          "name": "sda2",
          "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
          "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
              }
            ]
          }
        }
      ]
    }
  ]
}

Détection OS et adaptations

Le script lit /etc/os-release pour identifier l'environnement :

OS détecté Condition
proxmox /etc/pve existe OU ID=debian + VARIANT_ID=proxmox
ubuntu ID=ubuntu
debian ID=debian (fallback)

Le champ os est inclus dans le payload au niveau machine :

{ "hostname": "pve1", "os": "proxmox", "os_version": "8.2", ... }

Adaptations par OS :

  • Proxmox : enrichissement additionnel si disponible

    • Détection ZFS : zpool list -H -o name,size,alloc,free → ajoute zfs_pool sur le disque concerné
    • Détection Ceph OSD : si /var/lib/ceph/osd contient des liens vers le disque → champ proxmox_role: "ceph_osd"
    • Détection stockage VM : si disque dans /etc/pve/storage.cfgproxmox_role: "vm_storage"
    • Les boucles (loop*) et devices virtuels (dm-*) sont ignorés comme partout
  • Ubuntu : filtre des montages squashfs (snap) dans la liste des partitions — ils polluent df et lsblk

  • Debian : comportement par défaut, aucune adaptation spécifique


Taille du dossier /home

Si une partition est montée sur /home (ou si /home est dans la racine), le script calcule la taille occupée par chaque sous-dossier utilisateur via du -sb /home/* 2>/dev/null.

Le champ home_users est ajouté à la partition dont le point de montage est /home (ou / si /home n'est pas une partition dédiée) :

"home_users": [
  { "user": "gilles", "size_bytes": 91268055040, "size_human": "85 Go" },
  { "user": "alice",  "size_bytes": 32212254720, "size_human": "30 Go" }
]

Si /home n'existe pas ou est vide → home_users: [].
Si du échoue → home_users: null.

Cela permet d'identifier côté frontend les disques avec des données significatives à sauvegarder.


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 complètes

Écriture

Méthode Route Description
POST /api/ingest Reçoit le payload complet du script client

Lecture — Dashboard / frontend

Méthode Route Description
GET /api/disks Tous les disques — dernière observation par serial
GET /api/disks/{serial} Détail + historique complet d'un disque
GET /api/machines Toutes les machines avec last_seen et nombre de disques
GET /api/machines/{hostname} Détail d'une machine + ses disques courants
GET /api/machines/{hostname}/disks Disques d'une machine (dernière observation)

Lecture — Agents IA et apps externes

Méthode Route Description
GET /api/ai/summary Synthèse globale : machines, disques, statuts SMART, espaces
GET /api/ai/at-risk Disques en warn ou fail avec détail SMART
GET /api/ai/moved-disks Disques dont last_seen_host ≠ first_seen_host
GET /api/ai/backup-needed Disques avec home_users non vide, triés par taille décroissante
GET /api/ai/machines/{hostname} Vue complète d'une machine pour agent IA

Les routes /api/ai/* retournent du JSON compact et plat, sans pagination ni enveloppe, optimisé pour consommation LLM.

Évolutivité du schéma SQLite — Alembic

Le schéma évolue via Alembic (migrations versionnées) :

api/
├── alembic.ini
├── migrations/
│   ├── env.py
│   └── versions/
│       └── 0001_initial_schema.py
  • Chaque ajout de colonne ou de table = une nouvelle migration numérotée
  • Appliquées automatiquement au démarrage du conteneur (alembic upgrade head)
  • Permet d'ajouter des champs sans recréer la base ni perdre l'historique

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_hostfirst_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 + UUID (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

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

Dépôt source

Le script est versionné dans le dépôt Gitea : https://git.maison43gil.com/gilles/mes_hdd

URL raw du script : https://git.maison43gil.com/gilles/mes_hdd/raw/branch/main/inventaire.py

Commande one-liner (affichée par le frontend)

curl -fsSL https://git.maison43gil.com/gilles/mes_hdd/raw/branch/main/inventaire.py | sudo python3 -

Le frontend propose cette commande en copier-coller sur la page d'accueil, avec l'URL de l'API pré-remplie via variable d'environnement :

curl -fsSL https://git.maison43gil.com/gilles/mes_hdd/raw/branch/main/inventaire.py \
  | sudo MES_HDD_API=http://10.0.0.50:8088 python3 -

Variables d'environnement du script

Variable Défaut Description
MES_HDD_API http://10.0.0.50:8088 URL de base de l'API

Exécution locale

sudo python3 inventaire.py
# ou
sudo MES_HDD_API=http://192.168.1.x: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