sondes : quatorze, une par role, derivees en objets Icinga

boites (dovecot) · cache-apt (artefacts) · certificat (client_pki, 14/14)
collaboration (nextcloud) · collecte (prometheus) · file-courriel (postfix)
forge (forgejo) · identite (keycloak) · ingestion (loki) · moteur (icinga)
resolution (resolveur) · runner (serveur_ops) · tableaux (grafana)
voute (ops_tenant) — toutes vertes, sans une ligne ecrite dans Icinga.

DEUX PRINCIPES QUE LA PREMIERE SONDE A IMPOSES.
Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE : cible et seuils
sont des variables du role, on prouve le rouge avec un port ferme ou un
seuil impossible, sans rien casser. Et la sonde vit LA OU VIT LA VERITE :
« ce noeud est-il collecte ? » appartient a prometheus, pas au client —
une seule y voit les N noeuds, et surtout elle voit le cas SILENCIEUX.

ON DEMANDE AU SERVICE CE QU IL PENSE DE LUI-MEME quand il sait le dire
(healthz, /ready, status.php, decouverte OIDC). Quand il ne sait pas, on
va chercher la verite de terrain : « moteur » ne regarde ni le service ni
le port, il demande a la base depuis combien de temps elle n a pas ete
rafraichie — la lecon des sauvegardes appliquee a la supervision.

QUATRE FOIS J AI ECRIT LA SONDE AVANT DE MESURER, QUATRE FOIS ELLE A EU
TORT. La forge : port et chemin des depots inventes, elle ecoute en 3000
derriere l edge et n a legitimement aucun depot. Loki : « panne
persistante » conclue sur deux lectures a quelques secondes d intervalle
juste apres un redemarrage — deux mesures rapprochees ne distinguent pas
un etat d un instant. Keycloak : vise en 8443, il ecoute en 8080. Le
runner : git en root refuse un depot d un autre proprietaire. A chaque
fois le remede est le meme — lire la verite du role, ne pas la supposer.

