diff --git a/CHANGELOG.md b/CHANGELOG.md index 3507bb1..5fa5a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG — Set-OPS +## 2026-09-28 (3) — Un service qui n'a jamais rien reçu passe au rouge + +**Le trou.** Tous les services passifs (sauvegardes, santé, sondes de rôle) étaient +déclarés `enable_active_checks = false`, avec une commande qui exécutait `/bin/true`. Le +`ttl` de chaque envoi couvrait le silence qui SUIT un rapport ; un service qui n'en a +jamais reçu restait « en attente », gris, pas rouge. C'est ce qui a caché dix nœuds de +Chezlepro sans sauvegarde pendant seize jours. + +**Le correctif.** Un gabarit, `setops-rapport-attendu` (dans `setops-sauvegardes.conf.j2`), +que les trois familles importent : service ACTIF, commande `dummy` en CRITIQUE, +`check_interval` = le seuil — patron documenté d'Icinga 2 pour la fraîcheur. Chaque rapport +repousse l'échéance ; seul le silence la laisse arriver, y compris le silence initial. +Seuils = les `ttl` que les nœuds envoient (6 h, 45 min, 90 min), dans +`serveur_icinga_fraicheur_*`. Une liste qui en suit une autre : `test_fraicheur_icinga.py` +(dans `make test`) les compare et refuse tout service passif sans échéance — vérifié en +cassant volontairement une valeur. + +**Prouvé en vrai** sur `site-mon-01` : rapport envoyé avec un `ttl` de 60 s → CRITIQUE à +t+60 s (« AUCUN RAPPORT… ») → vrai rapport de santé → OK. Déployé sur les trois Icinga +(site, Chezlepro, Technolibre) : 120 + 138 + 138 services, tous avec échéance. + +**Ce que ça a aussitôt montré.** Chez les deux tenants, dix services n'avaient JAMAIS reçu +de rapport, et rougissent maintenant : `obs-01!journaux-frontiere`, et neuf sondes de rôle +(console, passerelle, édition, cache, filtrage, sites/apps servis) — leur `client_sante` +n'a pas été redéployé depuis que ces sondes existent. Leur Icinga surveillait aussi +encore `forge-01`, retiré du plan : la configuration est réalignée, mais `obs-01!collecte` +reste rouge tant que Prometheus scrute l'ancienne adresse (`10.x.21.11:9100`). + ## 2026-09-28 (2) — Deux locataires sur deux n'avaient pas de sauvegarde hors de leur flotte **Question de l'exploitant : les sauvegardes des tenants passent-elles ?** Non. Mesuré diff --git a/Makefile b/Makefile index dde196c..73a09dc 100644 --- a/Makefile +++ b/Makefile @@ -314,6 +314,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_portee_console.py python3 scripts/tests/test_devis_placement.py python3 scripts/tests/test_sonder.py + python3 scripts/tests/test_fraicheur_icinga.py # LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une # journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine diff --git a/roles/serveur_icinga/defaults/main.yml b/roles/serveur_icinga/defaults/main.yml index abb1ff0..d1a51c5 100644 --- a/roles/serveur_icinga/defaults/main.yml +++ b/roles/serveur_icinga/defaults/main.yml @@ -84,6 +84,18 @@ serveur_icinga_api_conf: "/etc/icinga2/conf.d/setops-api-users.conf" serveur_icinga_api_utilisateur: "setops-depot" serveur_icinga_api_motdepasse: "{{ vault_icinga_api_depot | default('') }}" +# LES SEUILS DE FRAICHEUR DES SERVICES PASSIFS (2026-09-28) — voir le gabarit +# `setops-rapport-attendu` dans `setops-sauvegardes.conf.j2`. Au-dela, sans rapport, le +# service passe CRITIQUE — y compris s'il n'a jamais rien recu. +# +# CE SONT LES `ttl` QUE LES NOEUDS ENVOIENT, recopies : ils vivent dans les roles qui +# rapportent (`client_backup`, `serveur_backup`, `client_sante`), que ce role ne charge +# pas. Une liste qui en suit une autre prend du retard : `test_fraicheur_icinga.py` les +# compare, et `make test` echoue a la premiere divergence. +serveur_icinga_fraicheur_sauvegarde: 21600 # = client_backup_ttl_icinga = serveur_backup_ttl_icinga +serveur_icinga_fraicheur_sante: 2700 # = client_sante_ttl_icinga +serveur_icinga_fraicheur_sondes: 5400 # = client_sante_ttl_sondes + # Hote portant les depots : DERIVE du groupe, jamais ecrit en dur. serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}" diff --git a/roles/serveur_icinga/templates/setops-sante.conf.j2 b/roles/serveur_icinga/templates/setops-sante.conf.j2 index 46bd866..95297d1 100644 --- a/roles/serveur_icinga/templates/setops-sante.conf.j2 +++ b/roles/serveur_icinga/templates/setops-sante.conf.j2 @@ -20,12 +20,11 @@ {% for noeud in serveur_icinga_sante_attendue | sort %} object Service "sante" { + import "setops-rapport-attendu" host_name = "{{ noeud }}" - check_command = "setops-passif" - enable_active_checks = false - enable_passive_checks = true - volatile = false - max_check_attempts = 1 + check_interval = {{ serveur_icinga_fraicheur_sante }}s + retry_interval = {{ serveur_icinga_fraicheur_sante }}s + vars.dummy_text = "AUCUN RAPPORT depuis {{ (serveur_icinga_fraicheur_sante / 60) | int }} min : ce noeud ne rapporte plus sa sante, ou ne l a jamais rapportee." vars.setops_source = "{{ noeud }}" } {% endfor %} diff --git a/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 b/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 index e182a30..1de0e0e 100644 --- a/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 +++ b/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 @@ -19,14 +19,31 @@ * lui-meme, par `client_backup` et son minuteur. Le code de ce fichier avait suivi la * decision — il attache les services aux vrais noeuds — mais l'en-tete decrivait encore * un acteur disparu, a celui-la meme qui l'ouvrirait pour comprendre qui pousse. - * Le `ttl` porte dans chaque envoi fait la fraicheur : sans nouvelle, Icinga bascule tout - * seul en « expire ». C'est le SILENCE qui doit alerter, pas seulement l'echec — le - * silence est precisement ce qui n'a alerte personne. + * C'est le SILENCE qui doit alerter, pas seulement l'echec — le silence est precisement + * ce qui n'a alerte personne. Voir `setops-rapport-attendu` ci-dessous. */ -object CheckCommand "setops-passif" { - // Les resultats arrivent par l'API ; cette commande n'est jamais executee. - command = [ "/bin/true" ] +/* + * UN RAPPORT ATTENDU, ET NON UN RAPPORT ESPERE (2026-09-28). + * + * Tous les services passifs de Set-OPS (sauvegardes, sante, sondes de role) importent ce + * gabarit. Ils etaient declares `enable_active_checks = false`, avec une commande qui + * executait `/bin/true`. Le `ttl` de chaque envoi couvrait le silence qui SUIT un + * rapport — pas celui d'un noeud qui n'a JAMAIS rapporte : un service passif sans aucun + * resultat reste « en attente », gris, jamais rouge. Dix noeuds de Chezlepro n'ont eu + * aucune sauvegarde du 2026-09-12 au 2026-09-28, et leurs services attendaient. + * + * Le patron documente d'Icinga 2 pour la fraicheur : le service est ACTIF, sa commande est + * `dummy` en CRITIQUE, et son `check_interval` est le seuil. Chaque rapport passif repousse + * l'echeance ; seul le silence la laisse arriver — y compris le silence initial. + */ +template Service "setops-rapport-attendu" { + check_command = "dummy" + enable_active_checks = true + enable_passive_checks = true + volatile = false + max_check_attempts = 1 + vars.dummy_state = 2 } {# @@ -51,12 +68,11 @@ object CheckCommand "setops-passif" { #} {% for noeud in serveur_icinga_sauvegarde_attendue | sort %} object Service "sauvegarde: {{ noeud }}" { + import "setops-rapport-attendu" host_name = "{{ serveur_icinga_hote_sauvegarde }}" - check_command = "setops-passif" - enable_active_checks = false - enable_passive_checks = true - volatile = false - max_check_attempts = 1 + check_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s + retry_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s + vars.dummy_text = "AUCUN RAPPORT depuis {{ (serveur_icinga_fraicheur_sauvegarde / 60) | int }} min : la verification de ce depot ne dit plus rien, ou n a jamais rien dit." vars.setops_source = "{{ noeud }}" } {% endfor %} @@ -74,12 +90,11 @@ object Service "sauvegarde: {{ noeud }}" { #} {% for noeud in serveur_icinga_sauvegarde_attendue | sort %} object Service "sauvegarde" { + import "setops-rapport-attendu" host_name = "{{ noeud }}" - check_command = "setops-passif" - enable_active_checks = false - enable_passive_checks = true - volatile = false - max_check_attempts = 1 + check_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s + retry_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s + vars.dummy_text = "AUCUN RAPPORT depuis {{ (serveur_icinga_fraicheur_sauvegarde / 60) | int }} min : la verification de ce depot ne dit plus rien, ou n a jamais rien dit." vars.setops_source = "{{ noeud }}" } {% endfor %} diff --git a/roles/serveur_icinga/templates/setops-sondes.conf.j2 b/roles/serveur_icinga/templates/setops-sondes.conf.j2 index f98191a..c690379 100644 --- a/roles/serveur_icinga/templates/setops-sondes.conf.j2 +++ b/roles/serveur_icinga/templates/setops-sondes.conf.j2 @@ -36,12 +36,11 @@ {% for hote in ((groups[role] | default([])) | intersect(groups['client_sante'] | default([]))) | sort %} {% for sonde in sondes %} object Service "{{ sonde.nom }}" { + import "setops-rapport-attendu" host_name = "{{ hote }}" - check_command = "setops-passif" - enable_active_checks = false - enable_passive_checks = true - volatile = false - max_check_attempts = 1 + check_interval = {{ serveur_icinga_fraicheur_sondes }}s + retry_interval = {{ serveur_icinga_fraicheur_sondes }}s + vars.dummy_text = "AUCUN RAPPORT depuis {{ (serveur_icinga_fraicheur_sondes / 60) | int }} min : cette sonde ne rapporte plus, ou n a jamais rapporte." vars.setops_source = "{{ hote }}" vars.setops_role = "{{ role }}" vars.setops_raison = "{{ (sonde.raison | default('')) | replace('"', "'") | replace('\n', ' ') | trim }}" diff --git a/scripts/tests/test_fraicheur_icinga.py b/scripts/tests/test_fraicheur_icinga.py new file mode 100644 index 0000000..57e0ae0 --- /dev/null +++ b/scripts/tests/test_fraicheur_icinga.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Les seuils de fraicheur d'Icinga suivent les `ttl` que les noeuds envoient. + +UNE LISTE QUI EN SUIT UNE AUTRE PREND DU RETARD (2026-09-28). Les services passifs de +Set-OPS passent CRITIQUES quand aucun rapport n'arrive dans leur `check_interval`. Ce +seuil vit dans `serveur_icinga` ; le `ttl` que chaque noeud envoie vit dans le role qui +rapporte. Si le seuil devient plus court que le rythme des rapports, tout rougit a tort ; +plus long, un noeud muet reste vert trop longtemps. Les deux doivent etre egaux. + +Et plus aucun service passif ne doit revenir a `enable_active_checks = false` : c'est +exactement ce qui laissait un service jamais alimente « en attente », jamais rouge. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import yaml + +RACINE = Path(__file__).resolve().parents[2] +ROLES = RACINE / "roles" + + +def defauts(role: str) -> dict: + return yaml.safe_load((ROLES / role / "defaults" / "main.yml").read_text(encoding="utf-8")) or {} + + +def main() -> int: + icinga = defauts("serveur_icinga") + paires = [ + ("serveur_icinga_fraicheur_sauvegarde", "client_backup", "client_backup_ttl_icinga"), + ("serveur_icinga_fraicheur_sauvegarde", "serveur_backup", "serveur_backup_ttl_icinga"), + ("serveur_icinga_fraicheur_sante", "client_sante", "client_sante_ttl_icinga"), + ("serveur_icinga_fraicheur_sondes", "client_sante", "client_sante_ttl_sondes"), + ] + echecs = 0 + for seuil, role, ttl in paires: + a, b = icinga.get(seuil), defauts(role).get(ttl) + if a is None or b is None or int(a) != int(b): + print(f"ECHEC {seuil} = {a} ≠ {role}.{ttl} = {b}") + echecs += 1 + else: + print(f"ok {seuil} = {role}.{ttl} = {a}") + + gabarits = ROLES / "serveur_icinga" / "templates" + for f in sorted(gabarits.glob("setops-*.conf.j2")): + code = re.sub(r"/\*.*?\*/|\{#.*?#\}", "", f.read_text(encoding="utf-8"), flags=re.S) + for bloc in re.findall(r'object Service "[^"]*" \{.*?\n\}', code, flags=re.S): + if "enable_passive_checks = true" in bloc and "setops-rapport-attendu" not in bloc: + print(f"ECHEC {f.name} : service passif sans `setops-rapport-attendu` :\n{bloc[:120]}") + echecs += 1 + if "enable_active_checks = false" in code: + print(f"ECHEC {f.name} : `enable_active_checks = false` — un service jamais " + f"alimente resterait « en attente », jamais rouge") + echecs += 1 + + if echecs: + print(f"\n{echecs} echec(s).") + return 1 + print("OK — fraicheur d'Icinga alignee sur les ttl, aucun service passif sans echeance.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())