alpinux-owni/docs/sauvegardes.md
Cédrix 89cc919906 Absorber alpinux-infra : un seul dépôt pour la machine
Il y avait deux dépôts d'infrastructure et aucune frontière entre eux. La
seule distinction défendable — « documentation » d'un côté, « fichiers de
configuration » de l'autre — ne tenait plus : infra avait fini par contenir
plus de documentation qu'owni, pendant qu'owni décrivait du concret (IP,
bases, comptes).

Tout vient donc ici : conf/ (vhosts de référence), dns/ (export de zone),
services/ (units systemd), scripts/ (sauvegarde) et docs/ (courrier sortant,
certificats, sauvegardes, et le déploiement par service).

Dans ce sens plutôt que l'inverse parce qu'« owni » nomme la machine, là où
« infra » ne dit pas de quoi il s'agit — et c'est le nom que Cédric emploie
spontanément, y compris pour le dossier de secrets.

Les renvois croisés entre les deux dépôts deviennent des liens internes : un
lien vers un dépôt qu'on s'apprête à archiver aurait pourri en silence.

Reste une incohérence, signalée plutôt que corrigée à la hâte :
docs/admin.md, static.md, wiki.md, dynamic.md et proxy-calendar.md décrivent
le déploiement de sites, pas la machine. Ils ont la même place ici que celle
que la messagerie n'avait pas — ils devraient rejoindre leurs dépôts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFbwnJurBwTs7x7t93ecku
2026-09-27 12:55:45 +02:00

233 lines
8 KiB
Markdown