Et le meme piege Jinja qu avec client_sante : ${#tableau[@]} contient {#.
Le remede etait deja au depot ; je l ai reecrit au lieu de le chercher.

RESTE : client_smtp, client_artefacts, client_journal, icingaweb2 et
ops_site. Ce sont des chemins de report, dont la panne se voit deja par le
silence des sondes qu ils portent.

make prouver : CONFORME, 64 OK, 0 echec, 0 saute (P64 : 14 sondes).

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 03:00:21 -04:00
parent 1832530f29
commit 6120e6f006
30 changed files with 518 additions and 1 deletions

View file

@ -1,5 +1,74 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-09-10 — Chaque role porte desormais sa sonde
**Quatorze sondes**, declarees dans `meta/supervision.yml`, deposees par le role qui
possede la verite, derivees en objets Icinga sans qu'une ligne soit ecrite a la main :
boites (dovecot) certificat (client_pki, 14/14) collaboration (nextcloud)
cache-apt (artefacts) collecte (prometheus) file-courriel (postfix)
forge (forgejo) identite (keycloak) ingestion (loki)
moteur (icinga) resolution (resolveur) runner (serveur_ops)
tableaux (grafana) voute (ops_tenant)
Les dix-neuf lignes `surveillance:` ecrites en prose et jamais executees commencent a
devenir des mesures.
### Deux principes que la premiere sonde a imposes
**Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE.** Cible et seuils sont des
variables du role : on prouve le rouge avec un port ferme ou un seuil impossible, sur une
machine reelle, sans rien casser, aussi souvent qu'on veut. Une sonde qu'on ne peut
prouver qu'en cassant un service ne sera prouvee qu'une fois. Eprouve sur `cache-apt` :
vert, CRITIQUE par port ferme, AVERTISSEMENT par seuil, retour au vert.
**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 lui-meme.
### On demande au service ce qu'il pense de lui-meme
Quand il sait le dire : `/api/healthz` de Forgejo (base + cache), `/api/health` de Grafana,
`/ready` de Loki, `status.php` de Nextcloud, la decouverte OIDC de Keycloak. C'est plus
juste que tout critere invente de l'exterieur — une forge dont la base est tombee sert
encore ses pages et repond a `/api/v1/version`.
Et quand il ne sait pas : on va chercher la verite de terrain. `moteur` ne regarde ni le
service ni le port — il demande a la base **depuis combien de temps elle n'a pas ete
rafraichie**. C'est la lecon des sauvegardes appliquee a la supervision elle-meme : un
moteur vert dont la synchronisation est tombee affiche eternellement le dernier etat connu.
### Quatre fois j'ai ecrit la sonde avant de mesurer, quatre 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 — elle est neuve. Deux cris sur un service sain.
**Loki** : 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. *Deux mesures rapprochees ne distinguent pas un
etat d'un instant.* Le delai de stabilisation est desormais un AVERTISSEMENT nomme.
**Keycloak** : vise en 8443, il ecoute en 8080 derriere l'edge.
**Le runner** : `git` en root refuse les depots d'un autre proprietaire — la sonde aurait
rendu un echec qui parle de git au lieu de parler du depot.
À chaque fois le remede est le meme : **lire la verite du role, ne pas la supposer**.
### Et le meme piege Jinja, une seconde fois
`${#tableau[@]}` contient `{#`. Le remede etait deja au depot depuis `client_sante` ; je
l'ai reecrit au lieu de le chercher. La correction balaie desormais tous les gabarits de
sonde.
### Ce qui reste
`client_smtp`, `client_artefacts`, `client_journal`, `serveur_icingaweb2` et
`serveur_ops_site` n'ont pas encore la leur. Le premier lot couvre les services dont la
chute arrete l'ecosysteme ; ceux-la sont des chemins de report, dont la panne se voit
deja par le silence des sondes qu'ils portent.
## 2026-09-09 (9) — La supervision passe juste apres la PKI (D-86) ## 2026-09-09 (9) — La supervision passe juste apres la PKI (D-86)
*On n'allume pas la lumiere une fois la maison finie.* *On n'allume pas la lumiere une fois la maison finie.*

View file

@ -76,7 +76,7 @@
| 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 | | 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). | | 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 | | 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 | | P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 14 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_dovecot/boites, serveur |
## Couverture des affirmations ✅ du registre ## Couverture des affirmations ✅ du registre

View file

@ -50,3 +50,9 @@ serveur_dovecot_sasl_port: 12345
# cote emetteur : l'un sans l'autre, la livraison interne s'arrete — soit elle part en # cote emetteur : l'un sans l'autre, la livraison interne s'arrete — soit elle part en
# clair, soit elle est differee sans fin. # clair, soit elle est differee sans fin.
serveur_dovecot_lmtp_tls: true serveur_dovecot_lmtp_tls: true
# --- Sonde de supervision -----------------------------------------------------------
# UN COMPTE QUI DOIT EXISTER. Le resoudre prouve la chaine entiere : demon, configuration,
# bind LDAP, mappage userdb. Variable, donc mise en defaut avec un nom inexistant sans
# toucher a l'annuaire.
serveur_dovecot_sonde_utilisateur: "daniel"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: boites
ttl: 5400
raison: 'Dovecot repond-il, et sait-il encore RESOUDRE un utilisateur dans l''annuaire ? Un Dovecot
dont le lien LDAP est tombe accepte les connexions et refuse toutes les authentifications : le service
a l''air debout et plus personne n''entre.'

View file

@ -154,3 +154,22 @@
enabled: true enabled: true
state: started state: started
daemon_reload: true daemon_reload: 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 « boites »
ansible.builtin.template:
src: sonde-boites.sh.j2
dest: /usr/local/lib/setops/sondes/boites.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,22 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_dovecot). Ne pas editer a la main.
#
# SONDE « boites » — Dovecot repond et resout un utilisateur dans l'annuaire
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_dovecot_sonde_utilisateur` (compte inexistant).
set -uo pipefail
UTILISATEUR={{ serveur_dovecot_sonde_utilisateur }}
systemctl is-active --quiet dovecot || { echo "Dovecot n'est pas actif."; exit 2; }
# ON RESOUT UN UTILISATEUR REEL, PAS UN PING DE SERVICE. C'est le bind LDAP qui tombe le
# plus souvent, et il ne se voit que quand on demande un compte : le demon reste debout,
# le port reste ouvert, et toute authentification echoue.
sortie=$(doveadm user "${UTILISATEUR}" 2>&1)
if ! printf '%s' "${sortie}" | grep -qE '^uid[[:space:]]'; then
echo "Dovecot ne resout pas « ${UTILISATEUR} » — l'annuaire ne repond plus : ${sortie:0:70}"
exit 2
fi
echo "Dovecot : « ${UTILISATEUR} » resolu dans l'annuaire, service actif."

View file

@ -106,3 +106,10 @@ serveur_icinga_sondes: {}
# Vide, aucun destinataire n'est pose et le deploiement le SIGNALE — plutot que de # Vide, aucun destinataire n'est pose et le deploiement le SIGNALE — plutot que de
# laisser croire qu'une alerte partira. # laisser croire qu'une alerte partira.
serveur_icinga_destinataire: "" serveur_icinga_destinataire: ""
# --- Sonde de supervision -----------------------------------------------------------
serveur_icinga_sonde_config_db: /etc/icingadb/config.yml
# Les controles passifs ont un `ttl` de 45 min au plus long ; une base qui n'a rien recu
# depuis deux fois cela ne reflete plus rien.
serveur_icinga_sonde_age_avert: 900
serveur_icinga_sonde_age_crit: 5400

View file

@ -0,0 +1,9 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: moteur
ttl: 5400
raison: 'L''etat des controles arrive-t-il encore jusqu''a la base ? On ne mesure ni le service ni
le port : on mesure la FRAICHEUR de ce que la base contient. Un moteur vert dont la synchronisation
est tombee affiche eternellement le dernier etat connu — une supervision figee qui a l''air d''aller
bien.'

View file

@ -350,3 +350,22 @@
`root@localhost`, que personne ne lit. La supervision VERRA les defauts sans `root@localhost`, que personne ne lit. La supervision VERRA les defauts sans
pouvoir les dire. Declarer l'adresse au plan. pouvoir les dire. Declarer l'adresse au plan.
when: serveur_icinga_destinataire | length == 0 when: serveur_icinga_destinataire | length == 0
# --- 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 « moteur »
ansible.builtin.template:
src: sonde-moteur.sh.j2
dest: /usr/local/lib/setops/sondes/moteur.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,39 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_icinga). Ne pas editer a la main.
#
# SONDE « moteur » — l'etat des controles arrive frais jusqu'a IcingaDB
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_icinga_sonde_age_crit` (age impossible).
set -uo pipefail
CONF={{ serveur_icinga_sonde_config_db }}
AVERT={{ serveur_icinga_sonde_age_avert }}
CRIT={{ serveur_icinga_sonde_age_crit }}
systemctl is-active --quiet icinga2 || { echo "Le moteur Icinga n'est pas actif."; exit 2; }
systemctl is-active --quiet icingadb || { echo "La synchronisation IcingaDB n'est pas active."; exit 2; }
# ON NE SE FIE PAS A `is-active` SEUL — c'est le mensonge que ce depot a deja paye avec
# les sauvegardes : un service vert sur un travail qui n'aboutit pas. On demande a la BASE
# depuis combien de temps elle n'a pas ete rafraichie.
hote=$(grep -m1 -A5 '^database:' "${CONF}" 2>/dev/null | grep -m1 'host:' | awk '{print $2}')
mdp=$(grep -m1 'password:' "${CONF}" 2>/dev/null | awk '{print $2}')
base=$(grep -m1 -A5 '^database:' "${CONF}" 2>/dev/null | grep -m1 'database:' | awk '{print $2}')
util=$(grep -m1 -A6 '^database:' "${CONF}" 2>/dev/null | grep -m1 'user:' | awk '{print $2}')
[[ -n "${hote}" && -n "${mdp}" ]] || { echo "Configuration IcingaDB illisible (${CONF})."; exit 2; }
age=$(PGPASSWORD="${mdp}" psql -h "${hote}" -U "${util:-icingadb}" -d "${base:-icingadb}" -Atc \
"select coalesce(extract(epoch from now()) - max(last_update)/1000, 999999)::bigint from service_state" 2>/dev/null)
[[ "${age}" =~ ^-?[0-9]+$ ]] || { echo "La base de supervision ne repond pas (${hote})."; exit 2; }
(( age < 0 )) && age=0
if (( age >= CRIT )); then
echo "Etat FIGE : rien de neuf en base depuis ${age} s — la supervision montre le passe.|age=${age}s;${AVERT};${CRIT};0;"
exit 2
fi
if (( age >= AVERT )); then
echo "Etat vieillissant : ${age} s sans mise a jour (seuil ${AVERT}).|age=${age}s;${AVERT};${CRIT};0;"
exit 1
fi
echo "Moteur et synchronisation vivants : base rafraichie il y a ${age} s.|age=${age}s;${AVERT};${CRIT};0;"

