diff --git a/CHANGELOG.md b/CHANGELOG.md index a08a105..caa0efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # CHANGELOG — Set-OPS +## 2026-10-04 (70) — Un tableau de synthèse, au site et chez les locataires + +**Demande de l'exploitant** : la seconde moitié de « rendre les tableaux plus parlants » — un +tableau d'accueil. Contenu arrêté avec lui avant d'écrire ; site ET locataires. + +**Fait** : `tableau-synthese.json.j2` (« Set-OPS — Synthèse », `setops-synthese`), quatre +rangées, quatre questions : +1. **Faut-il agir ?** — tuiles colorées : matériel des hyperviseurs (niveau 0/1/2 tiré du + même catalogue que le tableau Matériel et la sonde Icinga), swap le plus plein des + hyperviseurs, disque le plus plein des machines, correctifs de sécurité en attente, + redémarrages en attente, machines muettes ; +2. **Reste-t-il de la marge ?** — mémoire et swap des hyperviseurs, disque et mémoire par + machine (de la plus pleine à la moins pleine) ; +3. **Que se passe-t-il ?** — processeur et trafic réseau (interfaces physiques) ; +4. **Où regarder ensuite ?** — liens vers Matériel, Machines, PostgreSQL, Journaux et la vigie. +Chaque panneau dit ce qu'il mesure, ses seuils et quoi faire quand sa couleur change. + +**Un gabarit, deux visages** : au site (`setops_materiel` présent), il ajoute les hyperviseurs ; +chez un locataire, il ne voit que ses machines. Le nom des machines vient de +`node_uname_info` : les locataires n'ont pas l'étiquette `hote` du site. + +**Éprouvé avant de poser** : chaque requête exécutée sur le vrai Prometheus, au site et chez +Chezlepro. Une erreur trouvée ainsi : la tuile matériel affichait « 3 » — `count(x) > 0` garde +la valeur du compte ; elle rend désormais un niveau. Ce que la synthèse dit au premier +regard, au site : matériel « à planifier » (disque de gandalf), **swap de vishnu à 98,6 %**, +65 correctifs de sécurité en attente, 9 redémarrages en attente. + +**Appliqué** au site, puis aux deux locataires (17 et 14 panneaux), sans erreur de chargement. + +**Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). + ## 2026-10-04 (69) — Le tableau Matériel dit quoi faire **Demande de l'exploitant** : rendre les tableaux Grafana plus parlants — « phrases et diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index 42f8422..a730e8b 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -335,6 +335,21 @@ when: setops_materiel is defined notify: Redemarrer grafana +# --- LA SYNTHESE (2026-10-04) — la porte d'entree, au site comme chez un locataire ------ +# +# Le meme gabarit partout : il ajoute les hyperviseurs quand `setops_materiel` existe (au +# site), et ne voit que les machines de l'ecosysteme sinon. Pas de prefixe `setops-role-` : +# la moisson des tableaux orphelins ne doit pas le voir. +- name: Assembler le tableau de synthèse + ansible.builtin.template: + src: tableau-synthese.json.j2 + dest: "{{ serveur_grafana_dashboards_dir }}/setops-synthese.json" + owner: root + group: grafana + mode: "0640" + validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s" + notify: Redemarrer grafana + # --- UN ROLE QU'ON RETIRE DOIT POUVOIR DEFAIRE CE QU'IL A FAIT ------------------------ # # Meme lecon que les sondes orphelines de `client_sante`, et elle s'est deja payee ici : diff --git a/roles/serveur_grafana/templates/tableau-synthese.json.j2 b/roles/serveur_grafana/templates/tableau-synthese.json.j2 new file mode 100644 index 0000000..f421055 --- /dev/null +++ b/roles/serveur_grafana/templates/tableau-synthese.json.j2 @@ -0,0 +1,216 @@ +{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main. + + LE TABLEAU DE SYNTHESE (2026-10-04) — la porte d'entree de l'observatoire. + + Quatre rangees, quatre questions : faut-il agir ? reste-t-il de la marge ? que se + passe-t-il ? ou regarder ensuite ? Chaque panneau dit ce qu'il mesure, ou sont ses + seuils, et QUOI FAIRE quand sa couleur change — meme patron que le tableau Materiel. + + LE MEME GABARIT AU SITE ET CHEZ UN LOCATAIRE. Au site, `setops_materiel` existe : la + synthese ajoute les hyperviseurs (tuile materiel, memoire et swap) et regarde leur + activite. Chez un locataire, elle ne voit que ses propres machines. + + LE NOM DES MACHINES vient de `node_uname_info` (joint sur `instance`) : les locataires + n'ont pas l'etiquette `hote` du site, et une adresse IP ne parle a personne. +-#} +{%- set fabric = setops_materiel is defined -%} +{#- La tuile materiel rend un NIVEAU (0, 1, 2), pas un compte : `count(x) > 0` garde la + valeur du compte — trois secteurs en avertissement s'affichaient « 3 ». -#} +{%- set cat = setops_materiel | default({"temperatures": [], "controles": []}) -%} +{%- set nom = ' * on(instance) group_left(nodename) node_uname_info' -%} +{%- set ds = {"type": "prometheus", "uid": "${ds_prom}"} -%} +{%- set panneaux = [] -%} +{%- set y = namespace(v=0, id=0) -%} + +{#- ---- MODE D'EMPLOI ---------------------------------------------------------------- #} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "text", "id": y.id, "title": "", + "gridPos": {"h": 3, "w": 24, "x": 0, "y": y.v}, + "options": {"mode": "markdown", "content": + "**Synthèse " ~ ("du site" if fabric else "de l'écosystème") ~ ".** Quatre rangées, quatre questions : **faut-il agir ?** — **reste-t-il de la marge ?** — **que se passe-t-il ?** — **où regarder ensuite ?** " + ~ "Vert : rien à faire. Orange : à planifier. Rouge : à traiter. Survoler le titre d'un panneau (ⓘ) dit ce qu'il mesure et quoi faire s'il change de couleur."}}) -%} +{%- set y.v = y.v + 3 -%} + +{#- ---- RANGEE 1 : FAUT-IL AGIR ? ----------------------------------------------------- #} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Faut-il agir ?", "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%} +{%- set y.v = y.v + 1 -%} + +{%- set tuiles = [] -%} +{%- if fabric -%} +{%- set crit = [] -%} +{%- set avert = [] -%} +{%- for c in cat.controles -%} +{%- set _ = (crit if c.niveau == "crit" else avert).append(c.expr) -%} +{%- endfor -%} +{%- for f in cat.temperatures -%} +{%- if f.crit is not none -%}{%- set _ = crit.append("(" ~ f.expr_juge ~ ") > " ~ f.crit) -%}{%- endif -%} +{%- if f.avert is not none -%}{%- set _ = avert.append("(" ~ f.expr_juge ~ ") > " ~ f.avert) -%}{%- endif -%} +{%- endfor -%} +{%- set _ = tuiles.append({ + "titre": "Matériel des hyperviseurs", + "expr": "(count((" ~ (crit | join(") or (")) ~ ")) > 0) * 0 + 2 or (count((" ~ (avert | join(") or (")) ~ ")) > 0) * 0 + 1 or vector(0)", + "unite": "none", + "seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}, {"color": "red", "value": 2}], + "mappings": [{"type": "value", "options": {"0": {"text": "Rien à faire"}, "1": {"text": "À planifier"}, "2": {"text": "À traiter"}}}], + "desc": "Le pire verdict du tableau Matériel : températures et contrôles des disques, ventilateurs et NVMe, avec les mêmes seuils que le service Icinga « materiel ».\n\n**Si c'est orange ou rouge** : ouvrir le tableau « Set-OPS — Matériel » ; la tuile colorée y dit quel équipement, et sa description dit quoi faire."}) -%} +{%- set _ = tuiles.append({ + "titre": "Swap le plus plein (hyperviseurs)", + "expr": "max(100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"}))", + "unite": "percent", + "seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 50}, {"color": "red", "value": 90}], + "desc": "Le swap le plus rempli des trois hyperviseurs. Un hyperviseur qui swappe ralentit toutes les VM qu'il porte, sans aucune erreur visible.\n\n**Seuils** : orange à 50 %, rouge à 90 %.\n\n**Si c'est orange ou rouge** : la mémoire allouée aux VM dépasse ce que la machine peut tenir. Réduire la RAM des VM qui en ont trop (voir « Mémoire utilisée par machine »), arrêter les VM inutiles, ou déplacer une VM vers l'hyperviseur le moins chargé."}) -%} +{%- endif -%} +{%- set _ = tuiles.append({ + "titre": "Disque le plus plein (machines)", + "expr": "max(100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"}))", + "unite": "percent", + "seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}], + "desc": "Le disque système le plus rempli parmi les machines de cet écosystème.\n\n**Seuils** : orange à 80 %, rouge à 90 % — les mêmes que la sonde Icinga « disque ».\n\n**Si c'est orange ou rouge** : trouver la machine dans « Disque utilisé par machine », plus bas ; chercher ce qui grossit (journaux, cache apt, sauvegardes locales) ou agrandir son disque."}) -%} +{%- set _ = tuiles.append({ + "titre": "Correctifs de sécurité en attente", + "expr": "sum(apt_upgrades_pending{origin=~\"(?i).*securit.*\"}) or vector(0)", + "unite": "none", + "seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}], + "desc": "Nombre de paquets de sécurité Debian prêts à être installés, toutes machines confondues" ~ (" (hyperviseurs compris)" if fabric else "") ~ ".\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : les machines du socle les appliquent d'elles-mêmes dans la nuit ; si le compte ne redescend pas d'ici le lendemain, la sonde Icinga « correctifs » dit lesquelles bloquent." ~ (" Les hyperviseurs, eux, se mettent à jour à la main." if fabric else "")}) -%} +{%- set _ = tuiles.append({ + "titre": "Redémarrages en attente", + "expr": "count(node_reboot_required == 1) or vector(0)", + "unite": "none", + "seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}], + "desc": "Machines dont une mise à jour (noyau, bibliothèque système) ne prendra effet qu'au prochain redémarrage.\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : planifier les redémarrages, une machine à la fois, en dehors des heures d'usage. Tant qu'elles ne redémarrent pas, le correctif est installé mais pas actif."}) -%} +{%- set _ = tuiles.append({ + "titre": "Machines muettes", + "expr": "count(up == 0) or vector(0)", + "unite": "none", + "seuils": [{"color": "green", "value": none}, {"color": "red", "value": 1}], + "desc": "Cibles que Prometheus n'arrive plus à interroger : machine arrêtée, exportateur tombé, ou flux coupé.\n\n**Seuil** : rouge dès 1.\n\n**Si c'est rouge** : ce tableau ne voit plus cette machine, et tous ses chiffres sont figés. Vérifier qu'elle tourne, puis la sonde Icinga « collecte », qui la nomme."}) -%} + +{%- set largeur = 24 // (tuiles | length) -%} +{%- for t in tuiles -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "stat", "id": y.id, "title": t.titre, "description": t.desc, "datasource": ds, + "gridPos": {"h": 5, "w": largeur, "x": loop.index0 * largeur, "y": y.v}, + "fieldConfig": {"defaults": {"unit": t.unite, "decimals": 0, "noValue": "—", + "mappings": t.mappings | default([]), + "thresholds": {"mode": "absolute", "steps": t.seuils}}, "overrides": []}, + "options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center", + "textMode": "value", "orientation": "auto", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}}, + "targets": [{"refId": "A", "datasource": ds, "expr": t.expr}]}) -%} +{%- endfor -%} +{%- set y.v = y.v + 5 -%} + +{#- ---- RANGEE 2 : RESTE-T-IL DE LA MARGE ? ----------------------------------------- #} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Reste-t-il de la marge ?", "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%} +{%- set y.v = y.v + 1 -%} +{%- set w = 8 if fabric else 12 -%} +{%- set x = namespace(v=0) -%} +{%- if fabric -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "timeseries", "id": y.id, "title": "Mémoire et swap des hyperviseurs", "datasource": ds, + "description": "Pour chaque hyperviseur : la part de sa RAM utilisée, et celle de son swap.\n\n**Comment le lire** : une RAM plate autour de 80 % avec un swap qui monte veut dire que les VM réclament plus que la machine n'a. Le swap n'est pas une marge, c'est un ralentissement.\n\n**Seuils** (pointillés) : 80 % et 90 %.\n\n**Si une courbe de swap reste haute** : réduire la RAM allouée aux VM de cet hyperviseur, ou en déplacer une.", + "gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v}, + "fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0, "color": {"mode": "palette-classic"}, + "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}]}, + "custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 5, "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": ds, "legendFormat": "{{nodename}} · RAM", + "expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"fabric\"} / node_memory_MemTotal_bytes{job=\"fabric\"})" ~ nom}, + {"refId": "B", "datasource": ds, "legendFormat": "{{nodename}} · swap", + "expr": "100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"})" ~ nom}]}) -%} +{%- set x.v = x.v + w -%} +{%- endif -%} +{%- for p in [ + {"titre": "Disque utilisé par machine", "seuils": [80, 90], + "expr": "100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"})", + "desc": "Part utilisée du disque système de chaque machine, de la plus pleine à la moins pleine.\n\n**Seuils** : orange à 80 %, rouge à 90 %.\n\n**Si une barre passe à l'orange** : se connecter à la machine et chercher ce qui grossit (`du -xh / | sort -h | tail`) — le plus souvent des journaux ou un cache ; sinon, agrandir son disque."}, + {"titre": "Mémoire utilisée par machine", "seuils": [85, 95], + "expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"node\"} / node_memory_MemTotal_bytes{job=\"node\"})", + "desc": "Part de la RAM que chaque machine utilise vraiment (cache disque exclu).\n\n**Comment le lire** : une barre très courte sur une machine qui a beaucoup de RAM, c'est de la mémoire qu'on pourrait rendre à l'hyperviseur" ~ (" — et c'est exactement ce qui manque quand le swap des hyperviseurs monte." if fabric else ".") ~ "\n\n**Seuils** : orange à 85 %, rouge à 95 %.\n\n**Si une barre passe au rouge** : la machine manque de mémoire ; lui en donner un peu plus, ou alléger ce qu'elle fait tourner."} + ] -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "bargauge", "id": y.id, "title": p.titre, "description": p.desc, "datasource": ds, + "gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v}, + "fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0, + "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": p.seuils[0]}, {"color": "red", "value": p.seuils[1]}]}}, + "overrides": []}, + "options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}}, + "targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}", + "expr": "sort_desc(" ~ p.expr ~ nom ~ ")"}]}) -%} +{%- set x.v = x.v + w -%} +{%- endfor -%} +{%- set y.v = y.v + 9 -%} + +{#- ---- RANGEE 3 : QUE SE PASSE-T-IL ? ---------------------------------------------- #} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Que se passe-t-il ?", "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%} +{%- set y.v = y.v + 1 -%} +{%- set job = 'job="fabric"' if fabric else 'job="node"' -%} +{%- set qui = "hyperviseur" if fabric else "machine" -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "timeseries", "id": y.id, "title": "Processeur par " ~ qui, "datasource": ds, + "description": "Part du processeur occupée, par " ~ qui ~ ".\n\n**Comment le lire** : une courbe basse et plate est normale" ~ (" — 72 cœurs pour la flotte, occupés à quelques pour cent" if fabric else "") ~ ". Les pics racontent ce qui se passe : une reconstruction, une sauvegarde, un déploiement. Une courbe qui reste haute sans raison mérite qu'on cherche quelle machine consomme.\n\n**Si une courbe reste au-dessus de 80 %** : ouvrir le tableau « client metrique » pour voir quelle machine en est la cause.", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": y.v}, + "fieldConfig": {"defaults": {"unit": "percent", "min": 0, "decimals": 0, "color": {"mode": "palette-classic"}, + "custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}", + "expr": "100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"," ~ job ~ "}[5m])))" ~ nom}]}) -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "timeseries", "id": y.id, "title": "Trafic réseau par " ~ qui, "datasource": ds, + "description": "Débit entrant et sortant additionnés, par " ~ qui ~ " (interfaces physiques seulement).\n\n**Comment le lire** : " ~ ("le trafic courant est surtout celui de Ceph et des sauvegardes ; les grands pics marquent une reconstruction ou un déplacement de VM." if fabric else "les pics marquent les sauvegardes de la nuit, les mises à jour et les échanges entre services.") ~ "\n\n**Si un pic dure sans explication** : croiser avec le tableau « Journaux de la flotte » à la même heure.", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": y.v}, + "fieldConfig": {"defaults": {"unit": "Bps", "min": 0, "decimals": 1, "color": {"mode": "palette-classic"}, + "custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}", + "expr": "sum by (instance) (rate(node_network_receive_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]) + rate(node_network_transmit_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]))" ~ nom}]}) -%} +{%- set y.v = y.v + 8 -%} + +{#- ---- RANGEE 4 : OU REGARDER ENSUITE ? -------------------------------------------- #} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Où regarder ensuite ?", "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%} +{%- set y.v = y.v + 1 -%} +{%- set liens = [] -%} +{%- if fabric -%}{%- set _ = liens.append("- [**Matériel**](/d/setops-materiel) — températures, ventilateurs, disques des hyperviseurs et de la frontière.") -%}{%- endif -%} +{%- set _ = liens.append("- [**Machines**](/d/setops-client-metrique) — processeur, mémoire et disque de chaque machine, en détail.") -%} +{%- set _ = liens.append("- [**PostgreSQL**](/d/setops-postgresql) — bases de données : taille, connexions, requêtes.") -%} +{%- set _ = liens.append("- [**Journaux de la flotte**](/d/setops-journaux) — ce que les machines ont écrit, et quand.") -%} +{%- set _ = liens.append("- [**Vigie**](https://vigie." ~ domaine_interne ~ ") — les verdicts d'Icinga : ce qui est cassé, service par service.") -%} +{%- set y.id = y.id + 1 -%} +{%- set _ = panneaux.append({ + "type": "text", "id": y.id, "title": "", + "gridPos": {"h": 5, "w": 24, "x": 0, "y": y.v}, + "options": {"mode": "markdown", "content": liens | join("\n")}}) -%} + +{{ { + "uid": "setops-synthese", + "title": "Set-OPS — Synthèse", + "description": "La porte d'entrée de l'observatoire : faut-il agir, reste-t-il de la marge, que se passe-t-il, où regarder ensuite.", + "tags": ["set-ops", "derive", "synthese"], + "timezone": "browser", + "refresh": "1m", + "time": {"from": "now-7d", "to": "now"}, + "schemaVersion": 39, + "version": 1, + "graphTooltip": 1, + "templating": {"list": [ + {"name": "ds_prom", "label": "Source Prometheus", "type": "datasource", "query": "prometheus", "hide": 2, "current": {}} + ]}, + "panels": panneaux +} | tojson }}