2026-09-17 10:13:43 -04:00
#!/usr/bin/env python3
""" Le catalogue des capteurs materiels — une source, deux lecteurs : Grafana et Icinga.
POURQUOI UN CATALOGUE ( 2026 - 09 - 17 ) . Les hyperviseurs exposaient deja 68 temperatures , leurs
ventilateurs , leurs disques SMART et l ' usure de leurs NVMe — collectes par Prometheus depuis
le 2026 - 09 - 10 , regardes par personne . Le tableau de bord et la sonde doivent lire les MEMES
capteurs avec les MEMES seuils : ecrits deux fois , ils divergeraient , et un graphe rouge
cohabiterait avec une supervision verte .
CE QUE LE MATERIEL DIT DE LUI - MEME , ET POURQUOI ON NE LE CROIT PAS . Les puces annoncent
leurs propres limites ( ` temp_max ` , ` temp_crit ` ) . Sur ces cartes ASUS elles valent 80 et 125 ° C
pour la carte mere — des valeurs d ' usine du Super I/O, pas des limites du systeme. Les seuils
sont donc DECLARES ici , avec leur raison .
CE QU ' ON IGNORE, ET POURQUOI (releve du 2026-09-17 sur asgard, gandalf, vishnu) :
- ` AUXTIN0 . .5 ` du nct6799 : entrees non cablees ( - 62 , 0 , 12 ° C ) ;
- ` PCH_ * ` : lisent 0 sur ces cartes ;
- ` TSI0_TEMP ` et ` amdgpu edge ` : le meme silicium que ` Tctl ` , vu autrement ;
- la puce Wi - Fi : sans interet pour un hyperviseur ;
- les disques iSCSI ( ` type = " scsi " ` ) : des volumes distants , dont le materiel n ' est pas ici.
Les familles sans capteur sur un hote ( DDR5 et cartes reseau n ' existent que sur le noyau 6.14
de vishnu ) ne rendent simplement aucune serie : ce n ' est ni une panne ni un oubli.
"""
from __future__ import annotations
# Les jobs Prometheus qui portent du materiel. `fabric` : les hyperviseurs ; `frontiere` :
# le boitier OPNsense, quand son exportateur est en service.
JOBS = ' job=~ " fabric|frontiere " '
# LE COTE DROIT D'UNE JOINTURE EST TOUJOURS AGREGE (eprouve le 2026-09-17). Le jour ou l'on a
# ajoute l'etiquette `hote` aux cibles, Prometheus a garde cinq minutes la serie sans elle ET
# la serie avec : deux series pour une meme cle de jointure, et la requete refusee (« many-to-
# many matching not allowed »). Le tableau se vidait, la sonde rendait INCONNU. `group by` /
# `max by` ramene la cle a une seule serie, quel que soit l'historique des etiquettes.
def _hwmon ( chip_name : str , etiquettes : str | None = None ) - > str :
""" Temperatures hwmon d ' une puce, avec le libelle du capteur quand la puce en donne un. """
expr = f " node_hwmon_temp_celsius {{ { JOBS } }} "
if etiquettes :
expr = ( f " ( { expr } * on(instance, chip, sensor) group_left(label) "
f ' group by (instance, chip, sensor, label) '
f ' (node_hwmon_sensor_label {{ { JOBS } ,label=~ " { etiquettes } " }} )) ' )
return ( f " ( { expr } * on(instance, chip) group_left(chip_name) "
f ' group by (instance, chip, chip_name) '
f ' (node_hwmon_chip_names {{ { JOBS } ,chip_name=~ " { chip_name } " }} )) ' )
# UNE FAMILLE = une question qu'on se pose devant un equipement chaud.
# expr : les series (au moins les etiquettes `hote` et `capteur`)
# avert/crit: °C ; `None` = regardee, jamais jugee
# jugee_sur : la fenetre de lissage du verdict — un pic d'une seconde n'est pas une panne
TEMPERATURES = [
{
" cle " : " processeur " ,
" titre " : " Processeur " ,
" expr " : f ' label_replace( { _hwmon ( " k10temp " , " Tctl|Tccd1|Tccd2 " ) } , " capteur " , " $1 " , " label " , " (.*) " ) ' ,
" juge " : ' capteur= " Tctl " ' ,
# Le verdict ne regarde que Tctl : les Tccd sont des sondes de coeur, plus bruitees.
" expr_juge " : f ' label_replace( { _hwmon ( " k10temp " , " Tctl " ) } , " capteur " , " $1 " , " label " , " (.*) " ) ' ,
" avert " : 90 , " crit " : 95 , " jugee_sur " : " 5m " ,
2026-10-04 00:49:28 -04:00
" raison " : " Ryzen 9 7900X : il est conçu pour monter à 95 °C sous charge soutenue et s ' y brider. Un Tctl à 95 pendant cinq minutes veut dire qu ' il bride — la machine rend moins que ce qu ' on croit. Au-dessus, c ' est le refroidissement. " ,
" action " : " regarder la puissance du processeur plus bas. Si elle monte aussi, c ' est la charge : voir quelle VM consomme. Si elle reste basse, c ' est le refroidissement : ventilateurs, poussière, pâte thermique. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " carte_mere " ,
" titre " : " Carte mère " ,
" expr " : ( f ' label_replace( { _hwmon ( " nct6799 " , " SYSTIN|CPUTIN " ) } , " capteur " , " $1 " , " label " , '
f ' " (.*) " ) ' ) ,
" juge " : ' capteur= " SYSTIN " ' ,
" expr_juge " : f ' label_replace( { _hwmon ( " nct6799 " , " SYSTIN " ) } , " capteur " , " $1 " , " label " , " (.*) " ) ' ,
" avert " : 50 , " crit " : 60 , " jugee_sur " : " 10m " ,
2026-10-04 00:49:28 -04:00
" raison " : " SYSTIN est l ' air du boîtier. Il monte avec la pièce, pas avec la charge : un SYSTIN qui grimpe sur les trois machines à la fois parle de la salle, pas d ' une machine. " ,
" action " : " comparer les trois hyperviseurs. Tous ensemble : vérifier la climatisation ou l ' aération de la salle. Un seul : son flux d ' air (filtres, ventilateurs de façade). " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " nvme " ,
" titre " : " NVMe " ,
" expr " : ( f ' label_replace( { _hwmon ( " nvme " , " Composite " ) } , " capteur " , " $1 " , " chip " , '
f ' " nvme_(.*) " ) ' ) ,
" juge " : " " ,
" avert " : 70 , " crit " : 80 , " jugee_sur " : " 5m " ,
2026-10-04 00:49:28 -04:00
" raison " : " WD_BLACK SN850X : il ralentit ses écritures vers 80-85 °C pour se protéger. Ceph et les VM le vivent comme une latence, sans aucune erreur. " ,
" action " : " vérifier le flux d ' air autour du disque (dissipateur, ventilateur du boîtier). Une chaleur durable abrège aussi sa vie : à surveiller avec l ' usure plus bas. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " disques " ,
" titre " : " Disques SATA " ,
# LES SSD SAMSUNG NE PORTENT PAS L'ATTRIBUT 194 : leur temperature est la 190
# (« airflow »). Sans le repli, les deux 850 disparaissaient du tableau sans un mot.
" expr " : ( f ' label_replace((smartmon_temperature_celsius_raw_value {{ { JOBS } ,type= " sat " }} or '
f ' smartmon_airflow_temperature_cel_raw_value {{ { JOBS } ,type= " sat " }} ), '
f ' " capteur " , " $1 " , " disk " , " /dev/(.*) " ) ' ) ,
" juge " : " " ,
" avert " : 50 , " crit " : 60 , " jugee_sur " : " 15m " ,
2026-10-04 00:49:28 -04:00
" raison " : " WD Red Plus 10 To (OSD HDD de Ceph) et SSD Samsung 850 : la vie d ' un disque mécanique chute au-dessus de 50 °C, bien avant sa limite de 65. Celui de gandalf a déjà vu 63 °C (maximum à vie, relevé SMART du 2026-09-17). " ,
" action " : " améliorer la ventilation de la baie des disques. Un disque mécanique durablement au-dessus de 50 °C vieillit vite : croiser avec les secteurs défaillants, plus bas. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " memoire " ,
" titre " : " Mémoire DDR5 " ,
" expr " : ( f ' label_replace( { _hwmon ( " spd5118 " ) } , " capteur " , " barrette $1 " , " chip " , '
f ' " i2c_(.*) " ) ' ) ,
" juge " : " " ,
" avert " : 70 , " crit " : 80 , " jugee_sur " : " 5m " ,
2026-10-04 00:49:28 -04:00
" raison " : " Capteur JEDEC des barrettes (noyau 6.14, vishnu seulement). La DDR5 commence à corriger des erreurs au-delà de 85 °C. " ,
" action " : " vérifier le flux d ' air sur les barrettes. Au-delà de 85 °C, chercher des erreurs mémoire corrigées dans le journal du noyau (EDAC). " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " reseau " ,
" titre " : " Cartes réseau " ,
" expr " : ( f ' label_replace( { _hwmon ( " r8169.* " ) } , " capteur " , " $1 " , " chip_name " , '
f ' " (.*) " ) ' ) ,
" juge " : " " ,
" avert " : 95 , " crit " : 110 , " jugee_sur " : " 5m " ,
2026-10-04 00:49:28 -04:00
" raison " : " Realtek RTL8125 (noyau 6.14, vishnu seulement) ; limite annoncée 120 °C. " ,
" action " : " vérifier le flux d ' air autour de la carte. Une carte réseau trop chaude perd des liens : regarder aussi les erreurs d ' interface. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " frontiere " ,
" titre " : " Frontière (cœurs) " ,
" expr " : ( f ' label_replace(node_cpu_temperature_celsius {{ { JOBS } }} , " capteur " , " cœur $1 " , '
f ' " cpu " , " (.*) " ) ' ) ,
" juge " : " " ,
" avert " : 80 , " crit " : 90 , " jugee_sur " : " 5m " ,
2026-10-04 00:49:28 -04:00
" raison " : " Intel i3-5020U, Tjunction 100 °C. À 90, il bride — et c ' est tout le trafic du site qui ralentit. " ,
" action " : " la frontière n ' a qu ' un petit refroidissement passif : dégager son emplacement, vérifier la poussière. Elle bride à 90 °C, et tout le trafic du site avec elle. " ,
2026-09-17 10:13:43 -04:00
} ,
]
# CE QUI NE SE MESURE PAS EN DEGRES. Chaque controle rend une expression dont TOUTE serie
# presente est un defaut (etiquettes `hote` et `capteur`). Vide = sain.
CONTROLES = [
{
" cle " : " ventilateur_arrete " ,
" titre " : " Ventilateur arrêté " ,
" niveau " : " crit " ,
# UN CONNECTEUR VIDE N'EST PAS UN VENTILATEUR ARRETE. fan4, fan6 et fan7 lisent 0
# depuis toujours : on ne crie que pour un ventilateur qui A TOURNE dans les 24 h.
" expr " : ( f ' label_replace((node_hwmon_fan_rpm {{ { JOBS } }} == 0) and on(instance, chip, sensor) '
f ' (max_over_time(node_hwmon_fan_rpm {{ { JOBS } }} [1d]) > 300), " capteur " , " $1 " , '
f ' " sensor " , " (.*) " ) ' ) ,
" raison " : " Un ventilateur qui tournait hier et plus maintenant : la chaleur suit dans l ' heure. " ,
2026-10-04 00:49:28 -04:00
" action " : " aller voir la machine sans attendre : ventilateur bloqué, débranché ou mort. Surveiller les températures de cette machine jusqu ' à la réparation. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " smart_echec " ,
" titre " : " SMART : disque en échec " ,
" niveau " : " crit " ,
" expr " : ( f ' label_replace(smartmon_device_smart_healthy {{ { JOBS } ,type=~ " sat|nvme " }} == 0, '
f ' " capteur " , " $1 " , " disk " , " /dev/(.*) " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " Le disque déclare lui-même qu ' il va lâcher. Il faut le remplacer, pas l ' observer. " ,
" action " : " remplacer le disque. Pour un OSD Ceph : le sortir du cluster (« out ») et laisser Ceph recopier ses données avant de l ' arrêter. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " secteurs_degradation " ,
" titre " : " Disque : secteurs en progression " ,
" niveau " : " crit " ,
# LA PROGRESSION, PAS LE COMPTE. Le disque de gandalf porte 56 secteurs en attente,
# stables depuis au moins une semaine, Ceph `HEALTH_OK` (releve du 2026-09-17). Un
# verdict critique sur le compte serait rouge pour toujours — et une alarme toujours
# rouge est une alarme qu'on cesse de lire. Ce qui presse, c'est qu'il AUGMENTE.
" expr " : ( f ' label_replace((delta(smartmon_current_pending_sector_raw_value {{ { JOBS } }} [1d]) > 0) or '
f ' (delta(smartmon_reallocated_sector_ct_raw_value {{ { JOBS } }} [1d]) > 0) or '
f ' (delta(smartmon_offline_uncorrectable_raw_value {{ { JOBS } }} [1d]) > 0), '
f ' " capteur " , " $1 " , " disk " , " /dev/(.*) " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " Le disque perd des secteurs en ce moment : sa dégradation s ' accélère, le remplacer. " ,
" action " : " commander et remplacer le disque rapidement. Pour un OSD Ceph : le sortir du cluster (« out ») dès maintenant, pour que les données soient recopiées ailleurs pendant qu ' il lit encore. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " secteurs_defaillants " ,
" titre " : " Disque : secteurs défaillants " ,
" niveau " : " avert " ,
" expr " : ( f ' label_replace((smartmon_current_pending_sector_raw_value {{ { JOBS } }} > 0) or '
f ' (smartmon_offline_uncorrectable_raw_value {{ { JOBS } }} > 0) or '
f ' (smartmon_reallocated_sector_ct_raw_value {{ { JOBS } }} > 0), " capteur " , " $1 " , '
f ' " disk " , " /dev/(.*) " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " Le disque a des secteurs illisibles ou déjà remplacés. Stable, il tient ; à remplacer à la prochaine occasion. " ,
" action " : " noter le compte et le surveiller : tant qu ' il ne bouge pas, pas d ' urgence. Prévoir le remplacement ; s ' il augmente, le contrôle « secteurs en progression » passe au rouge. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " nvme_alerte " ,
" titre " : " NVMe : alerte du contrôleur " ,
" niveau " : " crit " ,
" expr " : ( f ' label_replace((nvme_critical_warning_total {{ { JOBS } }} > 0) or '
f ' (nvme_available_spare_ratio {{ { JOBS } }} < on(instance, device) group_left() '
f ' max by (instance, device) (nvme_available_spare_threshold_ratio {{ { JOBS } }} )), '
f ' " capteur " , " $1 " , " device " , " (.*) " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " Le contrôleur NVMe signale un avertissement critique ou a épuisé sa réserve. " ,
" action " : " remplacer le NVMe. S ' il porte un OSD Ceph, le sortir du cluster avant ; s ' il porte le système, planifier l ' intervention. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " nvme_usure " ,
" titre " : " NVMe : usure > 80 % " ,
" niveau " : " avert " ,
" expr " : ( f ' label_replace(nvme_percentage_used_ratio {{ { JOBS } }} > 0.8, " capteur " , " $1 " , '
f ' " device " , " (.*) " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " L ' endurance annoncée est presque consommée : à commander, pas encore à remplacer. " ,
" action " : " commander un remplaçant. Le disque fonctionne encore, mais il approche de la fin de son endurance garantie. " ,
2026-09-17 10:13:43 -04:00
} ,
{
" cle " : " donnees_perimees " ,
" titre " : " Relevés SMART périmés " ,
" niveau " : " avert " ,
# UNE SONDE MUETTE N'EST PAS UNE SONDE SAINE. Les releves SMART et NVMe viennent de
# minuteries (15 min) : si elles s'arretent, toutes les regles ci-dessus restent
# vertes sur des chiffres figes.
" expr " : ( f ' label_replace(time() - node_textfile_mtime_seconds {{ { JOBS } , '
f ' file=~ " .*(smartmon|nvme)[.]prom " }} > 3600, " capteur " , " $1 " , " file " , '
f ' " .*/(.*)[.]prom " ) ' ) ,
2026-10-04 00:49:28 -04:00
" raison " : " Les relevés SMART/NVMe n ' ont pas été rafraîchis depuis une heure : les verdicts ci-dessus sont figés. " ,
" action " : " vérifier sur l ' hyperviseur les minuteries prometheus-node-exporter-smartmon et -nvme (systemctl list-timers). Tant qu ' elles sont arrêtées, les autres voyants de disque ne veulent rien dire. " ,
2026-09-17 10:13:43 -04:00
} ,
]
def catalogue ( ) - > dict :
""" Ce que les roles recoivent (Grafana et Icinga). `expr_juge` vaut `expr` par defaut. """
temperatures = [ { * * f , " expr_juge " : f . get ( " expr_juge " ) or f [ " expr " ] } for f in TEMPERATURES ]
for f in temperatures :
f . pop ( " juge " , None )
return { " temperatures " : temperatures , " controles " : CONTROLES }
if __name__ == " __main__ " :
import json
print ( json . dumps ( catalogue ( ) , ensure_ascii = False , indent = 1 ) )