diff --git a/CHANGELOG.md b/CHANGELOG.md index 808ffa7..f33c79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # CHANGELOG — Set-OPS +## 2026-08-31 — La recette dependait de l'heure : un depot occupe n'est pas une sauvegarde cassee + +**55 preuves. `make valider` : rc=0.** La reconstruction de Chezlepro est validee de bout +en bout — 16 cibles Prometheus UP, 6 vhosts HTTPS, un courriel **reellement remis** (envoi +→ LMTP → Maildir), et six depots restic **restaures et verifies**, dont l'annuaire dont on +prouve qu'il se rejoue (`slapadd -u`). + +Un seul verdict rouge au premier passage : `data-sql-01`, la base. Et la meme commande, +rejouee a la main, restaurait ses cinq fichiers. + +**`restic` verrouille son depot pendant qu'il ecrit.** Une recette qui croise la fenetre de +sauvegarde lit donc un echec de RESTAURATION la ou il n'y a qu'une attente — *verdict juste +sur l'instant, faux sur le fond, et dependant de l'heure a laquelle on l'a lancee.* + +**On reessaie, mais on ne masque pas.** Trois tentatives espacees couvrent un verrou +d'ecriture ; au-dela l'echec est reel, et la recette rend desormais **la raison que restic +a donnee** au lieu d'un `-1` muet. Controle sur la machine — cle de dechiffrement retiree : + +``` +ECHEC — la RESTAURATION a échoué après 3 tentatives (snapshots=0) + — restic dit : Fatal: Resolving password failed +``` + +### Deux fautes a moi, en chemin + +`[ "$f" -lt 0 ] && printf ...` : **la derniere commande d'un script decide de son code de +sortie**, et un test faux rend 1. La tache echouait donc exactement sur les machines ou la +restauration avait REUSSI — trois hotes verts declares en echec. + +Puis la ligne `raison=` n'etait emise qu'en cas d'echec : le gabarit cherchait un champ +absent, rendait `None`, et explosait sur les **dix machines saines**. Elle est desormais +toujours emise, vide en cas de succes. *Un champ toujours present coute un octet et +supprime un cas.* + ## 2026-08-31 — Deux filets : un cache mort ne rend plus un tenant irreparable **55 preuves.** Le cache d'un tenant est **un maillon dont depend sa propre diff --git a/playbooks/valider.yml b/playbooks/valider.yml index 5df9ed3..68d8a7f 100644 --- a/playbooks/valider.yml +++ b/playbooks/valider.yml @@ -137,11 +137,40 @@ n=$(restic snapshots 2>/dev/null | grep -cE '^[0-9a-f]{8} ' || echo 0) if [ "$n" -lt 1 ]; then echo "sansobjet=0 snapshots=0 restored_files=-1 ldif_rejouable=-1"; exit 0; fi tmp=$(mktemp -d) - if restic restore latest --target "$tmp" >/dev/null 2>&1; then - f=$(find "$tmp" -type f | wc -l) - else - f=-1 - fi + # LE DEPOT PEUT ETRE OCCUPE, ET CE N'EST PAS UNE SAUVEGARDE CASSEE (2026-08-31). + # + # `restic` verrouille son depot pendant qu'il ecrit. Une recette qui croise la + # fenetre de sauvegarde lit donc un echec de RESTAURATION la ou il n'y a qu'une + # attente — verdict juste sur l'instant, faux sur le fond, et dependant de + # l'heure a laquelle on l'a lancee. Constate sur `data-sql-01`, dont la + # sauvegarde de 02h38 tenait encore le depot : `ECHEC` au premier passage, + # `OK — 5 fichiers` en rejouant la meme commande a la main. + # + # ON REESSAIE, MAIS ON NE MASQUE PAS. Trois tentatives espacees couvrent + # largement un verrou d'ecriture ; au-dela, l'echec est REEL et on rend la + # RAISON que restic a donnee, au lieu d'un `-1` muet qui laisse chercher. + f=-1 + err="" + for essai in 1 2 3; do + if err=$(restic restore latest --target "$tmp" 2>&1 >/dev/null); then + f=$(find "$tmp" -type f | wc -l) + break + fi + rm -rf "$tmp"; tmp=$(mktemp -d) + [ "$essai" -lt 3 ] && sleep 20 + done + # La raison voyage sur une ligne a part : le verdict la cite, et l'exploitant + # n'a pas a se connecter pour savoir ce que restic a dit. + # + # `if` ET NON `[ ... ] && ...` : la DERNIERE commande d'un script decide de son + # code de sortie, et un test faux rend 1. Ecrit en `&&`, cette ligne faisait + # echouer la tache exactement sur les machines ou la restauration avait REUSSI + # — trois hotes verts declares en echec (2026-08-31). + # TOUJOURS EMISE, vide en cas de succes. Une ligne parfois absente oblige son + # lecteur a gerer l'absence, et c'est la que le gabarit s'est casse : la + # recherche rendait `None` et le verdict explosait sur les DIX machines saines + # (2026-08-31). Un champ toujours present coute un octet et supprime un cas. + printf 'raison=%s\n' "$(printf '%s' "$err" | tr '\n' ' ' | cut -c1-160)" # L'annuaire est la seule charge dont on peut prouver ICI qu'elle est REJOUABLE, # sans rien écrire : `slapadd -u` est un essai à blanc. Éprouvé le 2026-08-12, # avec contrôle négatif (un LDIF corrompu sort en 1). @@ -163,6 +192,8 @@ restaures: "{{ recette_restore.stdout | regex_search('restored_files=(-?\\d+)', '\\1') | first | int }}" snaps: "{{ recette_restore.stdout | regex_search('snapshots=(\\d+)', '\\1') | first | int }}" rejouable: "{{ ((recette_restore.stdout | regex_search('ldif_rejouable=(-?\\d+)', '\\1')) or [-1]) | first | int }}" + # Ce que restic a dit, quand il a refuse. Vide sinon — la ligne est toujours la. + raison: "{{ ((recette_restore.stdout | regex_search('raison=(.*)', '\\1')) or ['']) | first | trim }}" # « L'instantané n'emporte aucun fichier » n'est PAS un échec de restauration : la # restauration a parfaitement fonctionné, il n'y avait rien à rendre. La machine ne # peut pas distinguer « les données ont disparu » de « il n'y en a pas encore ». @@ -182,5 +213,6 @@ 'OK — ' ~ snaps ~ ' snapshot(s), restauration vérifiée (' ~ restaures ~ ' fichiers)' ~ ((rejouable | int == 1) | ternary(', annuaire REJOUABLE (slapadd -u).', '.')))) }} fail_msg: >- - ÉCHEC — la RESTAURATION elle-même a échoué (snapshots={{ snaps }}){{ + ÉCHEC — la RESTAURATION elle-même a échoué après 3 tentatives + (snapshots={{ snaps }}){{ raison | ternary(' — restic dit : ' ~ raison, '') }}{{ (rejouable | int == 0) | ternary(', et l annuaire restauré N EST PAS rejouable', '') }}.