View file

@ -220,3 +220,9 @@ serveur_keycloak_cache_local: "{{ setops_cache_artefacts | default(lookup('env',
# il exige une relecture humaine, pas un remplacement silencieux. # il exige une relecture humaine, pas un remplacement silencieux.
serveur_keycloak_signature_empreinte: "861AB50E8CC6611FB6BC01A6B8F12EA26FD6EEBA" serveur_keycloak_signature_empreinte: "861AB50E8CC6611FB6BC01A6B8F12EA26FD6EEBA"
serveur_keycloak_signature_url: "{{ serveur_keycloak_url }}.asc" serveur_keycloak_signature_url: "{{ serveur_keycloak_url }}.asc"
# --- Sonde de supervision -----------------------------------------------------------
# Keycloak ecoute EN CLAIR derriere l'edge : viser le port TLS en dur casserait sur toute
# installation derriere un proxy (verifie le 2026-09-10 : 8080, pas 8443).
serveur_keycloak_sonde_url: "http://127.0.0.1:8080"
serveur_keycloak_sonde_realm: "{{ serveur_keycloak_realm }}"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: identite
ttl: 5400
raison: Le realm sert-il encore sa decouverte OIDC ? C'est la porte par laquelle tout le SSO de l'ecosysteme
entre. Un Keycloak debout dont le realm n'a pas charge repond sur son port et refuse toute connexion
— sans rien dire.

View file

