sondes : les six premieres, et le patron qui les rend eprouvables

Ajout de meta/supervision.yml a six roles, chacun deposant sa propre sonde
selon le contrat des greffons Nagios.

DEUX PRINCIPES POSES AU DOCUMENT DE CONCEPTION, parce que la premiere
sonde les a imposes :

1. UNE SONDE DOIT POUVOIR ETRE MISE EN DEFAUT PAR PARAMETRE. Cible et
seuils sont des variables du role : on la prouve rouge avec un port ferme
ou un seuil impossible, sur une machine reelle, sans rien casser, et
aussi souvent qu on veut. Une sonde qu on ne peut prouver qu en cassant un
service ne sera prouvee qu une fois.

2. LA SONDE VIT LA OU VIT LA VERITE. « Ce noeud est-il collecte ? » est
une sonde de serveur_prometheus, pas de client_metrique : une seule y voit
les N noeuds, et surtout elle voit le cas SILENCIEUX — celui qui a cesse d
etre collecte ne peut pas s en plaindre.

LES SIX : cache-apt (artefacts, repond + place), resolution (resolveur,
zone interne ET Internet — deux chemins distincts), forge (forgejo, son
propre /api/healthz), collecte (prometheus, 15/15 cibles), tableaux
(grafana, base ok), ingestion (loki, PRET a ingerer, pas seulement en
ecoute).

TROIS FOIS J AI ECRIT LA SONDE AVANT DE MESURER, ET TROIS FOIS ELLE A EU
TORT. La forge : port 443 et chemin des depots INVENTES — elle ecoute en
3000 derriere l edge et n a legitimement aucun depot. Loki : j ai conclu
« panne persistante » sur deux lectures prises a quelques secondes d
intervalle, juste apres un redemarrage ; l anneau etait ACTIVE et la
reponse est passee a ready moins d une minute plus tard. Le delai de
stabilisation est desormais un AVERTISSEMENT nomme, pas une panne.

On demande au service ce qu il pense de lui-meme quand il sait le dire
(healthz, /ready, /api/health) plutot que d inventer un critere de l
exterieur.

make prouver : CONFORME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-10 02:48:55 -04:00
parent a8af541059
commit 1832530f29
27 changed files with 519 additions and 1 deletions

View file

