2026-07-03 22:49:26 -04:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
|
|
|
|
|
# Sauvegarde applicative du nœud -> dépôt restic hors-nœud (chiffré, dédupliqué).
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
|
|
|
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
2026-10-04 14:27:11 -04:00
|
|
|
# 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 }} "$@"; }
|
2026-07-03 22:49:26 -04:00
|
|
|
|
2026-10-07 12:48:00 -04:00
|
|
|
# L'INSTANTANE D'AVANT RASAGE (2026-10-07). `--avant-raser` (unite
|
|
|
|
|
# `setops-sauvegarde-avant-raser`, lancee par `make sauvegarder-maintenant`) etiquette le
|
|
|
|
|
# depot « {{ client_backup_etiquette_avant_raser }} », et la retention garde cette etiquette.
|
|
|
|
|
# Sans elle, le premier depot de la machine reconstruite, le meme jour, chassait l'instantane
|
|
|
|
|
# d'avant par `--keep-daily` : apres la reconstruction de Technolibre (M4), il ne restait
|
|
|
|
|
# rien a quoi comparer l'etat remis. Un seul porte l'etiquette, le dernier : il est garde
|
|
|
|
|
# jusqu'a la reconstruction suivante, qui la lui retire.
|
|
|
|
|
AVANT_RASER=0
|
|
|
|
|
if [[ "${1:-}" == "--avant-raser" ]]; then AVANT_RASER=1; fi
|
|
|
|
|
ETIQUETTES=(--tag setops)
|
|
|
|
|
if (( AVANT_RASER )); then ETIQUETTES+=(--tag "{{ client_backup_etiquette_avant_raser }}"); fi
|
|
|
|
|
|
restauration : la reconstruction remet l'etat de l'incarnation precedente
Chaque role proprietaire (AC, bases, annuaire, Nextcloud, rspamd/DKIM,
courriel, forge, web) remet son etat au moment ou il le creerait neuf,
depuis le dernier instantane anterieur a la naissance de la machine.
La sauvegarde refuse de deposer tant qu'un etat d'avant attend.
Outil de noeud setops-restaurer ; cibles sauvegarder-maintenant,
restauration-etat, restauration-renoncer ; test_restauration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:39:22 -04:00
|
|
|
# 0. UN ETAT D'AVANT ATTEND-IL ? (2026-09-30)
|
|
|
|
|
# Sur une machine reconstruite, tant que l'etat de l'incarnation precedente n'a ete ni
|
|
|
|
|
# remis ni ecarte, deposer serait NUISIBLE : `restic forget --keep-daily` garde un
|
|
|
|
|
# instantane par jour, et celui de l'etat neuf chasserait celui d'avant, s'ils tombent le
|
|
|
|
|
# meme jour. On refuse donc, bruyamment (code 3). Voir `setops-restaurer garde`.
|
|
|
|
|
/usr/local/sbin/setops-restaurer garde
|
|
|
|
|
|
2026-07-03 22:49:26 -04:00
|
|
|
# 1. Dumps applicatifs (pg_dump, slapcat, forgejo dump...) vers le staging.
|
|
|
|
|
{% for job in client_backup_jobs %}
|
|
|
|
|
{% if job.commande is defined %}
|
|
|
|
|
install -d -m 0700 "{{ client_backup_staging }}/{{ job.nom }}"
|
|
|
|
|
{{ job.commande }}
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
|
|
|
|
# 2. Initialiser le dépôt s'il n'existe pas encore.
|
|
|
|
|
restic snapshots >/dev/null 2>&1 || restic init
|
|
|
|
|
|
|
|
|
|
# 3. Snapshot de tous les chemins déclarés.
|
|
|
|
|
CHEMINS=(
|
|
|
|
|
{% for job in client_backup_jobs %}
|
|
|
|
|
{% for c in job.chemins %}
|
|
|
|
|
"{{ c }}"
|
|
|
|
|
{% endfor %}
|
|
|
|
|
{% endfor %}
|
|
|
|
|
)
|
reconstruction from-zero : sept defauts, tous invisibles sur une flotte debout
Chezlepro reconstruit depuis zero en 10.17.x.x. Sept defauts sont tombes, et
AUCUN n'etait detectable autrement — c'est tout l'argument de la reconstruction
comme preuve.
1. aucune route vers le tenant sur le poste (posee a la main, jamais persistee)
2. backup-01 exigeait l'AC d'Icinga — dependance non declaree
3. ma correction inversait les couches : REFUSEE PAR P08 avant d'entrer
4. base Forgejo a moitie initialisee (sequelle de l'arret du n°2)
5. /etc/hosts : nom court avant le FQDN -> hostname -f faux sur les 14 machines
6. API Icinga jamais activee (garde `creates:` d'api setup)
7. restic refuse tout le lot si un chemin declare manque
LE CINQUIEME DEPASSAIT ICINGA. hostname -f rend le PREMIER nom : toute la flotte
se croyait appelee « mon-01 ». Visible sur le certificat d'Icinga, mais le meme
piege attendait Postfix (myhostname), les journaux, les certificats. FQDN d'abord.
CE QU'ON NE CORRIGE PAS : icinga2 api setup REECRIT NodeName d'apres le nom court
et nomme ses certificats d'apres lui. Aligne avant, il est ecrase ; aligne apres,
les certificats portent le mauvais nom. On adopte sa convention : le depot appelle
l'API par le nom court, celui que le certificat porte.
serveur_icinga_node_name derive desormais de l'INVENTAIRE, pas d'ansible_fqdn —
un fait qui depend du resolveur et rendait « mon-01 » alors que le FQDN etait bon.
Le commentaire ecrit pour avertir qu'une sequence accolade-diese casse les
gabarits Jinja contenait cette sequence, et cassait le gabarit. Neuf hotes en
echec pour un avertissement mal redige.
Sept devis CONFORME, prouver 36/36, make test 0. Neuf sauvegardes reussies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 03:36:56 -04:00
|
|
|
# Un chemin DECLARE peut ne pas exister encore : `/srv/web` avant la premiere webapp,
|
|
|
|
|
# le stockage de Nextcloud avant son installation. Restic refuse alors TOUT le lot
|
|
|
|
|
# (« all source directories/files do not exist ») et la sauvegarde entiere echoue —
|
|
|
|
|
# constate le 2026-08-13 sur trois hotes d'une flotte neuve.
|
|
|
|
|
# On ne sauvegarde donc que ce qui existe, et on le DIT quand il n'y a rien : c'est a la
|
|
|
|
|
# supervision de signaler un depot vide, pas au script de mourir.
|
|
|
|
|
# NOTE : ne JAMAIS ecrire la longueur d'un tableau bash dans ce gabarit — la sequence
|
|
|
|
|
# accolade-diese y ouvre un commentaire Jinja et casse le rendu. On compte a la main.
|
|
|
|
|
# (Ce commentaire a lui-meme casse le gabarit une fois, en citant la sequence.)
|
|
|
|
|
PRESENTS=()
|
|
|
|
|
N_PRESENTS=0
|
|
|
|
|
for c in "${CHEMINS[@]}"; do
|
|
|
|
|
if [[ -e "$c" ]]; then
|
|
|
|
|
PRESENTS+=("$c")
|
|
|
|
|
N_PRESENTS=$((N_PRESENTS + 1))
|
|
|
|
|
else
|
|
|
|
|
echo "absent, ignore : $c" >&2
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if (( N_PRESENTS == 0 )); then
|
|
|
|
|
echo "aucun chemin declare n'existe encore sur cet hote — rien a sauvegarder." >&2
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
2026-10-07 12:48:00 -04:00
|
|
|
restic backup --host "{{ inventory_hostname }}" "${ETIQUETTES[@]}" "${PRESENTS[@]}"
|
|
|
|
|
|
|
|
|
|
# 3 bis. L'etiquette passe au dernier depot d'avant rasage : les precedents rentrent dans
|
|
|
|
|
# la retention ordinaire. APRES le depot, jamais avant : s'il echoue, l'ancien reste garde.
|
|
|
|
|
if (( AVANT_RASER )); then
|
|
|
|
|
ANCIENS=$(restic snapshots --host "{{ inventory_hostname }}" --tag "{{ client_backup_etiquette_avant_raser }}" --json \
|
|
|
|
|
| python3 -c 'import json, sys
|
|
|
|
|
import datetime, re
|
|
|
|
|
def t(x):
|
|
|
|
|
s = re.sub(r"(\.\d{6})\d+", r"\1", x["time"])
|
|
|
|
|
return datetime.datetime.fromisoformat(re.sub(r"Z$", "+00:00", s)).timestamp()
|
|
|
|
|
s = sorted(json.load(sys.stdin) or [], key=t)
|
|
|
|
|
print(" ".join(x["id"] for x in s[:-1]))')
|
|
|
|
|
if [[ -n "${ANCIENS}" ]]; then
|
|
|
|
|
read -r -a ANCIENS_IDS <<< "${ANCIENS}"
|
|
|
|
|
restic tag --remove "{{ client_backup_etiquette_avant_raser }}" "${ANCIENS_IDS[@]}"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-07-03 22:49:26 -04:00
|
|
|
|
|
|
|
|
# 4. Rétention.
|
2026-10-07 12:48:00 -04:00
|
|
|
restic forget {{ client_backup_retention }} --keep-tag "{{ client_backup_etiquette_avant_raser }}" --prune
|