175 lines
8.9 KiB
Markdown
175 lines
8.9 KiB
Markdown
# Runbook — Sauvegardes vers Dedibackup
|
|
|
|
## Principe
|
|
|
|
Chaque jour à 03:15 (±5 min aléatoires), le timer systemd `sauvegarde-dedibackup.timer`
|
|
déclenche `sauvegarde-dedibackup.service`, qui exécute
|
|
`/usr/local/sbin/sauvegarde-dedibackup.sh` (déployé par le rôle Ansible
|
|
`sauvegardes`, voir `ansible/roles/sauvegardes/`).
|
|
|
|
Le script :
|
|
|
|
1. Archive `/etc`, `/home`, `/root` en `tar.zst` (streaming, sans fichier intermédiaire en clair), plus, depuis l'étape 13, le **portail** : export `mariadb-dump --single-transaction` de sa base (`portail.sql`, à la racine de l'archive), `/var/www/portail/.env` (APP_KEY) et `/var/www/portail/storage/app`. Avant le 2026-10-09, la base du portail n'était **pas** sauvegardée. Depuis le 2026-10-12, les **bases d'OpenStack** aussi : export `mariadb-dump --single-transaction` de toutes les bases de Kolla sauf `mysql` (`openstack.sql`, ≈ 30 Mo avant compression ; mot de passe lu dans `/etc/kolla/passwords.yml`, passé par l'entrée standard).
|
|
2. Chiffre le flux avec **age**, avec la clé publique du serveur (`/root/.age/cle-sauvegardes.txt`).
|
|
3. Envoie l'archive chiffrée en FTP vers Dedibackup (`dedibackup-dc3.online.net`, joignable uniquement depuis ce serveur).
|
|
4. Le dimanche (jour 7), duplique aussi l'archive du jour comme hebdomadaire.
|
|
5. Applique la rotation : garde les 7 quotidiennes et 4 hebdomadaires les plus récentes, supprime le reste sur Dedibackup (qui est limité à 1000 fichiers).
|
|
|
|
Le FTP de Dedibackup n'est pas chiffré en transport, mais le contenu de
|
|
l'archive l'est déjà (age) avant l'envoi : une interception du transfert ne
|
|
révèle rien.
|
|
|
|
## Où est la clé de déchiffrement
|
|
|
|
- Clé privée age : `/root/.age/cle-sauvegardes.txt`, permissions `600`. Elle ne quitte le serveur que pour la copie de secours d'Adam (ci-dessous), jamais vers Git ni ailleurs.
|
|
- Clé publique (sans risque à diffuser) : voir la sortie de `age-keygen` lors du déploiement, ou :
|
|
```
|
|
sudo grep 'public key' /root/.age/cle-sauvegardes.txt
|
|
```
|
|
|
|
**⚠️ Rappel pour Adam (pas fait automatiquement par Claude) : garder une copie de
|
|
`/root/.age/cle-sauvegardes.txt` en dehors du serveur** (gestionnaire de mots de
|
|
passe, clé USB chiffrée, etc.). Si le serveur est perdu, cette clé est la seule
|
|
façon de déchiffrer les sauvegardes existantes. Exemple pour la récupérer (à
|
|
lancer depuis le Mac, le fichier arrive en clair sur le Mac donc à stocker
|
|
immédiatement dans un endroit sûr puis à effacer du téléchargement) :
|
|
```
|
|
# La clé appartient à root : un simple scp échouerait. La redirection écrit
|
|
# le fichier sur le Mac sans jamais l'afficher.
|
|
ssh paillasse 'sudo cat /root/.age/cle-sauvegardes.txt' > ~/Desktop/cle-sauvegardes-syllabus.txt
|
|
```
|
|
|
|
Puis la ranger dans le gestionnaire de mots de passe (pièce jointe d'une
|
|
fiche « Syllabus — clé des sauvegardes ») ou sur une clé USB chiffrée, et
|
|
**supprimer la copie du Bureau**.
|
|
|
|
### Prouver que la copie fonctionne (sans montrer la clé)
|
|
|
|
Sur le serveur, chiffrer un message de test avec la clé **publique** ; sur le
|
|
Mac, le déchiffrer avec la **copie** (installer `age` : `brew install age`) :
|
|
|
|
```
|
|
ssh paillasse 'sudo bash -c "PUB=\$(grep -oP \"(?<=public key: )age1\\S+\" /root/.age/cle-sauvegardes.txt); echo \"copie OK \$(date +%F)\" | age -r \$PUB"' > ~/Desktop/test-cle.age
|
|
age -d -i <chemin de la copie> ~/Desktop/test-cle.age # doit afficher « copie OK … »
|
|
```
|
|
|
|
## Identifiants Dedibackup
|
|
|
|
Dans `/root/.dedibackup` (permissions `600`, jamais dans Git) :
|
|
`DEDIBACKUP_HOST`, `DEDIBACKUP_USER`, `DEDIBACKUP_PASS` — **valeurs entre
|
|
guillemets simples** si elles contiennent des caractères spéciaux (`#`, `$`,
|
|
espace…), sinon `bash` les tronque silencieusement en les lisant.
|
|
|
|
## Lancer une sauvegarde manuellement
|
|
|
|
```
|
|
ssh paillasse
|
|
sudo systemctl start sauvegarde-dedibackup.service
|
|
sudo journalctl -u sauvegarde-dedibackup.service --no-pager | tail -20
|
|
```
|
|
|
|
## Lister les sauvegardes présentes sur Dedibackup
|
|
|
|
```
|
|
sudo bash -c '
|
|
source /root/.dedibackup
|
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
|
|
-e "set ftp:ssl-allow no; cls -1 *.tar.zst.age; quit"
|
|
'
|
|
```
|
|
|
|
## Test de restauration du portail et d'OpenStack (automatique, chaque lundi 04:30)
|
|
|
|
`/usr/local/sbin/syllabus-test-restauration` (rôle `sauvegardes`, minuteur
|
|
`syllabus-test-restauration.timer`) télécharge la dernière quotidienne, la
|
|
déchiffre, restaure la base dans un conteneur MariaDB jetable (réseau
|
|
`portail_default`, aucun port publié), fait tourner le code du portail contre
|
|
cette copie (`migrate:status`, comptage), compare chaque table à la
|
|
production, puis publie `syllabus_derniere_restauration_reussie_timestamp`.
|
|
Alerte `RestaurationNonTestee` au-delà de 8 jours. Rien n'est écrit en
|
|
production ; le conteneur et les fichiers temporaires sont supprimés à la fin.
|
|
|
|
```
|
|
ssh paillasse "sudo /usr/local/sbin/syllabus-test-restauration" # à la demande
|
|
ssh paillasse "journalctl -u syllabus-test-restauration --no-pager | tail -40"
|
|
```
|
|
|
|
Des écarts de comptage sont normaux si la production a changé depuis la
|
|
sauvegarde ; une table absente de la copie fait échouer le test.
|
|
|
|
Étape 6 du test (depuis le 2026-10-12) : `openstack.sql` est restauré dans un
|
|
second conteneur jetable (`mariadb:11.4`, même version que Kolla). Le test
|
|
échoue si une base ou un nombre de tables diffère de la production, ou si les
|
|
tables stables (`keystone.project`, `keystone.user`, `glance.images`,
|
|
`nova_api.flavors`) n'ont pas le même nombre de lignes.
|
|
|
|
## Restaurer réellement le portail (sinistre)
|
|
|
|
1. Extraire l'archive dans un dossier temporaire (voir plus bas), puis `portail.sql` et `var/www/portail/.env`.
|
|
2. Redéployer le portail (`ansible-playbook portail.yml`) sur le serveur reconstruit ; remettre le `.env` restauré **avant** (APP_KEY identique, sinon sessions et données chiffrées perdues).
|
|
3. Importer la base : `docker exec -i portail_mariadb sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot' < portail.sql` (écrase la base `portail`).
|
|
4. `php artisan migrate --force`, puis vérifier une connexion prof.
|
|
|
|
## Restaurer réellement OpenStack (sinistre)
|
|
|
|
Procédure écrite, **jamais jouée en vrai** (il faudrait un second serveur) ;
|
|
seule la restauration des bases dans un conteneur est testée chaque semaine.
|
|
|
|
1. Serveur reconstruit : playbooks `durcissement`, `reseau-openstack`, puis
|
|
remettre `/etc/kolla/` depuis l'archive (**`passwords.yml` identique**,
|
|
sinon les mots de passe en base ne correspondent plus) avant `kolla.yml`.
|
|
2. `kolla-ansible deploy` crée des bases vides et les comptes MariaDB.
|
|
3. Arrêter les services qui écrivent (`docker stop` des conteneurs `nova_*`,
|
|
`neutron_*`, `heat_*`, `glance_api`, `keystone*`, `placement_api`), puis
|
|
importer (le mot de passe ne passe ni en argument ni à l'écran) :
|
|
```
|
|
sudo docker cp openstack.sql mariadb:/tmp/openstack.sql
|
|
printf '[client]\nuser=root\npassword="%s"\nhost=10.10.0.1\n' \
|
|
"$(sudo awk '/^database_password:/{print $2}' /etc/kolla/passwords.yml)" \
|
|
| sudo docker exec -i mariadb sh -c 'umask 077; cat > /tmp/r.cnf;
|
|
mariadb --defaults-extra-file=/tmp/r.cnf < /tmp/openstack.sql;
|
|
rm -f /tmp/r.cnf /tmp/openstack.sql'
|
|
```
|
|
Redémarrer les conteneurs arrêtés.
|
|
4. Les **fichiers** des images Glance ne sont pas sauvegardés (seulement leur
|
|
fiche) : reconstruire `paillasse-debian` (`docs/runbooks/images.md`) et
|
|
supprimer l'ancienne fiche.
|
|
5. Les VM d'élèves n'existent plus sur le nouvel hôte : supprimer toutes les
|
|
stacks restantes (`openstack stack list`, puis `stack delete`) ; le stock
|
|
chaud se reconstitue seul.
|
|
|
|
## Restaurer un fichier (procédure testée le 2026-10-07)
|
|
|
|
```
|
|
ssh paillasse
|
|
sudo bash -c '
|
|
set -e
|
|
source /root/.dedibackup
|
|
RESTORE_DIR=/tmp/restauration-$(date +%Y%m%d-%H%M)
|
|
mkdir -p "$RESTORE_DIR"
|
|
|
|
# 1. Télécharger l'archive voulue depuis Dedibackup
|
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
|
|
-e "set ftp:ssl-allow no; get quotidienne-AAAAMMJJ.tar.zst.age -o $RESTORE_DIR/archive.tar.zst.age; quit"
|
|
|
|
# 2. Déchiffrer puis extraire (tout le contenu, ou un chemin précis en argument de tar)
|
|
age -d -i /root/.age/cle-sauvegardes.txt "$RESTORE_DIR/archive.tar.zst.age" \
|
|
| tar -I zstd -xf - -C "$RESTORE_DIR" etc/un/fichier/precis
|
|
|
|
ls -la "$RESTORE_DIR"
|
|
'
|
|
```
|
|
|
|
Pour tout restaurer (sans préciser de chemin après `-xf -`), **ne jamais le
|
|
faire directement vers `/etc`, `/home` ou `/root`** sans réfléchir : ça
|
|
écraserait l'état actuel. Toujours restaurer vers un répertoire temporaire
|
|
(`/tmp/...`) puis copier à la main ce qui est nécessaire.
|
|
|
|
Les chemins dans l'archive n'ont **pas** de `/` ni de `./` au début (ex.
|
|
`etc/hostname`, pas `/etc/hostname` ni `./etc/hostname`) — `tar` les a retirés
|
|
à la création.
|
|
|
|
## Vérifié le 2026-10-07
|
|
|
|
- Sauvegarde envoyée : `quotidienne-20261007.tar.zst.age`, confirmée présente sur Dedibackup via `cls`.
|
|
- Restauration testée : `etc/hostname` extrait dans `/tmp/test-restauration-20261007/`, contenu conforme (`sd-124443`).
|