Set-OPS-Public/roles/serveur_grafana/templates/tableau-materiel.json.j2
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

230 lines
16 KiB
Django/Jinja

{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
LE TABLEAU DU MATERIEL — hyperviseurs et frontiere (2026-09-17).
Assemble depuis `scripts/materiel.py`, le MEME catalogue que la sonde Icinga lit : un
seuil change la-bas change la couleur ici et le verdict la-bas. Deux ecritures
divergeraient, et un graphe rouge cohabiterait avec une supervision verte.
Le JSON est construit comme une structure Jinja puis rendu d'un bloc par `tojson` :
aucune virgule a la main, donc aucun tableau que Grafana sauterait en silence.
-#}
{%- set cat = setops_materiel -%}
{%- set filtre = ' and on(hote) up{job=~"fabric|frontiere",hote=~"$hote"}' -%}
{%- set panneaux = [] -%}
{%- set y = namespace(v=0, id=0) -%}
{%- macro seuils(f) -%}
{{ {"mode": "absolute", "steps": [
{"color": "green", "value": none},
{"color": "orange", "value": f.avert},
{"color": "red", "value": f.crit}]} | tojson }}
{%- endmacro -%}
{#- LES PHRASES DES PANNEAUX (2026-10-04). Trois temps, toujours les memes : ce que ca
mesure (`raison`), ou sont les seuils, et QUOI FAIRE quand la couleur change (`action`,
dans le catalogue). Grafana les montre au survol du titre (icone i), en Markdown. -#}
{%- macro desc_temp(f) -%}
{{ f.raison ~ "\n\n**Seuils** : orange à " ~ f.avert ~ " °C, rouge à " ~ f.crit ~ " °C — le verdict Icinga est lissé sur " ~ (f.jugee_sur | replace("m", " min")) ~ " : un pic d'une seconde n'est pas une panne.\n\n**Si c'est orange ou rouge** : " ~ f.action }}
{%- endmacro -%}
{%- macro desc_ctrl(c) -%}
{{ c.raison ~ "\n\n**Si la tuile n'affiche pas 0** : " ~ c.action }}
{%- endmacro -%}
{#- ---- MODE D'EMPLOI ---------------------------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "text", "id": y.id, "title": "",
"gridPos": {"h": 4, "w": 24, "x": 0, "y": y.v},
"options": {"mode": "markdown", "content":
"**Comment lire ce tableau.** Chaque rangée répond à une question.\n\n"
~ "- **État en direct** — y a-t-il quelque chose à faire maintenant ? Vert : rien. Orange : à planifier. Rouge : à traiter.\n"
~ "- **Températures** — depuis quand, et sur quelle machine ? Les pointillés sont les seuils.\n"
~ "- **Ventilateurs et puissance** — une chaleur vient-elle de la charge ou du refroidissement ?\n"
~ "- **Stockage** — quels disques vieillissent, et lesquels se dégradent ?\n\n"
~ "Survoler le **titre** d'un panneau (icône ⓘ) : il dit ce qu'il mesure, pourquoi cela compte, et **quoi faire** s'il change de couleur. Seuils identiques à ceux du service Icinga « materiel »."}}) -%}
{%- set y.v = y.v + 4 -%}
{#- ---- RANGEE : ETAT EN DIRECT ------------------------------------------------------ #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "État en direct", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set tuiles = cat.temperatures | selectattr("cle", "in", ["processeur", "carte_mere", "nvme", "disques"]) | list -%}
{%- for f in tuiles -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "stat", "id": y.id,
"title": f.titre ~ " — maximum",
"description": desc_temp(f),
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 5, "w": 6, "x": loop.index0 * 6, "y": y.v},
"fieldConfig": {"defaults": {"unit": "celsius", "decimals": 0,
"thresholds": (seuils(f) | from_json)}, "overrides": []},
"options": {"colorMode": "background", "graphMode": "area", "justifyMode": "center",
"textMode": "value_and_name", "orientation": "vertical",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "max by (hote) ((" ~ f.expr_juge ~ ")" ~ filtre ~ ")",
"legendFormat": "{{hote}}"}]}) -%}
{%- endfor -%}
{%- set y.v = y.v + 5 -%}
{#- Les controles : une tuile chacun. Zero = vert ; sinon la couleur de SON niveau. #}
{%- set largeur = [24 // (cat.controles | length), 3] | max -%}
{%- for c in cat.controles -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "stat", "id": y.id,
"title": c.titre,
"description": desc_ctrl(c),
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 4, "w": largeur, "x": (loop.index0 * largeur) % 24, "y": y.v + ((loop.index0 * largeur) // 24) * 4},
"fieldConfig": {"defaults": {"unit": "none", "decimals": 0,
"noValue": "0",
"thresholds": {"mode": "absolute", "steps": [
{"color": "green", "value": none},
{"color": ("red" if c.niveau == "crit" else "orange"), "value": 1}]}},
"overrides": []},
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center",
"textMode": "value", "orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "count(count by (hote, capteur) ((" ~ c.expr ~ ")" ~ filtre ~ ")) or vector(0)"}]}) -%}
{%- endfor -%}
{%- set y.v = y.v + (((cat.controles | length) * largeur + 23) // 24) * 4 -%}
{#- ---- RANGEE : TEMPERATURES -------------------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Températures", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- for f in cat.temperatures -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id,
"title": f.titre,
"description": desc_temp(f),
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 12, "x": 0 if loop.index0 % 2 == 0 else 12, "y": y.v + (loop.index0 // 2) * 9},
"fieldConfig": {"defaults": {
"unit": "celsius", "decimals": 1,
"color": {"mode": "palette-classic"},
"thresholds": (seuils(f) | from_json),
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 8, "gradientMode": "opacity",
"showPoints": "never", "spanNulls": true,
"thresholdsStyle": {"mode": "dashed"}}},
"overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]},
"tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "(" ~ f.expr ~ ")" ~ filtre,
"legendFormat": "{{hote}} · {{capteur}}"}]}) -%}
{%- endfor -%}
{%- set y.v = y.v + (((cat.temperatures | length) + 1) // 2) * 9 -%}
{#- ---- RANGEE : VENTILATEURS ET PUISSANCE ----------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Ventilateurs et puissance", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id, "title": "Ventilateurs",
"description": "Vitesse de chaque ventilateur, en tours par minute. Seuls les connecteurs qui ont tourné dans les dernières 24 h sont tracés : un connecteur vide lit 0 depuis toujours et n'apprend rien.\n\n**À lire avec les températures** : une température qui monte pendant que les ventilateurs restent bas parle d'une courbe de ventilation trop douce ; une courbe qui tombe à 0, d'un ventilateur arrêté (tuile rouge plus haut).",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 12, "x": 0, "y": y.v},
"fieldConfig": {"defaults": {"unit": "rotrpm", "decimals": 0, "color": {"mode": "palette-classic"},
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 5, "showPoints": "never", "spanNulls": true}},
"overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min"]},
"tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "(node_hwmon_fan_rpm{job=~\"fabric|frontiere\"} and on(instance, chip, sensor) (max_over_time(node_hwmon_fan_rpm{job=~\"fabric|frontiere\"}[1d]) > 0))" ~ filtre,
"legendFormat": "{{hote}} · {{sensor}}"}]}) -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id, "title": "Puissance du processeur (PPT)",
"description": "Puissance du processeur (PPT), annoncée par le pilote amdgpu de l'APU intégré. Ce n'est pas la consommation de la machine entière : aucune mesure ne la donne.\n\n**À lire avec les températures** : puissance et température qui montent ensemble, c'est la charge. Une température qui monte **sans** que la puissance monte, c'est le refroidissement : ventilateurs, poussière, pâte thermique.",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 12, "x": 12, "y": y.v},
"fieldConfig": {"defaults": {"unit": "watt", "decimals": 1, "color": {"mode": "palette-classic"},
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 15, "gradientMode": "opacity", "showPoints": "never", "spanNulls": true}},
"overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]},
"tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "(node_hwmon_power_watt{job=~\"fabric|frontiere\"} * on(instance, chip, sensor) group_left(label) group by (instance, chip, sensor, label) (node_hwmon_sensor_label{job=~\"fabric|frontiere\",label=\"PPT\"}))" ~ filtre,
"legendFormat": "{{hote}}"}]}) -%}
{%- set y.v = y.v + 9 -%}
{#- ---- RANGEE : STOCKAGE ------------------------------------------------------------ #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Stockage", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "bargauge", "id": y.id, "title": "Usure des NVMe",
"description": "Part de l'endurance garantie déjà consommée par chaque NVMe, selon le disque lui-même.\n\n**Seuils** : orange à 80 %, rouge à 90 %.\n\n**Si c'est orange** : commander un remplaçant, rien ne presse encore. **Si c'est rouge** : planifier le remplacement ; pour un OSD Ceph, le sortir du cluster avant.",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 8, "x": 0, "y": y.v},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "decimals": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 0.8}, {"color": "red", "value": 0.9}]}},
"overrides": []},
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "nvme_percentage_used_ratio{job=~\"fabric|frontiere\"}" ~ filtre,
"legendFormat": "{{hote}} · {{device}}"}]}) -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "bargauge", "id": y.id, "title": "Secteurs défaillants",
"description": "Secteurs en attente, irréparables et réalloués, additionnés par disque SATA.\n\n**Un compte stable se surveille, un compte qui monte se remplace** : la tuile « secteurs en progression » passe au rouge dès qu'il augmente.\n\n**Si ce n'est pas 0** : prévoir le remplacement ; pour un OSD Ceph, le sortir du cluster (« out ») avant que la dégradation s'accélère.",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 8, "x": 8, "y": y.v},
"fieldConfig": {"defaults": {"unit": "none", "min": 0, "decimals": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 1}, {"color": "red", "value": 100}]}},
"overrides": []},
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "(sum by (hote, disk) (smartmon_current_pending_sector_raw_value{job=~\"fabric|frontiere\"} or smartmon_offline_uncorrectable_raw_value{job=~\"fabric|frontiere\"} or smartmon_reallocated_sector_ct_raw_value{job=~\"fabric|frontiere\"}))" ~ filtre,
"legendFormat": "{{hote}} · {{disk}}"}]}) -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "bargauge", "id": y.id, "title": "Heures de fonctionnement",
"description": "Âge réel de chaque disque, en heures sous tension.\n\n**Seuil** : orange au-delà de 40 000 heures (4,5 ans), quand un disque mécanique entre dans la zone où les pannes se multiplient.\n\n**Si c'est orange** : en prévoir le remplacement dans le budget, surtout s'il porte aussi des secteurs défaillants.",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"gridPos": {"h": 9, "w": 8, "x": 16, "y": y.v},
"fieldConfig": {"defaults": {"unit": "h", "min": 0, "decimals": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": none}, {"color": "orange", "value": 40000}]}},
"overrides": []},
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"expr": "(label_replace(smartmon_power_on_hours_raw_value{job=~\"fabric|frontiere\",type=\"sat\"}, \"dispositif\", \"$1\", \"disk\", \"/dev/(.*)\") or label_replace(nvme_power_on_hours_total{job=~\"fabric|frontiere\"}, \"dispositif\", \"$1\", \"device\", \"(.*)\"))" ~ filtre,
"legendFormat": "{{hote}} · {{dispositif}}"}]}) -%}
{{ {
"uid": "setops-materiel",
"title": "Set-OPS — Matériel",
"description": "Hyperviseurs et frontière : températures, ventilateurs, puissance, disques. Même catalogue et mêmes seuils que le service Icinga « materiel » (scripts/materiel.py).",
"tags": ["set-ops", "derive", "materiel", "fabric"],
"timezone": "browser",
"refresh": "1m",
"time": {"from": "now-24h", "to": "now"},
"schemaVersion": 39,
"version": 1,
"graphTooltip": 1,
"templating": {"list": [
{"name": "ds_prom", "label": "Source Prometheus", "type": "datasource", "query": "prometheus", "hide": 2, "current": {}},
{"name": "hote", "label": "Équipement", "type": "query",
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
"query": {"query": "label_values(up{job=~\"fabric|frontiere\"}, hote)", "refId": "hote"},
"definition": "label_values(up{job=~\"fabric|frontiere\"}, hote)",
"multi": true, "includeAll": true, "current": {"selected": true, "text": ["All"], "value": ["$__all"]},
"refresh": 2, "sort": 1}
]},
"panels": panneaux
} | tojson }}