sauvegardes : un controle hebdomadaire de restauration, rapporte a Icinga
restic check en relisant 10 pourcent des donnees, puis restauration reelle du dernier instantane avec --verify et comptage des fichiers. Service restauration, fraicheur de 8 jours, filtre du compte d API complete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bcf62bafe7
commit
b1c4775afe
9 changed files with 158 additions and 1 deletions
|
|
@ -120,3 +120,11 @@ client_backup_verification_horaire: "*-*-* 01/4:00:00"
|
||||||
client_backup_ttl_icinga: 21600
|
client_backup_ttl_icinga: 21600
|
||||||
client_backup_age_warn_h: 26
|
client_backup_age_warn_h: 26
|
||||||
client_backup_age_crit_h: 50
|
client_backup_age_crit_h: 50
|
||||||
|
|
||||||
|
# LE CONTROLE DE RESTAURATION (2026-09-28) — voir `templates/verifier-restauration.sh.j2`.
|
||||||
|
# Hebdomadaire : il relit une part du depot et restaure le dernier instantane pour de vrai.
|
||||||
|
client_backup_restauration_horaire: "Sun *-*-* 04:00:00"
|
||||||
|
client_backup_restauration_part: "10%"
|
||||||
|
# Huit jours : une semaine entre deux controles, plus une journee de marge. Au-dela sans
|
||||||
|
# nouvelle, Icinga passe le service au rouge (voir `serveur_icinga_fraicheur_restauration`).
|
||||||
|
client_backup_ttl_restauration: 691200
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,32 @@
|
||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
daemon_reload: true
|
daemon_reload: true
|
||||||
|
|
||||||
|
# LE CONTROLE DE RESTAURATION (2026-09-28). Meme destinataire, meme compte, un service a
|
||||||
|
# part : « l'instantane existe » et « l'instantane se rouvre » ne sont pas la meme question.
|
||||||
|
- name: Déployer le contrôle de restauration
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: verifier-restauration.sh.j2
|
||||||
|
dest: /usr/local/sbin/setops-verifier-restauration.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Déployer l'unité et le timer du contrôle de restauration
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ item }}.j2"
|
||||||
|
dest: "/etc/systemd/system/{{ item }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- setops-verification-restauration.service
|
||||||
|
- setops-verification-restauration.timer
|
||||||
|
|
||||||
|
- name: Activer le timer du contrôle de restauration
|
||||||
|
ansible.builtin.systemd_service:
|
||||||
|
name: setops-verification-restauration.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
daemon_reload: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# GENERE par Set-OPS (role client_backup). Ne pas editer a la main.
|
||||||
|
[Unit]
|
||||||
|
Description=La sauvegarde se restaure-t-elle ? (controle hebdomadaire, rapport a Icinga)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
ExecStart=/usr/local/sbin/setops-verifier-restauration.sh
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# GENERE par Set-OPS (role client_backup). Ne pas editer a la main.
|
||||||
|
[Unit]
|
||||||
|
Description=Controle hebdomadaire de restauration
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ client_backup_restauration_horaire }}
|
||||||
|
RandomizedDelaySec=3600
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
82
roles/client_backup/templates/verifier-restauration.sh.j2
Normal file
82
roles/client_backup/templates/verifier-restauration.sh.j2
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/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)"
|
||||||
|
|
||||||
|
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
|
||||||
|
rapporter 1 "Instantane restaurable mais VIDE : rien a restaurer. Legitime si ce noeud n'a pas encore de donnees."
|
||||||
|
else
|
||||||
|
rapporter 0 "Restauration verifiee : ${obtenus} fichier(s), ${octets} octets, contenu recompare ; integrite du depot relue sur ${PART}."
|
||||||
|
fi
|
||||||
|
|
@ -95,6 +95,7 @@ serveur_icinga_api_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
||||||
serveur_icinga_fraicheur_sauvegarde: 21600 # = client_backup_ttl_icinga = serveur_backup_ttl_icinga
|
serveur_icinga_fraicheur_sauvegarde: 21600 # = client_backup_ttl_icinga = serveur_backup_ttl_icinga
|
||||||
serveur_icinga_fraicheur_sante: 2700 # = client_sante_ttl_icinga
|
serveur_icinga_fraicheur_sante: 2700 # = client_sante_ttl_icinga
|
||||||
serveur_icinga_fraicheur_sondes: 5400 # = client_sante_ttl_sondes
|
serveur_icinga_fraicheur_sondes: 5400 # = client_sante_ttl_sondes
|
||||||
|
serveur_icinga_fraicheur_restauration: 691200 # = client_backup_ttl_restauration (hebdomadaire + 1 jour)
|
||||||
|
|
||||||
# Hote portant les depots : DERIVE du groupe, jamais ecrit en dur.
|
# Hote portant les depots : DERIVE du groupe, jamais ecrit en dur.
|
||||||
serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}"
|
serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ object ApiUser "{{ serveur_icinga_api_utilisateur }}" {
|
||||||
found » sur un objet qui existe, comme le 2026-09-02. Une heure perdue.
|
found » sur un objet qui existe, comme le 2026-09-02. Une heure perdue.
|
||||||
La portee reste etroite : des NOMS, jamais un joker.
|
La portee reste etroite : des NOMS, jamais un joker.
|
||||||
#}
|
#}
|
||||||
filter = {{ '{{' }} match("sauvegarde: *", service.name) || service.name == "sauvegarde" || service.name == "sante"{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}{% for s in sondes %} || service.name == "{{ s.nom }}"{% endfor %}{% endfor %} {{ '}}' }}
|
filter = {{ '{{' }} match("sauvegarde: *", service.name) || service.name == "sauvegarde" || service.name == "restauration" || service.name == "sante"{% for role, sondes in (serveur_icinga_sondes | default({})) | dictsort %}{% for s in sondes %} || service.name == "{{ s.nom }}"{% endfor %}{% endfor %} {{ '}}' }}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,3 +98,18 @@ object Service "sauvegarde" {
|
||||||
vars.setops_source = "{{ noeud }}"
|
vars.setops_source = "{{ noeud }}"
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{#
|
||||||
|
LA RESTAURATION, UN SERVICE A PART (2026-09-28). « L'instantane existe, recent, non
|
||||||
|
vide » (`sauvegarde`) et « il se rouvre, contenu recompare » ne sont pas la meme question.
|
||||||
|
Controle hebdomadaire : le seuil de fraicheur est de huit jours.
|
||||||
|
#}
|
||||||
|
{% for noeud in serveur_icinga_sauvegarde_attendue | sort %}
|
||||||
|
object Service "restauration" {
|
||||||
|
import "setops-rapport-attendu"
|
||||||
|
host_name = "{{ noeud }}"
|
||||||
|
check_interval = {{ serveur_icinga_fraicheur_restauration }}s
|
||||||
|
retry_interval = {{ serveur_icinga_fraicheur_restauration }}s
|
||||||
|
vars.dummy_text = "AUCUN CONTROLE DE RESTAURATION depuis {{ (serveur_icinga_fraicheur_restauration / 86400) | int }} jours : la sauvegarde n'a pas ete rouverte."
|
||||||
|
vars.setops_source = "{{ noeud }}"
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ def main() -> int:
|
||||||
("serveur_icinga_fraicheur_sauvegarde", "serveur_backup", "serveur_backup_ttl_icinga"),
|
("serveur_icinga_fraicheur_sauvegarde", "serveur_backup", "serveur_backup_ttl_icinga"),
|
||||||
("serveur_icinga_fraicheur_sante", "client_sante", "client_sante_ttl_icinga"),
|
("serveur_icinga_fraicheur_sante", "client_sante", "client_sante_ttl_icinga"),
|
||||||
("serveur_icinga_fraicheur_sondes", "client_sante", "client_sante_ttl_sondes"),
|
("serveur_icinga_fraicheur_sondes", "client_sante", "client_sante_ttl_sondes"),
|
||||||
|
("serveur_icinga_fraicheur_restauration", "client_backup", "client_backup_ttl_restauration"),
|
||||||
]
|
]
|
||||||
echecs = 0
|
echecs = 0
|
||||||
for seuil, role, ttl in paires:
|
for seuil, role, ttl in paires:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue