supervision : la sonde se declare dans le role, comme le flux
LE CONSTAT. 39 roles declarent leurs flux, 32 leur empreinte, 32 leur authentification — tous derives. Et 19 groupes sur 19 declaraient une surveillance en prose que RIEN n executait ; Icinga en surveillait deux. La carte disait ce qui etait surveille, et personne ne surveillait. LE MECANISME. Un role declare ses sondes dans meta/supervision.yml et depose lui-meme son script dans /usr/local/lib/setops/sondes/. Le porteur client_sante les fait toutes tourner et pousse un resultat passif par sonde, sans savoir ce qu elles mesurent. serveur_icinga derive les objets Service ET le filtre de permission d API des memes declarations. Ajouter une sonde ne demande de toucher ni au porteur ni a Icinga. PREMIERE SONDE : client_pki/certificat. Heures restantes sur le certificat reellement pose, chaine verifiee, et empreinte SERVIE comparee au disque quand un service le consomme. 14/14 au tenant, 7/7 au site. QUATRE OBSTACLES, ET TROIS SONT LA MEME LECON. La sonde a rendu 14/14 en CRITIQUE sur une PKI saine : openssl verify -CAfile racine ne trouve pas l intermediaire qui signe nos certificats. step certificate verify, lui, repond VALIDE. Deployee au site, elle a rendu 5/7 : le seuil d avertissement (12 h) etait AU-DESSUS du point de renouvellement (8 h, le tiers restant). Elle criait avant que le mecanisme ne soit cense agir. Seuils ramenes a 6 h et 3 h. Un seuil se DERIVE du moment ou le mecanisme surveille agit. Une alarme toujours allumee ne vaut pas mieux qu une alarme jamais allumee : elle apprend a ne plus regarder. Une sonde se prouve DEUX FOIS, verte sur le sain et rouge sur le casse. Le filtre d API etait ecrit avant la lecture des declarations : les services auraient existe et Icinga aurait refuse leurs resultats. Et mon controle negatif a casse un service reel : substituer le certificat d hote a fait propager un cert sans sa clef vers node_exporter. Un controle negatif se fait sur une COPIE. P64 tient les deux bouts : declaree sans etre deposee, ou deposee sans etre declaree. Trois controles negatifs rejoues. make prouver : CONFORME, 64 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
69b84f4e2c
commit
90228cb55e
17 changed files with 601 additions and 12 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 63 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 64 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
112
CHANGELOG.md
112
CHANGELOG.md
|
|
@ -1,5 +1,117 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-09 (6) — La supervision se DECLARE dans le role, comme les flux
|
||||
|
||||
**64 preuves (P01-P64).** Nouveau document : `docs/supervision-conception.md`.
|
||||
Premiere sonde : `client_pki/certificat`, **14/14 OK** sur la flotte.
|
||||
|
||||
### Le constat qui a decide
|
||||
|
||||
Mesure du depot sur lui-meme, au 2026-09-09 :
|
||||
|
||||
39 roles declarent leurs flux -> nftables + OPNsense, derives
|
||||
32 declarent leur empreinte -> ressources des VM, derivees
|
||||
32 declarent leur authentification -> habilitations, derivees
|
||||
19 groupes declarent `surveillance:` -> RIEN
|
||||
|
||||
Les dix-neuf lignes `surveillance:` de `docs/dependances-groupes.yml` sont ecrites,
|
||||
versionnees, relues — et **aucune n'etait executee**. Icinga surveillait deux choses.
|
||||
|
||||
C'est la classe d'echec de ce depot, en version documentaire : la carte dit ce qui est
|
||||
surveille, et personne ne surveille. *Une intention ecrite n'est pas une mesure.*
|
||||
|
||||
### Le mecanisme
|
||||
|
||||
Un role declare ses sondes dans `meta/supervision.yml` (nom, `ttl`, raison) et **depose
|
||||
lui-meme** son script dans `/usr/local/lib/setops/sondes/` — il connait ses chemins, ses
|
||||
seuils, son consommateur. Le porteur (`client_sante`) fait tourner tout ce qui vit dans ce
|
||||
repertoire et pousse un resultat passif par sonde ; il ne sait pas ce qu'elles mesurent, et
|
||||
c'est le but. `serveur_icinga` derive les `object Service` ET **le filtre de permission
|
||||
d'API** des memes declarations.
|
||||
|
||||
Ajouter une sonde ne demande de toucher ni au porteur, ni a Icinga. Comme un flux declare
|
||||
devient une regle nftables.
|
||||
|
||||
### La sonde du certificat, et ce qu'elle mesure VRAIMENT
|
||||
|
||||
Nos certificats vivent **24 heures**. Une machine dont le renouvellement s'arrete ne casse
|
||||
pas tout de suite : elle casse le LENDEMAIN, et tout ce qui parle TLS avec elle tombe
|
||||
ensemble.
|
||||
|
||||
Ce qu'on ne mesure pas, et c'est delibere : *« le minuteur est-il actif ? »* — un minuteur
|
||||
vert qui echoue chaque nuit est exactement le mensonge deja paye avec les sauvegardes.
|
||||
*« le certificat existe-t-il ? »* — un certificat perime existe.
|
||||
|
||||
Ce qu'on mesure : les heures restantes sur le certificat REELLEMENT pose, la chaine
|
||||
verifiee contre notre racine, et — quand un service le consomme — l'empreinte SERVIE
|
||||
comparee a celle du disque.
|
||||
|
||||
### Trois obstacles, et le premier est le plus instructif
|
||||
|
||||
**La sonde a rendu 14 machines sur 14 en CRITIQUE — sur une PKI qui se portait tres bien.**
|
||||
Elle utilisait `openssl verify -CAfile racine` ; or nos certificats d'hote sont signes par
|
||||
un INTERMEDIAIRE, et seule la racine vit sur le disque. `step certificate verify`, l'outil
|
||||
que le role installe deja, repond VALIDE.
|
||||
|
||||
*Une alarme toujours allumee ne vaut pas mieux qu'une alarme jamais allumee : elle apprend
|
||||
a ne plus regarder.* Une sonde se prouve donc DEUX FOIS — verte sur le sain, rouge sur le
|
||||
casse. Le document de conception porte la regle.
|
||||
|
||||
**Le filtre d'API etait ecrit avant la lecture des declarations.** Les services existaient,
|
||||
et Icinga aurait refuse leurs resultats avec un « 404 No objects found » sur des objets
|
||||
bien presents — une heure perdue sur ce message le 2026-09-02. La lecture est remontee
|
||||
avant le compte d'API, avec le pourquoi ecrit la ou l'on serait tente de la redescendre.
|
||||
|
||||
**Mon controle negatif a casse un service reel.** Substituer le certificat d'hote par un
|
||||
auto-signe a fait virer la sonde au rouge, comme voulu — et le script de synchronisation a
|
||||
propage ce certificat vers `node_exporter`, dont la clef etait restee l'ancienne :
|
||||
`tls: private key does not match public key`. Un service tombe pour eprouver une sonde. La
|
||||
regle qui en decoule est au document : **un controle negatif se fait sur une copie**,
|
||||
jamais en substituant l'artefact que d'autres consomment.
|
||||
|
||||
### Les quatre etats, tous eprouves
|
||||
|
||||
certificat absent -> [2] CRITIQUE
|
||||
signe par une autre autorite -> [2] CRITIQUE (24 h restantes : c'est la CHAINE qui parle)
|
||||
sous le seuil d'avertissement-> [1] AVERTISSEMENT
|
||||
etat sain -> [0] OK
|
||||
|
||||
Puis relu dans IcingaDB : `certificat : 14/14 OK`, `sante : 14/14 OK`.
|
||||
|
||||
### P64 tient les deux bouts
|
||||
|
||||
Une sonde DECLAREE dont le script n'est pas depose donnerait un service qui n'a jamais de
|
||||
resultat. Un script DEPOSE que rien ne declare serait refuse par le filtre d'API. Les deux
|
||||
se rompent seuls, la preuve tient les deux — plus la presence d'une `raison` et d'un `ttl`.
|
||||
Trois controles negatifs rejoues.
|
||||
|
||||
Ce qu'elle ne fait pas, et qu'aucune lecture statique ne fera : juger qu'une sonde MESURE
|
||||
quelque chose. Ca reste au controle negatif de chacune, exige par le document et trace ici.
|
||||
|
||||
### Et une QUATRIEME fois la meme lecon : les seuils criaient trop tot
|
||||
|
||||
Deployee sur le site, la sonde a rendu **5/7** — deux machines en AVERTISSEMENT, sur des
|
||||
certificats parfaitement sains.
|
||||
|
||||
Le seuil etait a 12 h. Or `cert-renewer@.service` porte
|
||||
`ExecCondition=step certificate needs-renewal`, qui dit oui au TIERS RESTANT : **8 h** pour
|
||||
un certificat de 24 h, reessaye toutes les 15 minutes. La sonde criait donc AVANT que le
|
||||
mecanisme ne soit cense agir.
|
||||
|
||||
Les seuils sont desormais SOUS le point de renouvellement — 6 h (le renouvellement est du
|
||||
depuis deux heures, huit fenetres manquees : ce n'est plus un hoquet) et 3 h. *Un seuil
|
||||
au-dessus du point de renouvellement ne previent pas : il ment.*
|
||||
|
||||
Un seuil ne se choisit pas a l'intuition : il se DERIVE du moment ou le mecanisme surveille
|
||||
est cense agir. C'est la meme faute que `openssl verify` quelques heures plus tot, sous une
|
||||
autre forme — et c'est encore la flotte reelle qui l'a dite.
|
||||
|
||||
**Etat final : 14/14 au tenant, 7/7 au site**, sur `certificat` comme sur `sante`.
|
||||
|
||||
### Reste
|
||||
|
||||
Dix-huit groupes attendent encore leur sonde. Le mecanisme, lui, ne demande plus rien.
|
||||
|
||||
## 2026-09-09 (5) — Retrait d'une garde qui ne pouvait pas se declencher
|
||||
|
||||
`client_sante` portait une branche « aucun `serveur_icinga` : rien a poser », et un `when`
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (62 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (64 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 36 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 58 scripts expliques et atteignables, 115 cibles make documentees, 67 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 39 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (38 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (35 genere(s) exempte(s)). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (35 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
|
|
@ -69,13 +69,14 @@
|
|||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (63 preuves, 67 roles, 41 groupes). |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (64 preuves, 67 roles, 41 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `2d9dc86` (publie le 2026-09-09). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). |
|
||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 1 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ README de rôles). Cette page comble ces deux trous.
|
|||
|---|---|---|
|
||||
| rôles | 67 | `roles/*/` |
|
||||
| README de rôles | 67 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 39 | `docs/*.md` |
|
||||
| documents | 40 | `docs/*.md` |
|
||||
| pièces d'audit | 40 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 82 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 63 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 64 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
propriétaire, que rien n'est codé en dur.
|
||||
|
|
|
|||
109
docs/supervision-conception.md
Normal file
109
docs/supervision-conception.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Supervision dérivée des rôles
|
||||
|
||||
> **Pour qui :** celui qui ajoute un rôle à Set-OPS et se demande comment sa supervision
|
||||
> arrive dans Icinga — et celui qui exploite et veut savoir d'où sortent les services
|
||||
> qu'il voit.
|
||||
|
||||
> **La règle en une phrase.** Un rôle déclare les sondes de sa propre supervision ; le
|
||||
> moteur en dérive les objets Icinga, la permission d'API et les preuves. Comme
|
||||
> `meta/flux.yml` engendre nftables *et* OPNsense.
|
||||
|
||||
## Pourquoi
|
||||
|
||||
Le 2026-09-09, mesure du dépôt sur lui-même :
|
||||
|
||||
```
|
||||
au 2026-09-09 :
|
||||
39 rôles déclarent leurs flux -> nftables + OPNsense, dérivés
|
||||
32 déclarent leur empreinte -> ressources des VM, dérivées
|
||||
32 déclarent leur authentification -> habilitations, dérivées
|
||||
19 groupes déclarent `surveillance:` -> RIEN
|
||||
```
|
||||
|
||||
Au 2026-09-09, les dix-neuf lignes `surveillance:` de `docs/dependances-groupes.yml` sont écrites,
|
||||
versionnées, relues — et **aucune n'est exécutée**. Icinga surveillait deux choses :
|
||||
`sauvegarde` et `sante`.
|
||||
|
||||
C'est la classe d'échec que ce dépôt nomme partout ailleurs, en version documentaire : la
|
||||
carte dit ce qui est surveillé, et personne ne surveille. *Une intention écrite n'est pas
|
||||
une mesure.*
|
||||
|
||||
## Le contrat d'une sonde
|
||||
|
||||
Une sonde est un **script local**, déposé par le rôle qui la possède, dans
|
||||
`/usr/local/lib/setops/sondes/<nom>.sh` (0750, root).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **sortie standard** | une ligne, le message lisible par un humain |
|
||||
| **code de sortie** | `0` OK · `1` AVERTISSEMENT · `2` CRITIQUE |
|
||||
| **réseau** | aucun besoin : c'est le porteur qui pousse le résultat |
|
||||
| **durée** | courte ; le porteur passe toutes les 15 min |
|
||||
|
||||
Le rôle qui possède la sonde la **déploie lui-même** : il connaît ses chemins, ses
|
||||
secrets, sa vérité de terrain. Il la **déclare** dans `meta/supervision.yml`, et c'est
|
||||
cette déclaration que le moteur lit.
|
||||
|
||||
```yaml
|
||||
# roles/<rôle>/meta/supervision.yml
|
||||
sondes:
|
||||
- nom: certificat
|
||||
ttl: 5400
|
||||
raison: "..."
|
||||
```
|
||||
|
||||
## Passif, et à durée de vie — jamais actif
|
||||
|
||||
Un contrôle **actif** ne voit pas la machine **muette** : si elle ne répond plus, la sonde
|
||||
échoue et on met ça sur le compte du réseau. Ici c'est le nœud qui parle, et le `ttl` de
|
||||
son envoi fait la fraîcheur — sans nouvelle, Icinga périme le service tout seul.
|
||||
|
||||
**Le silence alerte autant que l'échec.** C'est exactement ce qui a manqué à
|
||||
`openipmi.service` : une unité en échec à chaque démarrage pendant une semaine, et
|
||||
`systemctl --failed` à zéro partout parce qu'aucune machine n'avait redémarré.
|
||||
|
||||
C'est aussi ce qu'impose *la vérification suit la clé* : la sonde tourne là où vit la
|
||||
vérité, pas sur le superviseur. Un dépôt de sauvegarde chiffré côté client ne peut être
|
||||
jugé que par qui détient la clé.
|
||||
|
||||
## Ce qui n'a rien à faire ici
|
||||
|
||||
Une **métrique à seuil** — durée de collecte, volume de journaux, taux d'occupation —
|
||||
appartient à Prometheus et Grafana. Icinga répond à une seule question : *est-ce cassé ?*
|
||||
Mélanger les deux rendrait les deux moins lisibles.
|
||||
|
||||
## Chaque sonde vient avec son contrôle négatif
|
||||
|
||||
Une sonde qu'on n'a jamais vue échouer n'est pas une sonde, c'est une habitude. Dix-neuf
|
||||
voyants verts non éprouvés seraient un recul par rapport à deux voyants prouvés : ils
|
||||
rassureraient.
|
||||
|
||||
Toute sonde ajoutée doit donc être accompagnée de la manière de la faire échouer
|
||||
**pour de vrai**, et cette manière doit être rejouée au moins une fois, sur une machine
|
||||
réelle. Le CHANGELOG en porte la trace.
|
||||
|
||||
**Et contre un état sain, tout autant.** La première version de la sonde du certificat a
|
||||
rendu **14 machines sur 14 en CRITIQUE** — sur une PKI qui se portait très bien. Elle
|
||||
utilisait `openssl verify -CAfile racine`, alors que nos certificats sont signés par un
|
||||
*intermédiaire* que seul `step certificate verify` sait retrouver. Une alarme toujours
|
||||
allumée ne vaut pas mieux qu'une alarme jamais allumée : elle apprend à ne plus regarder.
|
||||
Une sonde se prouve donc **deux fois** — verte sur le sain, rouge sur le cassé.
|
||||
|
||||
## Un contrôle négatif ne doit pas pouvoir abîmer
|
||||
|
||||
Éprouver la sonde du certificat, le 2026-09-09, a consisté à **remplacer le certificat
|
||||
d'hôte** par un auto-signé. La sonde a bien viré au rouge — et le script de synchronisation
|
||||
a propagé ce certificat vers `node_exporter`, dont la clé était restée l'ancienne :
|
||||
|
||||
```
|
||||
failed to load X509KeyPair: tls: private key does not match public key
|
||||
```
|
||||
|
||||
Un service réel est tombé pour éprouver une sonde. Le contrôle avait un **rayon d'action**
|
||||
que je ne lui avais pas donné volontairement.
|
||||
|
||||
La règle qui en découle : un contrôle négatif se fait sur une **copie**, ou sur un chemin
|
||||
que la sonde accepte en paramètre — jamais en substituant l'artefact que d'autres
|
||||
consomment. Quand c'est impossible, on le fait sur la machine la moins critique, on
|
||||
l'annonce, et on vérifie l'état des consommateurs **après**, pas seulement celui de la
|
||||
sonde.
|
||||
|
|
@ -100,3 +100,30 @@ client_pki_reload_services: []
|
|||
# d'attendre le renouvellement automatique. 3600 = une heure : large devant le minuteur
|
||||
# (~14 min), serre devant la duree de vie (24 h).
|
||||
client_pki_marge_renouvellement: 3600
|
||||
|
||||
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
|
||||
#
|
||||
# LES SEUILS DECOULENT DU MOMENT OU LE RENOUVELLEMENT EST DU — pas d'une intuition.
|
||||
#
|
||||
# `cert-renewer@.service` porte `ExecCondition=step certificate needs-renewal`, qui dit
|
||||
# oui au TIERS RESTANT : 8 h pour un certificat de 24 h. Le minuteur repasse toutes les
|
||||
# 15 minutes (`OnCalendar=*:1/15`). En marche normale, un certificat ne descend donc
|
||||
# jamais durablement sous 8 h.
|
||||
#
|
||||
# PREMIERE VERSION FAUSSE, ET INSTRUCTIVE (2026-09-09) : avert a 12 h, crit a 4 h. Deux
|
||||
# machines du site sur sept sont passees en AVERTISSEMENT — sur des certificats
|
||||
# parfaitement sains, simplement pas encore eligibles au renouvellement. La sonde criait
|
||||
# AVANT que le mecanisme ne soit cense agir.
|
||||
#
|
||||
# Les seuils sont donc SOUS le point de renouvellement :
|
||||
# 6 h -> le renouvellement est du depuis 2 h et n'a pas abouti : huit fenetres de
|
||||
# quinze minutes manquees. Ce n'est plus un hoquet.
|
||||
# 3 h -> plus aucune marge ; la panne est certaine au prochain cycle.
|
||||
#
|
||||
# Un seuil au-dessus du point de renouvellement ne previent pas : il ment.
|
||||
client_pki_sonde_avert_h: 6
|
||||
client_pki_sonde_crit_h: 3
|
||||
|
||||
# Port local ou comparer le certificat SERVI a celui du disque. Vide = pas de comparaison
|
||||
# (l'hote n'expose rien en TLS, ou son consommateur n'ecoute pas en local).
|
||||
client_pki_sonde_port: ""
|
||||
|
|
|
|||
37
roles/client_pki/meta/supervision.yml
Normal file
37
roles/client_pki/meta/supervision.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||
#
|
||||
# CE QUE CETTE SONDE MESURE, ET POURQUOI CELA PLUTOT QUE LE RESTE.
|
||||
#
|
||||
# Nos certificats vivent 24 HEURES. Ce n'est pas un detail d'exploitation : c'est ce qui
|
||||
# rend la chaine entiere dependante d'un minuteur qui tourne. Une machine dont le
|
||||
# renouvellement s'arrete ne casse pas tout de suite — elle casse le LENDEMAIN, et alors
|
||||
# tout ce qui parle TLS avec elle tombe en meme temps.
|
||||
#
|
||||
# CE QU'ON NE MESURE PAS, ET C'EST DELIBERE :
|
||||
#
|
||||
# - « le minuteur est-il actif ? » — un minuteur vert qui echoue chaque nuit est
|
||||
# exactement le mensonge que ce depot a deja paye avec les sauvegardes. On mesure le
|
||||
# RESULTAT, pas l'intention.
|
||||
# - « le certificat existe-t-il ? » — un certificat perime existe.
|
||||
# - la duree de la poignee TLS, l'algorithme, la taille de clef : ce sont des metriques
|
||||
# ou des audits, pas la question « est-ce casse ? ».
|
||||
#
|
||||
# CE QU'ON MESURE : le temps qui reste avant expiration, sur le certificat REELLEMENT
|
||||
# POSE. Un renouvellement qui s'est arrete se voit des la premiere fenetre manquee, et
|
||||
# bien avant la panne.
|
||||
#
|
||||
# ET LE CERTIFICAT SERVI, PAS SEULEMENT CELUI DU DISQUE. Mesure du 2026-08 : un cert
|
||||
# renouvele sur disque restait servi PERIME en memoire tant que nginx/postfix/slapd
|
||||
# n'etaient pas recharges. Le fichier disait « tout va bien », le port disait le
|
||||
# contraire. La sonde compare donc les deux quand un service consomme le certificat.
|
||||
sondes:
|
||||
- nom: certificat
|
||||
# 5400 s = une heure et demie, soit six fois la periode du porteur (15 min). Cinq
|
||||
# passages peuvent manquer avant qu'Icinga ne perime : on ne crie pas pour un hoquet,
|
||||
# on crie pour un silence.
|
||||
ttl: 5400
|
||||
raison: >-
|
||||
Le certificat d'hote est-il encore valide assez longtemps, et le service qui le
|
||||
consomme sert-il bien CELUI-LA ? Nos certificats vivent 24 h : un renouvellement
|
||||
arrete ne se voit qu'au lendemain, quand tout ce qui parle TLS tombe ensemble.
|
||||
|
|
@ -275,3 +275,27 @@
|
|||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
|
||||
#
|
||||
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. Il connait ses chemins, ses
|
||||
# seuils, son consommateur — le superviseur, lui, ne connait que le verdict. C'est la
|
||||
# meme repartition que pour les flux : le role declare, le moteur derive.
|
||||
#
|
||||
# Le porteur (`client_sante`) fait tourner tout ce qui vit dans ce repertoire et pousse
|
||||
# un resultat passif par sonde. Il n'a pas a savoir ce que celle-ci mesure.
|
||||
- name: Assurer le repertoire des sondes de supervision
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/lib/setops/sondes
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer la sonde du certificat d'hote
|
||||
ansible.builtin.template:
|
||||
src: sonde-certificat.sh.j2
|
||||
dest: /usr/local/lib/setops/sondes/certificat.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
|
|
|
|||
68
roles/client_pki/templates/sonde-certificat.sh.j2
Normal file
68
roles/client_pki/templates/sonde-certificat.sh.j2
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/bin/bash
|
||||
# GENERE par Set-OPS (role client_pki). Ne pas editer a la main.
|
||||
#
|
||||
# SONDE : le certificat d'hote tiendra-t-il jusqu'au prochain renouvellement ?
|
||||
#
|
||||
# Contrat (docs/supervision-conception.md) : une ligne sur la sortie standard,
|
||||
# 0 = OK, 1 = AVERTISSEMENT, 2 = CRITIQUE. Aucun acces reseau requis — c'est le porteur
|
||||
# (`client_sante`) qui pousse le resultat a Icinga.
|
||||
set -uo pipefail
|
||||
|
||||
CERT="{{ client_pki_cert }}"
|
||||
RACINE="{{ client_pki_steppath }}/certs/root_ca.crt"
|
||||
AVERT={{ client_pki_sonde_avert_h }}
|
||||
CRIT={{ client_pki_sonde_crit_h }}
|
||||
|
||||
[[ -r "${CERT}" ]] || { echo "Aucun certificat d'hote lisible en ${CERT}."; exit 2; }
|
||||
|
||||
fin=$(openssl x509 -in "${CERT}" -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
[[ -n "${fin}" ]] || { echo "Certificat illisible : ${CERT}."; exit 2; }
|
||||
reste=$(( ( $(date -d "${fin}" +%s) - $(date +%s) ) / 3600 ))
|
||||
|
||||
# LA CHAINE, PAS SEULEMENT LA DATE. Un certificat non expire mais signe par une autre
|
||||
# racine que la notre est une panne aussi complete qu'un certificat perime — et elle
|
||||
# arrive : une AC reconstruite emet des certificats parfaitement valides que plus
|
||||
# personne ne verifie.
|
||||
#
|
||||
# `step certificate verify`, PAS `openssl verify` — et la premiere version de cette sonde
|
||||
# a fait la faute (2026-09-09). Nos certificats d'hote sont signes par un INTERMEDIAIRE,
|
||||
# et seule la racine vit sur le disque : `openssl verify -CAfile racine` ne trouve pas le
|
||||
# maillon manquant et refuse. Resultat, 14 machines sur 14 en CRITIQUE — sur une PKI qui
|
||||
# se portait tres bien. `step` sait lire la chaine que step-ca ecrit dans le fichier.
|
||||
#
|
||||
# LA LECON N'EST PAS « prendre le bon outil » : c'est qu'une sonde doit etre eprouvee
|
||||
# contre un etat SAIN autant que contre un etat casse. Une alarme toujours allumee ne vaut
|
||||
# pas mieux qu'une alarme jamais allumee — elle apprend a ne plus regarder.
|
||||
if ! step certificate verify "${CERT}" --roots "${RACINE}" >/dev/null 2>&1; then
|
||||
echo "Le certificat d'hote ne se verifie PAS contre ${RACINE} (${reste} h restantes)."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
{% if client_pki_reload_services %}
|
||||
# LE CERTIFICAT SERVI, PAS CELUI DU DISQUE (mesure du 2026-08).
|
||||
#
|
||||
# Un cert renouvele sur disque restait servi PERIME en memoire tant que le consommateur
|
||||
# n'etait pas recharge. Le fichier disait « tout va bien », le port disait le contraire —
|
||||
# et c'est le port que voient les autres machines. On compare donc les empreintes.
|
||||
disque=$(openssl x509 -in "${CERT}" -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2)
|
||||
{% for service in client_pki_reload_services %}
|
||||
{% if client_pki_sonde_port %}
|
||||
servi=$(echo | timeout 6 openssl s_client -connect 127.0.0.1:{{ client_pki_sonde_port }} 2>/dev/null \
|
||||
| openssl x509 -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2)
|
||||
if [[ -n "${servi}" && "${servi}" != "${disque}" ]]; then
|
||||
echo "{{ service }} sert un certificat DIFFERENT de celui du disque : recharger le service (${reste} h restantes)."
|
||||
exit 2
|
||||
fi
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
if (( reste <= CRIT )); then
|
||||
echo "Certificat d'hote : ${reste} h avant expiration — le renouvellement ne passe plus."
|
||||
exit 2
|
||||
fi
|
||||
if (( reste <= AVERT )); then
|
||||
echo "Certificat d'hote : ${reste} h avant expiration (seuil ${AVERT} h)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Certificat d'hote valide ${reste} h, chaine verifiee."
|
||||
|
|
@ -31,3 +31,8 @@ client_sante_ttl_icinga: 2700
|
|||
# et personne ne fait la difference. Si une unite doit etre toleree, elle est ECRITE ICI,
|
||||
# avec sa raison — pas absorbee par un filtre qui en cacherait d'autres.
|
||||
client_sante_unites_tolerees: []
|
||||
|
||||
# `ttl` des sondes de ROLE. Plus long que celui de `sante` : une sonde de role peut etre
|
||||
# plus lente (une poignee TLS, une requete), et on ne veut pas qu'un hoquet la perime.
|
||||
# Six passages du porteur.
|
||||
client_sante_ttl_sondes: 5400
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ TTL={{ client_sante_ttl_icinga }}
|
|||
MOI="{{ inventory_hostname }}"
|
||||
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
||||
|
||||
rapporter() { # $1=code $2=texte
|
||||
rapporter() { # $1=service $2=code $3=texte $4=ttl
|
||||
local charge reponse
|
||||
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
||||
"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]),
|
||||
"plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \
|
||||
"${MOI}!sante" "$1" "$2" "${TTL}")
|
||||
"${MOI}!$1" "$2" "$3" "$4")
|
||||
reponse=$(curl -sS --cacert "{{ client_sante_ca_verification }}" --max-time 20 \
|
||||
-u "{{ client_sante_icinga_utilisateur }}:${MOTDEPASSE}" \
|
||||
-H 'Accept: application/json' -H 'Content-Type: application/json' \
|
||||
|
|
@ -77,13 +77,40 @@ done
|
|||
echecs=("${restantes[@]}")
|
||||
{% endif %}
|
||||
|
||||
# LES SONDES DES ROLES, APRES LA SIENNE.
|
||||
#
|
||||
# Chaque role depose SA sonde dans ce repertoire ; le porteur ne sait pas ce qu'elles
|
||||
# mesurent, et c'est le but : ajouter une sonde n'oblige pas a toucher a ce script.
|
||||
# Contrat : une ligne sur stdout, 0/1/2 en code de sortie
|
||||
# (docs/supervision-conception.md).
|
||||
#
|
||||
# UNE SONDE QUI JETTE N'EST PAS UNE SONDE VERTE. Sans ce traitement, un script casse
|
||||
# sortirait en 127 et Icinga recevrait un code hors bornes — ou rien. On rapporte donc
|
||||
# CRITIQUE, en le disant : le pire serait qu'un script mort produise un service muet, que
|
||||
# le `ttl` finirait par perimer sans que personne sache pourquoi.
|
||||
sondes() {
|
||||
local f nom sortie code
|
||||
for f in /usr/local/lib/setops/sondes/*.sh; do
|
||||
[[ -x "$f" ]] || continue
|
||||
nom=$(basename "$f" .sh)
|
||||
sortie=$("$f" 2>&1); code=$?
|
||||
if (( code > 3 )); then
|
||||
rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "{{ client_sante_ttl_sondes }}"
|
||||
else
|
||||
rapporter "$nom" "$code" "${sortie:-(aucun message)}" "{{ client_sante_ttl_sondes }}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
n=${#echecs[@]}
|
||||
if [[ $n -eq 0 ]]; then
|
||||
rapporter 0 "Aucune unite systemd en echec."
|
||||
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
||||
sondes
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# CRITIQUE des la premiere, et non un seuil. Une unite en echec est soit un vrai
|
||||
# probleme, soit du bruit qu'il faut retirer : dans les deux cas il faut agir. Un seuil
|
||||
# ferait vivre le bruit indefiniment, et c'est exactement ce qu'on vient de corriger.
|
||||
rapporter 2 "$n unite(s) systemd en echec : ${echecs[*]}"
|
||||
rapporter "sante" 2 "$n unite(s) systemd en echec : ${echecs[*]}" "${TTL}"
|
||||
sondes
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ serveur_icinga_hotes_supervises: >-
|
|||
|
||||
serveur_icinga_hotes_conf: "/etc/icinga2/conf.d/setops-hotes.conf"
|
||||
serveur_icinga_sante_conf: "/etc/icinga2/conf.d/setops-sante.conf"
|
||||
serveur_icinga_sondes_conf: "/etc/icinga2/conf.d/setops-sondes.conf"
|
||||
# Rempli a l'execution depuis les `meta/supervision.yml` des roles.
|
||||
serveur_icinga_sondes: {}
|
||||
|
||||
# A QUI LA SUPERVISION PARLE — vide = personne, et on le DIT.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -172,6 +172,40 @@
|
|||
vault_icinga_api_depot requis : sans lui, ni le depot ni les noeuds ne peuvent
|
||||
rapporter l'etat de leurs sauvegardes, et personne ne les surveille.
|
||||
|
||||
# LU AVANT LE COMPTE D'API, ET CE N'EST PAS COSMETIQUE : le filtre de permission
|
||||
# DERIVE de ces declarations. Releve apres, il aurait ete ecrit sans les noms des
|
||||
# sondes — les services auraient existe, et Icinga aurait refuse leurs resultats
|
||||
# avec un 404 « No objects found » sur des objets bien presents. Mesure du
|
||||
# 2026-09-09 : `certificat` manquait au filtre pour cette seule raison.
|
||||
- name: Relever les sondes que les roles declarent
|
||||
ansible.builtin.find:
|
||||
paths: "{{ role_path }}/.."
|
||||
patterns: supervision.yml
|
||||
recurse: true
|
||||
depth: 3
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: serveur_icinga_metas_supervision
|
||||
|
||||
- name: Lire chaque declaration de supervision
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ item.path }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
loop: "{{ serveur_icinga_metas_supervision.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | dirname | dirname | basename }}"
|
||||
register: serveur_icinga_supervision_brute
|
||||
|
||||
- name: Assembler le registre des sondes (role -> sondes)
|
||||
ansible.builtin.set_fact:
|
||||
serveur_icinga_sondes: >-
|
||||
{{ dict(serveur_icinga_supervision_brute.results
|
||||
| map(attribute='item.path') | map('dirname') | map('dirname') | map('basename')
|
||||
| zip(serveur_icinga_supervision_brute.results
|
||||
| map(attribute='content') | map('b64decode') | map('from_yaml')
|
||||
| map(attribute='sondes'))) }}
|
||||
|
||||
- name: Deployer le compte d'API du depot de sauvegarde
|
||||
ansible.builtin.template:
|
||||
src: setops-api-users.conf.j2
|
||||
|
|
@ -185,6 +219,14 @@
|
|||
# LES HOTES D'ABORD : les fichiers de service s'y attachent, et Icinga refuse un service
|
||||
# dont l'hote n'existe pas. L'ordre dans `conf.d` n'est pas garanti par le nom, mais
|
||||
# Icinga charge tout le repertoire avant de resoudre — l'ordre de deploiement suffit.
|
||||
# LES SONDES DECLAREES PAR LES ROLES (docs/supervision-conception.md).
|
||||
#
|
||||
# On lit les `meta/supervision.yml` sur le CONTROLEUR, pas sur la cible : c'est le depot
|
||||
# qui fait foi, et la cible n'a aucune raison de porter les declarations des autres.
|
||||
#
|
||||
# Lecture explicite plutot que dependance a l'ordre de chargement des roles : un
|
||||
# `client_pki_*` visible ici parce qu'un autre role l'a charge avant serait un couplage
|
||||
# invisible, et il se romprait le jour ou l'ordre change.
|
||||
- name: Deployer les hotes supervises (definis une seule fois)
|
||||
ansible.builtin.template:
|
||||
src: setops-hotes.conf.j2
|
||||
|
|
@ -212,6 +254,15 @@
|
|||
mode: "0640"
|
||||
notify: Redemarrer icinga2
|
||||
|
||||
- name: Deployer les objets de supervision declares par les roles
|
||||
ansible.builtin.template:
|
||||
src: setops-sondes.conf.j2
|
||||
dest: "{{ serveur_icinga_sondes_conf }}"
|
||||
owner: root
|
||||
group: nagios
|
||||
mode: "0640"
|
||||
notify: Redemarrer icinga2
|
||||
|
||||
- name: Valider la configuration Icinga 2 avant de la rendre vivante
|
||||
ansible.builtin.command:
|
||||
cmd: icinga2 daemon -C
|
||||
|
|
|
|||
|
|
@ -36,7 +36,14 @@ object ApiUser "{{ serveur_icinga_api_utilisateur }}" {
|
|||
La portee reste etroite : ce compte ne peut poser un resultat que sur un service
|
||||
de sauvegarde, jamais ailleurs.
|
||||
#}
|
||||
filter = {{ '{{' }} match("sauvegarde: *", service.name) || service.name == "sauvegarde" || service.name == "sante" {{ '}}' }}
|
||||
{#
|
||||
LES NOMS SONT DERIVES, PAS RECOPIES. Une sonde declaree par un role entre dans ce
|
||||
filtre toute seule ; une sonde retiree en sort. Recopier la liste ici, c'etait la
|
||||
garantie qu'un jour elle divergerait — et le symptome serait un 404 « No objects
|
||||
found » sur un objet qui existe, comme le 2026-09-02. Une heure perdue.
|
||||
La portee reste etroite : des NOMS, jamais un joker.
|
||||
#}
|
||||
filter = {{ '{{' }} match("sauvegarde: *", service.name) || service.name == "sauvegarde" || service.name == "sante"{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}{% for s in sondes %} || service.name == "{{ s.nom }}"{% endfor %}{% endfor %} {{ '}}' }}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
34
roles/serveur_icinga/templates/setops-sondes.conf.j2
Normal file
34
roles/serveur_icinga/templates/setops-sondes.conf.j2
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
||||
*
|
||||
* LES SONDES QUE LES ROLES DECLARENT — voir docs/supervision-conception.md.
|
||||
*
|
||||
* Rien ici n'est ecrit a la main : chaque service vient d'un `meta/supervision.yml`, et
|
||||
* s'attache aux hotes qui portent le role qui le declare. Ajouter une sonde a un role la
|
||||
* fait apparaitre ici sans toucher a ce fichier — comme un flux declare devient une
|
||||
* regle nftables.
|
||||
*
|
||||
* CE QUE CA REMPLACE : dix-neuf lignes `surveillance:` en prose dans
|
||||
* `docs/dependances-groupes.yml`, ecrites, versionnees, relues — et qu'aucun mecanisme
|
||||
* n'executait. La carte disait ce qui etait surveille, et personne ne surveillait.
|
||||
*
|
||||
* Les HOTES sont definis dans `setops-hotes.conf` : ici, rien que des services.
|
||||
*/
|
||||
|
||||
{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}
|
||||
{% for hote in (groups[role] | default([])) | sort %}
|
||||
{% for sonde in sondes %}
|
||||
object Service "{{ sonde.nom }}" {
|
||||
host_name = "{{ hote }}"
|
||||
check_command = "setops-passif"
|
||||
enable_active_checks = false
|
||||
enable_passive_checks = true
|
||||
volatile = false
|
||||
max_check_attempts = 1
|
||||
vars.setops_source = "{{ hote }}"
|
||||
vars.setops_role = "{{ role }}"
|
||||
vars.setops_raison = "{{ (sonde.raison | default('')) | replace('"', "'") | replace('\n', ' ') | trim }}"
|
||||
}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
@ -2168,6 +2168,88 @@ def preuve_gabarit_minimal_et_repris() -> tuple[bool, str]:
|
|||
f"repris par le socle ou le durcissement.")
|
||||
|
||||
|
||||
def preuve_sondes_declarees_et_deposees() -> tuple[bool, str]:
|
||||
"""Une sonde declaree existe-t-elle vraiment, et un role qui en depose une la declare-t-il ?
|
||||
|
||||
CE QU'ELLE FERME (2026-09-09). `docs/dependances-groupes.yml` porte une ligne
|
||||
`surveillance:` pour DIX-NEUF groupes sur dix-neuf. Icinga en surveillait DEUX. Des
|
||||
intentions ecrites, versionnees, relues — et qu'aucun mecanisme n'executait. C'est la
|
||||
classe d'echec de ce depot, en version documentaire : la carte dit ce qui est
|
||||
surveille, et personne ne surveille.
|
||||
|
||||
La supervision se declare desormais dans `roles/<role>/meta/supervision.yml`, et le
|
||||
moteur en derive les objets Icinga ET la permission d'API. Cette preuve tient les deux
|
||||
bouts de la declaration, parce que chacun peut se rompre seul :
|
||||
|
||||
1. une sonde DECLAREE dont le script n'est pas depose : le service existerait dans
|
||||
Icinga et n'aurait jamais de resultat. Le `ttl` le perimerait — un service
|
||||
« expire » qui ne l'a jamais ete, que personne ne saurait interpreter ;
|
||||
2. un script DEPOSE que rien ne declare : la sonde tournerait, pousserait, et le
|
||||
compte d'API la refuserait (le filtre derive des declarations). Symptome :
|
||||
« 404 No objects found » sur un objet inexistant — une heure perdue le
|
||||
2026-09-02 sur exactement ce message.
|
||||
|
||||
CE QU'ELLE NE FAIT PAS. Elle ne juge pas si la sonde MESURE quelque chose d'utile, ni
|
||||
si elle peut echouer. Ca, aucune lecture statique ne le dira : c'est au controle
|
||||
negatif de chaque sonde, exige par `docs/supervision-conception.md` et trace au
|
||||
CHANGELOG.
|
||||
"""
|
||||
roles = RACINE / "roles"
|
||||
fautes, mesures = [], []
|
||||
for meta in sorted(roles.glob("*/meta/supervision.yml")):
|
||||
role = meta.parent.parent.name
|
||||
try:
|
||||
data = yaml.safe_load(meta.read_text(encoding="utf-8")) or {}
|
||||
except yaml.YAMLError as e:
|
||||
fautes.append(f"{role} : `meta/supervision.yml` illisible ({e})")
|
||||
continue
|
||||
sondes = data.get("sondes") or []
|
||||
if not sondes:
|
||||
fautes.append(f"{role} : `meta/supervision.yml` ne declare aucune sonde")
|
||||
continue
|
||||
taches = (roles / role / "tasks" / "main.yml")
|
||||
corps = taches.read_text(encoding="utf-8") if taches.is_file() else ""
|
||||
for s in sondes:
|
||||
nom = s.get("nom")
|
||||
if not nom:
|
||||
fautes.append(f"{role} : une sonde sans `nom`")
|
||||
continue
|
||||
if not s.get("raison"):
|
||||
fautes.append(f"{role}/{nom} : aucune `raison` — un service dont personne "
|
||||
f"ne sait ce qu'il mesure n'est pas une supervision")
|
||||
if not isinstance(s.get("ttl"), int):
|
||||
fautes.append(f"{role}/{nom} : `ttl` manquant — sans lui, le SILENCE "
|
||||
f"n'alerte pas, et c'est la moitie de l'interet")
|
||||
# Le role doit deposer le script au chemin du contrat.
|
||||
if f"/usr/local/lib/setops/sondes/{nom}.sh" not in corps:
|
||||
fautes.append(f"{role}/{nom} : declaree mais jamais deposee dans "
|
||||
f"`tasks/main.yml` — le service existerait sans jamais avoir "
|
||||
f"de resultat")
|
||||
mesures.append(f"{role}/{nom}")
|
||||
|
||||
# L'AUTRE SENS : un script depose que rien ne declare.
|
||||
for taches in sorted(roles.glob("*/tasks/main.yml")):
|
||||
role = taches.parent.parent.name
|
||||
corps = taches.read_text(encoding="utf-8")
|
||||
deposees = set(re.findall(r"/usr/local/lib/setops/sondes/([A-Za-z0-9_.-]+)\.sh", corps))
|
||||
meta = roles / role / "meta" / "supervision.yml"
|
||||
declarees = set()
|
||||
if meta.is_file():
|
||||
data = yaml.safe_load(meta.read_text(encoding="utf-8")) or {}
|
||||
declarees = {s.get("nom") for s in (data.get("sondes") or [])}
|
||||
for orpheline in sorted(deposees - declarees):
|
||||
fautes.append(f"{role}/{orpheline} : deposee mais absente de "
|
||||
f"`meta/supervision.yml` — le compte d'API la refuserait")
|
||||
|
||||
if fautes:
|
||||
return False, "Sondes de supervision :\n - " + "\n - ".join(fautes)
|
||||
if not mesures:
|
||||
return True, ("Aucune sonde declaree — la supervision se limite a `sante` et "
|
||||
"`sauvegarde` (cf. docs/supervision-conception.md).")
|
||||
return True, (f"{len(mesures)} sonde(s) declaree(s) ET deposee(s), chacune avec sa "
|
||||
f"raison et son `ttl` : {', '.join(mesures)}.")
|
||||
|
||||
|
||||
def preuve_cloud_init_rendu_puis_retire() -> tuple[bool, str]:
|
||||
"""cloud-init nait avec la VM, et ne lui survit pas.
|
||||
|
||||
|
|
@ -3097,6 +3179,8 @@ PREUVES: list[dict] = [
|
|||
"refs": ["AFF-033"], "func": preuve_schema_couvre_les_validateurs},
|
||||
{"id": "P63", "titre": "cloud-init nait avec la VM et ne lui survit pas", "refs": [],
|
||||
"func": preuve_cloud_init_rendu_puis_retire},
|
||||
{"id": "P64", "titre": "Sondes de supervision : declarees ET deposees", "refs": [],
|
||||
"func": preuve_sondes_declarees_et_deposees},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue