diff --git a/CHANGELOG.md b/CHANGELOG.md index 555b667..61513d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # CHANGELOG — Set-OPS +## 2026-09-10 (2) — `cache-apt` scindee, et le defaut que la scission a revele + +### La scission + +`cache-apt` fondait deux causes dont les DELAIS different : « ne repond pas » arrete tout +`apt` de l'ecosysteme — on agit dans la minute — tandis que « volume a 90 % » est un billet +pour demain. Les fondre obligeait soit a reveiller quelqu'un pour un disque, soit a traiter +une panne comme un billet. + +Deux sondes desormais, chacune avec son etat, son historique et son acquittement. Prouve +qu'elles sont INDEPENDANTES, et c'etait tout l'enjeu : + + port ferme -> cache-apt [2] CRITIQUE cache-apt-volume [0] OK + seuil impossible-> cache-apt [0] OK cache-apt-volume [1] AVERTISSEMENT + +### Ce que la verification a revele : `moteur` aurait alarme PARCE QUE tout allait bien + +En verifiant que les deux services arrivaient bien dans Icinga, un resultat pousse et +accepte (`code 200`) n'apparaissait pas en base. Hypothese testee et confirmee : +**IcingaDB n'ECRIT `service_state` QUE SUR CHANGEMENT D'ETAT.** Un OK identique repete ne +produit aucune ecriture ; un AVERTISSEMENT pousse ensuite est ecrit en 13 secondes. + +Or la sonde `moteur`, ecrite quelques heures plus tot, lisait exactement +`max(last_update)` de `service_state` pour juger que « l'etat est frais ». Elle mesurait +donc le CHANGEMENT, pas la fraicheur. + +**Consequence : sur un ecosysteme parfaitement stable — celui qu'on veut — plus rien ne +change, `last_update` vieillit, et la sonde serait passee en avertissement a 15 minutes +puis en critique a 90.** Une alarme qui se declenche PARCE QUE tout va bien, avec un delai +qui l'aurait rendue difficile a rattacher a sa cause. + +C'est le piege que `docs/supervision-conception.md` interdit — *une alarme toujours allumee +apprend a ne plus regarder* — sous sa forme la plus sournoise : differee. + +**La bonne source existait** : `icingadb_instance` porte le battement du synchroniseur, +ecrit en continu qu'il y ait ou non des changements. Releve a **1 seconde** sur un systeme +sain. Les seuils passent de 15/90 minutes a **1/5 minutes** : sur un battement, une minute +de silence est deja anormale. + +Controle negatif rejoue par parametre. + +### Etat + +Dix-huit services, tous verts sauf `sauvegarde` a 7/9 — deux noeuds sans donnee a +emporter, deja connu et confirme. + ## 2026-09-10 — Chaque role porte desormais sa sonde **Quatorze sondes**, declarees dans `meta/supervision.yml`, deposees par le role qui diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md index d1e9500..c8c6b30 100644 --- a/docs/audit/preuve-2026-09-10.md +++ b/docs/audit/preuve-2026-09-10.md @@ -76,7 +76,7 @@ | P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | | P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | | P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | -| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 14 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_dovecot/boites, serveur | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 15 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_artefacts/cache-apt-vol | ## Couverture des affirmations ✅ du registre diff --git a/roles/serveur_artefacts/meta/supervision.yml b/roles/serveur_artefacts/meta/supervision.yml index 1035c79..5683edb 100644 --- a/roles/serveur_artefacts/meta/supervision.yml +++ b/roles/serveur_artefacts/meta/supervision.yml @@ -3,5 +3,9 @@ sondes: - nom: cache-apt ttl: 5400 - raison: Le cache d'artefacts repond-il sur son port, et lui reste-t-il de la place ? Un cache muet - fait echouer tout apt de l'ecosysteme ; un cache plein le fait en silence. + raison: Le cache d'artefacts repond-il ? S'il se tait, TOUT apt de l'ecosysteme echoue — on agit dans + la minute. + - nom: cache-apt-volume + ttl: 5400 + raison: 'Reste-t-il de la place au cache ? Un cache plein cesse d''ecrire sans se plaindre, et l''echec + apparait chez les clients. Verdict distinct de la disponibilite : il appelle un billet, pas un reveil.' diff --git a/roles/serveur_artefacts/tasks/main.yml b/roles/serveur_artefacts/tasks/main.yml index 86fe80b..dec6e74 100644 --- a/roles/serveur_artefacts/tasks/main.yml +++ b/roles/serveur_artefacts/tasks/main.yml @@ -137,3 +137,11 @@ owner: root group: root mode: "0750" + +- name: Deposer la sonde « cache-apt-volume » + ansible.builtin.template: + src: sonde-cache-apt-volume.sh.j2 + dest: /usr/local/lib/setops/sondes/cache-apt-volume.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_artefacts/templates/sonde-cache-apt-volume.sh.j2 b/roles/serveur_artefacts/templates/sonde-cache-apt-volume.sh.j2 new file mode 100644 index 0000000..1c6d275 --- /dev/null +++ b/roles/serveur_artefacts/templates/sonde-cache-apt-volume.sh.j2 @@ -0,0 +1,31 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_artefacts). Ne pas editer a la main. +# +# SONDE « cache-apt-volume » — reste-t-il de la place au cache ? +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_artefacts_sonde_pct_avert` / `_pct_crit`. +# +# SEPAREE DE LA DISPONIBILITE (2026-09-10) : ce verdict-ci appelle un billet, pas un +# reveil. Un cache plein ne se plaint pas — il cesse simplement de servir ce qu'il n'a pas +# pu ecrire, et l'echec apparait chez les CLIENTS, loin d'ici. D'ou des seuils qui +# previennent au lieu de constater. +set -uo pipefail + +REP={{ serveur_artefacts_sonde_repertoire }} +AVERT={{ serveur_artefacts_sonde_pct_avert }} +CRIT={{ serveur_artefacts_sonde_pct_crit }} + +pct=$(df --output=pcent "${REP}" 2>/dev/null | tail -1 | tr -dc '0-9') +[[ -n "${pct}" ]] || { echo "Volume du cache illisible (${REP})."; exit 2; } +taille=$(du -sh "${REP}" 2>/dev/null | cut -f1) + +if (( pct >= CRIT )); then + echo "Volume du cache a ${pct}% (${taille}) — il va cesser d'ecrire, et l'echec se verra chez les clients.|pct=${pct}%;${AVERT};${CRIT};0;100" + exit 2 +fi +if (( pct >= AVERT )); then + echo "Volume du cache a ${pct}% (${taille}), seuil ${AVERT}%.|pct=${pct}%;${AVERT};${CRIT};0;100" + exit 1 +fi +echo "Volume du cache : ${taille} sur un volume a ${pct}%.|pct=${pct}%;${AVERT};${CRIT};0;100" diff --git a/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 b/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 index ef9154f..17ae8c8 100644 --- a/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 +++ b/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 @@ -1,34 +1,26 @@ #!/bin/bash # GENERE par Set-OPS (role serveur_artefacts). Ne pas editer a la main. # -# SONDE « cache-apt » — le cache apt repond, et il lui reste de la place +# SONDE « cache-apt » — le cache repond-il ? # -# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne sur la -# sortie standard, 0 OK / 1 AVERTISSEMENT / 2 CRITIQUE. Le porteur `client_sante` pousse. -# Mise en defaut PAR PARAMETRE : `serveur_artefacts_sonde_port` (port ferme) ou `_pct_crit` (seuil impossible). +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_artefacts_sonde_port` (port ferme). +# +# ELLE NE MESURE PLUS LE VOLUME, ET C'EST LE POINT (2026-09-10). Les deux etaient fondus +# dans une seule sonde. Or « le cache ne repond pas » arrete TOUT `apt` de l'ecosysteme — +# on agit dans la minute — tandis que « le volume est a 90 % » est un billet pour demain. +# Meme personne, meme geste peut-etre, mais pas le meme DELAI : les fondre obligeait soit +# a reveiller quelqu'un pour un disque, soit a traiter une panne comme un billet. +# +# Le volume vit desormais dans `cache-apt-volume`, avec son propre etat, son propre +# historique et son propre acquittement. set -uo pipefail PORT={{ serveur_artefacts_sonde_port }} -REP={{ serveur_artefacts_sonde_repertoire }} -AVERT={{ serveur_artefacts_sonde_pct_avert }} -CRIT={{ serveur_artefacts_sonde_pct_crit }} # ON DEMANDE UNE PAGE, ON NE SE CONTENTE PAS D'UN connect(). Un `connect()` reussit contre # un processus mort-vivant qui n'a plus rien a servir ; seule une reponse prouve le # service. C'est la lecon du depot : « un connect() TCP ne prouve rien ». code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "http://127.0.0.1:${PORT}/acng-report.html" 2>/dev/null) -[[ "${code}" == "200" ]] || { echo "Le cache ne repond pas sur ${PORT} (HTTP ${code:-aucune reponse})."; exit 2; } - -pct=$(df --output=pcent "${REP}" 2>/dev/null | tail -1 | tr -dc '0-9') -[[ -n "${pct}" ]] || { echo "Cache joignable, mais ${REP} illisible."; exit 1; } -taille=$(du -sh "${REP}" 2>/dev/null | cut -f1) - -if (( pct >= CRIT )); then - echo "Cache a ${pct}% du volume (${taille}) — apt echouera bientot pour tout l'ecosysteme." - exit 2 -fi -if (( pct >= AVERT )); then - echo "Cache a ${pct}% du volume (${taille}), seuil ${AVERT}%." - exit 1 -fi -echo "Cache apt : repond sur ${PORT}, ${taille} sur un volume a ${pct}%.|pct=${pct}%;${AVERT};${CRIT};0;100" +[[ "${code}" == "200" ]] || { echo "Le cache ne repond pas sur ${PORT} (HTTP ${code:-aucune reponse}) — tout apt de l'ecosysteme echoue."; exit 2; } +echo "Cache apt : repond sur ${PORT}." diff --git a/roles/serveur_icinga/defaults/main.yml b/roles/serveur_icinga/defaults/main.yml index 1270270..5ad0a57 100644 --- a/roles/serveur_icinga/defaults/main.yml +++ b/roles/serveur_icinga/defaults/main.yml @@ -109,7 +109,9 @@ serveur_icinga_destinataire: "" # --- Sonde de supervision ----------------------------------------------------------- serveur_icinga_sonde_config_db: /etc/icingadb/config.yml -# Les controles passifs ont un `ttl` de 45 min au plus long ; une base qui n'a rien recu -# depuis deux fois cela ne reflete plus rien. -serveur_icinga_sonde_age_avert: 900 -serveur_icinga_sonde_age_crit: 5400 +# SEUILS SUR LE BATTEMENT D'ICINGADB, qui bat en continu (releve : 1 s sur un systeme +# sain). Les premiers seuils — 15 et 90 min — visaient la fraicheur des ETATS, et +# auraient alarme sur un ecosysteme stable ou plus rien ne change. Sur un battement, une +# minute de silence est deja anormale. +serveur_icinga_sonde_age_avert: 60 +serveur_icinga_sonde_age_crit: 300 diff --git a/roles/serveur_icinga/templates/sonde-moteur.sh.j2 b/roles/serveur_icinga/templates/sonde-moteur.sh.j2 index e81c298..b13a119 100644 --- a/roles/serveur_icinga/templates/sonde-moteur.sh.j2 +++ b/roles/serveur_icinga/templates/sonde-moteur.sh.j2 @@ -23,17 +23,33 @@ base=$(grep -m1 -A5 '^database:' "${CONF}" 2>/dev/null | grep -m1 'database:' | util=$(grep -m1 -A6 '^database:' "${CONF}" 2>/dev/null | grep -m1 'user:' | awk '{print $2}') [[ -n "${hote}" && -n "${mdp}" ]] || { echo "Configuration IcingaDB illisible (${CONF})."; exit 2; } +# LE BATTEMENT D'ICINGADB, PAS LA FRAICHEUR DES ETATS (correction du 2026-09-10). +# +# La premiere version lisait `max(last_update)` de `service_state`. Mesure : IcingaDB +# n'ECRIT QUE SUR CHANGEMENT D'ETAT. Un resultat identique repete ne produit aucune +# ecriture — verifie en poussant un OK (rien ne bouge) puis un AVERTISSEMENT (ecrit en +# 13 s). +# +# La sonde mesurait donc le CHANGEMENT, pas la fraicheur. Sur un ecosysteme parfaitement +# STABLE — celui qu'on veut — plus rien ne change, `last_update` vieillit, et la sonde +# serait passee en avertissement a 15 min puis en critique a 90 min. Une alarme qui se +# declenche PARCE QUE tout va bien : exactement le piege que ce document interdit, avec +# un delai qui l'aurait rendue difficile a comprendre. +# +# `icingadb_instance` porte le battement du synchroniseur, ecrit en continu qu'il y ait +# ou non des changements. C'est lui qui dit « la chaine moteur -> Redis -> base est +# vivante » — releve a 1 seconde sur un systeme sain. age=$(PGPASSWORD="${mdp}" psql -h "${hote}" -U "${util:-icingadb}" -d "${base:-icingadb}" -Atc \ - "select coalesce(extract(epoch from now()) - max(last_update)/1000, 999999)::bigint from service_state" 2>/dev/null) + "select coalesce((extract(epoch from now()) - max(heartbeat)/1000)::bigint, 999999) from icingadb_instance" 2>/dev/null) [[ "${age}" =~ ^-?[0-9]+$ ]] || { echo "La base de supervision ne repond pas (${hote})."; exit 2; } (( age < 0 )) && age=0 if (( age >= CRIT )); then - echo "Etat FIGE : rien de neuf en base depuis ${age} s — la supervision montre le passe.|age=${age}s;${AVERT};${CRIT};0;" + echo "Synchronisation FIGEE : IcingaDB n'a pas battu depuis ${age} s — la supervision montre le passe.|age=${age}s;${AVERT};${CRIT};0;" exit 2 fi if (( age >= AVERT )); then - echo "Etat vieillissant : ${age} s sans mise a jour (seuil ${AVERT}).|age=${age}s;${AVERT};${CRIT};0;" + echo "Battement d'IcingaDB vieillissant : ${age} s (seuil ${AVERT}).|age=${age}s;${AVERT};${CRIT};0;" exit 1 fi -echo "Moteur et synchronisation vivants : base rafraichie il y a ${age} s.|age=${age}s;${AVERT};${CRIT};0;" +echo "Moteur et synchronisation vivants : IcingaDB a battu il y a ${age} s.|age=${age}s;${AVERT};${CRIT};0;"