sondes conditionnelles : attendues seulement la ou elles sont posees
seulement_si dans meta/supervision.yml, lu par le gabarit Icinga des sondes ; garde test_sondes_conditionnelles. journaux-frontiere ne rougit plus a vie chez les locataires ; Technolibre sans critique. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
185b000c60
commit
f5adffb0de
7 changed files with 144 additions and 0 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,5 +1,30 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-28 (4) — Une sonde posée sous condition n'est attendue que là où elle est posée
|
||||||
|
|
||||||
|
**Ce que la fraîcheur a fait voir.** Chez les deux locataires, `obs-01!journaux-frontiere`
|
||||||
|
est passé au rouge et y serait resté pour toujours. `serveur_loki` ne pose cette sonde que
|
||||||
|
si une étiquette de frontière est déclarée — au SITE, jamais chez un locataire, dont la
|
||||||
|
frontière appartient à l'hébergeur — et la RETIRE sinon. Icinga, lui, l'attendait sur tout
|
||||||
|
hôte du groupe. Trois rôles posent ainsi leur sonde sous `when:` (`journaux-frontiere`,
|
||||||
|
`audit`, `console-ops`) ; les deux autres n'étaient pas rouges par chance, leur condition
|
||||||
|
étant vraie partout.
|
||||||
|
|
||||||
|
**Le correctif.** La déclaration porte la même condition que le dépôt :
|
||||||
|
`seulement_si: <variable>` dans `meta/supervision.yml`, lue par `setops-sondes.conf.j2`
|
||||||
|
dans l'inventaire de l'hôte, sinon dans les défauts du rôle qui la définit (`defini_par`)
|
||||||
|
— la valeur même que ce rôle voit. `test_sondes_conditionnelles.py` (dans `make test`)
|
||||||
|
exige qu'un `when:` qui pose une sonde ait son `seulement_si`, sur la même variable, avec
|
||||||
|
un défaut littéral ; vérifié en retirant volontairement une condition.
|
||||||
|
|
||||||
|
Simulé puis appliqué : chez chaque locataire, un seul changement — `journaux-frontiere`
|
||||||
|
retiré d'`obs-01`. Technolibre n'a plus aucun critique ; Chezlepro n'en garde qu'un, son
|
||||||
|
exportateur PostgreSQL, faute du secret `vault_pg_exportateur`.
|
||||||
|
|
||||||
|
**Ce que ça révèle au passage.** Le site ne déclare `journaux-frontiere` nulle part : aucune
|
||||||
|
machine ne sonde aujourd'hui les journaux de la frontière — la panne de dix jours qu'elle
|
||||||
|
devait empêcher n'est pas couverte.
|
||||||
|
|
||||||
## 2026-09-28 (3) — Un service qui n'a jamais rien reçu passe au rouge
|
## 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
|
**Le trou.** Tous les services passifs (sauvegardes, santé, sondes de rôle) étaient
|
||||||
|
|
|
||||||
1
Makefile
1
Makefile
|
|
@ -315,6 +315,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
||||||
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
|
python3 scripts/tests/test_fraicheur_icinga.py
|
||||||
|
python3 scripts/tests/test_sondes_conditionnelles.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
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
sondes:
|
sondes:
|
||||||
- nom: audit
|
- nom: audit
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
|
# Posee par `auditd` seulement si `auditd_enabled` : Icinga ne l'attend qu'a ces
|
||||||
|
# conditions (voir `setops-sondes.conf.j2`, 2026-09-28).
|
||||||
|
seulement_si: auditd_enabled
|
||||||
|
defini_par: auditd
|
||||||
raison: >-
|
raison: >-
|
||||||
La piste d'audit s'ecrit-elle vraiment ? Des regles armees dans le noyau ne
|
La piste d'audit s'ecrit-elle vraiment ? Des regles armees dans le noyau ne
|
||||||
prouvent pas qu'un demon collecte : le 2026-08-30, quinze machines ont eu les
|
prouvent pas qu'un demon collecte : le 2026-08-30, quinze machines ont eu les
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,25 @@
|
||||||
* L'appartenance a un groupe sert donc DEUX choses qui ne coincident pas toujours : a
|
* L'appartenance a un groupe sert donc DEUX choses qui ne coincident pas toujours : a
|
||||||
* qui l'on deploie, et de qui l'on attend un rapport. La seconde se lit sur le porteur
|
* qui l'on deploie, et de qui l'on attend un rapport. La seconde se lit sur le porteur
|
||||||
* lui-meme — `client_sante` — et nulle part ailleurs.
|
* lui-meme — `client_sante` — et nulle part ailleurs.
|
||||||
|
|
||||||
|
UNE SONDE POSEE SOUS CONDITION N'EST ATTENDUE QUE LA OU ELLE EST POSEE (2026-09-28).
|
||||||
|
Trois roles posent leur sonde sous `when:` (`journaux-frontiere`, `audit`,
|
||||||
|
`console-ops`). Ce fichier les attendait pourtant sur tout hote du groupe : chaque
|
||||||
|
locataire portait un `journaux-frontiere` rouge a vie, sa frontiere appartenant au site.
|
||||||
|
La declaration porte donc la MEME condition que le depot : `seulement_si: <variable>`,
|
||||||
|
lue dans l'inventaire de l'hote, sinon dans les defauts du role qui la definit
|
||||||
|
(`defini_par`, par defaut le role declarant) — la valeur meme que ce role voit.
|
||||||
|
`test_sondes_conditionnelles.py` garde l'accord entre le `when:` et la declaration.
|
||||||
#}
|
#}
|
||||||
{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}
|
{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}
|
||||||
{% 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 %}
|
||||||
|
{% set condition = sonde.seulement_si | default('') %}
|
||||||
|
{% if condition %}
|
||||||
|
{% set defauts = lookup('file', role_path ~ '/../' ~ (sonde.defini_par | default(role)) ~ '/defaults/main.yml') | from_yaml %}
|
||||||
|
{% set valeur = hostvars[hote][condition] if hostvars[hote][condition] is defined else defauts.get(condition) %}
|
||||||
|
{% endif %}
|
||||||
|
{% if not condition or valeur %}
|
||||||
object Service "{{ sonde.nom }}" {
|
object Service "{{ sonde.nom }}" {
|
||||||
import "setops-rapport-attendu"
|
import "setops-rapport-attendu"
|
||||||
host_name = "{{ hote }}"
|
host_name = "{{ hote }}"
|
||||||
|
|
@ -45,6 +60,7 @@ object Service "{{ sonde.nom }}" {
|
||||||
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 }}"
|
||||||
}
|
}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ sondes:
|
||||||
|
|
||||||
- nom: journaux-frontiere
|
- nom: journaux-frontiere
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
|
# Posee seulement la ou une frontiere est ecoutee : au SITE, pas chez un locataire,
|
||||||
|
# dont la frontiere appartient a l'hebergeur. Sans cette ligne, Icinga l'attendait
|
||||||
|
# partout, et chaque locataire portait un rouge permanent (2026-09-28).
|
||||||
|
seulement_si: serveur_loki_sonde_frontiere_etiquette
|
||||||
raison: >-
|
raison: >-
|
||||||
La frontiere journalise-t-elle encore ? Sa destination syslog a deja pointe une VM
|
La frontiere journalise-t-elle encore ? Sa destination syslog a deja pointe une VM
|
||||||
rasee : toute la journalisation d'OPNsense s'est arretee dix jours sans que rien ne
|
rasee : toute la journalisation d'OPNsense s'est arretee dix jours sans que rien ne
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ sondes:
|
||||||
# REFUS sur une requete anonyme : c'est la seule facon de mesurer une serrure.
|
# REFUS sur une requete anonyme : c'est la seule facon de mesurer une serrure.
|
||||||
- nom: console-ops
|
- nom: console-ops
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
|
# Posee seulement si le GUI est actif (voir `setops-sondes.conf.j2`, 2026-09-28).
|
||||||
|
seulement_si: serveur_ops_gui_actif
|
||||||
raison: 'La console d''exploitation repond-elle, et son vestibule tient-il ? Un GUI
|
raison: 'La console d''exploitation repond-elle, et son vestibule tient-il ? Un GUI
|
||||||
mort derriere un nginx vivant rend une page d''erreur a qui sait deja entrer ; un
|
mort derriere un nginx vivant rend une page d''erreur a qui sait deja entrer ; un
|
||||||
vestibule tombe rend la fabric entiere a qui trouve l''URL. Le second ne fait
|
vestibule tombe rend la fabric entiere a qui trouve l''URL. Le second ne fait
|
||||||
|
|
|
||||||
92
scripts/tests/test_sondes_conditionnelles.py
Normal file
92
scripts/tests/test_sondes_conditionnelles.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Une sonde posee sous condition est declaree sous la MEME condition.
|
||||||
|
|
||||||
|
UNE LISTE QUI EN SUIT UNE AUTRE PREND DU RETARD (2026-09-28). Un role pose sa sonde dans
|
||||||
|
`/usr/local/lib/setops/sondes/` avec ses taches ; Icinga l'attend d'apres
|
||||||
|
`meta/supervision.yml`. Quand le depot est conditionnel (`when:`) et la declaration ne
|
||||||
|
l'est pas, Icinga attend une sonde qui n'existera jamais : `journaux-frontiere` a ete
|
||||||
|
rouge a vie chez chaque locataire, dont la frontiere appartient au site.
|
||||||
|
|
||||||
|
Trois regles :
|
||||||
|
1. toute sonde deposee sous `when:` est declaree avec `seulement_si` ;
|
||||||
|
2. la variable de `seulement_si` figure dans ce `when:` ;
|
||||||
|
3. elle a une valeur par defaut LITTERALE dans le role qui la definit — Icinga la lit
|
||||||
|
telle quelle, il ne rend pas le Jinja d'un autre role.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROLES = Path(__file__).resolve().parents[2] / "roles"
|
||||||
|
|
||||||
|
|
||||||
|
def depots_conditionnels() -> dict[str, str]:
|
||||||
|
"""{nom de sonde : texte du `when:`} pour chaque sonde deposee sous condition."""
|
||||||
|
out = {}
|
||||||
|
for f in sorted(ROLES.glob("*/tasks/*.yml")):
|
||||||
|
try:
|
||||||
|
taches = yaml.safe_load(f.read_text(encoding="utf-8")) or []
|
||||||
|
except yaml.YAMLError:
|
||||||
|
continue
|
||||||
|
for t in taches if isinstance(taches, list) else []:
|
||||||
|
if not isinstance(t, dict):
|
||||||
|
continue
|
||||||
|
module = next((v for v in t.values() if isinstance(v, dict) and "dest" in v), None)
|
||||||
|
dest = str((module or {}).get("dest", ""))
|
||||||
|
if "/setops/sondes/" in dest and t.get("when"):
|
||||||
|
quand = t["when"] if isinstance(t["when"], str) else " ".join(map(str, t["when"]))
|
||||||
|
out[dest.rsplit("/", 1)[-1].removesuffix(".sh")] = quand
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def declarations() -> dict[str, tuple[str, dict]]:
|
||||||
|
"""{nom de sonde : (role declarant, declaration)}."""
|
||||||
|
out = {}
|
||||||
|
for f in sorted(ROLES.glob("*/meta/supervision.yml")):
|
||||||
|
for s in (yaml.safe_load(f.read_text(encoding="utf-8")) or {}).get("sondes") or []:
|
||||||
|
out[s["nom"]] = (f.parents[1].name, s)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
depots, decl = depots_conditionnels(), declarations()
|
||||||
|
echecs = 0
|
||||||
|
for nom, quand in sorted(depots.items()):
|
||||||
|
role, s = decl.get(nom, (None, {}))
|
||||||
|
var = s.get("seulement_si")
|
||||||
|
if not var:
|
||||||
|
print(f"ECHEC « {nom} » est posee sous `when: {quand}` mais declaree sans "
|
||||||
|
f"`seulement_si` : Icinga l'attendrait la ou elle n'existe pas")
|
||||||
|
echecs += 1
|
||||||
|
continue
|
||||||
|
if var not in quand:
|
||||||
|
print(f"ECHEC « {nom} » : `seulement_si: {var}` n'est pas la condition du depot "
|
||||||
|
f"(`when: {quand}`)")
|
||||||
|
echecs += 1
|
||||||
|
continue
|
||||||
|
definisseur = s.get("defini_par") or role
|
||||||
|
defauts = yaml.safe_load((ROLES / definisseur / "defaults" / "main.yml")
|
||||||
|
.read_text(encoding="utf-8")) or {}
|
||||||
|
val = defauts.get(var, "<absente>")
|
||||||
|
if val == "<absente>" or (isinstance(val, str) and "{{" in val):
|
||||||
|
print(f"ECHEC « {nom} » : `{var}` doit avoir un defaut LITTERAL dans "
|
||||||
|
f"`{definisseur}/defaults/main.yml` (trouve : {val!r})")
|
||||||
|
echecs += 1
|
||||||
|
continue
|
||||||
|
print(f"ok {nom:<20} seulement_si {var} (defaut {val!r}, {definisseur})")
|
||||||
|
for nom, (role, s) in sorted(decl.items()):
|
||||||
|
if s.get("seulement_si") and nom not in depots:
|
||||||
|
print(f"ECHEC « {nom} » ({role}) declare `seulement_si` sans depot conditionnel")
|
||||||
|
echecs += 1
|
||||||
|
if echecs:
|
||||||
|
print(f"\n{echecs} echec(s).")
|
||||||
|
return 1
|
||||||
|
print("OK — chaque sonde conditionnelle est attendue sous sa propre condition.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Reference in a new issue