#!/bin/bash # GENERE par Set-OPS (role client_backup). Ne pas editer a la main. # # LA SAUVEGARDE SE RESTAURE-T-ELLE ? (2026-09-28) # # `setops-verification-depot` dit qu'un instantane EXISTE, qu'il est RECENT et qu'il n'est # pas VIDE. Il ne dit pas qu'on saurait le rouvrir. Le 2026-09-28, c'est a la main qu'on a # restaure les dumps PostgreSQL et LDAP pour le savoir — et le meme jour on decouvrait que # deux locataires sur deux ne deposaient rien hors de leur flotte depuis des semaines. # Une sauvegarde jamais restauree est une hypothese. # # CE QUE CE CONTROLE FAIT, chaque semaine, sur CE noeud (le seul a detenir la cle) : # 1. `restic check --read-data-subset` : l'integrite du depot, en RELISANT une part des # donnees — pas seulement l'index ; # 2. une vraie restauration du dernier instantane dans un repertoire temporaire, avec # `--verify` : le contenu restaure est recompare a l'instantane ; # 3. le NOMBRE de fichiers restaures face a celui de l'instantane. # Le repertoire est detruit ensuite, quoi qu'il arrive. 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_restauration }} PART="{{ client_backup_restauration_part }}" MOI="{{ inventory_hostname }}" MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)" # Les chemins que `setops-sauvegarde` emporte (voir `setops-verifier-mon-depot.sh`). CHEMINS=( {% for job in client_backup_jobs %} {% for c in job.chemins %} "{{ c }}" {% endfor %} {% endfor %} ) 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}!restauration" "$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 } CIBLE=$(mktemp -d /var/tmp/setops-restauration.XXXXXX) trap 'rm -rf "${CIBLE}"' EXIT # 1. Integrite, en relisant les donnees. if ! sortie=$(restic check --read-data-subset="${PART}" 2>&1); then rapporter 2 "DEPOT CORROMPU OU ILLISIBLE : restic check (${PART} relus) a echoue — $(printf '%s' "${sortie}" | tail -1 | cut -c1-120)" exit 0 fi # 2. Ce que l'instantane contient. attendus=$(restic ls latest --json 2>/dev/null | python3 -c ' import json, sys 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 -1) if (( attendus < 0 )); then rapporter 2 "AUCUN INSTANTANE lisible dans ${RESTIC_REPOSITORY} — rien a restaurer." exit 0 fi # 3. Restaurer pour de vrai, et recomparer. if ! sortie=$(restic restore latest --target "${CIBLE}" --verify 2>&1); then rapporter 2 "RESTAURATION EN ECHEC : $(printf '%s' "${sortie}" | tail -1 | cut -c1-140)" exit 0 fi obtenus=$(find "${CIBLE}" -type f | wc -l) octets=$(du -sb "${CIBLE}" | cut -f1) if (( obtenus != attendus )); then rapporter 2 "RESTAURATION INCOMPLETE : ${obtenus} fichier(s) restaure(s) sur ${attendus} dans l'instantane." elif (( attendus == 0 )); then # Avertissement dans les deux cas ; le message dit lequel. Le jugement d'une sauvegarde # qui manque ses donnees revient a la sonde `sauvegarde`, qui sait dater l'instantane. sur_place=0 for c in "${CHEMINS[@]}"; do [[ -e "$c" ]] && sur_place=$(( sur_place + $(find "$c" -type f 2>/dev/null | wc -l) )) done if (( sur_place == 0 )); then rapporter 1 "RIEN A RESTAURER : instantane vide, et ${CHEMINS[*]} ne contient aucun fichier sur ce noeud — comme attendu." else rapporter 1 "Instantane restaurable mais VIDE, alors que ${CHEMINS[*]} contient ${sur_place} fichier(s) sur ce noeud : voir la sonde sauvegarde." fi else rapporter 0 "Restauration verifiee : ${obtenus} fichier(s), ${octets} octets, contenu recompare ; integrite du depot relue sur ${PART}." fi