Set-OPS-Public/roles/client_backup/templates/verifier-mon-depot.sh.j2
Daniel Allaire 7976df08d5 client_backup : un instantane vide dit pourquoi
Devant un instantane vide, les sondes sauvegarde et restauration comptent
ce que les chemins sauvegardes contiennent sur le noeud : rien (RIEN A
SAUVEGARDER, avertissement), arrive apres l instantane (avertissement),
ou deja la a sa prise (critique : la sauvegarde manque ses donnees).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:06:31 -04:00

108 lines
5 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)"
# Les chemins que `setops-sauvegarde` emporte : de quoi dire, devant un instantane vide,
# s'il n'y avait RIEN a emporter ou si la sauvegarde a manque des donnees bien presentes.
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}!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
# INSTANTANE VIDE : on regarde ce que les chemins sauvegardes contiennent ICI, sur le
# noeud. Trois cas, trois messages (2026-10-04) :
# - rien sur le noeud : rien a sauvegarder, l'instantane vide est juste. Avertissement
# quand meme : un humain doit savoir que ce noeud n'est protege de rien ;
# - des fichiers posterieurs a l'instantane : ils sont arrives depuis, la prochaine
# sauvegarde les emportera. Avertissement ;
# - des fichiers deja la AU MOMENT de l'instantane : la sauvegarde les a manques.
# CRITIQUE — c'est une vraie panne.
sur_place=0; anciens=0
for c in "${CHEMINS[@]}"; do
[[ -e "$c" ]] || continue
sur_place=$(( sur_place + $(find "$c" -type f 2>/dev/null | wc -l) ))
anciens=$(( anciens + $(find "$c" -type f ! -newermt "@${secondes}" 2>/dev/null | wc -l) ))
done
if (( sur_place == 0 )); then
rapporter 1 "RIEN A SAUVEGARDER : ${CHEMINS[*]} ne contient aucun fichier sur ce noeud ; instantane vide, comme attendu."
elif (( anciens == 0 )); then
rapporter 1 "DONNEES ARRIVEES DEPUIS : ${sur_place} fichier(s) dans ${CHEMINS[*]}, tous posterieurs au dernier instantane (il y a ${age_h} h, vide). La prochaine sauvegarde les emportera."
else
rapporter 2 "N'EMPORTE RIEN : instantane vide alors que ${CHEMINS[*]} contenait deja ${anciens} fichier(s) a sa prise. La sauvegarde manque ses donnees."
fi
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