#!/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