icinga : un service passif qui n a jamais rien recu passe au rouge
Gabarit setops-rapport-attendu : actif, dummy critique, check_interval = le ttl que le noeud envoie. Prouve a t+60 s sur site-mon-01, deploye sur les trois Icinga ; la garde test_fraicheur_icinga lie seuils et ttl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
831c7d5cf4
commit
185b000c60
7 changed files with 146 additions and 26 deletions
28
CHANGELOG.md
28
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é
|
||||
|
|
|
|||
1
Makefile
1
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
|
||||
|
|
|
|||
|
|
@ -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('') }}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
66
scripts/tests/test_fraicheur_icinga.py
Normal file
66
scripts/tests/test_fraicheur_icinga.py
Normal file
|
|
@ -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())
|
||||
Loading…
Reference in a new issue