Set-OPS-Public/scripts/materiel.py
Daniel Allaire b3debd6ca9 grafana : le tableau Matériel dit quoi faire
- 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>
2026-10-04 00:49:28 -04:00

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))