supervision : c'est le DEPOT qui dit ou en sont les sauvegardes
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:39:05 -04:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Gere par Set-OPS (role serveur_backup). Ne pas editer a la main.
|
|
|
|
|
#
|
|
|
|
|
# Verifie les depots restic et POUSSE un resultat passif par noeud vers l'API Icinga.
|
|
|
|
|
#
|
|
|
|
|
# Pourquoi ici et pas sur le noeud source. Le noeud sait s'il a LANCE sa sauvegarde ; il
|
|
|
|
|
# ne sait pas si elle est ARRIVEE. Le depot, lui, voit ce qui existe reellement — et c'est
|
|
|
|
|
# la seule chose qui compte le jour d'une restauration. Le 2026-08-11, onze noeuds
|
|
|
|
|
# lancaient chaque nuit une sauvegarde qui n'emportait RIEN : une unite verte sur un depot
|
|
|
|
|
# vide. Trois criteres, donc, et pas un seul :
|
|
|
|
|
#
|
|
|
|
|
# 1. l'instantane EXISTE (sinon : la sauvegarde n'arrive pas)
|
|
|
|
|
# 2. il est RECENT (sinon : elle a cesse d'arriver)
|
|
|
|
|
# 3. il n'est pas VIDE (sinon : elle arrive mais ne porte rien)
|
|
|
|
|
#
|
|
|
|
|
# Le `ttl` envoye a Icinga fait la fraicheur : si ce script cesse de tourner, Icinga passe
|
|
|
|
|
# tout seul en « expire ». C'est le SILENCE qui alerte — c'est lui qui n'a alerte personne.
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
RACINE="{{ serveur_backup_racine }}"
|
reconstruction from-zero : sept defauts, tous invisibles sur une flotte debout
Chezlepro reconstruit depuis zero en 10.17.x.x. Sept defauts sont tombes, et
AUCUN n'etait detectable autrement — c'est tout l'argument de la reconstruction
comme preuve.
1. aucune route vers le tenant sur le poste (posee a la main, jamais persistee)
2. backup-01 exigeait l'AC d'Icinga — dependance non declaree
3. ma correction inversait les couches : REFUSEE PAR P08 avant d'entrer
4. base Forgejo a moitie initialisee (sequelle de l'arret du n°2)
5. /etc/hosts : nom court avant le FQDN -> hostname -f faux sur les 14 machines
6. API Icinga jamais activee (garde `creates:` d'api setup)
7. restic refuse tout le lot si un chemin declare manque
LE CINQUIEME DEPASSAIT ICINGA. hostname -f rend le PREMIER nom : toute la flotte
se croyait appelee « mon-01 ». Visible sur le certificat d'Icinga, mais le meme
piege attendait Postfix (myhostname), les journaux, les certificats. FQDN d'abord.
CE QU'ON NE CORRIGE PAS : icinga2 api setup REECRIT NodeName d'apres le nom court
et nomme ses certificats d'apres lui. Aligne avant, il est ecrase ; aligne apres,
les certificats portent le mauvais nom. On adopte sa convention : le depot appelle
l'API par le nom court, celui que le certificat porte.
serveur_icinga_node_name derive desormais de l'INVENTAIRE, pas d'ansible_fqdn —
un fait qui depend du resolveur et rendait « mon-01 » alors que le FQDN etait bon.
Le commentaire ecrit pour avertir qu'une sequence accolade-diese casse les
gabarits Jinja contenait cette sequence, et cassait le gabarit. Neuf hotes en
echec pour un avertissement mal redige.
Sept devis CONFORME, prouver 36/36, make test 0. Neuf sauvegardes reussies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 03:36:56 -04:00
|
|
|
API="https://{{ serveur_backup_icinga_hote }}:5665" # nom COURT : c'est celui du certificat
|
supervision : c'est le DEPOT qui dit ou en sont les sauvegardes
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:39:05 -04:00
|
|
|
TTL={{ serveur_backup_ttl_icinga }}
|
|
|
|
|
AGE_WARN={{ serveur_backup_age_warn_h }}
|
|
|
|
|
AGE_CRIT={{ serveur_backup_age_crit_h }}
|
|
|
|
|
|
|
|
|
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
|
|
|
|
|
|
|
|
|
# L'API Icinga veut du JSON : un envoi `--data-urlencode` est refuse en « Bad Request »
|
|
|
|
|
# et le corps est perdu (mesure le 2026-08-11). On construit donc la charge avec python3,
|
|
|
|
|
# qui echappe correctement le texte du diagnostic.
|
|
|
|
|
ECHECS=0
|
|
|
|
|
rapporter() { # $1=noeud $2=code $3=texte
|
|
|
|
|
local charge reponse
|
|
|
|
|
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
|
|
|
|
"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]),
|
|
|
|
|
"plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \
|
|
|
|
|
"${HOTE_DEPOT}!sauvegarde: $1" "$2" "$3" "${TTL}")
|
icinga : le pair est verifie — mais pas avec un certificat step-ca
Objectif : supprimer le `curl -k` du rapporteur. Fait, et la maniere a ete
imposee par la mesure.
SERVIR UN CERTIFICAT STEP-CA SUR L'API EST IMPOSSIBLE. Icinga le dit lui-meme :
« Our certificate will expire soon, but we own the CA. Renewing. » Il renouvelle
tout certificat expirant sous 30 jours ; les notres vivent 24 h ; possedant une
AC, il re-emet avec la sienne et ecrase le notre a chaque demarrage. Collision
entre deux politiques de PKI, et la notre n'est pas negociable.
Deux decouvertes en chemin, toutes deux par le garde-fou `icinga2 daemon -C`
ajoute au role, qui a ARRETE le deploiement avant de redemarrer la supervision :
- cert_path/key_path/ca_path sont DEPRECIES depuis 2.8 ; les poser reveille un
chemin de code herite qui exige en plus un objet Endpoint ;
- l'identite de l'API est le CN du certificat. NodeName valait « mon-01 », donc
le SAN aussi, alors qu'on appelle par le FQDN : aucune verification n'aurait
pu reussir. NodeName est desormais aligne sur le FQDN.
A LA PLACE : Icinga garde son AC (domaine de confiance FERME, legitime) et
backup-01 verifie le pair contre CETTE AC, recuperee depuis mon-01 au
deploiement. Le pair est authentifie ; seule la racine differe.
Controle negatif — une verification qu'on ne teste pas est un ornement : avec la
mauvaise AC (step-ca), curl refuse (« unable to get local issuer certificate ») ;
avec la bonne, les neuf rapports passent.
Sous-AC step-ca ecartee : elle poserait sur l'hote de supervision une cle capable
d'EMETTRE pour n'importe quel nom, alors qu'on a choisi le sens du flux pour que
compromettre mon-01 ne donne rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:33:50 -04:00
|
|
|
# --cacert, et surtout PAS -k : on verifie A QUI on parle. L'AC opposee est celle
|
|
|
|
|
# d'ICINGA (il refuse d'en servir une autre, cf. defaults). Un `-k` laisserait n'importe
|
|
|
|
|
# quel intercepteur recevoir l'etat des sauvegardes — et surtout, y repondre.
|
|
|
|
|
reponse=$(curl -sS --cacert "{{ serveur_backup_ca_verification }}" --max-time 20 \
|
supervision : c'est le DEPOT qui dit ou en sont les sauvegardes
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:39:05 -04:00
|
|
|
-u "{{ serveur_backup_icinga_utilisateur }}:${MOTDEPASSE}" \
|
|
|
|
|
-H 'Accept: application/json' -H 'Content-Type: application/json' \
|
|
|
|
|
-X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1)
|
|
|
|
|
# Ne PAS avaler l'echec : un rapporteur muet recreerait exactement le defaut qu'on
|
|
|
|
|
# corrige. Le `ttl` reste le filet — si ce script cesse d'aboutir, Icinga perime les
|
|
|
|
|
# services de lui-meme et c'est le silence qui alerte.
|
|
|
|
|
if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then
|
|
|
|
|
echo "ECHEC du rapport Icinga pour $1 : ${reponse}" >&2
|
|
|
|
|
ECHECS=$((ECHECS + 1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
|
|
|
|
HOTE_DEPOT="{{ inventory_hostname }}"
|
|
|
|
|
maintenant=$(date +%s)
|
|
|
|
|
|
|
|
|
|
for noeud in {{ serveur_backup_noeuds_attendus | sort | join(' ') }}; do
|
|
|
|
|
depot="${RACINE}/${noeud}"
|
|
|
|
|
if [[ ! -d "${depot}" ]]; then
|
|
|
|
|
rapporter "${noeud}" 2 "AUCUN DEPOT : ${depot} n'existe pas — ce noeud n'a jamais depose."
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
|
|
|
|
horodatage=$(printf '%s' "${json}" | python3 -c \
|
|
|
|
|
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
|
|
|
|
if [[ -z "${horodatage}" ]]; then
|
|
|
|
|
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# restic ecrit des fractions de seconde a precision variable : on tronque avant %z.
|
|
|
|
|
secondes=$(date -d "$(printf '%s' "${horodatage}" | sed -E 's/\.[0-9]+//')" +%s 2>/dev/null || echo 0)
|
|
|
|
|
age_h=$(( (maintenant - secondes) / 3600 ))
|
|
|
|
|
|
|
|
|
|
taille=$(restic -r "${depot}" stats latest --mode raw-data --json 2>/dev/null | python3 -c \
|
|
|
|
|
'import sys,json;print(json.load(sys.stdin).get("total_size",0))' 2>/dev/null || echo 0)
|
|
|
|
|
|
|
|
|
|
# « Vide » se mesure en FICHIERS, pas en octets. Un seuil en octets est un mauvais
|
|
|
|
|
# critere : l'export LDIF d'un annuaire a un compte pese 2,3 Ko et se ferait signaler a
|
|
|
|
|
# tort (mesure le 2026-08-11). Un instantane qui ne contient AUCUN fichier — seulement
|
|
|
|
|
# les repertoires traverses — n'emporte rien, et c'est exact quelle que soit la taille.
|
|
|
|
|
fichiers=$(restic -r "${depot}" ls latest --json 2>/dev/null | python3 -c \
|
|
|
|
|
'import sys,json
|
|
|
|
|
n=0
|
|
|
|
|
for l in sys.stdin:
|
|
|
|
|
try: d=json.loads(l)
|
|
|
|
|
except ValueError: continue
|
|
|
|
|
if d.get("struct_type")=="node" and d.get("type")=="file": n+=1
|
|
|
|
|
print(n)' 2>/dev/null || echo 0)
|
|
|
|
|
|
|
|
|
|
if (( fichiers == 0 )); then
|
|
|
|
|
# AVERTISSEMENT et non CRITIQUE : la machine ne peut pas distinguer « les donnees ont
|
|
|
|
|
# disparu » de « il n'y en a pas encore » (un /var/vmail sans courriel, un /srv/web
|
|
|
|
|
# sans webapp). C'est a un humain de trancher — mais il doit le VOIR.
|
|
|
|
|
rapporter "${noeud}" 1 \
|
|
|
|
|
"N'EMPORTE RIEN : instantane sans aucun fichier (${taille} octets). Legitime si ce noeud n'a pas encore de donnees — a confirmer."
|
|
|
|
|
elif (( age_h >= AGE_CRIT )); then
|
|
|
|
|
rapporter "${noeud}" 2 "PERIME : dernier instantane il y a ${age_h} h (seuil ${AGE_CRIT} h), ${fichiers} fichier(s)."
|
|
|
|
|
elif (( age_h >= AGE_WARN )); then
|
|
|
|
|
rapporter "${noeud}" 1 "EN RETARD : dernier instantane il y a ${age_h} h (seuil ${AGE_WARN} h), ${fichiers} fichier(s)."
|
|
|
|
|
else
|
|
|
|
|
rapporter "${noeud}" 0 "OK : instantane il y a ${age_h} h, ${fichiers} fichier(s), ${taille} octets."
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if (( ECHECS > 0 )); then
|
|
|
|
|
echo "${ECHECS} rapport(s) non deposes : Icinga ne sait donc pas ou en sont ces sauvegardes." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|