diff --git a/ansible/roles/sauvegardes/defaults/main.yml b/ansible/roles/sauvegardes/defaults/main.yml index 16f03dd..0e5123e 100644 --- a/ansible/roles/sauvegardes/defaults/main.yml +++ b/ansible/roles/sauvegardes/defaults/main.yml @@ -19,6 +19,19 @@ sauvegardes_portail_fichiers: - /var/www/portail/.env - /var/www/portail/storage/app sauvegardes_image_mariadb: "mariadb:11" +# OpenStack (bloc A) : bases de Kolla exportées par mariadb-dump. Le test de +# restauration utilise la même image que le serveur MariaDB de Kolla. +sauvegardes_openstack_conteneur: mariadb +sauvegardes_openstack_hote: 10.10.0.1 +sauvegardes_openstack_mots_de_passe: /etc/kolla/passwords.yml +sauvegardes_image_mariadb_openstack: "mariadb:11.4" +# Tables dont le nombre de lignes doit être identique entre la copie et la +# production (elles ne bougent pas au fil des sessions d'élèves). +sauvegardes_openstack_tables_stables: + - keystone.project + - keystone.user + - glance.images + - nova_api.flavors sauvegardes_php_version: "8.3" # Test de restauration hebdomadaire (CLAUDE.md : restauration testée chaque semaine). sauvegardes_test_restauration_quand: "Mon *-*-* 04:30:00" diff --git a/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 b/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 index c4da74f..0890550 100644 --- a/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 +++ b/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 @@ -36,6 +36,25 @@ if [ "$(docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ sauvegard > "$WORKDIR/portail.sql" EN_PLUS+=(-C "$WORKDIR" portail.sql) fi + +# OpenStack (bloc A, 2026-10-12) : export logique des bases de Kolla (Keystone, +# Nova, Neutron, Glance, Heat, Placement, Grafana), sans redémarrage ni +# verrou (--single-transaction). Les comptes MariaDB (base mysql) ne sont pas +# exportés : Kolla les recrée au déploiement. Le mot de passe est lu dans +# passwords.yml et passé par l'entrée standard, jamais en argument. +if [ "$(docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ sauvegardes_openstack_conteneur }} 2>/dev/null)" = "true" ]; then + kolla_sql() { + printf '[client]\nuser=root\npassword="%s"\nhost=%s\n' \ + "$(awk '/^database_password:/{print $2}' {{ sauvegardes_openstack_mots_de_passe }})" "{{ sauvegardes_openstack_hote }}" \ + | docker exec -i {{ sauvegardes_openstack_conteneur }} "$@" + } + BASES=$(kolla_sql mariadb --defaults-extra-file=/dev/stdin -N -e \ + "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('mysql','information_schema','performance_schema','sys')") + # shellcheck disable=SC2086 # une base par mot + kolla_sql mariadb-dump --defaults-extra-file=/dev/stdin --single-transaction \ + --routines --triggers --events --databases $BASES > "$WORKDIR/openstack.sql" + EN_PLUS+=(-C "$WORKDIR" openstack.sql) +fi for chemin in {{ sauvegardes_portail_fichiers | join(' ') }}; do if [ -e "$chemin" ]; then EN_PLUS+=("$chemin") diff --git a/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 b/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 index 0ccf010..2ae8d76 100644 --- a/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 +++ b/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 @@ -16,9 +16,10 @@ source /root/.dedibackup CLE="{{ sauvegardes_age_cle }}" APP="/var/www/portail" CONTENEUR="syllabus-restauration" +CONTENEUR_OS="syllabus-restauration-openstack" DOSSIER=$(mktemp -d /var/tmp/restauration-XXXXXX) nettoyer() { - docker rm -f "$CONTENEUR" >/dev/null 2>&1 || true + docker rm -f "$CONTENEUR" "$CONTENEUR_OS" >/dev/null 2>&1 || true rm -rf "$DOSSIER" } trap nettoyer EXIT @@ -34,8 +35,9 @@ echo "$DERNIERE ($(du -h "$DOSSIER/archive.age" | cut -f1))" echo "== 2. Déchiffrement et extraction" mkdir "$DOSSIER/extrait" age -d -i "$CLE" "$DOSSIER/archive.age" \ - | tar -I zstd -xf - -C "$DOSSIER/extrait" portail.sql var/www/portail/.env + | tar -I zstd -xf - -C "$DOSSIER/extrait" portail.sql openstack.sql var/www/portail/.env [ -s "$DOSSIER/extrait/portail.sql" ] || { echo "portail.sql absent de l'archive"; exit 1; } +[ -s "$DOSSIER/extrait/openstack.sql" ] || { echo "openstack.sql absent de l'archive"; exit 1; } APP_KEY_RESTAUREE=$(grep -oP '^APP_KEY=\K.*' "$DOSSIER/extrait/var/www/portail/.env") if [ "$APP_KEY_RESTAUREE" = "$(grep -oP '^APP_KEY=\K.*' "$APP/.env")" ]; then echo "APP_KEY restaurée identique à la production (valeur non affichée)" @@ -90,6 +92,40 @@ TABLES_MANQUANTES=$(join -v2 "$DOSSIER/copie.txt" "$DOSSIER/production.txt" | wc [ "$TABLES_MANQUANTES" = "0" ] || { echo "$TABLES_MANQUANTES table(s) de production absente(s) de la copie"; exit 1; } echo "lab_sessions vues par le portail dans la copie : $(artisan_copie tinker --execute='echo App\Models\LabSession::count();' 2>/dev/null | tail -1)" +echo "== 6. Base OpenStack : restauration dans un conteneur jetable" +docker run -d --name "$CONTENEUR_OS" --network portail_default \ + -e MARIADB_ROOT_PASSWORD="$MDP" {{ sauvegardes_image_mariadb_openstack }} >/dev/null +for _ in $(seq 1 60); do + docker exec "$CONTENEUR_OS" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -e "SELECT 1"' >/dev/null 2>&1 && break + sleep 1 +done +docker exec -i "$CONTENEUR_OS" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot' < "$DOSSIER/extrait/openstack.sql" +# Même requête des deux côtés : la copie (root du conteneur jetable) et la +# production (root de Kolla, mot de passe passé par l'entrée standard). +requete_copie() { + docker exec "$CONTENEUR_OS" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot -N -e "$0"' "$1" +} +requete_production() { + printf '[client]\nuser=root\npassword="%s"\nhost=%s\n' \ + "$(awk '/^database_password:/{print $2}' {{ sauvegardes_openstack_mots_de_passe }})" "{{ sauvegardes_openstack_hote }}" \ + | docker exec -i {{ sauvegardes_openstack_conteneur }} mariadb --defaults-extra-file=/dev/stdin -N -e "$1" +} +PAR_BASE="SELECT table_schema, COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys') GROUP BY table_schema ORDER BY 1" +requete_copie "$PAR_BASE" > "$DOSSIER/os-copie.txt" +requete_production "$PAR_BASE" > "$DOSSIER/os-production.txt" +printf "%-14s %8s %8s\n" base tables production +join -a1 -a2 -e absente -o 0,1.2,2.2 "$DOSSIER/os-copie.txt" "$DOSSIER/os-production.txt" | while read -r b c p; do + printf "%-14s %8s %8s\n" "$b" "$c" "$p" +done +cmp -s "$DOSSIER/os-copie.txt" "$DOSSIER/os-production.txt" || { echo "bases ou tables OpenStack différentes"; exit 1; } +for t in {{ sauvegardes_openstack_tables_stables | join(' ') }}; do + c=$(requete_copie "SELECT COUNT(*) FROM $t") + p=$(requete_production "SELECT COUNT(*) FROM $t") + echo "$t : copie $c, production $p" + [ "$c" = "$p" ] || { echo "écart sur $t"; exit 1; } +done +echo "projet paillasse dans la copie : $(requete_copie "SELECT COUNT(*) FROM keystone.project WHERE name='paillasse'")" + DOSSIER_TEXTFILE="{{ sauvegardes_dossier_textfile | default('/var/lib/node_exporter/textfile') }}" cat > "$DOSSIER_TEXTFILE/restauration.prom.tmp" < /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) ```