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
|
# 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
|
## 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é
|
**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_portee_console.py
|
||||||
python3 scripts/tests/test_devis_placement.py
|
python3 scripts/tests/test_devis_placement.py
|
||||||
python3 scripts/tests/test_sonder.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
|
# 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
|
# 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_utilisateur: "setops-depot"
|
||||||
serveur_icinga_api_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
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.
|
# Hote portant les depots : DERIVE du groupe, jamais ecrit en dur.
|
||||||
serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}"
|
serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,11 @@
|
||||||
|
|
||||||
{% for noeud in serveur_icinga_sante_attendue | sort %}
|
{% for noeud in serveur_icinga_sante_attendue | sort %}
|
||||||
object Service "sante" {
|
object Service "sante" {
|
||||||
|
import "setops-rapport-attendu"
|
||||||
host_name = "{{ noeud }}"
|
host_name = "{{ noeud }}"
|
||||||
check_command = "setops-passif"
|
check_interval = {{ serveur_icinga_fraicheur_sante }}s
|
||||||
enable_active_checks = false
|
retry_interval = {{ serveur_icinga_fraicheur_sante }}s
|
||||||
enable_passive_checks = true
|
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."
|
||||||
volatile = false
|
|
||||||
max_check_attempts = 1
|
|
||||||
vars.setops_source = "{{ noeud }}"
|
vars.setops_source = "{{ noeud }}"
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,31 @@
|
||||||
* lui-meme, par `client_backup` et son minuteur. Le code de ce fichier avait suivi la
|
* 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
|
* 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.
|
* 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
|
* C'est le SILENCE qui doit alerter, pas seulement l'echec — le silence est precisement
|
||||||
* seul en « expire ». C'est le SILENCE qui doit alerter, pas seulement l'echec — le
|
* ce qui n'a alerte personne. Voir `setops-rapport-attendu` ci-dessous.
|
||||||
* silence est precisement ce qui n'a alerte personne.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
object CheckCommand "setops-passif" {
|
/*
|
||||||
// Les resultats arrivent par l'API ; cette commande n'est jamais executee.
|
* UN RAPPORT ATTENDU, ET NON UN RAPPORT ESPERE (2026-09-28).
|
||||||
command = [ "/bin/true" ]
|
*
|
||||||
|
* 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 %}
|
{% for noeud in serveur_icinga_sauvegarde_attendue | sort %}
|
||||||
object Service "sauvegarde: {{ noeud }}" {
|
object Service "sauvegarde: {{ noeud }}" {
|
||||||
|
import "setops-rapport-attendu"
|
||||||
host_name = "{{ serveur_icinga_hote_sauvegarde }}"
|
host_name = "{{ serveur_icinga_hote_sauvegarde }}"
|
||||||
check_command = "setops-passif"
|
check_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s
|
||||||
enable_active_checks = false
|
retry_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s
|
||||||
enable_passive_checks = true
|
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."
|
||||||
volatile = false
|
|
||||||
max_check_attempts = 1
|
|
||||||
vars.setops_source = "{{ noeud }}"
|
vars.setops_source = "{{ noeud }}"
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -74,12 +90,11 @@ object Service "sauvegarde: {{ noeud }}" {
|
||||||
#}
|
#}
|
||||||
{% for noeud in serveur_icinga_sauvegarde_attendue | sort %}
|
{% for noeud in serveur_icinga_sauvegarde_attendue | sort %}
|
||||||
object Service "sauvegarde" {
|
object Service "sauvegarde" {
|
||||||
|
import "setops-rapport-attendu"
|
||||||
host_name = "{{ noeud }}"
|
host_name = "{{ noeud }}"
|
||||||
check_command = "setops-passif"
|
check_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s
|
||||||
enable_active_checks = false
|
retry_interval = {{ serveur_icinga_fraicheur_sauvegarde }}s
|
||||||
enable_passive_checks = true
|
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."
|
||||||
volatile = false
|
|
||||||
max_check_attempts = 1
|
|
||||||
vars.setops_source = "{{ noeud }}"
|
vars.setops_source = "{{ noeud }}"
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,11 @@
|
||||||
{% for hote in ((groups[role] | default([])) | intersect(groups['client_sante'] | default([]))) | sort %}
|
{% for hote in ((groups[role] | default([])) | intersect(groups['client_sante'] | default([]))) | sort %}
|
||||||
{% for sonde in sondes %}
|
{% for sonde in sondes %}
|
||||||
object Service "{{ sonde.nom }}" {
|
object Service "{{ sonde.nom }}" {
|
||||||
|
import "setops-rapport-attendu"
|
||||||
host_name = "{{ hote }}"
|
host_name = "{{ hote }}"
|
||||||
check_command = "setops-passif"
|
check_interval = {{ serveur_icinga_fraicheur_sondes }}s
|
||||||
enable_active_checks = false
|
retry_interval = {{ serveur_icinga_fraicheur_sondes }}s
|
||||||
enable_passive_checks = true
|
vars.dummy_text = "AUCUN RAPPORT depuis {{ (serveur_icinga_fraicheur_sondes / 60) | int }} min : cette sonde ne rapporte plus, ou n a jamais rapporte."
|
||||||
volatile = false
|
|
||||||
max_check_attempts = 1
|
|
||||||
vars.setops_source = "{{ hote }}"
|
vars.setops_source = "{{ hote }}"
|
||||||
vars.setops_role = "{{ role }}"
|
vars.setops_role = "{{ role }}"
|
||||||
vars.setops_raison = "{{ (sonde.raison | default('')) | replace('"', "'") | replace('\n', ' ') | trim }}"
|
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