@ -324,3 +324,22 @@
when: when:
- serveur_keycloak_account_theme | length > 0 - serveur_keycloak_account_theme | length > 0
- not ansible_check_mode - 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 « identite »
ansible.builtin.template:
src: sonde-identite.sh.j2
dest: /usr/local/lib/setops/sondes/identite.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,24 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_keycloak). Ne pas editer a la main.
#
# SONDE « identite » — le realm sert sa decouverte OIDC
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_keycloak_sonde_realm` (realm inexistant).
set -uo pipefail
URL={{ serveur_keycloak_sonde_url }}
REALM={{ serveur_keycloak_sonde_realm }}
s=$(curl -s --max-time 10 "${URL}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null)
[[ -n "${s}" ]] || { echo "Keycloak ne repond pas sur ${URL}."; exit 2; }
printf '%s' "${s}" | grep -q '"issuer"' \
|| { echo "Le realm « ${REALM} » ne sert pas sa decouverte OIDC : ${s:0:70}"; exit 2; }
# LES POINTS D'ENTREE QUE LES CLIENTS UTILISENT VRAIMENT. Un document de decouverte
# tronque se lit comme du JSON valide et casse chaque client a la premiere redirection.
for cle in authorization_endpoint token_endpoint jwks_uri; do
printf '%s' "${s}" | grep -q "\"${cle}\"" \
|| { echo "Decouverte OIDC incomplete pour « ${REALM} » : ${cle} absent."; exit 2; }
done
echo "Realm « ${REALM} » : decouverte OIDC complete."

View file

@ -120,3 +120,6 @@ serveur_nextcloud_marqueur_admin: "/etc/setops-nextcloud-admin-sha256"
# puis pousse par SSH : aucun flux nouveau, et l'artefact devient deployable hors ligne. # puis pousse par SSH : aucun flux nouveau, et l'artefact devient deployable hors ligne.
# Surchargeable par `setops_cache_artefacts` pour partager un cache commun. # Surchargeable par `setops_cache_artefacts` pour partager un cache commun.
serveur_nextcloud_cache_local: "{{ setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops') }}" serveur_nextcloud_cache_local: "{{ setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops') }}"
# --- Sonde de supervision -----------------------------------------------------------
serveur_nextcloud_sonde_url: "http://127.0.0.1"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: collaboration
ttl: 5400
raison: Nextcloud est-il installe, hors maintenance et sans migration en attente ? Ces trois etats
se lisent d'un coup dans `status.php`, et chacun rend le service inutilisable sans que la page d'accueil
cesse de repondre.

View file

@ -53,3 +53,22 @@
- name: Identite visuelle Alliance Boreale (theme + ciel etoile) - name: Identite visuelle Alliance Boreale (theme + ciel etoile)
ansible.builtin.import_tasks: theme.yml ansible.builtin.import_tasks: theme.yml
when: serveur_nextcloud_branding | bool when: serveur_nextcloud_branding | bool
# --- 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 « collaboration »
ansible.builtin.template:
src: sonde-collaboration.sh.j2
dest: /usr/local/lib/setops/sondes/collaboration.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,35 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_nextcloud). Ne pas editer a la main.
#
# SONDE « collaboration » — installe, hors maintenance, sans migration en attente
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_nextcloud_sonde_url` (port ferme).
set -uo pipefail
URL={{ serveur_nextcloud_sonde_url }}
s=$(curl -sk --max-time 10 "${URL}/status.php" 2>/dev/null)
[[ -n "${s}" ]] || { echo "Nextcloud ne repond pas sur ${URL}."; exit 2; }
verdict=$(printf '%s' "${s}" | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
print("2|status.php illisible"); raise SystemExit
if not d.get("installed"):
print("2|instance NON INSTALLEE")
elif d.get("maintenance"):
print("2|en MODE MAINTENANCE — personne ne peut travailler")
elif d.get("needsDbUpgrade"):
print("2|migration de base EN ATTENTE — le service tourne sur un schema perime")
else:
print("0|version " + str(d.get("versionstring") or d.get("version") or "?"))
' 2>/dev/null)
code=${verdict%%|*}; texte=${verdict#*|}
[[ -n "${code}" ]] || { echo "Verdict Nextcloud illisible."; exit 2; }
[[ "${code}" == "0" ]] && { echo "Nextcloud : ${texte}, installe et disponible."; exit 0; }
echo "Nextcloud EN DEFAUT — ${texte}."
exit 2

View file

@ -182,3 +182,8 @@ serveur_ops_cache_roues: >-
{{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/roues-{{ {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/roues-{{
(lookup('file', serveur_ops_roues_source) ~ serveur_ops_ansible) | hash('sha1') | truncate(12, true, '') }} (lookup('file', serveur_ops_roues_source) ~ serveur_ops_ansible) | hash('sha1') | truncate(12, true, '') }}
serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne" serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne"
# --- Sonde de supervision -----------------------------------------------------------
serveur_ops_sonde_racine: /opt/setops
serveur_ops_sonde_venv: /opt/setops/venv
serveur_ops_sonde_branche: main

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: runner
ttl: 5400
raison: 'Le poste d''exploitation peut-il encore agir ? Deux choses : ses depots sont-ils PROPRES
et sur leur branche (une modification locale fait diverger le runner du depot que tout le monde
lit), et Ansible repond-il dans son venv.'

View file

@ -520,3 +520,22 @@
owner: "{{ serveur_ops_utilisateur }}" owner: "{{ serveur_ops_utilisateur }}"
group: "{{ serveur_ops_utilisateur }}" group: "{{ serveur_ops_utilisateur }}"
mode: "0644" mode: "0644"
# --- 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 « runner »
ansible.builtin.template:
src: sonde-runner.sh.j2
dest: /usr/local/lib/setops/sondes/runner.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,43 @@
#jinja2: comment_start_string:'{=#', comment_end_string:'#=}'
#!/bin/bash
# (Le gabarit redefinit le delimiteur de commentaire Jinja : `${#tableau[@]}` contient
# la sequence `{#`, que Jinja lirait comme un debut de commentaire. Le shell en a
# besoin ; c'est donc a Jinja de s'ecarter.)
# GENERE par Set-OPS (role serveur_ops). Ne pas editer a la main.
#
# SONDE « runner » — depots propres sur leur branche, et Ansible repond
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_ops_sonde_venv` (chemin inexistant).
set -uo pipefail
VENV={{ serveur_ops_sonde_venv }}
BRANCHE={{ serveur_ops_sonde_branche }}
RACINE={{ serveur_ops_sonde_racine }}
# `safe.directory` : les depots appartiennent au compte d'exploitation, la sonde tourne en
# root. Sans cela git refuse — « proprietaire douteux » — et la sonde rendrait un echec
# qui parle de git au lieu de parler du depot.
git_() { git -c safe.directory='*' -C "$1" "${@:2}" 2>/dev/null; }
ennuis=(); n=0
for d in "${RACINE}"/*/; do
[[ -d "${d}.git" ]] || continue
n=$((n+1)); nom=$(basename "${d}")
b=$(git_ "${d}" rev-parse --abbrev-ref HEAD)
[[ "${b}" == "${BRANCHE}" ]] || ennuis+=("${nom} sur « ${b:-?} »")
# UN DEPOT SALE SUR LE RUNNER EST UNE DIVERGENCE SILENCIEUSE : ce qui s'applique a la
# flotte n'est plus ce que le depot contient, et personne ne le sait.
[[ -z "$(git_ "${d}" status --porcelain)" ]] || ennuis+=("${nom} MODIFIE localement")
done
(( n > 0 )) || { echo "Aucun depot sous ${RACINE} — le runner n'a rien a appliquer."; exit 2; }
v=$("${VENV}/bin/ansible" --version 2>/dev/null | head -1)
[[ -n "${v}" ]] || { echo "Ansible ne repond pas dans ${VENV} — le runner ne peut rien appliquer."; exit 2; }
if (( ${#ennuis[@]} > 0 )); then
echo "Runner : ${ennuis[*]} — ce qui s'applique diverge du depot.|depots=${n}"
exit 2
fi
echo "Runner : ${n} depot(s) propres sur « ${BRANCHE} », ${v}.|depots=${n}"

View file

@ -79,3 +79,11 @@ serveur_ops_tenant_cle_deposer: false
serveur_ops_tenant_cle_source: "" serveur_ops_tenant_cle_source: ""
serveur_ops_tenant_cle_destination: >- serveur_ops_tenant_cle_destination: >-
{{ serveur_ops_tenant_racine }}/.config/setops-vault-{{ serveur_ops_tenant_depot | lower }} {{ serveur_ops_tenant_racine }}/.config/setops-vault-{{ serveur_ops_tenant_depot | lower }}
# --- Sonde de supervision -----------------------------------------------------------
# Le chemin ou le RUNNER lit la voute de son ecosysteme.
# LE DEPOT VIENT DU ROLE, PAS D'UNE SUPPOSITION : `serveur_ops_tenant_depot` designe
# deja le dossier clone sur le runner.
serveur_ops_tenant_sonde_depot: "{{ serveur_ops_tenant_depot }}"
serveur_ops_tenant_sonde_voute: >-
/opt/setops/{{ serveur_ops_tenant_sonde_depot }}/inventories/principal/group_vars/all/vault.yml

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: voute
ttl: 5400
raison: 'La voute de l''ecosysteme est-elle presente ET CHIFFREE la ou le runner la lit ? Une voute
en clair sur un runner est la fuite la plus discrete qui soit : rien ne casse, tout continue de
fonctionner, et les secrets sont lisibles par quiconque accede a la machine.'

View file

@ -177,3 +177,22 @@
when: when:
- serveur_ops_tenant_cle_deposer | bool - serveur_ops_tenant_cle_deposer | bool
- not ansible_check_mode - 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 « voute »
ansible.builtin.template:
src: sonde-voute.sh.j2
dest: /usr/local/lib/setops/sondes/voute.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,24 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_ops_tenant). Ne pas editer a la main.
#
# SONDE « voute » — la voute est presente et chiffree
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_ops_tenant_sonde_voute` (chemin inexistant).
set -uo pipefail
VOUTE={{ serveur_ops_tenant_sonde_voute }}
[[ -f "${VOUTE}" ]] || { echo "Aucune voute en ${VOUTE} — le runner ne peut pas deployer."; exit 2; }
# LA PRESENCE NE SUFFIT PAS, ET C'EST TOUT L'INTERET DE CETTE SONDE. Un fichier existe
# aussi bien chiffre qu'en clair ; seul son entete le dit.
head -c 20 "${VOUTE}" | grep -q '^\$ANSIBLE_VAULT' \
|| { echo "LA VOUTE EST EN CLAIR : ${VOUTE} — secrets lisibles sur ce runner."; exit 2; }
perm=$(stat -c '%a' "${VOUTE}" 2>/dev/null)
if [[ "${perm}" != "600" && "${perm}" != "640" && "${perm}" != "400" ]]; then
echo "Voute chiffree mais lisible trop largement (${perm}) : ${VOUTE}"
exit 1
fi
echo "Voute presente et chiffree (${perm})."

View file

@ -61,3 +61,10 @@ serveur_postfix_tls_dir: "/etc/postfix/tls"
# Zero-confiance : verifier le TLS de la livraison LMTP vers Dovecot (cert step-ca). # Zero-confiance : verifier le TLS de la livraison LMTP vers Dovecot (cert step-ca).
serveur_postfix_lmtp_tls_verify: false serveur_postfix_lmtp_tls_verify: false
serveur_postfix_lmtp_tls_cafile: "/etc/step/certs/root_ca.crt" serveur_postfix_lmtp_tls_cafile: "/etc/step/certs/root_ca.crt"
# --- Sonde de supervision -----------------------------------------------------------
# UN COURRIEL EN ATTENTE N'EST PAS UNE PANNE — un relais distant momentanement muet suffit
# a en garder quelques-uns. C'est l'ACCUMULATION qui parle. Les seuils sont bas parce que
# cet ecosysteme n'emet presque rien : dix messages en attente y sont deja anormaux.
serveur_postfix_sonde_avert: 10
serveur_postfix_sonde_crit: 50

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: file-courriel
ttl: 5400
raison: Combien de courriels attendent dans la file ? Une file qui monte est le premier signe d'une
remise qui ne se fait plus — et c'est un signe qu'on voit AVANT que quiconque se plaigne de ne rien
recevoir.

View file

@ -214,3 +214,22 @@
enabled: true enabled: true
state: started state: started
daemon_reload: true daemon_reload: 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 « file-courriel »
ansible.builtin.template:
src: sonde-file-courriel.sh.j2
dest: /usr/local/lib/setops/sondes/file-courriel.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,29 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_postfix). Ne pas editer a la main.
#
# SONDE « file-courriel » — la file d'attente ne s'accumule pas
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_postfix_sonde_avert` / `_crit` (seuils impossibles).
set -uo pipefail
AVERT={{ serveur_postfix_sonde_avert }}
CRIT={{ serveur_postfix_sonde_crit }}
systemctl is-active --quiet postfix || { echo "Postfix n'est pas actif."; exit 2; }
# `mailq` PLUTOT QUE LE COMPTE DE FICHIERS : la file vit dans plusieurs repertoires
# (incoming, active, deferred, hold) et seul Postfix sait ce qu'il compte comme en
# attente. Un `find` sur `/var/spool` donnerait un chiffre plausible et faux.
n=$(mailq 2>/dev/null | grep -cE '^[0-9A-F]{6,}' || true)
diff=$(mailq 2>/dev/null | grep -cE '^[0-9A-F]+\*?[[:space:]]' || true)
if (( n >= CRIT )); then
echo "File de courriel : ${n} message(s) en attente — la remise ne se fait plus.|file=${n};${AVERT};${CRIT};0;"
exit 2
fi
if (( n >= AVERT )); then
echo "File de courriel : ${n} message(s) en attente (seuil ${AVERT}).|file=${n};${AVERT};${CRIT};0;"
exit 1
fi
echo "File de courriel : ${n} message(s) en attente.|file=${n};${AVERT};${CRIT};0;"