From 988d35745e1ffe448151301ecec1449a4fcdf70e Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 9 Sep 2026 22:14:30 -0400 Subject: [PATCH] sondes : le contrat ETAIT celui de Nagios, sans le savoir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Question posee : « tu connais le paquet monitoring-plugins ? » Oui — et le contrat que docs/supervision-conception.md decrivait quelques heures plus tot EST le sien, mot pour mot. Je l avais reinvente sans le nommer, alors que positionnement.md dit l inverse : adopter aux seuils, ne pas reimplementer. Le document nomme desormais l API des greffons Nagios, ajoute le code 3 (INCONNU) et la partie « | metriques » qui manquaient, et dit la consequence : un greffon standard EST une sonde valide, sans colle. Le paquet en fournit 54. On n ecrit du shell que lorsque la verite a mesurer est propre a Set-OPS. Le porteur separe maintenant texte et performance_data. ESSAYER UN VRAI GREFFON A REVELE DEUX DEFAUTS DU PORTEUR. Un envoi refuse faisait taire TOUTES les sondes suivantes : rapporter sortait en exit 1. Une sonde deposee mais non declaree supprimait le rapport des autres, sante comprise — le tableau ne devenait pas rouge, il devenait vide, et le ttl le perimait des heures plus tard sans dire pourquoi. Aucun delai de garde sur les sondes. check_disk 2.4.0-3+deb13u1 sur mon-01 tourne sans fin (etat R) quels que soient ses arguments : un greffon STANDARD, sur une machine saine, qui boucle. Sans garde il figeait le rapport entier toutes les quinze minutes. Chaque sonde tourne desormais sous timeout 20 ; au-dela on rapporte INCONNU en le disant. La lecon n est pas que les greffons standards sont mauvais : c est qu adopter un standard ne dispense pas de l eprouver, et que le porteur doit survivre a une sonde qui se comporte mal. CONSTATE AU PASSAGE, NON CORRIGE : la configuration d exemple d Icinga crie en permanence sur mon-01 — swap sur une VM sans swap, http sur un port ou rien n ecoute, apt pour un paquet. Trois alarmes qui ne peuvent que rester rouges, dans le seul endroit qui doit rester lisible. Etat : certificat 14/14, sante 14/14 au tenant ; 7/7 au site. make prouver : CONFORME, 64 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 53 +++++++++++++++++++ docs/supervision-conception.md | 22 ++++++-- roles/client_sante/defaults/main.yml | 4 ++ .../client_sante/templates/setops-sante.sh.j2 | 51 +++++++++++++++--- 4 files changed, 119 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd3402..b6055db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # CHANGELOG — Set-OPS +## 2026-09-09 (7) — Le contrat des sondes ETAIT celui de Nagios, sans le savoir + +Question posee : « tu connais le paquet `monitoring-plugins` ? » + +Oui — et le contrat que `docs/supervision-conception.md` decrivait quelques heures plus +tot EST le sien, mot pour mot : une ligne sur stdout, `0/1/2` en code de sortie. Je l'avais +donc **reinvente sans le nommer**, alors que `positionnement.md` dit exactement l'inverse : +*adopter aux seuils, ne pas reimplementer*. + +Le document nomme desormais l'**API des greffons Nagios**, ajoute `3 = INCONNU` et la +partie `| metriques` qui manquaient, et dit la consequence : **un greffon standard EST une +sonde valide, sans la moindre colle**. Le paquet en fournit 54 — `check_disk`, +`check_load`, `check_procs`, `check_ntp_time`, `check_smtp`, `check_pgsql`... On n'ecrit du +shell que lorsque la verite a mesurer est propre a Set-OPS, comme +`client_pki/certificat`, qui compare l'empreinte SERVIE a celle du disque : aucun greffon +ne sait ca. + +Le porteur separe maintenant le texte des metriques (`performance_data`), parce que c'est +ce que le contrat porte et qu'Icinga sait les tracer. + +### Essayer un vrai greffon a revele DEUX defauts du porteur + +**1. Un envoi refuse faisait taire toutes les sondes suivantes.** `rapporter` sortait en +`exit 1` : une sonde deposee mais non declaree — donc refusee par le filtre d'API — +supprimait le rapport de toutes les autres, `sante` comprise. Le tableau ne devenait pas +rouge, il devenait VIDE, et le `ttl` le perimait des heures plus tard sans dire pourquoi. +*Un rapporteur qui ne peut pas dire UNE chose doit quand meme dire les autres.* + +**2. Aucun delai de garde sur les sondes.** Et ce n'est pas theorique : + + check_disk 2.4.0-3+deb13u1 sur mon-01 : etat R, ne rend jamais la main + quels que soient ses arguments (-p /, sans -p, seuils en % ou en G) + +Un greffon STANDARD, sur une machine saine, qui **boucle**. Sans delai de garde il figeait +le rapport entier, toutes les quinze minutes, indefiniment. Chaque sonde tourne desormais +sous `timeout 20` ; au-dela, on rapporte INCONNU en le disant. + +*La lecon n'est pas « les greffons standards sont mauvais ».* C'est qu'adopter un standard +ne dispense pas de l'eprouver — et que le porteur doit survivre a une sonde qui se comporte +mal, standard ou maison. + +### Trois voyants rouges permanents, sur l'hote de supervision + +Constate en cherchant : la configuration d'exemple livree par Icinga surveille `localhost` +et crie en permanence sur `mon-01` — + + swap : SWAP CRITICAL - 0% free (une VM sans swap) + http : connect to 127.0.0.1:80 (rien n'ecoute la) + apt : 1 package upgradable + +Trois alarmes qui ne peuvent que rester rouges, dans le seul endroit qui doit rester +lisible. **Non corrige** — c'est une decision d'exploitation, pas une correction de code. + ## 2026-09-09 (6) — La supervision se DECLARE dans le role, comme les flux **64 preuves (P01-P64).** Nouveau document : `docs/supervision-conception.md`. diff --git a/docs/supervision-conception.md b/docs/supervision-conception.md index 4c327e1..a5023f9 100644 --- a/docs/supervision-conception.md +++ b/docs/supervision-conception.md @@ -28,18 +28,34 @@ C'est la classe d'échec que ce dépôt nomme partout ailleurs, en version docum carte dit ce qui est surveillé, et personne ne surveille. *Une intention écrite n'est pas une mesure.* -## Le contrat d'une sonde +## Le contrat d'une sonde : c'est celui des greffons Nagios Une sonde est un **script local**, déposé par le rôle qui la possède, dans `/usr/local/lib/setops/sondes/.sh` (0750, root). | | | |---|---| -| **sortie standard** | une ligne, le message lisible par un humain | -| **code de sortie** | `0` OK · `1` AVERTISSEMENT · `2` CRITIQUE | +| **sortie standard** | `TEXTE lisible` puis, optionnellement, `\| métriques` | +| **code de sortie** | `0` OK · `1` AVERTISSEMENT · `2` CRITIQUE · `3` INCONNU | | **réseau** | aucun besoin : c'est le porteur qui pousse le résultat | | **durée** | courte ; le porteur passe toutes les 15 min | +> **Ce contrat n'est pas de nous.** C'est l'**API des greffons Nagios**, que +> `monitoring-plugins` implémente depuis vingt ans et qu'Icinga parle nativement. La +> première rédaction de ce document la décrivait sans la nommer — autrement dit la +> réinventait. `positionnement.md` dit l'inverse : *adopter aux seuils, ne pas +> réimplémenter*. + +**La conséquence pratique est grande.** Un greffon standard **est** une sonde valide, sans +la moindre colle : `check_disk`, `check_load`, `check_procs`, `check_ntp_time`, +`check_file_age`, `check_smtp`, `check_pgsql`… Le paquet en fournit **54**. Une sonde ne +s'écrit à la main que lorsque la vérité à mesurer est propre à Set-OPS — et c'était le cas +pour `client_pki/certificat`, qui compare l'empreinte *servie* à celle du disque : aucun +greffon ne sait ça. + +Écrire du shell là où un greffon existe, c'est se donner du code à maintenir *et* se priver +de vingt ans de cas limites déjà rencontrés par d'autres. + Le rôle qui possède la sonde la **déploie lui-même** : il connaît ses chemins, ses secrets, sa vérité de terrain. Il la **déclare** dans `meta/supervision.yml`, et c'est cette déclaration que le moteur lit. diff --git a/roles/client_sante/defaults/main.yml b/roles/client_sante/defaults/main.yml index 4944466..0a8a69a 100644 --- a/roles/client_sante/defaults/main.yml +++ b/roles/client_sante/defaults/main.yml @@ -36,3 +36,7 @@ client_sante_unites_tolerees: [] # plus lente (une poignee TLS, une requete), et on ne veut pas qu'un hoquet la perime. # Six passages du porteur. client_sante_ttl_sondes: 5400 + +# Delai de garde de CHAQUE sonde. Une sonde est cense repondre vite ; celle qui depasse +# n'a pas de verdict, et on le dit (INCONNU) plutot que de figer tout le rapport. +client_sante_delai_sonde: 20 diff --git a/roles/client_sante/templates/setops-sante.sh.j2 b/roles/client_sante/templates/setops-sante.sh.j2 index bbcaea3..083a8f9 100644 --- a/roles/client_sante/templates/setops-sante.sh.j2 +++ b/roles/client_sante/templates/setops-sante.sh.j2 @@ -28,20 +28,41 @@ API="https://{{ client_sante_icinga_hote }}.{{ domaine_interne }}:5665" TTL={{ client_sante_ttl_icinga }} MOI="{{ inventory_hostname }}" MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)" +# Compte des envois refuses : l'unite systemd doit finir en echec si le rapporteur n'a +# pas pu parler, sinon rien ne distingue « tout va bien » de « personne n'ecoute ». +ECHECS_RAPPORT=0 -rapporter() { # $1=service $2=code $3=texte $4=ttl +rapporter() { # $1=service $2=code $3=texte[|metriques] $4=ttl local charge reponse - charge=$(python3 -c 'import json,sys; print(json.dumps({ - "type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]), - "plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \ + # LES METRIQUES SONT SEPAREES DU TEXTE, parce que le contrat des greffons Nagios les + # separe : `TEXTE | cle=valeur;avert;crit;min;max`. Tout pousser dans `plugin_output` + # afficherait une ligne illisible a l'ecran ET priverait Icinga de donnees qu'il sait + # tracer. C'est en adoptant le contrat qu'on herite de ce qu'il porte. + charge=$(python3 -c 'import json,sys +texte, _, perf = sys.argv[3].partition("|") +charge = {"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]), + "plugin_output": texte.strip() or "(aucun message)", "ttl": int(sys.argv[4])} +if perf.strip(): + charge["performance_data"] = perf.split() +print(json.dumps(charge))' \ "${MOI}!$1" "$2" "$3" "$4") reponse=$(curl -sS --cacert "{{ client_sante_ca_verification }}" --max-time 20 \ -u "{{ client_sante_icinga_utilisateur }}:${MOTDEPASSE}" \ -H 'Accept: application/json' -H 'Content-Type: application/json' \ -X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1) if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then - echo "ECHEC du rapport Icinga : ${reponse}" >&2 - exit 1 + # ON N'INTERROMPT PAS — mesure du 2026-09-09. + # + # `exit 1` ici faisait taire TOUTES LES SONDES SUIVANTES. Une sonde inconnue d'Icinga + # (deposee mais pas declaree, donc refusee par le filtre d'API) suffisait a supprimer + # le rapport de toutes les autres — y compris `sante`. Le tableau ne devenait pas + # rouge : il devenait VIDE, et le `ttl` le perimait des heures plus tard sans que rien + # ne dise pourquoi. + # + # Un rapporteur qui ne peut pas dire UNE chose doit quand meme dire les autres. + echo "ECHEC du rapport Icinga pour « $1 » : ${reponse}" >&2 + ECHECS_RAPPORT=$((ECHECS_RAPPORT + 1)) + return 1 fi } @@ -93,7 +114,20 @@ sondes() { for f in /usr/local/lib/setops/sondes/*.sh; do [[ -x "$f" ]] || continue nom=$(basename "$f" .sh) - sortie=$("$f" 2>&1); code=$? + # DELAI DE GARDE SUR CHAQUE SONDE — mesure du 2026-09-09. + # + # Sans lui, une sonde qui BOUCLE fige le rapport entier, toutes les quinze minutes, + # indefiniment. Ce n'est pas theorique : `check_disk` 2.4.0-3+deb13u1 tourne sans fin + # sur `mon-01`, quels que soient ses arguments (etat `R`, pas un blocage d'E/S). + # Un greffon STANDARD, sur une machine saine. + # + # Une sonde qui n'a pas rendu son verdict en 20 s n'a pas de verdict : on rapporte + # INCONNU (3) en le disant, plutot que de laisser le porteur mourir en silence. + sortie=$(timeout {{ client_sante_delai_sonde }} "$f" 2>&1); code=$? + if (( code == 124 )); then + rapporter "$nom" 3 "Sonde ${nom} sans reponse apres {{ client_sante_delai_sonde }} s — aucun verdict." "{{ client_sante_ttl_sondes }}" + continue + fi if (( code > 3 )); then rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "{{ client_sante_ttl_sondes }}" else @@ -106,7 +140,7 @@ n=${#echecs[@]} if [[ $n -eq 0 ]]; then rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}" sondes - exit 0 + exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 )) fi # CRITIQUE des la premiere, et non un seuil. Une unite en echec est soit un vrai @@ -114,3 +148,4 @@ fi # ferait vivre le bruit indefiniment, et c'est exactement ce qu'on vient de corriger. rapporter "sante" 2 "$n unite(s) systemd en echec : ${echecs[*]}" "${TTL}" sondes +exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))