#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 ))