137 lines
7.6 KiB
Django/Jinja
137 lines
7.6 KiB
Django/Jinja
#!/bin/bash
|
|
# Géré par Ansible (rôle sauvegardes). Test de restauration du portail
|
|
# (étape 13, puis chaque semaine) :
|
|
# 1. télécharge la dernière sauvegarde quotidienne de Dedibackup ;
|
|
# 2. la déchiffre (clé age du serveur) et en extrait la base et le .env ;
|
|
# 3. restaure la base dans un conteneur MariaDB jetable, isolé ;
|
|
# 4. fait tourner le code du portail contre cette copie (migrations,
|
|
# comptages), avec l'APP_KEY restaurée ;
|
|
# 5. compare les tables à la production et publie une métrique Prometheus.
|
|
# Ne modifie jamais la production. N'affiche aucun secret.
|
|
set -euo pipefail
|
|
|
|
set +x
|
|
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" "$CONTENEUR_OS" >/dev/null 2>&1 || true
|
|
rm -rf "$DOSSIER"
|
|
}
|
|
trap nettoyer EXIT
|
|
|
|
echo "== 1. Dernière sauvegarde quotidienne"
|
|
DERNIERE=$(lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
|
|
-e "set ftp:ssl-allow no; cls -1 quotidienne-*.tar.zst.age; quit" | sort | tail -1)
|
|
[ -n "$DERNIERE" ] || { echo "aucune sauvegarde trouvée"; exit 1; }
|
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
|
|
-e "set ftp:ssl-allow no; get $DERNIERE -o $DOSSIER/archive.age; quit"
|
|
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 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)"
|
|
else
|
|
echo "APP_KEY restaurée DIFFÉRENTE de la production"; exit 1
|
|
fi
|
|
|
|
echo "== 3. Restauration dans un conteneur jetable"
|
|
MDP=$(openssl rand -hex 16)
|
|
# Réseau du portail (Docker tourne sans pont par défaut), aucun port publié :
|
|
# seul l'hôte joint ce conteneur, par son adresse interne.
|
|
docker run -d --name "$CONTENEUR" --network portail_default \
|
|
-e MARIADB_ROOT_PASSWORD="$MDP" {{ sauvegardes_image_mariadb }} >/dev/null
|
|
for _ in $(seq 1 60); do
|
|
docker exec "$CONTENEUR" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -e "SELECT 1"' >/dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
docker exec -i "$CONTENEUR" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot' < "$DOSSIER/extrait/portail.sql"
|
|
IP=$(docker inspect -f '{% raw %}{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}{% endraw %}' "$CONTENEUR")
|
|
echo "base restaurée dans $CONTENEUR"
|
|
|
|
echo "== 4. Le portail tourne contre la copie"
|
|
# APP_CONFIG_CACHE vers un fichier inexistant : Laravel ignore la config mise
|
|
# en cache (qui pointe vers la production) et lit ces variables en priorité.
|
|
artisan_copie() {
|
|
env APP_CONFIG_CACHE="$DOSSIER/config-absente.php" APP_KEY="$APP_KEY_RESTAUREE" \
|
|
DB_HOST="$IP" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD="$MDP" DB_DATABASE={{ sauvegardes_portail_base }} \
|
|
CACHE_STORE=array SESSION_DRIVER=array QUEUE_CONNECTION=sync LOG_CHANNEL=stderr \
|
|
php{{ sauvegardes_php_version }} "$APP/artisan" "$@"
|
|
}
|
|
# Échec de connexion = échec du test (set -e), pas un « 0 en attente » trompeur.
|
|
STATUT=$(artisan_copie migrate:status)
|
|
APPLIQUEES=$(grep -c " Ran" <<<"$STATUT" || true)
|
|
EN_ATTENTE=$(grep -c "Pending" <<<"$STATUT" || true)
|
|
echo "migrations dans la copie : $APPLIQUEES appliquées, $EN_ATTENTE en attente"
|
|
[ "$APPLIQUEES" -gt 0 ] && [ "$EN_ATTENTE" = "0" ] || exit 1
|
|
|
|
echo "== 5. Comparaison avec la production (lignes par table)"
|
|
compter() {
|
|
docker exec "$1" sh -c 'export MYSQL_PWD="$MARIADB_ROOT_PASSWORD"
|
|
for t in $(mariadb -uroot -N -e "SHOW TABLES" {{ sauvegardes_portail_base }}); do
|
|
echo "$t $(mariadb -uroot -N -e "SELECT COUNT(*) FROM \`$t\`" {{ sauvegardes_portail_base }})"
|
|
done'
|
|
}
|
|
compter "$CONTENEUR" | sort > "$DOSSIER/copie.txt"
|
|
compter {{ sauvegardes_portail_conteneur }} | sort > "$DOSSIER/production.txt"
|
|
printf "%-28s %10s %10s\n" table copie production
|
|
join -a1 -a2 -e "absente" -o 0,1.2,2.2 "$DOSSIER/copie.txt" "$DOSSIER/production.txt" | while read -r t c p; do
|
|
printf "%-28s %10s %10s%s\n" "$t" "$c" "$p" "$([ "$c" = "$p" ] || echo ' ≠ (écart depuis la sauvegarde ?)')"
|
|
done
|
|
TABLES_MANQUANTES=$(join -v2 "$DOSSIER/copie.txt" "$DOSSIER/production.txt" | wc -l)
|
|
[ "$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" <<METRIQUE
|
|
# HELP syllabus_derniere_restauration_reussie_timestamp Horodatage Unix du dernier test de restauration du portail réussi.
|
|
# TYPE syllabus_derniere_restauration_reussie_timestamp gauge
|
|
syllabus_derniere_restauration_reussie_timestamp $(date +%s)
|
|
METRIQUE
|
|
mv "$DOSSIER_TEXTFILE/restauration.prom.tmp" "$DOSSIER_TEXTFILE/restauration.prom"
|
|
logger -t syllabus-test-restauration "Restauration de $DERNIERE réussie"
|
|
echo "== RESTAURATION RÉUSSIE ($DERNIERE)"
|