# Sauvegardes d'owni — faire, et surtout défaire
Une sauvegarde qu'on n'a jamais restaurée est une hypothèse. Ce document dit
autant comment récupérer que comment sauvegarder, et la première partie utile
est la seconde.
## Ce qui est sauvegardé
`scripts/sauvegarder-owni.sh`, lancé **depuis le poste**, tire chaque nuit les
sept bases de la machine :
| Base | Service |
|---|---|
| `c1_messagerie_db` | messagerie.alpinux.org |
| `c1dolibarr` | fichier des adhérents |
| `c1gitea` | tous les dépôts |
| `c1alpid` | le SSO Keycloak |
| `c1_installparty` | installparty.alpinux.org |
| `c1_evenements` | événements |
| `dbispconfig` | la configuration d'ISPConfig elle-même |
Environ **1,5 Mo compressés** pour l'ensemble. Rangées par jour dans
`~/Sauvegardes/owni/AAAA-MM-JJ/`, gardées 30 jours.
Tiré depuis le poste et non poussé depuis owni : une machine compromise ne
doit pas pouvoir atteindre les sauvegardes de ce qu'elle héberge.
### Ce qui n'est pas sauvegardé
- **Les fichiers des sites.** Le code est dans Gitea, et Gitea est sur cette
même machine — ce qui ne protège de rien. Les dépôts sont clonés sur le
poste, ce qui protège un peu mieux.
- **Les boîtes mail** (`/var/vmail`, 1,3 Go) : les rebonds et le courrier
postmaster, dont l'essentiel est déjà repris en base.
- **`/etc`**, donc la configuration système hors ISPConfig.
Ces trois points sont des choix, pas des oublis. À reconsidérer le jour où
l'un d'eux manquera.
## Lancer
```sh
cd ~/Projects/org.alpinux.owni/infra
./scripts/sauvegarder-owni.sh --essai # montre, n'écrit rien
./scripts/sauvegarder-owni.sh
```
En cron, sur le poste — `crontab -e` :
```cron
30 3 * * * cd $HOME/Projects/org.alpinux.owni/infra && ./scripts/sauvegarder-owni.sh >> $HOME/Sauvegardes/owni/journal.txt 2>&1
```
Le poste doit être allumé à 3 h 30. S'il ne l'est pas, la nuit est sautée :
regarder `journal.txt` de temps en temps, ou lancer le script à la main après
une longue absence.
> **Ces fichiers contiennent des données personnelles** — 258 adresses
> d'abonnés et le fichier des adhérents. Le dossier est en `700`, les fichiers
> en `600`. Sur un poste portable, envisager un chiffrement du disque ou des
> archives.
---
# Restaurer
## Avant tout : regarder ce qu'on va restaurer
Ne jamais restaurer une sauvegarde sans l'avoir ouverte. Une sauvegarde
tronquée écrase des données saines.
```sh
S=~/Sauvegardes/owni/2026-09-27/c1_messagerie_db_2026-09-27.sql.gz
zcat "$S" | tail -2 # doit finir par « Dump completed on … »
zcat "$S" | grep -c "INSERT INTO"
zcat "$S" | grep -oE "^CREATE TABLE \`[a-z_]+\`" | wc -l
```
Le script refuse déjà d'enregistrer un dump sans sa ligne finale, mais
l'habitude de vérifier vaut mieux que la confiance.
## Essayer d'abord à côté, jamais directement
**La bonne méthode** : restaurer dans une base temporaire, regarder, puis
basculer. On ne remplace jamais une base en production par un fichier qu'on
n'a pas inspecté.
```sh
scp ~/Sauvegardes/owni/2026-09-27/c1_messagerie_db_2026-09-27.sql.gz \
abonnelc@owni.alpinux.org:/tmp/
ssh abonnelc@owni.alpinux.org
sudo mysql -e "CREATE DATABASE essai_restauration CHARACTER SET utf8mb4;"
zcat /tmp/c1_messagerie_db_2026-09-27.sql.gz | sudo mysql essai_restauration
# Est-ce bien ce qu'on croit ?
sudo mysql -t essai_restauration -e "
SELECT COUNT(*) AS abonnes FROM abonnes;
SELECT COUNT(*) AS campagnes FROM campagnes;
SELECT MAX(creee_le) AS derniere FROM campagnes;"
```
Comparer avec la production **avant** de décider quoi que ce soit :
```sh
sudo mysql -t c1_messagerie_db -e "
SELECT COUNT(*) AS abonnes FROM abonnes;
SELECT COUNT(*) AS campagnes FROM campagnes;"
```
Une fois satisfait, effacer l'essai : `sudo mysql -e "DROP DATABASE essai_restauration;"`
## Restaurer pour de bon
### Le piège de la messagerie : arrêter le worker d'abord
`bin/worker-envoi.php` tourne **chaque minute**. Restaurer la base pendant
qu'il tourne, c'est lui rendre une file d'envois déjà partis — et
**réexpédier une campagne à 250 personnes**.
```sh
# 1. Couper le cron de la messagerie
sudo mv /etc/cron.d/messagerie /root/messagerie.cron.suspendu
# Vérifier qu'aucun worker n'est en cours
pgrep -af worker-envoi.php || echo "aucun worker en cours"
# 2. Mettre la base de côté plutôt que l'écraser
sudo mysqldump --single-transaction c1_messagerie_db \
| gzip -c > /root/avant-restauration-$(date +%Y%m%d-%H%M).sql.gz
# 3. Restaurer
zcat /tmp/c1_messagerie_db_2026-09-27.sql.gz | sudo mysql c1_messagerie_db
# 4. Vérifier avant de rouvrir les vannes
sudo mysql -t c1_messagerie_db -e "
SELECT statut, COUNT(*) FROM envois GROUP BY statut;
SELECT COUNT(*) FROM abonnes WHERE statut='actif';"
# 5. Remettre le cron — et seulement alors
sudo mv /root/messagerie.cron.suspendu /etc/cron.d/messagerie
```
Entre 1 et 5, le site reste consultable : c'est l'envoi qui est suspendu, pas
l'application.
> Attention au nom du fichier dans `/etc/cron.d/` : **un point dans le nom et
> il est ignoré en silence**. `messagerie` fonctionne, `messagerie.cron` non.
> C'est pourquoi on le déplace vers `/root/` plutôt que de le renommer sur
> place.
### Les autres services : arrêter avant, redémarrer après
```sh
# Gitea
sudo systemctl stop gitea
zcat /tmp/c1gitea_*.sql.gz | sudo mysql c1gitea
sudo systemctl start gitea
# Keycloak (le SSO) — plus personne ne peut se connecter nulle part pendant
sudo systemctl stop keycloak
zcat /tmp/c1alpid_*.sql.gz | sudo mysql c1alpid
sudo systemctl start keycloak
```
Restaurer `c1gitea` sans arrêter Gitea laisse le service avec un état en
mémoire qui ne correspond plus à la base : au mieux des erreurs, au pire des
écritures par-dessus la restauration.
### Ne restaurer qu'une table
Le cas le plus fréquent — on a vidé une table par erreur, le reste est bon.
```sh
# Extraire la seule table qui compte
zcat c1_messagerie_db_2026-09-27.sql.gz \
| sed -n '/^-- Table structure for table `abonnes`/,/^-- Table structure for table `abonnements`/p' \
> /tmp/abonnes.sql
grep -c "INSERT INTO" /tmp/abonnes.sql # regarder avant d'appliquer
sudo mysql c1_messagerie_db < /tmp/abonnes.sql
```
Le `sed` va d'une table à la suivante dans l'ordre du dump : vérifier que la
table nommée en second est bien celle qui suit, sinon on emporte trop ou trop
peu.
### `dbispconfig` : en dernier recours seulement
La configuration d'ISPConfig décrit les sites, les bases, les boîtes mail et
les certificats. La restaurer remet **toute la machine** dans un état
antérieur, y compris des sites créés depuis. À ne faire que pour reconstruire
un serveur perdu, jamais pour corriger un détail.
## Après une restauration
- **Vérifier ce qui compte**, pas seulement que la commande a rendu la main :
compter les lignes des tables principales, ouvrir une page, envoyer un
message de test.
- **La messagerie** : vérifier que la file d'envois ne contient pas de vieux
messages en attente avant de rouvrir le cron.
- **Noter ce qui s'est passé** — dans le ticket, ou ici. Une restauration est
rare ; ce qu'on y apprend se perd si personne ne l'écrit.
## Ce qui a été vérifié, et ce qui ne l'est pas
**Le 27/09/2026, la sauvegarde de `c1_messagerie_db` a été restaurée pour de
vrai** — dans une base séparée, sur un MariaDB 11 de développement. Elle est
donc lisible et complète :
| | |
|---|---|
| tables | 21 |
| abonnés | 258, dont 238 actifs |
| campagnes | 48 |
| envois | 255 |
| rebonds | 35 |
Ce sont les chiffres de la production. Le dump n'est pas une intention : il
remonte.
**Ce qui reste non vérifié :**
- la restauration **sur owni même**, avec l'arrêt du cron et la bascule
décrits plus haut. C'est la partie où l'on peut casser quelque chose, et
c'est donc celle qui mériterait un essai un jour de calme ;
- les six autres bases, dont la sauvegarde a réussi mais qu'on n'a pas
rechargées ;
- le comportement du cron nocturne dans la durée — poste éteint, réseau
coupé, disque plein.
Voir le ticket
[#1](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-owni/issues/1).