- materiel.py : textes accentués et champ action par mesure (seuils inchangés) - tableau-materiel : mode d'emploi en tête ; descriptions en trois temps (ce que ça mesure, seuils, quoi faire) - CHANGELOG 69 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
14 KiB
Python
227 lines
14 KiB
Python
#!/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",
|
|
"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.",
|
|
},
|
|
{
|
|
"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",
|
|
"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).",
|
|
},
|
|
{
|
|
"cle": "nvme",
|
|
"titre": "NVMe",
|
|
"expr": (f'label_replace({_hwmon("nvme", "Composite")}, "capteur", "$1", "chip", '
|
|
f'"nvme_(.*)")'),
|
|
"juge": "",
|
|
"avert": 70, "crit": 80, "jugee_sur": "5m",
|
|
"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.",
|
|
},
|
|
{
|
|
"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",
|
|
"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.",
|
|
},
|
|
{
|
|
"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",
|
|
"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).",
|
|
},
|
|
{
|
|
"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",
|
|
"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.",
|
|
},
|
|
{
|
|
"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",
|
|
"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.",
|
|
},
|
|
]
|
|
|
|
# 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.",
|
|
"action": "aller voir la machine sans attendre : ventilateur bloqué, débranché ou mort. Surveiller les températures de cette machine jusqu'à la réparation.",
|
|
},
|
|
{
|
|
"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/(.*)")'),
|
|
"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.",
|
|
},
|
|
{
|
|
"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/(.*)")'),
|
|
"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.",
|
|
},
|
|
{
|
|
"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/(.*)")'),
|
|
"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.",
|
|
},
|
|
{
|
|
"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", "(.*)")'),
|
|
"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.",
|
|
},
|
|
{
|
|
"cle": "nvme_usure",
|
|
"titre": "NVMe : usure > 80 %",
|
|
"niveau": "avert",
|
|
"expr": (f'label_replace(nvme_percentage_used_ratio{{{JOBS}}} > 0.8, "capteur", "$1", '
|
|
f'"device", "(.*)")'),
|
|
"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.",
|
|
},
|
|
{
|
|
"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")'),
|
|
"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.",
|
|
},
|
|
]
|
|
|
|
|
|
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))
|