From 6d23121dc2ddec1f51a7b81c90138bc649406668 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 9 Sep 2026 20:32:08 -0400 Subject: [PATCH] supervision : systemctl --failed entre dans Icinga (role client_sante) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CE QU IL FERME. openipmi.service echouait a chaque demarrage sur les quatorze machines depuis le 2026-09-02, et systemctl --failed rendait ZERO partout : non parce qu elles allaient bien, mais parce qu aucune n avait redemarre depuis. Il a fallu qu un humain redemarre une machine pour que le defaut existe aux yeux de quelqu un. Un controle qui ne peut echouer qu au demarrage ne mesure rien tant que rien ne demarre. PASSIF, ET A DUREE DE VIE. Un controle actif ne voit pas la machine MUETTE. Ici c est le noeud qui parle, et le ttl de son envoi fait la fraicheur : sans nouvelle, Icinga perime le service tout seul. Le silence alerte autant que l echec. Le minuteur declenche AU DEMARRAGE autant que toutes les 15 min : les echecs de cette famille naissent au boot. CRITIQUE DES LA PREMIERE UNITE, jamais un seuil — une unite en echec est soit un vrai probleme soit du bruit a retirer, et un seuil ferait vivre le bruit. Les tolerances se nomment une par une, vide par defaut. CONTROLE NEGATIF. Unite factice sur obs-01, etat relu dans IcingaDB : CRITICAL, et le verdict NOMME l unite. Les treize autres OK. Apres nettoyage : 14/14 OK. UN CONFLIT EVITE. setops-sauvegardes.conf definissait les object Host ; un second fichier de controle aurait redefini les memes, et Icinga refuse un objet en double — la configuration entiere aurait ete rejetee, donc AUCUNE supervision, en voulant en ajouter. Les hotes vivent maintenant dans setops-hotes.conf, definis une fois. TROIS OBSTACLES. ${#tableau[@]} contient {# que Jinja lit comme un debut de commentaire (remede : comment_start_string en tete du gabarit). Ma premiere sonde a traduit un 403 « Missing permission: objects/query/service » en « 0 service » — encore un echec qui ecrasait permission ; l etat se lit dans IcingaDB. Et un echec apt transitoire sur mon-01, local et disparu au second essai : mesure avant conclusion. NON FAIT : le SITE n a pas recu client_sante. 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 | 83 +++++++++++++ docs/audit/preuve-2026-09-09.md | 18 +-- docs/carte-set-ops.md | 4 +- docs/catalogue-services.md | 5 +- docs/couches-deploiement.yml | 4 + docs/dependances-groupes.yml | 6 + docs/registre-flux.md | 3 +- playbooks/groupes/client_sante.yml | 39 ++++++ roles/client_sante/README.md | 41 +++++++ roles/client_sante/defaults/main.yml | 33 +++++ roles/client_sante/handlers/main.yml | 4 + roles/client_sante/meta/flux.yml | 12 ++ roles/client_sante/meta/integration.yml | 15 +++ roles/client_sante/tasks/main.yml | 113 ++++++++++++++++++ .../templates/setops-sante.service.j2 | 9 ++ .../client_sante/templates/setops-sante.sh.j2 | 76 ++++++++++++ .../templates/setops-sante.timer.j2 | 16 +++ roles/serveur_icinga/defaults/main.yml | 22 ++++ roles/serveur_icinga/tasks/main.yml | 21 ++++ .../templates/setops-api-users.conf.j2 | 11 +- .../templates/setops-hotes.conf.j2 | 21 ++++ .../templates/setops-sante.conf.j2 | 31 +++++ .../templates/setops-sauvegardes.conf.j2 | 16 +-- 23 files changed, 575 insertions(+), 28 deletions(-) create mode 100644 playbooks/groupes/client_sante.yml create mode 100644 roles/client_sante/README.md create mode 100644 roles/client_sante/defaults/main.yml create mode 100644 roles/client_sante/handlers/main.yml create mode 100644 roles/client_sante/meta/flux.yml create mode 100644 roles/client_sante/meta/integration.yml create mode 100644 roles/client_sante/tasks/main.yml create mode 100644 roles/client_sante/templates/setops-sante.service.j2 create mode 100644 roles/client_sante/templates/setops-sante.sh.j2 create mode 100644 roles/client_sante/templates/setops-sante.timer.j2 create mode 100644 roles/serveur_icinga/templates/setops-hotes.conf.j2 create mode 100644 roles/serveur_icinga/templates/setops-sante.conf.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac094d..e3fbfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ # CHANGELOG — Set-OPS +## 2026-09-09 (4) — `systemctl --failed` entre dans la supervision + +**63 preuves. `make prouver` : CONFORME, 63 OK, 0 echec, 0 saute.** +Nouveau role `client_sante`, integration **universelle** (67 roles). + +### Ce qu'il ferme + +Le defaut trouve quelques heures plus tot : `openipmi.service` echouait a CHAQUE +demarrage sur les quatorze machines depuis le 2026-09-02, et `systemctl --failed` rendait +ZERO partout — non parce qu'elles allaient bien, mais parce qu'AUCUNE n'avait redemarre +depuis. Il a fallu qu'un humain redemarre une machine pour que le defaut existe aux yeux +de quelqu'un. + +*Un controle qui ne peut echouer qu'au demarrage ne mesure rien tant que rien ne demarre.* + +### PASSIF, et a duree de vie + +Un controle ACTIF ne voit pas la machine MUETTE : si elle ne repond plus, la sonde echoue +et on met ca sur le compte du reseau. Ici c'est le NOEUD qui parle, et le `ttl` de son +envoi fait la fraicheur — sans nouvelle, Icinga perime le service tout seul. **Le silence +alerte autant que l'echec**, et le silence est precisement ce qui n'a alerte personne. + +Le minuteur declenche **au demarrage** (`OnBootSec=2min`) autant que toutes les 15 min : +les echecs de cette famille NAISSENT au boot, et attendre le premier quart d'heure +laisserait une fenetre pendant laquelle la machine est en panne et le tableau au vert. + +**CRITIQUE des la premiere unite**, jamais un seuil. Une unite en echec est soit un vrai +probleme, soit du bruit a retirer : dans les deux cas il faut agir. Un seuil ferait vivre +le bruit indefiniment — exactement ce qu'on vient de corriger. + +Les tolerances se nomment **une par une** (`client_sante_unites_tolerees`, vide par +defaut). Jamais un motif large : un filtre qui cache une unite en cache d'autres, et on ne +s'en apercoit que le jour ou l'on cherche pourquoi rien n'a alerte. + +### Le controle negatif + +Une garantie qu'on n'a jamais vue echouer n'est pas une garantie. Unite factice posee sur +`obs-01`, rapport rejoue, etat relu dans IcingaDB : + + obs-01 sante CRITICAL 1 unite(s) systemd en echec : setops-controle-negatif.service + (13 autres) sante OK Aucune unite systemd en echec. + +Le verdict NOMME l'unite. Apres nettoyage : **14/14 OK**. + +### Un conflit evite de justesse + +`setops-sauvegardes.conf` definissait les `object Host`. Un second fichier de controle +aurait redefini les memes, et **Icinga refuse un objet en double** : la configuration +entiere aurait ete rejetee, donc AUCUNE supervision — en voulant en ajouter. Les hotes +vivent desormais dans `setops-hotes.conf`, definis UNE fois ; les fichiers de controle +n'attachent que des services. Verifie : 14 hotes, 14 services, zero doublon, +`icinga2 daemon -C` valide. + +### Trois obstacles, et ce qu'ils apprennent + +**`${#tableau[@]}` contient `{#`**, que Jinja lit comme un debut de commentaire — le rendu +echouait sur « Missing end of comment tag ». Le shell a besoin de `${#...}` ; c'est donc a +Jinja de s'ecarter (`#jinja2: comment_start_string:...` en tete du gabarit). + +**Ma premiere sonde a traduit un refus en « 0 service ».** Le compte `setops-depot` ne peut +que POSER un resultat, pas lire — moindre privilege voulu. L'API rendait +`{"error":403,"status":"Missing permission: objects/query/service"}`, et mon script, qui +cherchait une clef `results`, a affiche « 0 service(s) sante ». *Encore un « echec » qui +ecrasait « permission ».* L'etat se lit dans IcingaDB, pas par ce compte. + +**Et un echec `apt` transitoire** sur `mon-01` : `503 unexpected range` puis « Message has +been manipulated » sur la signature de `debian-security`. Alarmant a la lecture, local a +une machine (13 autres propres), et disparu au second essai — un hoquet du cache +apt-cacher-ng, pas un incident de signature. Mesure avant conclusion : les deux +mandataires servaient le meme contenu, au meme condensat. + +### Le meme compte d'API, elargi de deux noms a trois + +Un second compte serait plus pur — un secret par usage. Il exigerait une clef de voute de +plus dans CHAQUE ecosysteme, donc un geste manuel a chaque nouvel ecosysteme, pour une +portee identique : ce compte ne peut deja que poser un resultat passif, sur des services +NOMMES. Le filtre passe de `sauvegarde: *` / `sauvegarde` a ces deux-la plus `sante`. Aucun +pouvoir nouveau. + +### Non fait + +Le SITE n'a pas recu `client_sante`. Sept machines, meme role, meme raison. + ## 2026-09-09 (3) — Le redemarrage a tenu, et il a montre autre chose ### Ce que le redemarrage prouve diff --git a/docs/audit/preuve-2026-09-09.md b/docs/audit/preuve-2026-09-09.md index 23775d0..05d7e06 100644 --- a/docs/audit/preuve-2026-09-09.md +++ b/docs/audit/preuve-2026-09-09.md @@ -20,8 +20,8 @@ | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. | -| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 40 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 99 flux, schéma + matrice OK. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 100 flux, schéma + matrice OK. | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | @@ -38,30 +38,30 @@ | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. | | P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | | P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 80 regle(s). | -| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. | | 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, 66 roles avec README. | +| 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). | -| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). | +| 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). | | 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. | -| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 39 role(s) serveur/client tous nommes, 40 groupe(s) cite(s) en table existent tous. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 40 role(s) serveur/client tous nommes, 41 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 54 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | | P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. | -| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. | +| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | | P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). | | P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos | | P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). | | P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | -| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (117 lignes). | +| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (118 lignes). | | P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 169 regles `pass`), tous non consignes et tous motives. | | P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | | P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. | @@ -69,7 +69,7 @@ | 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, 66 roles, 40 groupes). | +| 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). | | 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). | diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 3f38165..57bde35 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -23,8 +23,8 @@ README de rôles). Cette page comble ces deux trous. | Ce qu'on compte | Combien | Comment on le mesure | |---|---|---| -| rôles | 66 | `roles/*/` | -| README de rôles | 66 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | +| 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` | | pièces d'audit | 40 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index b65de96..8cfb454 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -65,7 +65,7 @@ Ce que la reconstruction couvre, par capacité : | Forge du genome du site | `serveur_forge_site` | designe LA forge dont les ecosystemes de ce site se reproduisent (D-81), et l'ouvre a eux. Marqueur : `serveur_forgejo` installe. | | Resolveur du site | `serveur_resolveur_site` | designe LE resolveur que les ecosystemes de ce site interrogent tant qu'ils n'ont pas le leur. Marqueur : `serveur_resolveur` installe. | | Depot de sauvegarde du site | `serveur_backup_site` | designe LE depot ou les ecosystemes de ce site posent leur etat tant qu'ils n'ont pas le leur. Marqueur : `serveur_backup` installe. | -| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte | +| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp`, `client_sante` | collecte, relais et rapport de santé sur toute la flotte | *Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail` (→ Postfix), `client_dns` (→ plancher `/etc/hosts` + `client_resolveur`), `client_ldap` @@ -174,6 +174,7 @@ boîtes (Dovecot). La coupure suit l'exposition, pas le logiciel. | Journaux vers Loki | `client_journal` | **tout hôte** (universelle) | | Résolution locale (Unbound) | `client_resolveur` | **tout hôte** (universelle) | | Source d'artefacts (cache apt) | `client_artefacts` | **tout hôte** (universelle) | +| Santé du nœud (unités en échec) | `client_sante` | **tout hôte** (universelle) | | Relais SMTP | `client_smtp` | déclaré par hôte, dans le plan | | Sauvegarde restic | `client_backup` | déclaré par hôte — **obligatoire pour tout détenteur d'état** (P36) | @@ -199,7 +200,7 @@ dans le plan. ## Ordre de déploiement — le raisonnement > **L'ordre exécutable n'est pas ici.** Il vit dans `docs/couches-deploiement.yml` et -> `docs/dependances-groupes.yml`, que P08 prouve cohérents (40 groupes classés, aucun +> `docs/dependances-groupes.yml`, que P08 prouve cohérents (41 groupes classés, aucun > cycle, aucune arête en arrière) et que `make reconstruire` suit. Ce qui suit en est le > **raisonnement**, utile pour comprendre pourquoi cet ordre-là — et pour placer un > service nouveau. Les phases sont franchies : les « intégrations à prévoir » ci-dessous diff --git a/docs/couches-deploiement.yml b/docs/couches-deploiement.yml index 34d7712..082fb32 100644 --- a/docs/couches-deploiement.yml +++ b/docs/couches-deploiement.yml @@ -99,3 +99,7 @@ couches: - client_backup - client_resolveur - client_artefacts + # Le rapport de sante vient en DERNIER de la couche : il constate ce que tout le + # reste a laisse derriere lui. Le poser plus tot ferait rapporter une machine + # a moitie deployee, et le premier verdict serait faux. + - client_sante diff --git a/docs/dependances-groupes.yml b/docs/dependances-groupes.yml index 7258e6e..abda1f2 100644 --- a/docs/dependances-groupes.yml +++ b/docs/dependances-groupes.yml @@ -12,6 +12,12 @@ groupes: raison: "Les exporters clients doivent etre collectes par Prometheus." surveillance: "Verifier targets Prometheus, scrape duration et erreurs de collecte." + client_sante: + requiert_groupes_actifs: + - serveur_icinga + raison: "Le rapport de sante depose un resultat passif sur l'API d'Icinga." + surveillance: "Verifier que chaque noeud rapporte : un service `sante` EXPIRE vaut un echec." + client_journal: requiert_groupes_actifs: - serveur_loki diff --git a/docs/registre-flux.md b/docs/registre-flux.md index d608322..d1a0174 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -13,6 +13,7 @@ | `client_pki` | egress | 8443 | tcp | serveur_step_ca | tls-requis | Émission/renouvellement des certificats par ACME et récupération de la racine auprès de l'AC interne. | | `client_resolveur` | egress | 53 | udp | serveur_resolveur | clair | Résoudre auprès du résolveur de l'écosystème, et de personne d'autre. | | `client_resolveur` | egress | 53 | tcp | serveur_resolveur | clair | Réponses longues et bascule TCP, obligatoires en DNS. | +| `client_sante` | egress | 5665 | tcp | serveur_icinga | tls | Depot d'un resultat passif sur l'API Icinga (process-check-result) : les unites systemd en echec du noeud. Le pair est authentifie par l'AC d'Icinga. | | `client_smtp` | egress | 25 | tcp | serveur_postfix | starttls | Relais des notifications locales vers le MTA central (Postfix), STARTTLS. | | `serveur_artefacts` | ingress | 3142 | tcp | flotte | clair | Toute la flotte prend ses paquets ici. En clair, et c'est correct : l'intégrité d'un dépôt apt vient de ses signatures, qu'apt vérifie de toute façon — un intermédiaire ne peut pas altérer un paquet sans se faire prendre. | | `serveur_artefacts` | egress | 80 | tcp | externe | clair | Remplir le cache depuis les dépôts Debian amont (deb.debian.org, security). | @@ -112,6 +113,6 @@ - **n-a** : 3 flux - **ssh** : 8 flux - **starttls** : 6 flux -- **tls** : 9 flux +- **tls** : 10 flux - **tls-cible** : 2 flux - **tls-requis** : 34 flux diff --git a/playbooks/groupes/client_sante.yml b/playbooks/groupes/client_sante.yml new file mode 100644 index 0000000..457c55e --- /dev/null +++ b/playbooks/groupes/client_sante.yml @@ -0,0 +1,39 @@ +--- +# client_sante — L'ORDRE FAIT PARTIE DE L'INTEGRATION. +# +# Cette integration depend de `serveur_icinga`, et sa metadonnee le declare +# (`roles/client_sante/meta/integration.yml`). L'hote qui PORTE la supervision est donc +# configure AVANT ceux qui lui rapportent — sinon les noeuds poussent un resultat passif +# vers une API qui n'existe pas encore, et le premier verdict de chaque machine est un +# echec qui accuse le reseau. +# +# Ce fichier est le miroir de la declaration ; la preuve P44 refuse tout ecart. +# +# `any_errors_fatal` sur le premier play : si la supervision n'a pas pu etre configuree, +# y raccrocher des rapporteurs ne produit que du bruit. + +- name: Intégration client_sante — le serveur d'abord + hosts: client_sante:&serveur_icinga + become: true + any_errors_fatal: true + module_defaults: &defauts_sante + ansible.builtin.apt: + lock_timeout: 300 + gather_facts: true + pre_tasks: &pre_sante + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + roles: + - client_sante + +- name: Intégration client_sante — puis les hôtes qui s'y adressent + hosts: client_sante:!serveur_icinga + become: true + module_defaults: *defauts_sante + gather_facts: true + pre_tasks: *pre_sante + roles: + - client_sante diff --git a/roles/client_sante/README.md b/roles/client_sante/README.md new file mode 100644 index 0000000..97610d4 --- /dev/null +++ b/roles/client_sante/README.md @@ -0,0 +1,41 @@ +# client_sante + +Le nœud rapporte à Icinga ses **unités systemd en échec**, par résultat passif. + +## Pourquoi + +Trouvé le 2026-09-09 : `openipmi.service` échouait à **chaque démarrage** sur les quatorze +machines de Chezlepro depuis le 2026-09-02. `systemctl --failed` rendait pourtant **zéro** +partout — non parce qu'elles allaient bien, mais parce qu'**aucune n'avait redémarré** +depuis. Six jours et vingt heures pour la première. Il a fallu qu'un humain redémarre une +machine pour que le défaut existe aux yeux de quelqu'un. + +*Un contrôle qui ne peut échouer qu'au démarrage ne mesure rien tant que rien ne démarre.* + +Et le dégât n'est pas cosmétique : une unité en échec permanent **use le seul signal qui +devrait alerter**. Le jour où une vraie unité tombe, le compte passe de 1 à 2 et personne +ne fait la différence. + +## Passif, et à durée de vie + +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**, et le silence est précisément ce qui n'a alerté +personne. + +Le minuteur déclenche **au démarrage** (`OnBootSec=2min`) autant que périodiquement : les +échecs de cette famille naissent au boot, et attendre le premier quart d'heure laisserait +une fenêtre pendant laquelle la machine est en panne et le tableau au vert. + +## Tolérances + +`client_sante_unites_tolerees` nomme les unités acceptées **une par une**, jamais un motif +large : un filtre qui cache une unité en cache d'autres, et on ne s'en aperçoit que le jour +où l'on cherche pourquoi rien n'a alerté. Le défaut est la liste **vide** — une unité en +échec est soit un vrai problème, soit du bruit à retirer ; dans les deux cas il faut agir. + +## 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. diff --git a/roles/client_sante/defaults/main.yml b/roles/client_sante/defaults/main.yml new file mode 100644 index 0000000..bfdb474 --- /dev/null +++ b/roles/client_sante/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# A QUI L'ON RAPPORTE — derive du groupe, jamais ecrit en dur. Vide = pas de supervision +# dans cet ecosysteme, et le role ne pose alors ni script ni minuteur : un timer qui +# echoue chaque heure apprend a ignorer le rouge. +client_sante_icinga_hote: "{{ (groups['serveur_icinga'] | default([]) | first) | default('') }}" + +# LE MEME COMPTE D'API QUE LE RAPPORT DE SAUVEGARDE, et c'est un choix. +# +# Un second compte serait plus pur — un secret par usage. Il exigerait une CLEF DE VOUTE +# DE PLUS dans chaque ecosysteme, donc un geste manuel a chaque nouvel ecosysteme, pour +# une portee identique : ce compte ne peut deja QUE poser un resultat passif, et +# seulement sur des services nommes. On elargit le filtre de deux noms a trois, on ne +# donne aucun pouvoir nouveau. +client_sante_icinga_utilisateur: "setops-depot" +client_sante_icinga_motdepasse: "{{ vault_icinga_api_depot | default('') }}" +client_sante_ca_verification: "/etc/setops/icinga-ca.crt" +client_sante_icinga_ca_source: "/var/lib/icinga2/ca/ca.crt" + +# `ttl` du resultat passif. AU-DELA, ICINGA PERIME LE SERVICE DE LUI-MEME — c'est ce qui +# fait que le SILENCE alerte, et pas seulement l'echec. Un noeud eteint, un timer casse, +# un reseau coupe : trois facons de ne plus rien dire, et les trois doivent se voir. +# +# Trois fois la periode : deux passages peuvent manquer sans crier au loup. +client_sante_periode: "15min" +client_sante_ttl_icinga: 2700 + +# UNITES TOLEREES — nommees, jamais un motif large. +# +# Vide par defaut, et c'est le bon defaut : une unite en echec permanent use le seul +# signal qui devrait alerter. Le jour ou une VRAIE unite tombe, le compte passe de 1 a 2 +# 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: [] diff --git a/roles/client_sante/handlers/main.yml b/roles/client_sante/handlers/main.yml new file mode 100644 index 0000000..34a9aca --- /dev/null +++ b/roles/client_sante/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: Recharger systemd + ansible.builtin.systemd: + daemon_reload: true diff --git a/roles/client_sante/meta/flux.yml b/roles/client_sante/meta/flux.yml new file mode 100644 index 0000000..3da7ca7 --- /dev/null +++ b/roles/client_sante/meta/flux.yml @@ -0,0 +1,12 @@ +--- +# Flux reseau du rapport de sante (resultat passif -> API Icinga). Voir +# docs/flux-conception.md. +flux: + - sens: egress + port: 5665 + protocole: tcp + pair: serveur_icinga + chiffrement: tls + raison: >- + Depot d'un resultat passif sur l'API Icinga (process-check-result) : les unites + systemd en echec du noeud. Le pair est authentifie par l'AC d'Icinga. diff --git a/roles/client_sante/meta/integration.yml b/roles/client_sante/meta/integration.yml new file mode 100644 index 0000000..5f5fd76 --- /dev/null +++ b/roles/client_sante/meta/integration.yml @@ -0,0 +1,15 @@ +--- +# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le +# raisonnement, et docs/decisions-architecture.md (D-33). +integration: + # DE QUI CETTE INTEGRATION DEPEND. Le noeud pousse un resultat PASSIF vers Icinga : + # sans destinataire, le rapport n'irait nulle part, et un timer qui echoue chaque + # quart d'heure apprend a ignorer le rouge. + serveur: serveur_icinga + universelle: true + raison: >- + Une unite systemd en echec ne se signale a personne. Le noeud est le seul a pouvoir + le dire, et le SILENCE doit alerter autant que l'echec — d'ou un resultat passif a + duree de vie, plutot qu'un controle actif qui ne verrait pas la machine muette. + # Pas d'exemption : l'hote de supervision a lui aussi des unites, et rien ne dit + # qu'elles vont bien. diff --git a/roles/client_sante/tasks/main.yml b/roles/client_sante/tasks/main.yml new file mode 100644 index 0000000..6b9ea62 --- /dev/null +++ b/roles/client_sante/tasks/main.yml @@ -0,0 +1,113 @@ +--- +# 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 + +- 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: 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 + + - 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: 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 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: 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 diff --git a/roles/client_sante/templates/setops-sante.service.j2 b/roles/client_sante/templates/setops-sante.service.j2 new file mode 100644 index 0000000..04a561a --- /dev/null +++ b/roles/client_sante/templates/setops-sante.service.j2 @@ -0,0 +1,9 @@ +# GENERE par Set-OPS (role client_sante). +[Unit] +Description=Rapporter les unites systemd en echec a Icinga +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/setops-sante.sh diff --git a/roles/client_sante/templates/setops-sante.sh.j2 b/roles/client_sante/templates/setops-sante.sh.j2 new file mode 100644 index 0000000..5b10a2b --- /dev/null +++ b/roles/client_sante/templates/setops-sante.sh.j2 @@ -0,0 +1,76 @@ +#jinja2: comment_start_string:'{=#', comment_end_string:'#=}' +#!/bin/bash +# GENERE par Set-OPS (role client_sante). Ne pas editer a la main. +# +# (Le gabarit redefinit le delimiteur de commentaire Jinja : `${#tableau[@]}` contient +# la sequence `{#`, que Jinja lirait comme un debut de commentaire — et le rendu +# echouait sur « Missing end of comment tag ». Le shell a besoin de `${#...}` ; c'est +# donc a Jinja de s'ecarter.) +# +# CE NOEUD RAPPORTE SES UNITES EN ECHEC — parce que personne d'autre ne peut le voir. +# +# CE QUI A REVELE LE BESOIN (2026-09-09). `openipmi.service` echouait a CHAQUE demarrage +# sur les quatorze machines depuis le 2026-09-02. `systemctl --failed` rendait pourtant +# ZERO partout — non parce qu'elles allaient bien, mais parce qu'aucune n'avait REDEMARRE +# depuis. Six jours et vingt heures pour la premiere. Il a fallu qu'un humain redemarre +# une machine pour que le defaut existe aux yeux de quelqu'un. +# +# Un controle qui ne peut echouer qu'au demarrage ne mesure rien tant que rien ne demarre. +# +# POURQUOI UN RAPPORT PASSIF, ET PAS UN CONTROLE ACTIF. Un controle actif ne voit pas la +# machine MUETTE : si elle ne repond plus, la sonde echoue et on croit a un probleme de +# reseau. Ici c'est le noeud qui parle, avec un `ttl` : sans nouvelle, Icinga perime le +# service tout seul. Le silence alerte autant que l'echec — et le silence est precisement +# ce qui n'a alerte personne. +set -uo pipefail + +API="https://{{ client_sante_icinga_hote }}.{{ domaine_interne }}:5665" +TTL={{ client_sante_ttl_icinga }} +MOI="{{ inventory_hostname }}" +MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)" + +rapporter() { # $1=code $2=texte + 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}") + 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' \ + -X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1) + if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then + echo "ECHEC du rapport Icinga : ${reponse}" >&2 + exit 1 + fi +} + +# `--plain` et `--no-legend` : la sortie doit etre analysable, pas jolie. Sans eux, +# systemd insere une puce « ● » en tete de ligne, qui devient le premier champ. +mapfile -t echecs < <(systemctl list-units --state=failed --plain --no-legend 2>/dev/null \ + | awk '{print $1}') + +{% if client_sante_unites_tolerees %} +# UNITES TOLEREES, NOMMEES UNE PAR UNE. Jamais un motif large : un filtre qui cache une +# unite en cache d'autres, et on ne s'en apercoit que le jour ou l'on cherche pourquoi +# rien n'a alerte. +tolerees=({% for u in client_sante_unites_tolerees %}"{{ u }}" {% endfor %}) +restantes=() +for u in "${echecs[@]}"; do + garder=1 + for t in "${tolerees[@]}"; do [[ "$u" == "$t" ]] && garder=0 && break; done + [[ $garder -eq 1 ]] && restantes+=("$u") +done +echecs=("${restantes[@]}") +{% endif %} + +n=${#echecs[@]} +if [[ $n -eq 0 ]]; then + rapporter 0 "Aucune unite systemd en echec." + 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[*]}" diff --git a/roles/client_sante/templates/setops-sante.timer.j2 b/roles/client_sante/templates/setops-sante.timer.j2 new file mode 100644 index 0000000..b53d7f4 --- /dev/null +++ b/roles/client_sante/templates/setops-sante.timer.j2 @@ -0,0 +1,16 @@ +# GENERE par Set-OPS (role client_sante). +[Unit] +Description=Rapport de sante du noeud (unites en echec) + +[Timer] +# AU DEMARRAGE AUSSI, et c'est le point : les echecs de ce type naissent au boot. +# Attendre le premier quart d'heure laisserait une fenetre pendant laquelle la machine +# est en panne et le tableau au vert. +OnBootSec=2min +OnUnitActiveSec={{ client_sante_periode }} +# Sans dispersion, toute la flotte frappe l'API a la meme seconde. +RandomizedDelaySec=60 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/serveur_icinga/defaults/main.yml b/roles/serveur_icinga/defaults/main.yml index fdf33f0..3c9e959 100644 --- a/roles/serveur_icinga/defaults/main.yml +++ b/roles/serveur_icinga/defaults/main.yml @@ -71,6 +71,28 @@ serveur_icinga_sauvegarde_attendue: >- | select('defined') | flatten | unique | list) | intersect(groups['client_backup'] | default([])) }} +# LES NOEUDS DONT ON RAPPORTE LA SANTE : tous ceux du plan. +# +# `serveur_debian` est le socle — tout hote deploye le porte. Il n'y a donc pas de liste a +# tenir : un noeud qui entre dans l'ecosysteme entre dans la supervision, et un noeud +# rase en sort. Une liste ecrite a la main aurait ce defaut precis : elle ne bouge que +# quand quelqu'un y pense. +serveur_icinga_sante_attendue: "{{ groups['serveur_debian'] | default([]) }}" + +# LES HOTES A DEFINIR, UNE SEULE FOIS — l'union de ce que les controles attachent. +# +# Icinga REFUSE un `object Host` en double : deux fichiers de controle qui definissent le +# meme hote font rejeter TOUTE la configuration, donc AUCUNE supervision. Les hotes vivent +# donc dans `setops-hotes.conf`, et les fichiers de controle n'attachent que des services. +serveur_icinga_hotes_supervises: >- + {{ (serveur_icinga_sante_attendue + + serveur_icinga_sauvegarde_attendue + + ([serveur_icinga_hote_sauvegarde] if serveur_icinga_hote_sauvegarde else [])) + | unique | list }} + +serveur_icinga_hotes_conf: "/etc/icinga2/conf.d/setops-hotes.conf" +serveur_icinga_sante_conf: "/etc/icinga2/conf.d/setops-sante.conf" + # A QUI LA SUPERVISION PARLE — vide = personne, et on le DIT. # # Icinga livre son exemple avec `root@localhost`. Une supervision qui voit tout et diff --git a/roles/serveur_icinga/tasks/main.yml b/roles/serveur_icinga/tasks/main.yml index e9c597e..501e599 100644 --- a/roles/serveur_icinga/tasks/main.yml +++ b/roles/serveur_icinga/tasks/main.yml @@ -182,6 +182,18 @@ no_log: true notify: Redemarrer icinga2 +# 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. +- name: Deployer les hotes supervises (definis une seule fois) + ansible.builtin.template: + src: setops-hotes.conf.j2 + dest: "{{ serveur_icinga_hotes_conf }}" + owner: root + group: nagios + mode: "0640" + notify: Redemarrer icinga2 + - name: Deployer les objets de supervision des sauvegardes ansible.builtin.template: src: setops-sauvegardes.conf.j2 @@ -191,6 +203,15 @@ mode: "0640" notify: Redemarrer icinga2 +- name: Deployer les objets de supervision de la sante des noeuds + ansible.builtin.template: + src: setops-sante.conf.j2 + dest: "{{ serveur_icinga_sante_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 diff --git a/roles/serveur_icinga/templates/setops-api-users.conf.j2 b/roles/serveur_icinga/templates/setops-api-users.conf.j2 index 9388c52..2a26ca3 100644 --- a/roles/serveur_icinga/templates/setops-api-users.conf.j2 +++ b/roles/serveur_icinga/templates/setops-api-users.conf.j2 @@ -1,7 +1,14 @@ /* * Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main. * - * Compte d'API dedie au DEPOT de sauvegarde, pour deposer ses resultats passifs. + * Compte d'API par lequel un NOEUD depose ses resultats passifs. + * + * IL S'APPELLE ENCORE `setops-depot` (2026-09-09) : il servait d'abord au seul rapport de + * sauvegarde. Il porte desormais aussi le rapport de SANTE — un second compte serait plus + * pur, un secret par usage, mais exigerait une clef de voute de plus dans chaque + * ecosysteme, donc un geste manuel a chaque nouvel ecosysteme, pour une portee identique. + * Le renommer churnerait les voutes sans rien gagner. Le filtre ci-dessous reste etroit : + * trois noms de service, et aucun autre pouvoir. * Portee minimale : uniquement `actions/process-check-result`, et uniquement sur les * services de sauvegarde. Ce compte ne peut ni lire la configuration, ni agir ailleurs. */ @@ -29,7 +36,7 @@ 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" {{ '}}' }} + filter = {{ '{{' }} match("sauvegarde: *", service.name) || service.name == "sauvegarde" || service.name == "sante" {{ '}}' }} } ] } diff --git a/roles/serveur_icinga/templates/setops-hotes.conf.j2 b/roles/serveur_icinga/templates/setops-hotes.conf.j2 new file mode 100644 index 0000000..87203c1 --- /dev/null +++ b/roles/serveur_icinga/templates/setops-hotes.conf.j2 @@ -0,0 +1,21 @@ +/* + * Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main. + * + * LES HOTES, DEFINIS UNE SEULE FOIS. + * + * Ils vivaient dans `setops-sauvegardes.conf` — le seul fichier qui en avait besoin. Des + * qu'un second controle est arrive (la sante des noeuds, 2026-09-09), le meme `object + * Host` se serait retrouve dans deux fichiers, et Icinga REFUSE un objet en double : la + * configuration entiere aurait ete rejetee, donc AUCUNE supervision. + * + * Les fichiers de controle n'attachent plus que des SERVICES. Un controle de plus + * n'ajoute plus un hote de plus. + */ + +{% for noeud in serveur_icinga_hotes_supervises | sort %} +object Host "{{ noeud }}" { + check_command = "hostalive" + address = "{{ noeud }}.{{ domaine_interne }}" + vars.role = "{{ 'depot de sauvegarde' if noeud == serveur_icinga_hote_sauvegarde else 'noeud de l ecosysteme' }}" +} +{% endfor %} diff --git a/roles/serveur_icinga/templates/setops-sante.conf.j2 b/roles/serveur_icinga/templates/setops-sante.conf.j2 new file mode 100644 index 0000000..46bd866 --- /dev/null +++ b/roles/serveur_icinga/templates/setops-sante.conf.j2 @@ -0,0 +1,31 @@ +/* + * Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main. + * + * SANTE DES NOEUDS : les unites systemd en echec. + * + * NE POSAIT PROBLEME QU'AU DEMARRAGE, ET C'EST POURQUOI PERSONNE NE L'A VU. + * `openipmi.service` echouait a chaque demarrage sur les quatorze machines depuis le + * 2026-09-02. `systemctl --failed` rendait ZERO partout — non parce qu'elles allaient + * bien, mais parce qu'aucune n'avait REDEMARRE depuis. Il a fallu qu'un humain redemarre + * une machine, le 2026-09-09, pour que le defaut existe aux yeux de quelqu'un. + * + * PASSIF, ET AVEC UNE DUREE DE VIE. Un controle ACTIF ne voit pas la machine muette : si + * elle ne repond plus, la sonde echoue et on met ca sur le compte du reseau. Ici c'est le + * noeud qui parle, et le `ttl` de son envoi fait la fraicheur — sans nouvelle, Icinga + * perime le service tout seul. Le silence alerte autant que l'echec, et le silence est + * precisement ce qui n'a alerte personne. + * + * Les HOTES sont definis dans `setops-hotes.conf` — ici, rien que des services. + */ + +{% for noeud in serveur_icinga_sante_attendue | sort %} +object Service "sante" { + host_name = "{{ noeud }}" + check_command = "setops-passif" + enable_active_checks = false + enable_passive_checks = true + volatile = false + max_check_attempts = 1 + vars.setops_source = "{{ noeud }}" +} +{% endfor %} diff --git a/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 b/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 index 15c6015..8985451 100644 --- a/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 +++ b/roles/serveur_icinga/templates/setops-sauvegardes.conf.j2 @@ -6,6 +6,10 @@ * mois (2026-07-03 -> 2026-08-11). Il porte sur la VERITE DE TERRAIN, cote depot : * l'instantane du noeud existe-t-il, est-il RECENT, et n'est-il pas VIDE. * + * Les HOTES sont definis ailleurs (`setops-hotes.conf`) depuis le 2026-09-09 : deux + * fichiers qui definissent le meme `object Host` font rejeter TOUTE la configuration. + * Ce fichier n'attache que des services. + * * Les resultats sont POUSSES par `backup-01`, qui est le seul a pouvoir lire ses depots. * Le `ttl` porte dans chaque envoi fait la fraicheur : sans nouvelle, Icinga bascule tout * seul en « expire ». C'est le SILENCE qui doit alerter, pas seulement l'echec — le @@ -22,12 +26,6 @@ object CheckCommand "setops-passif" { L'ECOSYSTEME A SON PROPRE DEPOT : c'est LUI qui rapporte, parce qu'il est le seul a voir ce qui est reellement arrive. Les services s'attachent donc a son hote. #} -object Host "{{ serveur_icinga_hote_sauvegarde }}" { - check_command = "hostalive" - address = "{{ serveur_icinga_hote_sauvegarde }}.{{ domaine_interne }}" - vars.role = "depot de sauvegarde" -} - {% for noeud in serveur_icinga_sauvegarde_attendue | sort %} object Service "sauvegarde: {{ noeud }}" { host_name = "{{ serveur_icinga_hote_sauvegarde }}" @@ -54,12 +52,6 @@ object Service "sauvegarde: {{ noeud }}" { donnerait « idm-01 / sauvegarde: idm-01 ». #} {% for noeud in serveur_icinga_sauvegarde_attendue | sort %} -object Host "{{ noeud }}" { - check_command = "hostalive" - address = "{{ noeud }}.{{ domaine_interne }}" - vars.role = "detenteur d etat" -} - object Service "sauvegarde" { host_name = "{{ noeud }}" check_command = "setops-passif"