sondes : le contrat 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. 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
90228cb55e
commit
988d35745e
4 changed files with 119 additions and 11 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -1,5 +1,58 @@
|
||||||
# CHANGELOG — Set-OPS
|
# 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
|
## 2026-09-09 (6) — La supervision se DECLARE dans le role, comme les flux
|
||||||
|
|
||||||
**64 preuves (P01-P64).** Nouveau document : `docs/supervision-conception.md`.
|
**64 preuves (P01-P64).** Nouveau document : `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
|
carte dit ce qui est surveillé, et personne ne surveille. *Une intention écrite n'est pas
|
||||||
une mesure.*
|
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
|
Une sonde est un **script local**, déposé par le rôle qui la possède, dans
|
||||||
`/usr/local/lib/setops/sondes/<nom>.sh` (0750, root).
|
`/usr/local/lib/setops/sondes/<nom>.sh` (0750, root).
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **sortie standard** | une ligne, le message lisible par un humain |
|
| **sortie standard** | `TEXTE lisible` puis, optionnellement, `\| métriques` |
|
||||||
| **code de sortie** | `0` OK · `1` AVERTISSEMENT · `2` CRITIQUE |
|
| **code de sortie** | `0` OK · `1` AVERTISSEMENT · `2` CRITIQUE · `3` INCONNU |
|
||||||
| **réseau** | aucun besoin : c'est le porteur qui pousse le résultat |
|
| **réseau** | aucun besoin : c'est le porteur qui pousse le résultat |
|
||||||
| **durée** | courte ; le porteur passe toutes les 15 min |
|
| **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
|
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
|
secrets, sa vérité de terrain. Il la **déclare** dans `meta/supervision.yml`, et c'est
|
||||||
cette déclaration que le moteur lit.
|
cette déclaration que le moteur lit.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
# plus lente (une poignee TLS, une requete), et on ne veut pas qu'un hoquet la perime.
|
||||||
# Six passages du porteur.
|
# Six passages du porteur.
|
||||||
client_sante_ttl_sondes: 5400
|
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
|
||||||
|
|
|
||||||
|
|
@ -28,20 +28,41 @@ API="https://{{ client_sante_icinga_hote }}.{{ domaine_interne }}:5665"
|
||||||
TTL={{ client_sante_ttl_icinga }}
|
TTL={{ client_sante_ttl_icinga }}
|
||||||
MOI="{{ inventory_hostname }}"
|
MOI="{{ inventory_hostname }}"
|
||||||
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
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
|
local charge reponse
|
||||||
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
# LES METRIQUES SONT SEPAREES DU TEXTE, parce que le contrat des greffons Nagios les
|
||||||
"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]),
|
# separe : `TEXTE | cle=valeur;avert;crit;min;max`. Tout pousser dans `plugin_output`
|
||||||
"plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \
|
# 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")
|
"${MOI}!$1" "$2" "$3" "$4")
|
||||||
reponse=$(curl -sS --cacert "{{ client_sante_ca_verification }}" --max-time 20 \
|
reponse=$(curl -sS --cacert "{{ client_sante_ca_verification }}" --max-time 20 \
|
||||||
-u "{{ client_sante_icinga_utilisateur }}:${MOTDEPASSE}" \
|
-u "{{ client_sante_icinga_utilisateur }}:${MOTDEPASSE}" \
|
||||||
-H 'Accept: application/json' -H 'Content-Type: application/json' \
|
-H 'Accept: application/json' -H 'Content-Type: application/json' \
|
||||||
-X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1)
|
-X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1)
|
||||||
if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then
|
if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then
|
||||||
echo "ECHEC du rapport Icinga : ${reponse}" >&2
|
# ON N'INTERROMPT PAS — mesure du 2026-09-09.
|
||||||
exit 1
|
#
|
||||||
|
# `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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +114,20 @@ sondes() {
|
||||||
for f in /usr/local/lib/setops/sondes/*.sh; do
|
for f in /usr/local/lib/setops/sondes/*.sh; do
|
||||||
[[ -x "$f" ]] || continue
|
[[ -x "$f" ]] || continue
|
||||||
nom=$(basename "$f" .sh)
|
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
|
if (( code > 3 )); then
|
||||||
rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "{{ client_sante_ttl_sondes }}"
|
rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "{{ client_sante_ttl_sondes }}"
|
||||||
else
|
else
|
||||||
|
|
@ -106,7 +140,7 @@ n=${#echecs[@]}
|
||||||
if [[ $n -eq 0 ]]; then
|
if [[ $n -eq 0 ]]; then
|
||||||
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
||||||
sondes
|
sondes
|
||||||
exit 0
|
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CRITIQUE des la premiere, et non un seuil. Une unite en echec est soit un vrai
|
# 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.
|
# 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}"
|
rapporter "sante" 2 "$n unite(s) systemd en echec : ${echecs[*]}" "${TTL}"
|
||||||
sondes
|
sondes
|
||||||
|
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue