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
|
||||
|
||||
## 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
|
||||
|
||||
**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_sonder.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
|
||||
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
sondes:
|
||||
- nom: audit
|
||||
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: >-
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,10 +31,25 @@
|
|||
* 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
|
||||
* 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 hote in ((groups[role] | default([])) | intersect(groups['client_sante'] | default([]))) | sort %}
|
||||
{% 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 }}" {
|
||||
import "setops-rapport-attendu"
|
||||
host_name = "{{ hote }}"
|
||||
|
|
@ -45,6 +60,7 @@ object Service "{{ sonde.nom }}" {
|
|||
vars.setops_role = "{{ role }}"
|
||||
vars.setops_raison = "{{ (sonde.raison | default('')) | replace('"', "'") | replace('\n', ' ') | trim }}"
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ sondes:
|
|||
|
||||
- nom: journaux-frontiere
|
||||
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: >-
|
||||
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
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ sondes:
|
|||
# REFUS sur une requete anonyme : c'est la seule facon de mesurer une serrure.
|
||||
- nom: console-ops
|
||||
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
|
||||
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
|
||||
|
|
|
|||
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