La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping. 16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF 11 cibles Prometheus dont 3 hyperviseurs, job fabric separe LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle : leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl de la machine qui tient tout le reste. La frontiere, elle, n accueille aucun agent : controle actif, une entree par PATTE, parce qu une interface eteinte coupe une zone pendant que les autres vont bien. ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne connait pas les reseaux du site, et sa route par defaut est GELEE (D-57). Le porteur de sante y expirait en 20 s. LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant exactement le raisonnement qu on venait de refaire. Sa liste s arretait a 10.0.34.0/24 quand le site en declare six : les zones sauvegarde et supervision sont nees, les routes n ont pas suivi. Le symptome ne ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis a un probleme de reseau chez l exploitant. make routes-fabric-etat compare desormais TROIS choses : zones declarees, routes declarees dans /etc/network/interfaces, routes vivantes dans le noyau. Le cas le plus traitre est vivante mais non declaree : tout fonctionne, la supervision est verte, et la panne attend la prochaine maintenance. Eprouvee dans les deux sens. Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un boitier - l underlay le disait en prose, illisible par le moteur ; il porte desormais etat: reserve. Et un service passif n existe que pour un hote qui peut POUSSER : l appartenance a un groupe sert deux choses qui ne coincident pas toujours, a qui l on deploie et de qui l on attend un rapport. Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
151 lines
7.5 KiB
Django/Jinja
151 lines
7.5 KiB
Django/Jinja
#jinja2: comment_start_string:'{=#', comment_end_string:'#=}'
|
|
#!/bin/bash
|
|
# GENERE par Set-OPS (role client_sante). Ne pas editer a la main.
|
|
#
|
|
# (Le gabarit redefinit le delimiteur de commentaire Jinja : `${#tableau[@]}` contient
|
|
# la sequence `{#`, que Jinja lirait comme un debut de commentaire — et le rendu
|
|
# echouait sur « Missing end of comment tag ». Le shell a besoin de `${#...}` ; c'est
|
|
# donc a Jinja de s'ecarter.)
|
|
#
|
|
# CE NOEUD RAPPORTE SES UNITES EN ECHEC — parce que personne d'autre ne peut le voir.
|
|
#
|
|
# CE QUI A REVELE LE BESOIN (2026-09-09). `openipmi.service` echouait a CHAQUE demarrage
|
|
# sur les quatorze machines depuis le 2026-09-02. `systemctl --failed` rendait pourtant
|
|
# ZERO partout — non parce qu'elles allaient bien, mais parce qu'aucune n'avait REDEMARRE
|
|
# depuis. Six jours et vingt heures pour la premiere. Il a fallu qu'un humain redemarre
|
|
# une machine pour que le defaut existe aux yeux de quelqu'un.
|
|
#
|
|
# Un controle qui ne peut echouer qu'au demarrage ne mesure rien tant que rien ne demarre.
|
|
#
|
|
# POURQUOI UN RAPPORT PASSIF, ET PAS UN CONTROLE ACTIF. Un controle actif ne voit pas la
|
|
# machine MUETTE : si elle ne repond plus, la sonde echoue et on croit a un probleme de
|
|
# reseau. Ici c'est le noeud qui parle, avec un `ttl` : sans nouvelle, Icinga perime le
|
|
# service tout seul. Le silence alerte autant que l'echec — et le silence est precisement
|
|
# ce qui n'a alerte personne.
|
|
set -uo pipefail
|
|
|
|
API="{{ client_sante_icinga_url }}"
|
|
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[|metriques] $4=ttl
|
|
local charge reponse
|
|
# 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
|
|
# 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
|
|
}
|
|
|
|
# `--plain` et `--no-legend` : la sortie doit etre analysable, pas jolie. Sans eux,
|
|
# systemd insere une puce « ● » en tete de ligne, qui devient le premier champ.
|
|
# SA PROPRE UNITE EST EXCLUE, ET CE N'EST PAS SE MENAGER.
|
|
#
|
|
# Mesure du 2026-09-09 sur le site : le pare-feu a bloque les rapports pendant une heure,
|
|
# `setops-sante.service` a donc echoue, et cinq machines se sont mises a rapporter
|
|
# CRITIQUE en citant... leur propre rapporteur. Le blocage corrige, l'accusation restait :
|
|
# systemd garde l'etat `failed` jusqu'a un `reset-failed`. Un rapporteur qui trebuche une
|
|
# fois s'accuserait indefiniment.
|
|
#
|
|
# La sante du rapporteur est DEJA mesuree, et mieux : par la FRAICHEUR de ses envois. S'il
|
|
# ne peut plus parler, Icinga perime le service tout seul (`ttl`) — ce qui se voit
|
|
# precisement quand il ne peut PAS ecrire, alors que sa propre unite en echec ne se voit
|
|
# que quand il le peut. Se compter soi-meme, c'est mesurer deux fois la meme chose, dont
|
|
# une fois mal.
|
|
mapfile -t echecs < <(systemctl list-units --state=failed --plain --no-legend 2>/dev/null \
|
|
| awk '{print $1}' | grep -vx 'setops-sante.service')
|
|
|
|
{% if client_sante_unites_tolerees %}
|
|
# UNITES TOLEREES, NOMMEES UNE PAR UNE. Jamais un motif large : un filtre qui cache une
|
|
# unite en cache d'autres, et on ne s'en apercoit que le jour ou l'on cherche pourquoi
|
|
# rien n'a alerte.
|
|
tolerees=({% for u in client_sante_unites_tolerees %}"{{ u }}" {% endfor %})
|
|
restantes=()
|
|
for u in "${echecs[@]}"; do
|
|
garder=1
|
|
for t in "${tolerees[@]}"; do [[ "$u" == "$t" ]] && garder=0 && break; done
|
|
[[ $garder -eq 1 ]] && restantes+=("$u")
|
|
done
|
|
echecs=("${restantes[@]}")
|
|
{% endif %}
|
|
|
|
# LES SONDES DES ROLES, APRES LA SIENNE.
|
|
#
|
|
# Chaque role depose SA sonde dans ce repertoire ; le porteur ne sait pas ce qu'elles
|
|
# mesurent, et c'est le but : ajouter une sonde n'oblige pas a toucher a ce script.
|
|
# Contrat : une ligne sur stdout, 0/1/2 en code de sortie
|
|
# (docs/supervision-conception.md).
|
|
#
|
|
# UNE SONDE QUI JETTE N'EST PAS UNE SONDE VERTE. Sans ce traitement, un script casse
|
|
# sortirait en 127 et Icinga recevrait un code hors bornes — ou rien. On rapporte donc
|
|
# CRITIQUE, en le disant : le pire serait qu'un script mort produise un service muet, que
|
|
# le `ttl` finirait par perimer sans que personne sache pourquoi.
|
|
sondes() {
|
|
local f nom sortie code
|
|
for f in /usr/local/lib/setops/sondes/*.sh; do
|
|
[[ -x "$f" ]] || continue
|
|
nom=$(basename "$f" .sh)
|
|
# 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
|
|
rapporter "$nom" "$code" "${sortie:-(aucun message)}" "{{ client_sante_ttl_sondes }}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
n=${#echecs[@]}
|
|
if [[ $n -eq 0 ]]; then
|
|
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
|
sondes
|
|
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
|
fi
|
|
|
|
# CRITIQUE des la premiere, et non un seuil. Une unite en echec est soit un vrai
|
|
# probleme, soit du bruit qu'il faut retirer : dans les deux cas il faut agir. Un seuil
|
|
# 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 ))
|