Les quatre scripts qui touchent au depot appellent restic avec --retry-lock (client_backup_attente_verrou, 30 min) : deux minuteurs partis ensemble ne fabriquent plus de faux critique (aucun instantane, depot corrompu). Eprouve sur un depot jetable, dans les deux sens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.8 KiB
Django/Jinja
107 lines
4.8 KiB
Django/Jinja
#!/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"
|
|
# UN VERROU N'EST PAS UNE PANNE (2026-10-04). La sauvegarde, la verification du depot, le
|
|
# controle de restauration et `setops-restaurer` partagent ce depot ; leurs minuteurs peuvent
|
|
# partir ensemble (`Persistent=true` rattrape au demarrage tout ce qui a ete manque). Sans
|
|
# attente, restic refuse aussitot un depot verrouille, et une sonde concluait « aucun
|
|
# instantane » ou « depot corrompu ». On attend le verrou au lieu de conclure.
|
|
restic() { command restic --retry-lock {{ client_backup_attente_verrou }} "$@"; }
|
|
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
|