@ -0,0 +1,100 @@
# Preuve de conformite — Set-OPS — 2026-09-10
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`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 (64 OK · 0 echec · 0 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| 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 : 41 groupes classes, aucun cycle, aucune arete en arriere. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 103 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. |
| 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 | ✅ 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. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| 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), 82 regle(s). |
| 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, 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 | 44 document(s) declarent leur lecteur (36 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 : 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, 118 regle(s) du site. |
| 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 (121 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 183 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. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
| 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 (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 | 7 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_forgejo/forge, serveur_g |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-09-10._

View file

@ -26,7 +26,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 | 40 | `docs/*.md` |
| pièces d'audit | 40 | `docs/audit/*` |
| pièces d'audit | 41 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 83 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |

View file

@ -105,6 +105,27 @@ utilisait `openssl verify -CAfile racine`, alors que nos certificats sont signé
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é.
## Une sonde doit pouvoir être mise en défaut *par paramètre*
Corollaire des deux règles précédentes, et c'est ce qui rend la discipline tenable à
l'échelle : une sonde dont on ne peut prouver le rouge qu'en **cassant un service** ne sera
prouvée qu'une fois, puis plus jamais.
Chaque sonde expose donc sa cible et ses seuils en variables du rôle. On la met en défaut
en lui donnant un port fermé, un nom qui n'existe pas, un seuil impossible — sur une
machine réelle, sans rien abîmer, et aussi souvent qu'on veut.
## Où vit la sonde : là où vit la vérité, pas là où vit le symptôme
*« La vérification suit la clé. »* Un nœud sait qu'il a **lancé** sa sauvegarde ; seul le
dépôt sait qu'elle a **abouti**. Un nœud sait qu'il **expose** ses métriques ; seul
Prometheus sait qu'il les **collecte**.
D'où un choix qui peut surprendre : *« ce nœud est-il bien collecté ? »* est une sonde de
**`serveur_prometheus`**, pas de `client_metrique`. Une seule sonde y voit les N nœuds à la
fois — et surtout, elle voit le cas **silencieux** : celui qui a cessé d'être collecté et
qui, par définition, ne peut pas s'en plaindre lui-même.
## 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

View file

@ -63,3 +63,13 @@ serveur_artefacts_journal: "/var/log/apt-cacher-ng"
# Vide = ce cache va directement à l'amont Debian. C'est le cas du cache du SITE lui-même,
# et de tout écosystème qui s'est émancipé (docs/filiation-emancipation.md).
serveur_artefacts_amont: ""
# --- Sonde de supervision -----------------------------------------------------------
# La cible et les seuils sont des VARIABLES pour que la sonde puisse etre mise en defaut
# sans casser le service (docs/supervision-conception.md).
serveur_artefacts_sonde_port: "{{ serveur_artefacts_port }}"
serveur_artefacts_sonde_repertoire: /var/cache/apt-cacher-ng
# Un cache plein ne se plaint pas : il cesse simplement de servir ce qu'il n'a pas pu
# ecrire. On prend de l'avance.
serveur_artefacts_sonde_pct_avert: 85
serveur_artefacts_sonde_pct_crit: 93

View file

@ -0,0 +1,7 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: cache-apt
ttl: 5400
raison: Le cache d'artefacts repond-il sur son port, et lui reste-t-il de la place ? Un cache muet
fait echouer tout apt de l'ecosysteme ; un cache plein le fait en silence.

View file

@ -118,3 +118,22 @@
port: "{{ serveur_artefacts_port }}"
timeout: 30
when: not ansible_check_mode
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « cache-apt »
ansible.builtin.template:
src: sonde-cache-apt.sh.j2
dest: /usr/local/lib/setops/sondes/cache-apt.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,34 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_artefacts). Ne pas editer a la main.
#
# SONDE « cache-apt » — le cache apt repond, et il lui reste de la place
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne sur la
# sortie standard, 0 OK / 1 AVERTISSEMENT / 2 CRITIQUE. Le porteur `client_sante` pousse.
# Mise en defaut PAR PARAMETRE : `serveur_artefacts_sonde_port` (port ferme) ou `_pct_crit` (seuil impossible).
set -uo pipefail
PORT={{ serveur_artefacts_sonde_port }}
REP={{ serveur_artefacts_sonde_repertoire }}
AVERT={{ serveur_artefacts_sonde_pct_avert }}
CRIT={{ serveur_artefacts_sonde_pct_crit }}
# ON DEMANDE UNE PAGE, ON NE SE CONTENTE PAS D'UN connect(). Un `connect()` reussit contre
# un processus mort-vivant qui n'a plus rien a servir ; seule une reponse prouve le
# service. C'est la lecon du depot : « un connect() TCP ne prouve rien ».
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "http://127.0.0.1:${PORT}/acng-report.html" 2>/dev/null)
[[ "${code}" == "200" ]] || { echo "Le cache ne repond pas sur ${PORT} (HTTP ${code:-aucune reponse})."; exit 2; }
pct=$(df --output=pcent "${REP}" 2>/dev/null | tail -1 | tr -dc '0-9')
[[ -n "${pct}" ]] || { echo "Cache joignable, mais ${REP} illisible."; exit 1; }
taille=$(du -sh "${REP}" 2>/dev/null | cut -f1)
if (( pct >= CRIT )); then
echo "Cache a ${pct}% du volume (${taille}) — apt echouera bientot pour tout l'ecosysteme."
exit 2
fi
if (( pct >= AVERT )); then
echo "Cache a ${pct}% du volume (${taille}), seuil ${AVERT}%."
exit 1
fi
echo "Cache apt : repond sur ${PORT}, ${taille} sur un volume a ${pct}%.|pct=${pct}%;${AVERT};${CRIT};0;100"

View file

@ -153,3 +153,10 @@ serveur_forgejo_cache_local: "{{ setops_cache_artefacts | default(lookup('env',
# publie, et le binaire vient de codeberg.org. C'est ce qui manque a Keycloak.
serveur_forgejo_signature_empreinte: "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
serveur_forgejo_signature_url: "{{ serveur_forgejo_url }}.asc"
# --- Sonde de supervision -----------------------------------------------------------
# L'ADRESSE VIENT DU ROLE, PAS D'UNE SUPPOSITION : la forge ecoute en clair sur son
# `http_port` (l'edge fait le TLS devant). Viser 443 en dur, comme la premiere version de
# la sonde, cassait sur toute forge derriere un edge — c'est-a-dire toutes celles des
# tenants.
serveur_forgejo_sonde_url: "http://127.0.0.1:{{ serveur_forgejo_http_port }}"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: forge
ttl: 5400
raison: La forge repond-elle a son API, et son depot Git est-il lisible ? Une forge qui sert sa page
d'accueil mais ne peut plus lire ses depots est une panne complete pour tout ce qui clone — et elle
a l'air en ligne.

View file

@ -470,3 +470,22 @@
group: root
mode: "0600"
no_log: true
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « forge »
ansible.builtin.template:
src: sonde-forge.sh.j2
dest: /usr/local/lib/setops/sondes/forge.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,50 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_forgejo). Ne pas editer a la main.
#
# SONDE « forge » — la forge est-elle en etat de servir ?
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_forgejo_sonde_url` (port ferme).
#
# ON DEMANDE A FORGEJO CE QU'IL PENSE DE LUI-MEME. `/api/healthz` verifie sa BASE et son
# CACHE et rend `pass` ou non. C'est plus juste que tout ce qu'on inventerait de
# l'exterieur : une forge dont la base est tombee sert encore ses pages statiques et
# repond a `/api/v1/version` — elle a l'air vivante et ne peut plus rien faire.
#
# PREMIERE VERSION FAUSSE, ET INSTRUCTIVE (2026-09-10). Elle visait le port 443 et
# comptait les depots sous `/var/lib/forgejo/repositories`. Les deux etaient INVENTES :
# la forge du tenant ecoute en 3000 derriere l'edge, et elle n'a legitimement AUCUN depot
# — elle est neuve. La sonde criait donc deux fois sur un service sain. On lit la verite
# du role, on ne la suppose pas.
set -uo pipefail
URL={{ serveur_forgejo_sonde_url }}
sortie=$(curl -s --max-time 10 "${URL}/api/healthz" 2>/dev/null)
[[ -n "${sortie}" ]] || { echo "La forge ne repond pas sur ${URL}."; exit 2; }
verdict=$(printf '%s' "${sortie}" | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
print("2|reponse illisible de /api/healthz"); raise SystemExit
etat = d.get("status")
mauvais = sorted(n for n, v in (d.get("checks") or {}).items()
if any((c or {}).get("status") != "pass" for c in (v or [])))
if etat == "pass" and not mauvais:
print("0|" + ", ".join(sorted(d.get("checks") or {})) + " : pass")
else:
print("2|statut " + str(etat) + (" ; en defaut : " + ", ".join(mauvais) if mauvais else ""))
' 2>/dev/null)
code=${verdict%%|*}
texte=${verdict#*|}
[[ -n "${code}" ]] || { echo "Verdict de sante illisible."; exit 2; }
if [[ "${code}" == "0" ]]; then
echo "Forge en etat de servir — ${texte}."
exit 0
fi
echo "Forge EN DEFAUT — ${texte}."
exit 2

View file

@ -58,3 +58,6 @@ serveur_grafana_marqueur_admin: "/etc/grafana/.setops-admin-sha256"
# Chemins du paquet Debian — la CLI ne les devine pas (voir tasks/main.yml).
serveur_grafana_homepath: "/usr/share/grafana"
serveur_grafana_donnees: "/var/lib/grafana"
# --- Sonde de supervision -----------------------------------------------------------
serveur_grafana_sonde_url: "http://127.0.0.1:{{ serveur_grafana_port }}"

View file

@ -0,0 +1,7 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: tableaux
ttl: 5400
raison: 'Grafana repond-il, et sa base tient-elle ? Un Grafana dont la base est tombee sert encore
sa page de connexion : il a l''air en ligne et ne peut plus rien montrer.'

View file

@ -222,3 +222,22 @@
group: root
mode: "0600"
no_log: true
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « tableaux »
ansible.builtin.template:
src: sonde-tableaux.sh.j2
dest: /usr/local/lib/setops/sondes/tableaux.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,17 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
#
# SONDE « tableaux » — Grafana repond et sa base tient
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_grafana_sonde_url` (port ferme).
set -uo pipefail
URL={{ serveur_grafana_sonde_url }}
s=$(curl -s --max-time 10 "${URL}/api/health" 2>/dev/null)
[[ -n "${s}" ]] || { echo "Grafana ne repond pas sur ${URL}."; exit 2; }
base=$(printf '%s' "${s}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("database","?"))' 2>/dev/null)
ver=$(printf '%s' "${s}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version","?"))' 2>/dev/null)
[[ "${base}" == "ok" ]] || { echo "Grafana repond mais sa base est en defaut (${base:-illisible})."; exit 2; }
echo "Grafana ${ver} : base ok."

View file

@ -18,3 +18,6 @@ serveur_loki_tls_actif: false
serveur_loki_tls_dir: "/etc/loki/tls"
serveur_loki_tls_source_cert: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.crt"
serveur_loki_tls_source_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key"
# --- Sonde de supervision -----------------------------------------------------------
serveur_loki_sonde_url: "https://127.0.0.1:{{ serveur_loki_port }}"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: ingestion
ttl: 5400
raison: Loki est-il PRET a ingerer ? Un Loki qui ecoute mais dont l'ingesteur n'est pas pret accepte
les connexions et perd les journaux — la panne se voit des semaines plus tard, quand on cherche
une trace qui n'a jamais ete ecrite.

View file

@ -166,3 +166,22 @@
name: "{{ serveur_loki_service }}"
enabled: true
state: started
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « ingestion »
ansible.builtin.template:
src: sonde-ingestion.sh.j2
dest: /usr/local/lib/setops/sondes/ingestion.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,37 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_loki). Ne pas editer a la main.
#
# SONDE « ingestion » — l'ingesteur est pret, pas seulement le port ouvert
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_loki_sonde_url` (port ferme).
set -uo pipefail
URL={{ serveur_loki_sonde_url }}
# `/ready` ET PAS UN connect(). Loki ouvre son port bien avant que son ingesteur ne soit
# pret ; pendant cette fenetre il accepte et perd. C'est exactement « un connect() ne
# prouve rien », applique a un demon qui demarre.
s=$(curl -sk --max-time 10 "${URL}/ready" 2>/dev/null)
[[ -n "${s}" ]] || { echo "Loki ne repond pas sur ${URL}."; exit 2; }
if printf '%s' "${s}" | grep -qi '^ready'; then
echo "Loki pret a ingerer."
exit 0
fi
# LE DELAI DE STABILISATION N'EST PAS UNE PANNE, ET LE MESSAGE LE DIT LUI-MEME.
# Apres un redemarrage, Loki repond « Ingester not ready: waiting for Ns AFTER BEING
# READY » — l'ingesteur EST pret, il purge son delai. Mesure du 2026-09-10 : l'anneau
# etait `ACTIVE` et la reponse est passee a `ready` moins d'une minute plus tard.
#
# J'ai d'abord conclu « panne persistante » sur deux lectures prises a quelques secondes
# d'intervalle, juste apres un redemarrage. Deux mesures rapprochees ne distinguent pas un
# etat d'un instant. On nomme donc ce cas pour ce qu'il est : un AVERTISSEMENT transitoire,
# pas une panne — sans pour autant le taire, parce qu'un delai qui ne finirait jamais
# serait, lui, une vraie panne.
if printf '%s' "${s}" | grep -qi 'after being ready'; then
echo "Loki sort d'un redemarrage (delai de stabilisation) : ${s:0:80}"
exit 1
fi
echo "Loki n'est PAS pret a ingerer : ${s:0:90}"
exit 2

View file

@ -18,3 +18,6 @@ serveur_prometheus_cibles_supplementaires: []
# TLS vers node_exporter (zero-confiance) : scrape en https + verif contre root_ca.
serveur_prometheus_metriques_tls: false
serveur_prometheus_metriques_ca: "/etc/step/certs/root_ca.crt"
# --- Sonde de supervision -----------------------------------------------------------
serveur_prometheus_sonde_url: "http://127.0.0.1:9090"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: collecte
ttl: 5400
raison: 'Toutes les cibles declarees sont-elles reellement collectees ? C''est la seule sonde qui
voit le cas SILENCIEUX : un noeud qui a cesse d''etre scrape ne peut pas s''en plaindre lui-meme,
par definition.'

View file

@ -30,3 +30,22 @@
name: "{{ serveur_prometheus_service }}"
enabled: true
state: started
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « collecte »
ansible.builtin.template:
src: sonde-collecte.sh.j2
dest: /usr/local/lib/setops/sondes/collecte.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,33 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_prometheus). Ne pas editer a la main.
#
# SONDE « collecte » — toutes les cibles declarees repondent au scrape
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_prometheus_sonde_url` (port ferme).
set -uo pipefail
URL={{ serveur_prometheus_sonde_url }}
# LA VERITE VIT ICI, PAS CHEZ LE NOEUD. Un noeud sait qu'il EXPOSE ses metriques ; seul
# Prometheus sait qu'il les COLLECTE. Et c'est le seul endroit d'ou l'on voit celui qui a
# disparu — un noeud muet n'envoie pas d'alerte disant qu'il est muet.
sortie=$(curl -s --max-time 10 "${URL}/api/v1/targets?state=active" 2>/dev/null)
[[ -n "${sortie}" ]] || { echo "Prometheus ne repond pas sur ${URL}."; exit 2; }
verdict=$(printf '%s' "${sortie}" | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)["data"]["activeTargets"]
except Exception:
print("2|reponse illisible"); raise SystemExit
bas = sorted({(t.get("labels") or {}).get("instance", "?") for t in d if t.get("health") != "up"})
print(("2|" if bas else "0|") + str(len(d) - len(bas)) + "/" + str(len(d)) +
(" ; muettes : " + ", ".join(bas[:6]) if bas else ""))
' 2>/dev/null)
code=${verdict%%|*}; texte=${verdict#*|}
[[ -n "${code}" ]] || { echo "Verdict de collecte illisible."; exit 2; }
[[ "${code}" == "0" ]] && { echo "Collecte : ${texte} cibles repondent."; exit 0; }
echo "Collecte INCOMPLETE : ${texte}"
exit 2

View file

@ -75,3 +75,11 @@ serveur_resolveur_transitaires: []
# services du site — ce qui est exactement ce qu'on veut constater d'une émancipation, et
# non une panne à réparer.
serveur_resolveur_zones_deleguees: []
# --- Sonde de supervision -----------------------------------------------------------
# DEUX noms, parce qu'il y a DEUX chemins : l'autoritatif local pour la zone souveraine,
# la recursion pour le reste. Une sonde qui n'en verifierait qu'un laisserait l'autre
# tomber en silence. Variables, donc mises en defaut sans toucher au service.
serveur_resolveur_sonde_nom_interne: "{{ ansible_fqdn | default(ansible_hostname) }}"
serveur_resolveur_sonde_nom_public: "deb.debian.org"
serveur_resolveur_sonde_delai: 3

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: resolution
ttl: 5400
raison: 'Le resolveur repond-il POUR LA ZONE INTERNE ET pour un nom de l''Internet ? Les deux voies
sont distinctes : la zone interne vient de l''autoritatif local, le reste de la recursion. Une seule
qui tombe casse la moitie de l''ecosysteme, et l''autre moitie continue de dire que tout va bien.'

View file

@ -53,3 +53,22 @@
loop_control:
label: "{{ item.nom }} {{ item.type }}"
when: not ansible_check_mode
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « resolution »
ansible.builtin.template:
src: sonde-resolution.sh.j2
dest: /usr/local/lib/setops/sondes/resolution.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,32 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_resolveur). Ne pas editer a la main.
#
# SONDE « resolution » — il resout la zone interne ET l'Internet
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_resolveur_sonde_nom_interne` / `_nom_public` (noms inexistants).
set -uo pipefail
INTERNE={{ serveur_resolveur_sonde_nom_interne }}
PUBLIC={{ serveur_resolveur_sonde_nom_public }}
DELAI={{ serveur_resolveur_sonde_delai }}
# ON INTERROGE LA LOOPBACK, PAS `getent`. `getent` passerait par le plancher
# /etc/hosts et rendrait une reponse alors que le resolveur est mort — un vert sur un
# service eteint. On demande donc au demon lui-meme.
r1=$(dig +short +time=${DELAI} +tries=1 @127.0.0.1 "${INTERNE}" 2>/dev/null | head -1)
r2=$(dig +short +time=${DELAI} +tries=1 @127.0.0.1 "${PUBLIC}" 2>/dev/null | head -1)
if [[ -z "${r1}" && -z "${r2}" ]]; then
echo "Le resolveur ne repond ni pour ${INTERNE} ni pour ${PUBLIC} — il est muet."
exit 2
fi
if [[ -z "${r1}" ]]; then
echo "Zone interne MUETTE (${INTERNE}) — l'ecosysteme ne se resout plus lui-meme ; l'Internet repond (${PUBLIC} -> ${r2})."
exit 2
fi
if [[ -z "${r2}" ]]; then
echo "Recursion MUETTE (${PUBLIC}) — la zone interne repond (${INTERNE} -> ${r1}), mais rien du dehors."
exit 2
fi
echo "Resolution : ${INTERNE} -> ${r1}, ${PUBLIC} -> ${r2}."