Some checks are pending
verifier / verifier (push) Waiting to run
serveur_backup verifiait pour tout le monde — juste tant que le depot vivait dans l ecosysteme. Depuis qu ils deposent chez leur hebergeur, le site heberge des octets chiffres COTE CLIENT : il ne peut ni les lire ni dire s ils valent quelque chose. La verification revient donc au seul qui detient la cle, le noeud. client_backup verifie SON depot distant — pas le fait d avoir lance sa sauvegarde. Une unite verte sur un depot vide est ce qui a menti un mois. serveur_icinga n exige plus un hote serveur_backup et se branche sur deux modeles : depot local (services sur son hote, nommes sauvegarde: <noeud>) ou pas de depot (services sur chaque noeud, nommes sauvegarde). Le 404 qui n etait pas une absence : les noeuds recevaient No objects found alors que icinga2 object list montrait le service charge. Le filtre du compte d API ne portait que la premiere forme de nom — c est la PERMISSION qui refusait, avec les mots d une absence. Aussi : ingress 5665 depuis client_backup, le pair ne nommait que serveur_backup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
81 lines
3.7 KiB
Django/Jinja
81 lines
3.7 KiB
Django/Jinja
#!/bin/bash
|
|
# GENERE par Set-OPS (role client_backup). Ne pas editer a la main.
|
|
#
|
|
# CE NOEUD VERIFIE SON PROPRE DEPOT, ET PAS SON PROPRE TIMER.
|
|
#
|
|
# Un noeud sait qu'il a LANCE sa sauvegarde ; il ne sait pas qu'elle a ABOUTI. La
|
|
# difference n'est pas theorique : onze machines ont lance chaque nuit un timer vert
|
|
# pendant un mois sur des depots qui n'emportaient rien (2026-07-03 -> 2026-08-11).
|
|
#
|
|
# Ce controle interroge donc le DEPOT DISTANT — l'instantane existe-t-il, est-il RECENT,
|
|
# n'est-il pas VIDE. Le noeud est desormais le seul a pouvoir le faire : le depot vit
|
|
# chez l'hebergeur, qui heberge des octets chiffres cote client et ne detient aucune de
|
|
# nos cles.
|
|
set -uo pipefail
|
|
|
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
|
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
|
API="https://{{ client_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
|
TTL={{ client_backup_ttl_icinga }}
|
|
AGE_WARN={{ client_backup_age_warn_h }}
|
|
AGE_CRIT={{ client_backup_age_crit_h }}
|
|
MOI="{{ inventory_hostname }}"
|
|
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
|
|
|
rapporter() { # $1=code $2=texte
|
|
local charge reponse
|
|
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
|
"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]),
|
|
"plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \
|
|
"${MOI}!sauvegarde" "$1" "$2" "${TTL}")
|
|
reponse=$(curl -sS --cacert "{{ client_backup_ca_verification }}" --max-time 20 \
|
|
-u "{{ client_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)
|
|
if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then
|
|
echo "ECHEC du rapport Icinga : ${reponse}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
maintenant=$(date +%s)
|
|
|
|
json=$(restic 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
|
|
# ON NE DISTINGUE PAS ICI « depot vide » de « depot injoignable », et c'est voulu :
|
|
# dans les deux cas il n'y a PAS de sauvegarde exploitable, et c'est ce qui compte.
|
|
# Le detail est dans le journal du noeud, pas dans un verdict qui se voudrait plus
|
|
# precis qu'il ne peut l'etre.
|
|
rapporter 2 "AUCUN INSTANTANE lisible dans ${RESTIC_REPOSITORY} — depot vide ou injoignable."
|
|
exit 0
|
|
fi
|
|
|
|
secondes=$(date -d "$(printf '%s' "${horodatage}" | sed -E 's/\.[0-9]+//')" +%s 2>/dev/null || echo 0)
|
|
age_h=$(( (maintenant - secondes) / 3600 ))
|
|
|
|
taille=$(restic 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)
|
|
|
|
fichiers=$(restic 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 /srv/web sans webapp). C'est a un humain
|
|
# de trancher — mais il doit le VOIR.
|
|
rapporter 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 2 "PERIME : dernier instantane il y a ${age_h} h (seuil ${AGE_CRIT} h), ${fichiers} fichier(s)."
|
|
elif (( age_h >= AGE_WARN )); then
|
|
rapporter 1 "EN RETARD : dernier instantane il y a ${age_h} h (seuil ${AGE_WARN} h), ${fichiers} fichier(s)."
|
|
else
|
|
rapporter 0 "OK : instantane il y a ${age_h} h, ${fichiers} fichier(s), ${taille} octets."
|
|
fi
|