{#- 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 }}