diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md new file mode 100644 index 0000000..cfe43f1 --- /dev/null +++ b/docs/audit/preuve-2026-09-10.md @@ -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._ diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index eb8914b..165755e 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -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 | diff --git a/docs/supervision-conception.md b/docs/supervision-conception.md index a5023f9..db9a96a 100644 --- a/docs/supervision-conception.md +++ b/docs/supervision-conception.md @@ -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 diff --git a/roles/serveur_artefacts/defaults/main.yml b/roles/serveur_artefacts/defaults/main.yml index cba2adc..1ea2749 100644 --- a/roles/serveur_artefacts/defaults/main.yml +++ b/roles/serveur_artefacts/defaults/main.yml @@ -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 diff --git a/roles/serveur_artefacts/meta/supervision.yml b/roles/serveur_artefacts/meta/supervision.yml new file mode 100644 index 0000000..1035c79 --- /dev/null +++ b/roles/serveur_artefacts/meta/supervision.yml @@ -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. diff --git a/roles/serveur_artefacts/tasks/main.yml b/roles/serveur_artefacts/tasks/main.yml index 80a8303..86fe80b 100644 --- a/roles/serveur_artefacts/tasks/main.yml +++ b/roles/serveur_artefacts/tasks/main.yml @@ -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" diff --git a/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 b/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 new file mode 100644 index 0000000..ef9154f --- /dev/null +++ b/roles/serveur_artefacts/templates/sonde-cache-apt.sh.j2 @@ -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" diff --git a/roles/serveur_forgejo/defaults/main.yml b/roles/serveur_forgejo/defaults/main.yml index b39687d..8378ce5 100644 --- a/roles/serveur_forgejo/defaults/main.yml +++ b/roles/serveur_forgejo/defaults/main.yml @@ -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 }}" diff --git a/roles/serveur_forgejo/meta/supervision.yml b/roles/serveur_forgejo/meta/supervision.yml new file mode 100644 index 0000000..7b3734a --- /dev/null +++ b/roles/serveur_forgejo/meta/supervision.yml @@ -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. diff --git a/roles/serveur_forgejo/tasks/main.yml b/roles/serveur_forgejo/tasks/main.yml index ccb582c..760173a 100644 --- a/roles/serveur_forgejo/tasks/main.yml +++ b/roles/serveur_forgejo/tasks/main.yml @@ -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" diff --git a/roles/serveur_forgejo/templates/sonde-forge.sh.j2 b/roles/serveur_forgejo/templates/sonde-forge.sh.j2 new file mode 100644 index 0000000..1637f7c --- /dev/null +++ b/roles/serveur_forgejo/templates/sonde-forge.sh.j2 @@ -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 diff --git a/roles/serveur_grafana/defaults/main.yml b/roles/serveur_grafana/defaults/main.yml index c28c089..7a883f2 100644 --- a/roles/serveur_grafana/defaults/main.yml +++ b/roles/serveur_grafana/defaults/main.yml @@ -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 }}" diff --git a/roles/serveur_grafana/meta/supervision.yml b/roles/serveur_grafana/meta/supervision.yml new file mode 100644 index 0000000..fd2874f --- /dev/null +++ b/roles/serveur_grafana/meta/supervision.yml @@ -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.' diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index 10ada0c..5e9526d 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -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" diff --git a/roles/serveur_grafana/templates/sonde-tableaux.sh.j2 b/roles/serveur_grafana/templates/sonde-tableaux.sh.j2 new file mode 100644 index 0000000..3428ed3 --- /dev/null +++ b/roles/serveur_grafana/templates/sonde-tableaux.sh.j2 @@ -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." diff --git a/roles/serveur_loki/defaults/main.yml b/roles/serveur_loki/defaults/main.yml index 1c47b17..89d8287 100644 --- a/roles/serveur_loki/defaults/main.yml +++ b/roles/serveur_loki/defaults/main.yml @@ -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 }}" diff --git a/roles/serveur_loki/meta/supervision.yml b/roles/serveur_loki/meta/supervision.yml new file mode 100644 index 0000000..b9c0864 --- /dev/null +++ b/roles/serveur_loki/meta/supervision.yml @@ -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. diff --git a/roles/serveur_loki/tasks/main.yml b/roles/serveur_loki/tasks/main.yml index 15306a9..597c77a 100644 --- a/roles/serveur_loki/tasks/main.yml +++ b/roles/serveur_loki/tasks/main.yml @@ -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" diff --git a/roles/serveur_loki/templates/sonde-ingestion.sh.j2 b/roles/serveur_loki/templates/sonde-ingestion.sh.j2 new file mode 100644 index 0000000..257eee0 --- /dev/null +++ b/roles/serveur_loki/templates/sonde-ingestion.sh.j2 @@ -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 diff --git a/roles/serveur_prometheus/defaults/main.yml b/roles/serveur_prometheus/defaults/main.yml index 78a1b84..3fc5a21 100644 --- a/roles/serveur_prometheus/defaults/main.yml +++ b/roles/serveur_prometheus/defaults/main.yml @@ -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" diff --git a/roles/serveur_prometheus/meta/supervision.yml b/roles/serveur_prometheus/meta/supervision.yml new file mode 100644 index 0000000..495b09c --- /dev/null +++ b/roles/serveur_prometheus/meta/supervision.yml @@ -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.' diff --git a/roles/serveur_prometheus/tasks/main.yml b/roles/serveur_prometheus/tasks/main.yml index fe6636e..8adb028 100644 --- a/roles/serveur_prometheus/tasks/main.yml +++ b/roles/serveur_prometheus/tasks/main.yml @@ -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" diff --git a/roles/serveur_prometheus/templates/sonde-collecte.sh.j2 b/roles/serveur_prometheus/templates/sonde-collecte.sh.j2 new file mode 100644 index 0000000..61cb520 --- /dev/null +++ b/roles/serveur_prometheus/templates/sonde-collecte.sh.j2 @@ -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 diff --git a/roles/serveur_resolveur/defaults/main.yml b/roles/serveur_resolveur/defaults/main.yml index 69d9081..4b024f8 100644 --- a/roles/serveur_resolveur/defaults/main.yml +++ b/roles/serveur_resolveur/defaults/main.yml @@ -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 diff --git a/roles/serveur_resolveur/meta/supervision.yml b/roles/serveur_resolveur/meta/supervision.yml new file mode 100644 index 0000000..8cea477 --- /dev/null +++ b/roles/serveur_resolveur/meta/supervision.yml @@ -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.' diff --git a/roles/serveur_resolveur/tasks/main.yml b/roles/serveur_resolveur/tasks/main.yml index 0c55191..3d9d692 100644 --- a/roles/serveur_resolveur/tasks/main.yml +++ b/roles/serveur_resolveur/tasks/main.yml @@ -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" diff --git a/roles/serveur_resolveur/templates/sonde-resolution.sh.j2 b/roles/serveur_resolveur/templates/sonde-resolution.sh.j2 new file mode 100644 index 0000000..a4bec4f --- /dev/null +++ b/roles/serveur_resolveur/templates/sonde-resolution.sh.j2 @@ -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}."