From 69b84f4e2c1dbf91173e7f3151267b45bf21d6af Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 9 Sep 2026 21:06:08 -0400 Subject: [PATCH] client_sante : retrait d une garde qui ne pouvait pas se declencher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le role portait une branche « aucun serveur_icinga : rien a poser » et un when sur tout son bloc. Ni l une ni l autre ne pouvait s executer. Eprouve sur le modele public, dans une copie jetable : instancier ne pose une integration UNIVERSELLE que si son serveur existe dans l ecosysteme. Sans Icinga au plan, le groupe client_sante est ABSENT de l inventaire genere — comme client_metrique et client_journal le sont deja. Le role n est donc jamais appele sans destinataire. Une garde qui ne peut pas se declencher n est pas une garde : elle rassure sans rien tenir, et elle coute le jour ou l on cherche pourquoi rien n a alerte. Ce qui la remplace se declenche vraiment, et les deux cotes sont eprouves : hote vide -> FAILED, secret vide -> FAILED, et l echec dit lequel des deux manque. Le bloc, prive de sa condition, est aplati : douze taches a plat. Rejoue sur les deux flottes : zero changement sur zero hote. make prouver : CONFORME, 63 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 26 ++++ docs/audit/preuve-2026-09-09.md | 2 +- roles/client_sante/README.md | 13 +- roles/client_sante/tasks/main.yml | 200 +++++++++++++++--------------- 4 files changed, 139 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0edb6..233ce2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # CHANGELOG — Set-OPS +## 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` +sur tout son bloc. **Ni l'une ni l'autre ne pouvait s'executer.** + +Eprouve sur le modele public, dans une copie jetable : `instancier` ne pose une +integration UNIVERSELLE que si son serveur existe dans l'ecosysteme. Sans Icinga au plan, +le groupe `client_sante` est **absent** de l'inventaire genere — exactement comme +`client_metrique` et `client_journal` le sont deja. Le role n'est donc jamais appele sans +destinataire, et sa branche defensive etait du decor. + +*Une garde qui ne peut pas se declencher n'est pas une garde : elle rassure sans rien +tenir.* Et elle coute deux fois — a la lecture, puis le jour ou l'on cherche pourquoi rien +n'a alerte. + +Ce qui la remplace se declenche vraiment, et les deux cotes sont eprouves : + + -e client_sante_icinga_hote='' -> FAILED (assertion sur l'hote) + -e client_sante_icinga_motdepasse='' -> FAILED (assertion sur le secret) + +L'echec dit LEQUEL des deux manque. Le bloc, prive de sa condition, est aplati : douze +taches a plat au lieu de onze indentees sous un `when` toujours vrai. + +Rejoue sur les deux flottes : **zero changement sur zero hote**. `make prouver` : +CONFORME, 63 OK, 0 echec, 0 saute. + ## 2026-09-09 (4) — `systemctl --failed` entre dans la supervision **63 preuves. `make prouver` : CONFORME, 63 OK, 0 echec, 0 saute.** diff --git a/docs/audit/preuve-2026-09-09.md b/docs/audit/preuve-2026-09-09.md index f37ab26..b06c0fe 100644 --- a/docs/audit/preuve-2026-09-09.md +++ b/docs/audit/preuve-2026-09-09.md @@ -44,7 +44,7 @@ | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | | 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 : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). | +| 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)). | | 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). | diff --git a/roles/client_sante/README.md b/roles/client_sante/README.md index 97610d4..ba2dc93 100644 --- a/roles/client_sante/README.md +++ b/roles/client_sante/README.md @@ -37,5 +37,14 @@ où l'on cherche pourquoi rien n'a alerté. Le défaut est la liste **vide** — ## Ce dont il dépend -Un hôte `serveur_icinga` dans l'écosystème (sinon rien n'est posé, et c'est dit), le secret -`vault_icinga_api_depot`, et le flux sortant TCP 5665 vers lui. +Un hôte `serveur_icinga` dans l'écosystème, le secret `vault_icinga_api_depot`, et le flux +TCP 5665 vers lui — **déclaré des deux côtés**. Le côté serveur +(`roles/serveur_icinga/meta/flux.yml`) nomme le pair `serveur_debian`, pas `client_sante` : +la frontière ne résout que les rôles que les machines *portent au plan*, et une intégration +universelle n'y figure pas puisqu'elle est dérivée. L'oubli s'est payé le 2026-09-09 — +déploiement à 7/7, et cinq rapports sur sept en `TimeoutError`. + +Le rôle n'a **pas** de branche « et s'il n'y a pas de supervision ». Il n'en a pas besoin : +`instancier` ne pose une intégration universelle que si son serveur existe, donc le groupe +`client_sante` est absent d'un écosystème sans Icinga. À la place, un `assert` qui *peut* +échouer, et qui dit lequel des deux manque. diff --git a/roles/client_sante/tasks/main.yml b/roles/client_sante/tasks/main.yml index 6b9ea62..762e9f8 100644 --- a/roles/client_sante/tasks/main.yml +++ b/roles/client_sante/tasks/main.yml @@ -1,113 +1,115 @@ --- # LE NOEUD RAPPORTE SES UNITES EN ECHEC. Voir le gabarit du script pour le pourquoi. # -# Conditionne a l'existence d'un `serveur_icinga` : sans destinataire, le rapport -# n'irait nulle part, et un timer qui echoue chaque quart d'heure apprend a ignorer -# le rouge. Meme regle que `client_backup`. -- name: Aucune supervision dans cet écosystème — rien à poser - ansible.builtin.debug: - msg: >- - Aucun hôte `serveur_icinga` : le rapport de santé n'est pas posé. Ce n'est pas une - erreur — c'est un écosystème sans supervision, et le dire vaut mieux que d'installer - un minuteur qui échouerait dans le vide. - when: not client_sante_icinga_hote +# CE ROLE N'A PAS A SE DEMANDER S'IL Y A UNE SUPERVISION — IL EN A UNE (2026-09-09). +# +# Il portait une branche « aucun serveur_icinga : rien a poser », et un `when` sur tout le +# bloc. Ni l'une ni l'autre ne pouvait s'executer : `client_sante` est une INTEGRATION +# UNIVERSELLE, et `instancier` ne pose une integration universelle que si son serveur +# existe dans l'ecosysteme. Eprouve sur le modele public — sans Icinga au plan, le groupe +# `client_sante` est ABSENT de l'inventaire genere ; avec, les quatre noeuds y entrent. +# Le role n'est donc jamais appele sans destinataire. +# +# Une garde qui ne peut pas se declencher n'est pas une garde : elle rassure sans rien +# tenir. Ce qui la remplace se declenche vraiment — si ce role s'execute, un destinataire +# ET son secret sont exiges, et l'echec dit LEQUEL des deux manque. +- name: Exiger un destinataire et de quoi lui parler + ansible.builtin.assert: + that: + - client_sante_icinga_hote | length > 0 + - client_sante_icinga_motdepasse | length > 0 + fail_msg: >- + Rapport de santé impossible : + {{ "aucun hôte `serveur_icinga` dans l'inventaire" + if not client_sante_icinga_hote + else "`vault_icinga_api_depot` absent de la voûte" }}. + Le nœud verrait ses unités en échec sans pouvoir le dire. -- name: Poser le rapport de santé - when: client_sante_icinga_hote | length > 0 - block: - - name: Exiger de quoi rapporter à Icinga - ansible.builtin.assert: - that: - - client_sante_icinga_motdepasse | length > 0 - fail_msg: >- - `vault_icinga_api_depot` requis : un hôte `serveur_icinga` existe, mais aucun - secret d'API. Le nœud verrait ses unités en échec sans pouvoir le dire. +- name: Assurer le répertoire des secrets Set-OPS + ansible.builtin.file: + path: /etc/setops + state: directory + owner: root + group: root + mode: "0755" - - name: Assurer le répertoire des secrets Set-OPS - ansible.builtin.file: - path: /etc/setops - state: directory - owner: root - group: root - mode: "0755" +- name: Déposer le mot de passe d'API Icinga + ansible.builtin.copy: + content: "{{ client_sante_icinga_motdepasse }}\n" + dest: /etc/setops/icinga-api.pass + owner: root + group: root + mode: "0600" + no_log: true - - name: Déposer le mot de passe d'API Icinga - ansible.builtin.copy: - content: "{{ client_sante_icinga_motdepasse }}\n" - dest: /etc/setops/icinga-api.pass - owner: root - group: root - mode: "0600" - no_log: true +# L'AC d'ICINGA, PAS CELLE DE step-ca : Icinga refuse de servir un certificat qu'il +# n'a pas emis (il renouvelle tout ce qui expire sous 30 jours, nos certificats vivent +# 24 h). Domaine de confiance ferme, pair authentifie malgre tout. +- name: L'AC d'Icinga est-elle déjà disponible ? + ansible.builtin.stat: + path: "{{ client_sante_icinga_ca_source }}" + delegate_to: "{{ client_sante_icinga_hote }}" + register: client_sante_ca_presente - # L'AC d'ICINGA, PAS CELLE DE step-ca : Icinga refuse de servir un certificat qu'il - # n'a pas emis (il renouvelle tout ce qui expire sous 30 jours, nos certificats vivent - # 24 h). Domaine de confiance ferme, pair authentifie malgre tout. - - name: L'AC d'Icinga est-elle déjà disponible ? - ansible.builtin.stat: - path: "{{ client_sante_icinga_ca_source }}" - delegate_to: "{{ client_sante_icinga_hote }}" - register: client_sante_ca_presente +- name: Récupérer l'AC d'Icinga depuis l'hôte de supervision + when: client_sante_ca_presente.stat.exists + ansible.builtin.slurp: + src: "{{ client_sante_icinga_ca_source }}" + delegate_to: "{{ client_sante_icinga_hote }}" + register: client_sante_ca_icinga - - name: Récupérer l'AC d'Icinga depuis l'hôte de supervision - when: client_sante_ca_presente.stat.exists - ansible.builtin.slurp: - src: "{{ client_sante_icinga_ca_source }}" - delegate_to: "{{ client_sante_icinga_hote }}" - register: client_sante_ca_icinga +- name: Déposer l'AC d'Icinga pour la vérification du pair + when: client_sante_ca_presente.stat.exists + ansible.builtin.copy: + content: "{{ client_sante_ca_icinga.content | b64decode }}" + dest: "{{ client_sante_ca_verification }}" + owner: root + group: root + mode: "0644" - - name: Déposer l'AC d'Icinga pour la vérification du pair - when: client_sante_ca_presente.stat.exists - ansible.builtin.copy: - content: "{{ client_sante_ca_icinga.content | b64decode }}" - dest: "{{ client_sante_ca_verification }}" - owner: root - group: root - mode: "0644" +- name: Installer curl pour le rapport passif + ansible.builtin.apt: + name: curl + state: present - - name: Installer curl pour le rapport passif - ansible.builtin.apt: - name: curl - state: present +- name: Déployer le script de rapport de santé + ansible.builtin.template: + src: setops-sante.sh.j2 + dest: /usr/local/sbin/setops-sante.sh + owner: root + group: root + mode: "0750" - - name: Déployer le script de rapport de santé - ansible.builtin.template: - src: setops-sante.sh.j2 - dest: /usr/local/sbin/setops-sante.sh - owner: root - group: root - mode: "0750" +- name: Déployer l'unité systemd du rapport + ansible.builtin.template: + src: setops-sante.service.j2 + dest: /etc/systemd/system/setops-sante.service + owner: root + group: root + mode: "0644" + notify: Recharger systemd - - name: Déployer l'unité systemd du rapport - ansible.builtin.template: - src: setops-sante.service.j2 - dest: /etc/systemd/system/setops-sante.service - owner: root - group: root - mode: "0644" - notify: Recharger systemd +- name: Déployer le minuteur du rapport + ansible.builtin.template: + src: setops-sante.timer.j2 + dest: /etc/systemd/system/setops-sante.timer + owner: root + group: root + mode: "0644" + notify: Recharger systemd - - name: Déployer le minuteur du rapport - ansible.builtin.template: - src: setops-sante.timer.j2 - dest: /etc/systemd/system/setops-sante.timer - owner: root - group: root - mode: "0644" - notify: Recharger systemd +- name: Activer le rapport de santé + ansible.builtin.systemd: + name: setops-sante.timer + enabled: true + state: started + daemon_reload: true - - name: Activer le rapport de santé - ansible.builtin.systemd: - name: setops-sante.timer - enabled: true - state: started - daemon_reload: true - - # PREMIER RAPPORT IMMEDIAT, et ce n'est pas une commodite : sans lui, le service - # reste « en attente » jusqu'au premier declenchement, et un deploiement qui vient - # d'installer la supervision laisserait un tableau vide qu'on prendrait pour un - # tableau sain. - - name: Rapporter tout de suite (ne pas laisser un tableau vide passer pour sain) - ansible.builtin.command: /usr/local/sbin/setops-sante.sh - changed_when: false - failed_when: false +# PREMIER RAPPORT IMMEDIAT, et ce n'est pas une commodite : sans lui, le service +# reste « en attente » jusqu'au premier declenchement, et un deploiement qui vient +# d'installer la supervision laisserait un tableau vide qu'on prendrait pour un +# tableau sain. +- name: Rapporter tout de suite (ne pas laisser un tableau vide passer pour sain) + ansible.builtin.command: /usr/local/sbin/setops-sante.sh + changed_when: false + failed_when: false