From a1fb6f550426d2c2197b43f64e8be786ec8ed746 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 11:40:35 +0100 Subject: [PATCH] feat : Utilisation web disponible et simplification du deployement des scripts (WIP) --- BackupVaultWarden/backup-vaultwarden.sh | 0 CheckStorage/check-storage.sh | 0 RebuildBdd/Checkup/check-postgresql.sh | 11 +- RebuildBdd/Checkup/check-target-readiness.sh | 42 +- RebuildBdd/Config/.env.exemple | 38 ++ RebuildBdd/Config/Targets/prod.env.example | 30 + RebuildBdd/Config/Targets/test.env.example | 38 ++ RebuildBdd/README.md | 566 +++++++++++++++++++ RebuildBdd/bootstrap-target-host.sh | 422 ++++++++++---- RebuildBdd/create-target-config.sh | 160 ++++++ RebuildBdd/rebuild-bdd-core.sh | 10 +- RebuildBdd/run-rebuild-bdd.sh | 227 ++++---- RecetteScripts/backup-bdd-recette.sh | 0 RecetteScripts/check-statut-recette.sh | 0 14 files changed, 1287 insertions(+), 257 deletions(-) mode change 100644 => 100755 BackupVaultWarden/backup-vaultwarden.sh mode change 100644 => 100755 CheckStorage/check-storage.sh mode change 100644 => 100755 RebuildBdd/Checkup/check-target-readiness.sh create mode 100644 RebuildBdd/Config/.env.exemple create mode 100644 RebuildBdd/Config/Targets/prod.env.example create mode 100644 RebuildBdd/Config/Targets/test.env.example create mode 100644 RebuildBdd/README.md mode change 100644 => 100755 RebuildBdd/bootstrap-target-host.sh create mode 100644 RebuildBdd/create-target-config.sh mode change 100644 => 100755 RecetteScripts/backup-bdd-recette.sh mode change 100644 => 100755 RecetteScripts/check-statut-recette.sh diff --git a/BackupVaultWarden/backup-vaultwarden.sh b/BackupVaultWarden/backup-vaultwarden.sh old mode 100644 new mode 100755 diff --git a/CheckStorage/check-storage.sh b/CheckStorage/check-storage.sh old mode 100644 new mode 100755 diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 13ffc2b..5c8562f 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -74,23 +74,16 @@ if ! "$SUDO_BIN" -n true >/dev/null 2>&1; then fail "sudo non interactif indisponible pour l'utilisateur courant" fi -if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then - fail "sudo -n -u postgres indisponible" -fi - if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then fail "PGPORT invalide : $PGPORT" fi -POSTGRES_INSTALLED="no" - if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; then [[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no" log "PostgreSQL absent : installation en cours..." "$SUDO_BIN" -n apt update >/dev/null 2>&1 || fail "échec de apt update" "$SUDO_BIN" -n apt install -y $POSTGRES_PACKAGE_LIST >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL" - POSTGRES_INSTALLED="yes" log "Installation PostgreSQL terminée." else log "PostgreSQL déjà installé." @@ -140,6 +133,10 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then fi fi +if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then + fail "sudo -n -u postgres indisponible" +fi + if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}" fi diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh old mode 100644 new mode 100755 index 60d9f9a..0bc61e8 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -6,15 +6,6 @@ set -euo pipefail # # Prépare la machine cible pour permettre l'exécution non interactive du # script de rebuild depuis une interface web. -# -# Ce script : -# - charge et valide le .env ; -# - crée/corrige les dossiers et permissions locales ; -# - génère la clé SSH si absente ; -# - alimente known_hosts pour le serveur de backup ; -# - teste la connexion SSH non interactive vers le serveur de backup ; -# - vérifie/installe la config sudoers si autorisée ; -# - lance ensuite check-postgresql.sh. ############################################################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -128,6 +119,8 @@ validate_env_values() { [[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide" [[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide" [[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide" + BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}" + [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide" } prepare_log_file() { @@ -182,32 +175,26 @@ prepare_ssh_key() { mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir" chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir" - if [[ ! -f "$SSH_KEY" ]]; then - log "Clé SSH absente : génération de ${SSH_KEY}" - ssh-keygen -t ed25519 -N "" -f "$SSH_KEY" >/dev/null 2>&1 || \ - fail "impossible de générer la clé SSH" - fi - + [[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY" chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée" - [[ -f "${SSH_KEY}.pub" ]] || fail "clé publique absente : ${SSH_KEY}.pub" - chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" + [[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub" + [[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" log "Clé SSH prête." } prepare_known_hosts() { - local ssh_dir known_hosts ssh_port + local ssh_dir known_hosts ssh_dir="$(dirname "$SSH_KEY")" known_hosts="${ssh_dir}/known_hosts" - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" touch "$known_hosts" || fail "impossible de créer known_hosts" chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts" if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then - log "Ajout de ${BACKUP_REMOTE_HOST}:${ssh_port} à known_hosts" - ssh-keyscan -p "$ssh_port" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ + log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts" + ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}" else log "Host déjà présent dans known_hosts." @@ -215,13 +202,12 @@ prepare_known_hosts() { } test_backup_ssh() { - local ssh_port ssh_timeout - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" + local ssh_timeout ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" ssh \ -i "$SSH_KEY" \ - -p "$ssh_port" \ + -p "$BACKUP_REMOTE_SSH_PORT" \ -o IdentitiesOnly=yes \ -o BatchMode=yes \ -o ConnectTimeout="$ssh_timeout" \ @@ -277,9 +263,6 @@ check_sudo_non_interactive() { sudo -n true >/dev/null 2>&1 || \ fail "sudo non interactif indisponible pour ${USER}" - sudo -n -u postgres true >/dev/null 2>&1 || \ - fail "sudo -n -u postgres indisponible pour ${USER}" - log "sudo non interactif validé." } @@ -295,13 +278,10 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" + sudo -n -u postgres true >/dev/null 2>&1 || fail "sudo -n -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } -############################################################################### -# Main -############################################################################### - [[ -f "$ENV_FILE" ]] || { echo '{"status":"error","message":"fichier .env introuvable"}' exit 1 diff --git a/RebuildBdd/Config/.env.exemple b/RebuildBdd/Config/.env.exemple new file mode 100644 index 0000000..ff66962 --- /dev/null +++ b/RebuildBdd/Config/.env.exemple @@ -0,0 +1,38 @@ +############################################################################### +# config/global.env.example +############################################################################### + +# Defaults d'exécution +ALLOW_OVERWRITE=no +RESTORE_ROLES=yes + +# Dépôt scripts +GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git +GLOBAL_REPO_BRANCH=main + +# Backup central +GLOBAL_BACKUP_REMOTE_USER=backup +GLOBAL_BACKUP_REMOTE_HOST=192.168.1.60 +GLOBAL_BACKUP_REMOTE_PORT=22 +GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups + +# Clé SSH de lecture backup copiée sur les cibles +GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly +GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub +GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes + +# Defaults PostgreSQL +GLOBAL_PGHOST=127.0.0.1 +GLOBAL_PGPORT=5432 + +# Defaults scripts +GLOBAL_REMOTE_ROLES_DIR_NAME=user +GLOBAL_EXCLUDED_RESTORE_ROLES="postgres" + +# Defaults bootstrap / cible +GLOBAL_ENABLE_BOOTSTRAP=yes +GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes +GLOBAL_AUTO_INSTALL_POSTGRES=yes +GLOBAL_AUTO_CREATE_PGUSER=yes +GLOBAL_PGUSER_SUPERUSER=no +GLOBAL_AUTO_CONFIGURE_SUDOERS=no \ No newline at end of file diff --git a/RebuildBdd/Config/Targets/prod.env.example b/RebuildBdd/Config/Targets/prod.env.example new file mode 100644 index 0000000..7070ae3 --- /dev/null +++ b/RebuildBdd/Config/Targets/prod.env.example @@ -0,0 +1,30 @@ + +############################################################################### +# CIBLE : prod +############################################################################### + +# TARGET_HOST_prod=10.0.0.20 +# TARGET_PORT_prod=22 +# TARGET_BOOTSTRAP_USER_prod=backup_liot +# TARGET_BOOTSTRAP_SSH_KEY_prod=/home/matteo/.ssh/id_ed25519_target_prod +# TARGET_RUNTIME_USER_prod=backup_liot +# TARGET_ENABLE_BOOTSTRAP_prod=yes +# TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_prod=yes +# TARGET_REPO_DIR_prod=/home/backup_liot/RebuildBdd +# TARGET_ENV_FILE_prod=/home/backup_liot/RebuildBdd/.env +# TARGET_ENV_NAME_prod=PROD +# TARGET_PGHOST_prod=127.0.0.1 +# TARGET_PGPORT_prod=5432 +# TARGET_PGUSER_prod=backup_liot +# TARGET_PGPASSWORD_prod=change_me_prod_password +# TARGET_DBS_prod="sirh inventory ferme" +# TARGET_BACKUP_SUBDIR_prod=bdd-prod +# TARGET_BACKUP_LOG_DIR_prod=/home/backup_liot/logs/rebuild_bdd +# TARGET_LOCAL_RESTORE_BASE_DIR_prod=/home/backup_liot/RebuildBdd/restore_tmp +# TARGET_SSH_KEY_prod=/home/backup_liot/.ssh/id_ed25519_backup_readonly +# TARGET_REMOTE_ROLES_DIR_NAME_prod=user +# TARGET_EXCLUDED_RESTORE_ROLES_prod="postgres" +# TARGET_AUTO_INSTALL_POSTGRES_prod=yes +# TARGET_AUTO_CREATE_PGUSER_prod=yes +# TARGET_PGUSER_SUPERUSER_prod=no +# TARGET_AUTO_CONFIGURE_SUDOERS_prod=no \ No newline at end of file diff --git a/RebuildBdd/Config/Targets/test.env.example b/RebuildBdd/Config/Targets/test.env.example new file mode 100644 index 0000000..ff66962 --- /dev/null +++ b/RebuildBdd/Config/Targets/test.env.example @@ -0,0 +1,38 @@ +############################################################################### +# config/global.env.example +############################################################################### + +# Defaults d'exécution +ALLOW_OVERWRITE=no +RESTORE_ROLES=yes + +# Dépôt scripts +GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git +GLOBAL_REPO_BRANCH=main + +# Backup central +GLOBAL_BACKUP_REMOTE_USER=backup +GLOBAL_BACKUP_REMOTE_HOST=192.168.1.60 +GLOBAL_BACKUP_REMOTE_PORT=22 +GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups + +# Clé SSH de lecture backup copiée sur les cibles +GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly +GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub +GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes + +# Defaults PostgreSQL +GLOBAL_PGHOST=127.0.0.1 +GLOBAL_PGPORT=5432 + +# Defaults scripts +GLOBAL_REMOTE_ROLES_DIR_NAME=user +GLOBAL_EXCLUDED_RESTORE_ROLES="postgres" + +# Defaults bootstrap / cible +GLOBAL_ENABLE_BOOTSTRAP=yes +GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes +GLOBAL_AUTO_INSTALL_POSTGRES=yes +GLOBAL_AUTO_CREATE_PGUSER=yes +GLOBAL_PGUSER_SUPERUSER=no +GLOBAL_AUTO_CONFIGURE_SUDOERS=no \ No newline at end of file diff --git a/RebuildBdd/README.md b/RebuildBdd/README.md new file mode 100644 index 0000000..ed2ff86 --- /dev/null +++ b/RebuildBdd/README.md @@ -0,0 +1,566 @@ +# RebuildBdd + +Orchestration de reconstruction de bases PostgreSQL à partir de dumps distants, avec préparation automatique des machines cibles, exécution non interactive et intégration web. + +--- + +## Objectif + +Ce projet permet de : + +- préparer automatiquement une machine cible neuve ou partiellement configurée ; +- déployer et mettre à jour les scripts sur la cible ; +- préparer PostgreSQL localement sur la cible ; +- récupérer le dernier dump disponible depuis un serveur de backup ; +- restaurer une base PostgreSQL de manière non interactive ; +- exposer un flux exploitable depuis une interface web via des retours JSON. + +--- + +## Fonctionnement global + +Le flux standard est le suivant : + +1. **Création ou mise à jour de la configuration d’une cible** +2. **Bootstrap initial de la cible** +3. **Précheck de préparation** +4. **Rebuild de la base** + +En pratique : + +- `create-target-config.sh` crée un fichier de configuration cible ; +- `bootstrap-target-host.sh` prépare la machine cible ; +- `Checkup/check-target-readiness.sh` valide l’environnement ; +- `rebuild-bdd-core.sh` exécute la restauration ; +- `run-rebuild-bdd.sh` orchestre l’ensemble. + +--- + +## Architecture + +### Configuration + +Le projet utilise deux niveaux de configuration : + +#### 1. Configuration globale +Fichier : + +```bash +config/global.env +```` + +Contient les paramètres stables, par exemple : + +* dépôt Git des scripts ; +* serveur de backup ; +* clé SSH de lecture backup ; +* valeurs par défaut PostgreSQL ; +* options globales de bootstrap. + +#### 2. Configuration par cible + +Fichiers : + +```bash +config/targets/.env +``` + +Chaque fichier cible contient : + +* accès SSH bootstrap ; +* répertoires locaux de la cible ; +* paramètres PostgreSQL ; +* sous-répertoire backup associé ; +* options spécifiques à la cible. + +--- + +## Arborescence recommandée + +```bash +RebuildBdd/ +├── bootstrap-target-host.sh +├── create-target-config.sh +├── run-rebuild-bdd.sh +├── rebuild-bdd-core.sh +├── config/ +│ ├── global.env +│ └── targets/ +│ ├── test.env +│ └── prod.env +└── Checkup/ + ├── check-postgresql.sh + └── check-target-readiness.sh +``` + +--- + +## Scripts + +### `create-target-config.sh` + +Crée ou met à jour un fichier cible dans : + +```bash +config/targets/.env +``` + +Usage : + +```bash +./create-target-config.sh \ + --target test \ + --host 192.168.1.50 \ + --port 22 \ + --bootstrap-user backup_liot \ + --bootstrap-key /home/user/.ssh/id_ed25519_target_test \ + --runtime-user backup_liot \ + --repo-dir /home/backup_liot/RebuildBdd \ + --env-name RECETTE \ + --pguser backup_liot \ + --pgpassword secret \ + --dbs "sirh inventory ferme" \ + --backup-subdir bdd-recette +``` + +--- + +### `bootstrap-target-host.sh` + +Prépare une machine cible neuve ou quasi neuve : + +* connexion SSH bootstrap ; +* installation des paquets minimum ; +* création des dossiers ; +* génération du `.env` cible ; +* copie de la clé SSH backup ; +* préparation de `known_hosts` ; +* installation éventuelle d’un `sudoers.d` minimal ; +* synchronisation du dépôt ; +* exécution de `check-postgresql.sh`. + +Usage : + +```bash +./bootstrap-target-host.sh --target test +``` + +Mode JSON : + +```bash +./bootstrap-target-host.sh --target test --json-only +``` + +--- + +### `Checkup/check-postgresql.sh` + +Prépare PostgreSQL localement sur la cible : + +* installation si absent ; +* démarrage du service ; +* test de disponibilité ; +* création du rôle PostgreSQL cible si nécessaire. + +Ce script est prévu pour fonctionner en non interactif avec `sudo -n`. + +--- + +### `Checkup/check-target-readiness.sh` + +Valide la préparation complète de la cible : + +* lecture du `.env` cible ; +* vérification des chemins ; +* permissions locales ; +* permissions SSH ; +* `known_hosts` ; +* accès SSH au serveur de backup ; +* exécution de `check-postgresql.sh`. + +Mode JSON disponible pour usage web. + +--- + +### `rebuild-bdd-core.sh` + +Script métier de reconstruction : + +* validation des paramètres ; +* connexion au serveur de backup ; +* récupération du dernier dump ; +* récupération éventuelle du fichier des rôles ; +* suppression/recréation de la base si autorisé ; +* restauration des rôles ; +* restauration du dump PostgreSQL ; +* retour JSON final. + +--- + +### `run-rebuild-bdd.sh` + +Script orchestrateur principal. + +Il peut : + +* lancer le bootstrap si activé pour la cible ; +* synchroniser le dépôt distant ; +* lancer le précheck ; +* exécuter le rebuild. + +Usage : + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --request-id web_001 \ + --non-interactive +``` + +--- + +## Prérequis + +### Machine de lancement + +Doit disposer de : + +* `bash` +* `ssh` +* `scp` +* `git` +* `python3` + +### Machine cible + +Le bootstrap suppose : + +* accès SSH fonctionnel ; +* utilisateur bootstrap existant ; +* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial. + +### Serveur de backup + +Doit : + +* être joignable en SSH depuis la cible ; +* accepter la clé de lecture backup ; +* contenir les dumps dans l’arborescence attendue. + +--- + +## Structure des backups attendue + +Exemple : + +```bash +/home/malio-b/backups/ +├── bdd-recette/ +│ ├── sirh/ +│ │ ├── sirh_2026-03-16_19-00-01.dump +│ ├── inventory/ +│ ├── ferme/ +│ └── user/ +│ ├── user_2026-03-16_19-00-01.sql +``` + +Le script recherche : + +* le dernier dump dans : + +```bash +//_*.dump +``` + +* le dernier fichier rôles dans : + +```bash +//user_*.sql +``` + +--- + +## Configuration + +### 1. Créer la configuration globale + +Copier : + +```bash +config/global.env.example +``` + +vers : + +```bash +config/global.env +``` + +Renseigner ensuite : + +* dépôt Git ; +* serveur de backup ; +* clé SSH backup ; +* defaults globaux. + +--- + +### 2. Créer une cible + +Deux possibilités. + +#### A. À la main + +Créer un fichier : + +```bash +config/targets/test.env +``` + +à partir de : + +```bash +config/targets/test.env.example +``` + +#### B. Via script + +Utiliser : + +```bash +./create-target-config.sh ... +``` + +--- + +## Exécution locale + +### Bootstrap seul + +```bash +./bootstrap-target-host.sh --target test +``` + +### Rebuild complet + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --non-interactive +``` + +--- + +## Intégration web + +L’interface web ne doit envoyer que les paramètres métier de l’exécution : + +```json +{ + "target": "test", + "db": "sirh", + "overwrite": "yes", + "restore_roles": "yes", + "request_id": "web_20260317_001" +} +``` + +Le backend transforme cela en commande : + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --request-id web_20260317_001 \ + --non-interactive +``` + +### Important + +Le web ne doit pas transmettre directement : + +* les clés SSH ; +* les mots de passe PostgreSQL ; +* les paramètres bas niveau de la cible ; +* les chemins système sensibles. + +Ces informations doivent être stockées dans la configuration serveur. + +--- + +## Ajouter une nouvelle machine depuis le web + +Le flux recommandé est : + +1. créer ou mettre à jour `config/targets/.env` +2. lancer `bootstrap-target-host.sh --target ` +3. lancer ensuite `run-rebuild-bdd.sh --target ...` + +Le bouton web **“Ajouter une machine”** doit donc : + +* créer la configuration cible ; +* déclencher le bootstrap ; +* vérifier le retour ; +* rendre ensuite la cible disponible pour les rebuilds. + +--- + +## Sorties JSON + +### Succès + +Exemple : + +```json +{ + "status": "success", + "message": "restauration terminée avec succès", + "request_id": "web_001", + "environment": "RECETTE", + "database": "sirh", + "dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump", + "log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log" +} +``` + +### Erreur + +Exemple : + +```json +{ + "status": "error", + "message": "la base existe déjà et overwrite n'est pas autorisé", + "request_id": "web_001", + "environment": "RECETTE", + "database": "sirh", + "dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump", + "log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log" +} +``` + +--- + +## Sécurité + +### Recommandations minimales + +* utiliser des clés SSH dédiées ; +* limiter la clé backup à la lecture seule ; +* restreindre les permissions des fichiers de config ; +* exécuter les scripts avec un utilisateur dédié ; +* ne pas exposer les secrets dans l’interface web ; +* valider strictement toutes les entrées côté backend. + +### `sudoers` + +Le bootstrap peut installer un `sudoers.d` minimal pour l’utilisateur runtime : + +```sudoers + ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl + ALL=(postgres) NOPASSWD: /usr/bin/psql +``` + +Adapter si d’autres commandes doivent être autorisées. + +--- + +## Logs + +Les logs de rebuild sont stockés dans : + +```bash +TARGET_BACKUP_LOG_DIR +``` + +Exemple : + +```bash +/home/backup_liot/logs/rebuild_bdd/ +``` + +Le chemin du log est renvoyé dans le JSON final. + +--- + +## Limites connues + +* le bootstrap initial nécessite un accès SSH bootstrap valide ; +* le bootstrap ne remplace pas une mauvaise architecture réseau ; +* les secrets doivent être gérés proprement par la couche web/backend ; +* des verrous d’exécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus. + +--- + +## Recommandations de validation + +Avant mise en production, tester au minimum : + +1. bootstrap d’une machine neuve ; +2. rebuild complet d’une base ; +3. refus si la base existe et `overwrite=no` ; +4. relance complète une seconde fois sur la même cible ; +5. accès backup invalide ; +6. PostgreSQL absent au départ ; +7. `sudo -n` indisponible. + +--- + +## Commandes utiles + +### Créer une cible + +```bash +./create-target-config.sh \ + --target test \ + --host 192.168.1.50 \ + --port 22 \ + --bootstrap-user backup_liot \ + --bootstrap-key /home/matteo/.ssh/id_ed25519_target_test \ + --runtime-user backup_liot \ + --repo-dir /home/backup_liot/RebuildBdd \ + --env-name RECETTE \ + --pguser backup_liot \ + --pgpassword secret \ + --dbs "sirh inventory ferme" \ + --backup-subdir bdd-recette +``` + +### Bootstrap + +```bash +./bootstrap-target-host.sh --target test +``` + +### Rebuild + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --non-interactive +``` + +--- + +## État du projet + +Le projet permet désormais une utilisation : + +* locale ; +* automatisée ; +* intégrée au web ; + +avec préparation des cibles, exécution non interactive et retour JSON. + +``` diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh old mode 100644 new mode 100755 index c23ad21..9181ad3 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -1,40 +1,28 @@ #!/usr/bin/env bash set -euo pipefail -############################################################################### -# bootstrap-target-host.sh -# -# Bootstrap initial d'une machine cible neuve ou quasi neuve. -# -# Ce script est lancé depuis la machine de pilotage et : -# 1. charge le .env local ; -# 2. récupère la configuration bootstrap de la cible ; -# 3. teste la connexion SSH de bootstrap ; -# 4. installe le socle minimal sur la cible ; -# 5. crée les dossiers de travail ; -# 6. génère le .env cible ; -# 7. clone ou met à jour le dépôt distant. -# -# À ce stade, il ne gère pas encore : -# - la clé SSH backup ; -# - known_hosts backup ; -# - sudoers PostgreSQL ; -# - le lancement du rebuild. -############################################################################### - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env" +CONFIG_DIR="${SCRIPT_DIR}/config" +GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" + +GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" +TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" -ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}" TARGET_NAME="${TARGET_NAME:-}" CLI_TARGET="" JSON_ONLY="${JSON_ONLY:-no}" while [[ $# -gt 0 ]]; do case "$1" in - --env-file) - [[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; } - ENV_FILE="$2" + --global-env-file) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; } + GLOBAL_ENV_FILE="$2" + shift 2 + ;; + --targets-dir) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; } + TARGETS_DIR="$2" shift 2 ;; --target) @@ -53,6 +41,13 @@ while [[ $# -gt 0 ]]; do esac done +json_escape() { + python3 - <<'PY' "$1" +import json, sys +print(json.dumps(sys.argv[1])) +PY +} + print_stdout() { [[ "$JSON_ONLY" == "yes" ]] || echo "$*" } @@ -64,7 +59,9 @@ log() { fail() { local msg="$1" if [[ "$JSON_ONLY" == "yes" ]]; then - printf '{"status":"error","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + printf '{"status":%s,"message":%s}\n' \ + "$(json_escape "error")" \ + "$(json_escape "$msg")" else echo "ERROR: $msg" >&2 fi @@ -74,7 +71,9 @@ fail() { success() { local msg="$1" if [[ "$JSON_ONLY" == "yes" ]]; then - printf '{"status":"success","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + printf '{"status":%s,"message":%s}\n' \ + "$(json_escape "success")" \ + "$(json_escape "$msg")" else log "$msg" fi @@ -84,99 +83,120 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" } -sanitize_key() { - local value="${1:-}" - value="${value//[^a-zA-Z0-9_]/_}" - printf '%s' "$value" -} - -get_env_var() { - local var_name="$1" - printf '%s' "${!var_name:-}" +to_bool_yes_no() { + local v="${1:-}" + v="${v,,}" + case "$v" in + yes|y|oui|o|true|1) echo "yes" ;; + no|n|non|false|0|"") echo "no" ;; + *) return 1 ;; + esac } shell_quote() { printf "%q" "$1" } -[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE" - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a +cleanup() { + rm -f "${TMP_ENV_FILE:-}" +} +trap cleanup EXIT TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" [[ -n "$TARGET_NAME" ]] || fail "target manquante" -SAFE_TARGET="$(sanitize_key "$TARGET_NAME")" +TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET_NAME}.env" -BOOTSTRAP_HOST="$(get_env_var "TARGET_HOST_${SAFE_TARGET}")" -BOOTSTRAP_PORT="$(get_env_var "TARGET_PORT_${SAFE_TARGET}")" -BOOTSTRAP_USER="$(get_env_var "TARGET_BOOTSTRAP_USER_${SAFE_TARGET}")" -BOOTSTRAP_SSH_KEY="$(get_env_var "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET}")" +[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE" +[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE" -TARGET_REPO_URL="$(get_env_var "TARGET_REPO_URL_${SAFE_TARGET}")" -TARGET_REPO_BRANCH="$(get_env_var "TARGET_REPO_BRANCH_${SAFE_TARGET}")" -TARGET_REPO_DIR="$(get_env_var "TARGET_REPO_DIR_${SAFE_TARGET}")" -TARGET_ENV_FILE_PATH="$(get_env_var "TARGET_ENV_FILE_${SAFE_TARGET}")" +set -a +# shellcheck disable=SC1090 +source "$GLOBAL_ENV_FILE" +# shellcheck disable=SC1090 +source "$TARGET_ENV_SOURCE" +set +a -TARGET_ENV_NAME_VALUE="$(get_env_var "TARGET_ENV_NAME_${SAFE_TARGET}")" -TARGET_PGHOST_VALUE="$(get_env_var "TARGET_PGHOST_${SAFE_TARGET}")" -TARGET_PGPORT_VALUE="$(get_env_var "TARGET_PGPORT_${SAFE_TARGET}")" -TARGET_PGUSER_VALUE="$(get_env_var "TARGET_PGUSER_${SAFE_TARGET}")" -TARGET_PGPASSWORD_VALUE="$(get_env_var "TARGET_PGPASSWORD_${SAFE_TARGET}")" -TARGET_DBS_VALUE="$(get_env_var "TARGET_DBS_${SAFE_TARGET}")" +BOOTSTRAP_HOST="${TARGET_HOST:-}" +BOOTSTRAP_PORT="${TARGET_PORT:-22}" +BOOTSTRAP_USER="${TARGET_BOOTSTRAP_USER:-}" +BOOTSTRAP_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" -TARGET_BACKUP_REMOTE_USER_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_HOST_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_DIR_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_SSH_PORT_${SAFE_TARGET}")" -TARGET_BACKUP_LOG_DIR_VALUE="$(get_env_var "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET}")" +TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" +TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-}}" +TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_ENV_FILE_PATH="${TARGET_ENV_FILE:-}" -TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="$(get_env_var "TARGET_LOCAL_RESTORE_BASE_DIR_${SAFE_TARGET}")" -TARGET_REMOTE_ROLES_DIR_NAME_VALUE="$(get_env_var "TARGET_REMOTE_ROLES_DIR_NAME_${SAFE_TARGET}")" -TARGET_SSH_KEY_VALUE="$(get_env_var "TARGET_SSH_KEY_${SAFE_TARGET}")" -TARGET_AUTO_INSTALL_POSTGRES_VALUE="$(get_env_var "TARGET_AUTO_INSTALL_POSTGRES_${SAFE_TARGET}")" -TARGET_AUTO_CREATE_PGUSER_VALUE="$(get_env_var "TARGET_AUTO_CREATE_PGUSER_${SAFE_TARGET}")" -TARGET_PGUSER_SUPERUSER_VALUE="$(get_env_var "TARGET_PGUSER_SUPERUSER_${SAFE_TARGET}")" -TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="$(get_env_var "TARGET_AUTO_CONFIGURE_SUDOERS_${SAFE_TARGET}")" -TARGET_EXCLUDED_RESTORE_ROLES_VALUE="$(get_env_var "TARGET_EXCLUDED_RESTORE_ROLES_${SAFE_TARGET}")" +TARGET_ENV_NAME_VALUE="${TARGET_ENV_NAME:-}" +TARGET_PGHOST_VALUE="${TARGET_PGHOST:-${GLOBAL_PGHOST:-}}" +TARGET_PGPORT_VALUE="${TARGET_PGPORT:-${GLOBAL_PGPORT:-}}" +TARGET_PGUSER_VALUE="${TARGET_PGUSER:-}" +TARGET_PGPASSWORD_VALUE="${TARGET_PGPASSWORD:-}" +TARGET_DBS_VALUE="${TARGET_DBS:-}" -[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST_${SAFE_TARGET} manquante" -[[ -n "$BOOTSTRAP_PORT" ]] || BOOTSTRAP_PORT="22" -[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER_${SAFE_TARGET} manquante" -[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET} manquante" +TARGET_BACKUP_REMOTE_USER_VALUE="${TARGET_BACKUP_REMOTE_USER:-${GLOBAL_BACKUP_REMOTE_USER:-}}" +TARGET_BACKUP_REMOTE_HOST_VALUE="${TARGET_BACKUP_REMOTE_HOST:-${GLOBAL_BACKUP_REMOTE_HOST:-}}" +TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="${TARGET_BACKUP_REMOTE_SSH_PORT:-${GLOBAL_BACKUP_REMOTE_PORT:-22}}" +GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR:-}" +TARGET_BACKUP_SUBDIR_VALUE="${TARGET_BACKUP_SUBDIR:-}" +TARGET_BACKUP_LOG_DIR_VALUE="${TARGET_BACKUP_LOG_DIR:-}" +TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY:-${GLOBAL_BACKUP_SSH_PRIVATE_KEY:-}}" +TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY:-${GLOBAL_BACKUP_SSH_PUBLIC_KEY:-}}" +TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE="${TARGET_BACKUP_KNOWN_HOSTS_STRICT:-${GLOBAL_BACKUP_KNOWN_HOSTS_STRICT:-yes}}" + +TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_LOCAL_RESTORE_BASE_DIR:-${TARGET_REPO_DIR}/restore_tmp}" +TARGET_REMOTE_ROLES_DIR_NAME_VALUE="${TARGET_REMOTE_ROLES_DIR_NAME:-${GLOBAL_REMOTE_ROLES_DIR_NAME:-user}}" +TARGET_SSH_KEY_VALUE="${TARGET_SSH_KEY:-/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly}" +TARGET_AUTO_INSTALL_POSTGRES_VALUE="${TARGET_AUTO_INSTALL_POSTGRES:-${GLOBAL_AUTO_INSTALL_POSTGRES:-yes}}" +TARGET_AUTO_CREATE_PGUSER_VALUE="${TARGET_AUTO_CREATE_PGUSER:-${GLOBAL_AUTO_CREATE_PGUSER:-yes}}" +TARGET_PGUSER_SUPERUSER_VALUE="${TARGET_PGUSER_SUPERUSER:-${GLOBAL_PGUSER_SUPERUSER:-no}}" +TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="${TARGET_AUTO_CONFIGURE_SUDOERS:-${GLOBAL_AUTO_CONFIGURE_SUDOERS:-no}}" +TARGET_EXCLUDED_RESTORE_ROLES_VALUE="${TARGET_EXCLUDED_RESTORE_ROLES:-${GLOBAL_EXCLUDED_RESTORE_ROLES:-postgres}}" + +TARGET_RUNTIME_USER_VALUE="${TARGET_RUNTIME_USER:-$BOOTSTRAP_USER}" +TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE="${TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-${GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-yes}}" + +[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST manquante" +[[ "$BOOTSTRAP_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide" +[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante" +[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante" [[ -f "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap introuvable : $BOOTSTRAP_SSH_KEY" [[ -r "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap non lisible : $BOOTSTRAP_SSH_KEY" -[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${SAFE_TARGET} manquante" -[[ -n "$TARGET_REPO_BRANCH" ]] || fail "TARGET_REPO_BRANCH_${SAFE_TARGET} manquante" -[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${SAFE_TARGET} manquante" -[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE_${SAFE_TARGET} manquante" +[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant" +[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant" +[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" +[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE manquante" -[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD_${SAFE_TARGET} manquante" -[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS_${SAFE_TARGET} manquante" +[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME manquante" +[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST/GLOBAL_PGHOST manquant" +[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT/GLOBAL_PGPORT manquant" +[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER manquante" +[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD manquante" +[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS manquante" -[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_REMOTE_DIR_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET} manquante" +[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_USER/TARGET_BACKUP_REMOTE_USER manquant" +[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_HOST/TARGET_BACKUP_REMOTE_HOST manquant" +[[ -n "$GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_BASE_DIR manquant" +[[ -n "$TARGET_BACKUP_SUBDIR_VALUE" ]] || fail "TARGET_BACKUP_SUBDIR manquante" +[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR manquante" +TARGET_BACKUP_REMOTE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE%/}/${TARGET_BACKUP_SUBDIR_VALUE}" -[[ -n "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" ]] || TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="22" -[[ -n "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE" ]] || TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_REPO_DIR}/restore_tmp" -[[ -n "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE" ]] || TARGET_REMOTE_ROLES_DIR_NAME_VALUE="user" -[[ -n "$TARGET_SSH_KEY_VALUE" ]] || TARGET_SSH_KEY_VALUE="/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly" -[[ -n "$TARGET_AUTO_INSTALL_POSTGRES_VALUE" ]] || TARGET_AUTO_INSTALL_POSTGRES_VALUE="yes" -[[ -n "$TARGET_AUTO_CREATE_PGUSER_VALUE" ]] || TARGET_AUTO_CREATE_PGUSER_VALUE="yes" -[[ -n "$TARGET_PGUSER_SUPERUSER_VALUE" ]] || TARGET_PGUSER_SUPERUSER_VALUE="no" -[[ -n "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE" ]] || TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="no" -[[ -n "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE" ]] || TARGET_EXCLUDED_RESTORE_ROLES_VALUE="postgres" +[[ -n "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "GLOBAL_BACKUP_SSH_PRIVATE_KEY/TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY manquant" +[[ -f "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" +[[ -r "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" + +if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then + [[ -f "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" + [[ -r "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" +fi + +[[ "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" =~ ^[0-9]+$ ]] || fail "port backup invalide" +to_bool_yes_no "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE" >/dev/null || fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" +to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || fail "TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO invalide" + +ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")" require_cmd ssh require_cmd scp @@ -202,24 +222,41 @@ set -euo pipefail export DEBIAN_FRONTEND=noninteractive -if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y bash git python3 sudo curl openssh-client ca-certificates - else - apt-get update - apt-get install -y bash git python3 sudo curl openssh-client ca-certificates +run_root() { + if [ \"\$(id -u)\" -eq 0 ]; then + \"\$@\" + return 0 fi -else + + if command -v sudo >/dev/null 2>&1; then + sudo -n \"\$@\" || { + echo 'sudo -n indisponible pour le bootstrap' >&2 + exit 1 + } + return 0 + fi + + echo 'ni root ni sudo disponible pour le bootstrap' >&2 + exit 1 +} + +if ! command -v apt-get >/dev/null 2>&1; then echo 'apt-get absent sur la cible' >&2 exit 1 fi +run_root apt-get update +run_root apt-get install -y bash git python3 sudo curl openssh-client ca-certificates postgresql-client + mkdir -p $(shell_quote "$(dirname "$TARGET_REPO_DIR")") mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")") mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE") mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE") mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") + +chmod 700 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") || true +touch $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") +chmod 644 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") || true " log "Installation du socle minimal sur la cible" @@ -227,10 +264,6 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ || fail "échec de préparation système distante" TMP_ENV_FILE="$(mktemp)" -cleanup() { - rm -f "$TMP_ENV_FILE" -} -trap cleanup EXIT cat >"$TMP_ENV_FILE" </dev/null 2>&1 \ || fail "échec de copie du .env cible" +REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" +REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" + +log "Copie de la clé privée backup sur la cible" +scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "$TARGET_SSH_KEY_VALUE")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé privée backup" + +if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then + log "Copie de la clé publique backup sur la cible" + scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "${TARGET_SSH_KEY_VALUE}.pub")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé publique backup" +fi + +REMOTE_SSH_PERMS_CMD=" +set -euo pipefail +chmod 700 $(shell_quote "$REMOTE_SSH_DIR") +chmod 600 $(shell_quote "$TARGET_SSH_KEY_VALUE") +if [[ -f $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") ]]; then + chmod 644 $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") +fi +touch $(shell_quote "$REMOTE_KNOWN_HOSTS") +chmod 644 $(shell_quote "$REMOTE_KNOWN_HOSTS") +" + +log "Correction des permissions SSH côté cible" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SSH_PERMS_CMD" \ + || fail "échec de correction des permissions SSH sur la cible" + +REMOTE_KNOWN_HOSTS_CMD=" +set -euo pipefail + +if ! command -v ssh-keyscan >/dev/null 2>&1; then + echo 'ssh-keyscan absent sur la cible' >&2 + exit 1 +fi + +if ! ssh-keygen -F $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") -f $(shell_quote "$REMOTE_KNOWN_HOSTS") >/dev/null 2>&1; then + ssh-keyscan -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") -H $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") >> $(shell_quote "$REMOTE_KNOWN_HOSTS") 2>/dev/null +fi +" + +log "Ajout du serveur de backup dans known_hosts côté cible" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_KNOWN_HOSTS_CMD" \ + || fail "échec de préparation known_hosts sur la cible" + +STRICT_OPTION="yes" +case "${TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE,,}" in + yes|y|oui|o|true|1) STRICT_OPTION="yes" ;; + no|n|non|false|0) STRICT_OPTION="no" ;; + *) fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" ;; +esac + +REMOTE_BACKUP_TEST_CMD=" +set -euo pipefail + +ssh \ + -i $(shell_quote "$TARGET_SSH_KEY_VALUE") \ + -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") \ + -o IdentitiesOnly=yes \ + -o BatchMode=yes \ + -o ConnectTimeout=8 \ + -o StrictHostKeyChecking=$(shell_quote "$STRICT_OPTION") \ + $(shell_quote "${TARGET_BACKUP_REMOTE_USER_VALUE}@${TARGET_BACKUP_REMOTE_HOST_VALUE}") \ + test -d $(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE") +" + +log "Test de la connexion SSH cible -> backup" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_BACKUP_TEST_CMD" \ + || fail "la cible ne peut pas accéder au serveur de backup avec la clé fournie" + +if [[ "$ALLOW_PASSWORDLESS_SUDO" == "yes" ]]; then + REMOTE_SUDOERS_CMD=" +set -euo pipefail + +run_root() { + if [ \"\$(id -u)\" -eq 0 ]; then + \"\$@\" + return 0 + fi + + if command -v sudo >/dev/null 2>&1; then + sudo -n \"\$@\" || { + echo 'sudo -n indisponible pour installer sudoers' >&2 + exit 1 + } + return 0 + fi + + echo 'ni root ni sudo disponible pour sudoers' >&2 + exit 1 +} + +if ! command -v visudo >/dev/null 2>&1; then + run_root apt-get update + run_root apt-get install -y sudo +fi + +TMP_SUDOERS_FILE=\$(mktemp) +cat >\"\$TMP_SUDOERS_FILE\" </dev/null 2>&1 || { + rm -f \"\$TMP_SUDOERS_FILE\" + echo 'fichier sudoers généré invalide' >&2 + exit 1 +} + +run_root install -m 440 \"\$TMP_SUDOERS_FILE\" /etc/sudoers.d/rebuild-bdd-${TARGET_RUNTIME_USER_VALUE} +rm -f \"\$TMP_SUDOERS_FILE\" +" + + log "Installation du sudoers non interactif minimal" + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" \ + || fail "échec d'installation du sudoers non interactif" +else + log "Installation du sudoers non interactif désactivée." +fi + REMOTE_REPO_CMD=" set -euo pipefail @@ -283,4 +442,47 @@ log "Clone / mise à jour du dépôt distant" ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \ || fail "échec de synchronisation du dépôt sur la cible" +REMOTE_VALIDATE_SUDO_ROOT_CMD=" +set -euo pipefail +command -v sudo >/dev/null 2>&1 || { + echo 'sudo absent sur la cible' >&2 + exit 1 +} +sudo -n true >/dev/null 2>&1 || { + echo 'sudo -n indisponible' >&2 + exit 1 +} +" + +log "Validation initiale de sudo -n" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \ + || fail "sudo -n invalide sur la cible" + +REMOTE_RUN_CHECK_PG_CMD=" +set -euo pipefail + +CHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-postgresql.sh") +ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH") + +[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\" + +\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive +" + +log "Préparation PostgreSQL via check-postgresql.sh" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ + || fail "échec de préparation PostgreSQL pendant le bootstrap" + +REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" +set -euo pipefail +sudo -n -u postgres true >/dev/null 2>&1 || { + echo 'sudo -n -u postgres indisponible après préparation PostgreSQL' >&2 + exit 1 +} +" + +log "Validation finale de sudo -n -u postgres" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \ + || fail "sudo -n -u postgres invalide sur la cible" + success "bootstrap initial terminé pour ${TARGET_NAME}" \ No newline at end of file diff --git a/RebuildBdd/create-target-config.sh b/RebuildBdd/create-target-config.sh new file mode 100644 index 0000000..1d17ae6 --- /dev/null +++ b/RebuildBdd/create-target-config.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="${SCRIPT_DIR}/config" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" + +TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" + +TARGET="" +HOST="" +PORT="22" +BOOTSTRAP_USER="" +BOOTSTRAP_SSH_KEY="" +RUNTIME_USER="" +REPO_DIR="" +ENV_FILE="" +ENV_NAME="" +PGHOST="" +PGPORT="" +PGUSER="" +PGPASSWORD="" +DBS="" +BACKUP_SUBDIR="" +BACKUP_LOG_DIR="" +LOCAL_RESTORE_BASE_DIR="" +SSH_KEY_TARGET_PATH="" +ENABLE_BOOTSTRAP="yes" +ALLOW_PASSWORDLESS_SUDO="yes" +AUTO_INSTALL_POSTGRES="yes" +AUTO_CREATE_PGUSER="yes" +PGUSER_SUPERUSER="no" +AUTO_CONFIGURE_SUDOERS="no" +REMOTE_ROLES_DIR_NAME="user" +EXCLUDED_RESTORE_ROLES="postgres" +FORCE="no" + +while [[ $# -gt 0 ]]; do + case "$1" in + --targets-dir) TARGETS_DIR="$2"; shift 2 ;; + --target) TARGET="$2"; shift 2 ;; + --host) HOST="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --bootstrap-user) BOOTSTRAP_USER="$2"; shift 2 ;; + --bootstrap-key) BOOTSTRAP_SSH_KEY="$2"; shift 2 ;; + --runtime-user) RUNTIME_USER="$2"; shift 2 ;; + --repo-dir) REPO_DIR="$2"; shift 2 ;; + --env-file) ENV_FILE="$2"; shift 2 ;; + --env-name) ENV_NAME="$2"; shift 2 ;; + --pghost) PGHOST="$2"; shift 2 ;; + --pgport) PGPORT="$2"; shift 2 ;; + --pguser) PGUSER="$2"; shift 2 ;; + --pgpassword) PGPASSWORD="$2"; shift 2 ;; + --dbs) DBS="$2"; shift 2 ;; + --backup-subdir) BACKUP_SUBDIR="$2"; shift 2 ;; + --backup-log-dir) BACKUP_LOG_DIR="$2"; shift 2 ;; + --local-restore-base-dir) LOCAL_RESTORE_BASE_DIR="$2"; shift 2 ;; + --ssh-key-target-path) SSH_KEY_TARGET_PATH="$2"; shift 2 ;; + --enable-bootstrap) ENABLE_BOOTSTRAP="$2"; shift 2 ;; + --allow-passwordless-sudo) ALLOW_PASSWORDLESS_SUDO="$2"; shift 2 ;; + --auto-install-postgres) AUTO_INSTALL_POSTGRES="$2"; shift 2 ;; + --auto-create-pguser) AUTO_CREATE_PGUSER="$2"; shift 2 ;; + --pguser-superuser) PGUSER_SUPERUSER="$2"; shift 2 ;; + --auto-configure-sudoers) AUTO_CONFIGURE_SUDOERS="$2"; shift 2 ;; + --remote-roles-dir-name) REMOTE_ROLES_DIR_NAME="$2"; shift 2 ;; + --excluded-restore-roles) EXCLUDED_RESTORE_ROLES="$2"; shift 2 ;; + --force) FORCE="yes"; shift ;; + *) echo "Argument inconnu : $1" >&2; exit 1 ;; + esac +done + +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + +to_bool_yes_no() { + local v="${1:-}" + v="${v,,}" + case "$v" in + yes|y|oui|o|true|1) echo "yes" ;; + no|n|non|false|0|"") echo "no" ;; + *) return 1 ;; + esac +} + +[[ -n "$TARGET" ]] || fail "--target manquant" +[[ "$TARGET" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "target invalide" + +[[ -n "$HOST" ]] || fail "--host manquant" +[[ -n "$BOOTSTRAP_USER" ]] || fail "--bootstrap-user manquant" +[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "--bootstrap-key manquant" +[[ -n "$REPO_DIR" ]] || fail "--repo-dir manquant" +[[ -n "$ENV_NAME" ]] || fail "--env-name manquant" +[[ -n "$PGUSER" ]] || fail "--pguser manquant" +[[ -n "$PGPASSWORD" ]] || fail "--pgpassword manquant" +[[ -n "$DBS" ]] || fail "--dbs manquant" +[[ -n "$BACKUP_SUBDIR" ]] || fail "--backup-subdir manquant" +[[ "$PORT" =~ ^[0-9]+$ ]] || fail "--port invalide" + +[[ -n "$RUNTIME_USER" ]] || RUNTIME_USER="$BOOTSTRAP_USER" +[[ -n "$ENV_FILE" ]] || ENV_FILE="${REPO_DIR}/.env" +[[ -n "$PGHOST" ]] || PGHOST="127.0.0.1" +[[ -n "$PGPORT" ]] || PGPORT="5432" +[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "--pgport invalide" +[[ -n "$BACKUP_LOG_DIR" ]] || BACKUP_LOG_DIR="/home/${RUNTIME_USER}/logs/rebuild_bdd" +[[ -n "$LOCAL_RESTORE_BASE_DIR" ]] || LOCAL_RESTORE_BASE_DIR="${REPO_DIR}/restore_tmp" +[[ -n "$SSH_KEY_TARGET_PATH" ]] || SSH_KEY_TARGET_PATH="/home/${RUNTIME_USER}/.ssh/id_ed25519_backup_readonly" + +ENABLE_BOOTSTRAP="$(to_bool_yes_no "$ENABLE_BOOTSTRAP")" || fail "--enable-bootstrap invalide" +ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$ALLOW_PASSWORDLESS_SUDO")" || fail "--allow-passwordless-sudo invalide" +AUTO_INSTALL_POSTGRES="$(to_bool_yes_no "$AUTO_INSTALL_POSTGRES")" || fail "--auto-install-postgres invalide" +AUTO_CREATE_PGUSER="$(to_bool_yes_no "$AUTO_CREATE_PGUSER")" || fail "--auto-create-pguser invalide" +PGUSER_SUPERUSER="$(to_bool_yes_no "$PGUSER_SUPERUSER")" || fail "--pguser-superuser invalide" +AUTO_CONFIGURE_SUDOERS="$(to_bool_yes_no "$AUTO_CONFIGURE_SUDOERS")" || fail "--auto-configure-sudoers invalide" + +mkdir -p "$TARGETS_DIR" || fail "impossible de créer $TARGETS_DIR" + +TARGET_FILE="${TARGETS_DIR}/${TARGET}.env" +if [[ -f "$TARGET_FILE" && "$FORCE" != "yes" ]]; then + fail "fichier déjà existant : $TARGET_FILE (utiliser --force pour écraser)" +fi + +cat >"$TARGET_FILE" <&2; exit 1; } - ENV_FILE="$2" + --global-env-file) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; } + GLOBAL_ENV_FILE="$2" + shift 2 + ;; + --targets-dir) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; } + TARGETS_DIR="$2" shift 2 ;; --target) @@ -50,10 +57,6 @@ while [[ $# -gt 0 ]]; do NON_INTERACTIVE="yes" shift ;; - --json-only) - JSON_ONLY="yes" - shift - ;; *) echo "Argument inconnu : $1" >&2 exit 1 @@ -61,33 +64,17 @@ while [[ $# -gt 0 ]]; do esac done -json_escape() { - python3 - <<'PY' "$1" -import json, sys -print(json.dumps(sys.argv[1])) -PY -} - -print_json_and_exit() { - local status="$1" - local message="$2" - local exit_code="$3" - - printf '{' - printf '"status":%s,' "$(json_escape "$status")" - printf '"message":%s,' "$(json_escape "$message")" - printf '"target":%s,' "$(json_escape "${TARGET:-}")" - printf '"request_id":%s' "$(json_escape "${REQUEST_ID:-}")" - printf '}\n' - exit "$exit_code" +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } fail() { - print_json_and_exit "error" "$*" 1 + log "ERROR: $*" >&2 + exit 1 } require_cmd() { - command -v "$1" >/dev/null 2>&1 + command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" } to_bool_yes_no() { @@ -104,124 +91,112 @@ is_tty() { [[ -t 0 && -t 1 ]] } -print_stdout() { - [[ "$JSON_ONLY" == "yes" ]] || echo "$*" -} - -sanitize_key() { - local s="${1:-}" - s="${s//[^a-zA-Z0-9_]/_}" - printf "%s" "$s" -} - -get_target_var() { - local target="$1" - local key="$2" - local safe_target - safe_target="$(sanitize_key "$target")" - local var_name="TARGET_${key}_${safe_target}" - printf "%s" "${!var_name:-}" -} - shell_quote() { printf "%q" "$1" } -[[ -f "$ENV_FILE" ]] || { - echo '{"status":"error","message":"fichier .env IA introuvable"}' - exit 1 +cleanup() { + rm -f "${BOOTSTRAP_JSON:-}" } +trap cleanup EXIT + +[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE" +[[ -d "$TARGETS_DIR" ]] || fail "dossier targets introuvable : $TARGETS_DIR" set -a # shellcheck disable=SC1090 -source "$ENV_FILE" +source "$GLOBAL_ENV_FILE" set +a -for cmd in bash ssh python3; do - require_cmd "$cmd" || fail "commande requise absente sur IA : $cmd" -done +require_cmd ssh +require_cmd git +require_cmd python3 TARGET="${CLI_TARGET:-${TARGET:-}}" REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}" -REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}" ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}" RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}" +REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-$(date '+%Y%m%d%H%M%S')_$RANDOM}}" ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || fail "ALLOW_OVERWRITE invalide" RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || fail "RESTORE_ROLES invalide" -: "${TARGETS:?Variable TARGETS manquante dans le .env IA}" - -read -r -a TARGETS_ARRAY <<< "$TARGETS" -[[ "${#TARGETS_ARRAY[@]}" -gt 0 ]] || fail "aucune cible définie dans TARGETS" - if [[ -z "$TARGET" ]]; then if [[ "$NON_INTERACTIVE" == "yes" ]]; then fail "TARGET manquante en mode non interactif" fi + mapfile -t TARGET_LIST < <(find "$TARGETS_DIR" -maxdepth 1 -type f -name '*.env' -printf '%f\n' | sed 's/\.env$//' | LC_ALL=C sort) + + [[ "${#TARGET_LIST[@]}" -gt 0 ]] || fail "aucune cible définie dans ${TARGETS_DIR}" + if is_tty; then - print_stdout "Cibles disponibles :" - for i in "${!TARGETS_ARRAY[@]}"; do - print_stdout " $((i + 1))) ${TARGETS_ARRAY[$i]}" + echo "Cibles disponibles :" + for i in "${!TARGET_LIST[@]}"; do + echo " $((i + 1))) ${TARGET_LIST[$i]}" done echo read -r -p "Sélectionnez le numéro de la cible : " TARGET_INDEX [[ "$TARGET_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de cible invalide" - (( TARGET_INDEX >= 1 && TARGET_INDEX <= ${#TARGETS_ARRAY[@]} )) || fail "numéro hors plage" - TARGET="${TARGETS_ARRAY[$((TARGET_INDEX - 1))]}" + (( TARGET_INDEX >= 1 && TARGET_INDEX <= ${#TARGET_LIST[@]} )) || fail "numéro hors plage" + TARGET="${TARGET_LIST[$((TARGET_INDEX - 1))]}" else fail "TARGET manquante et aucune interaction terminal disponible" fi fi -TARGET_ALLOWED="no" -for candidate in "${TARGETS_ARRAY[@]}"; do - if [[ "$candidate" == "$TARGET" ]]; then - TARGET_ALLOWED="yes" - break - fi -done -[[ "$TARGET_ALLOWED" == "yes" ]] || fail "cible refusée : non présente dans TARGETS" +TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET}.env" +[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE" -TARGET_HOST="$(get_target_var "$TARGET" "HOST")" -TARGET_USER="$(get_target_var "$TARGET" "USER")" -TARGET_SSH_KEY="$(get_target_var "$TARGET" "SSH_KEY")" -TARGET_SSH_PORT="$(get_target_var "$TARGET" "SSH_PORT")" -TARGET_SSH_CONNECT_TIMEOUT="$(get_target_var "$TARGET" "SSH_CONNECT_TIMEOUT")" +set -a +# shellcheck disable=SC1090 +source "$TARGET_ENV_SOURCE" +set +a -TARGET_REPO_URL="$(get_target_var "$TARGET" "REPO_URL")" -TARGET_REPO_BRANCH="$(get_target_var "$TARGET" "REPO_BRANCH")" -TARGET_REPO_DIR="$(get_target_var "$TARGET" "REPO_DIR")" -TARGET_CORE_SCRIPT="$(get_target_var "$TARGET" "CORE_SCRIPT")" -TARGET_ENV_FILE="$(get_target_var "$TARGET" "ENV_FILE")" - -TARGET_SSH_PORT="${TARGET_SSH_PORT:-22}" -TARGET_SSH_CONNECT_TIMEOUT="${TARGET_SSH_CONNECT_TIMEOUT:-8}" -TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-main}" - -[[ -n "$TARGET_HOST" ]] || fail "TARGET_HOST_${TARGET} manquante" -[[ -n "$TARGET_USER" ]] || fail "TARGET_USER_${TARGET} manquante" -[[ -n "$TARGET_SSH_KEY" ]] || fail "TARGET_SSH_KEY_${TARGET} manquante" -[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${TARGET} manquante" -[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${TARGET} manquante" -[[ -n "$TARGET_CORE_SCRIPT" ]] || fail "TARGET_CORE_SCRIPT_${TARGET} manquante" -[[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE_${TARGET} manquante" +TARGET_HOST="${TARGET_HOST:-}" +TARGET_PORT="${TARGET_PORT:-22}" +TARGET_USER="${TARGET_BOOTSTRAP_USER:-}" +TARGET_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" +TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" +TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-main}}" +TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_ENV_FILE="${TARGET_ENV_FILE:-}" +TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-yes}}" +[[ -n "$TARGET_HOST" ]] || fail "TARGET_HOST manquante" +[[ "$TARGET_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide" +[[ -n "$TARGET_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante" +[[ -n "$TARGET_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante" [[ -f "$TARGET_SSH_KEY" ]] || fail "clé SSH cible introuvable : $TARGET_SSH_KEY" +[[ -r "$TARGET_SSH_KEY" ]] || fail "clé SSH cible non lisible : $TARGET_SSH_KEY" -if [[ -z "$REQUEST_ID" ]]; then - REQUEST_ID="$(date '+%Y%m%d%H%M%S')_$$" -fi +[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant" +[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant" +[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" +[[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE manquante" + +TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide" + +BOOTSTRAP_SCRIPT_LOCAL="${SCRIPT_DIR}/bootstrap-target-host.sh" +[[ -f "$BOOTSTRAP_SCRIPT_LOCAL" ]] || fail "script bootstrap introuvable : $BOOTSTRAP_SCRIPT_LOCAL" +[[ -x "$BOOTSTRAP_SCRIPT_LOCAL" ]] || chmod 700 "$BOOTSTRAP_SCRIPT_LOCAL" || fail "chmod impossible sur $BOOTSTRAP_SCRIPT_LOCAL" if [[ -z "$REQUESTED_DB" ]]; then + DBS_FOR_TARGET="${TARGET_DBS:-}" if [[ "$NON_INTERACTIVE" == "yes" ]]; then fail "REQUESTED_DB manquante en mode non interactif" fi + read -r -a DBS_ARRAY <<< "$DBS_FOR_TARGET" + [[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "TARGET_DBS vide" + if is_tty; then + echo "Bases disponibles :" + for i in "${!DBS_ARRAY[@]}"; do + echo " $((i + 1))) ${DBS_ARRAY[$i]}" + done + echo read -r -p "Nom exact de la base à restaurer : " REQUESTED_DB - [[ -n "$REQUESTED_DB" ]] || fail "nom de base vide" else fail "REQUESTED_DB manquante et aucune interaction terminal disponible" fi @@ -229,15 +204,52 @@ fi [[ "$REQUESTED_DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide" +if [[ "$TARGET_ENABLE_BOOTSTRAP" == "yes" ]]; then + log "Bootstrap initial activé pour la cible ${TARGET}" + BOOTSTRAP_JSON="/tmp/bootstrap_target_${REQUEST_ID}.json" + + "$BOOTSTRAP_SCRIPT_LOCAL" \ + --global-env-file "$GLOBAL_ENV_FILE" \ + --targets-dir "$TARGETS_DIR" \ + --target "$TARGET" \ + --json-only >"$BOOTSTRAP_JSON" || { + cat "$BOOTSTRAP_JSON" 2>/dev/null || true + fail "échec du bootstrap initial de la cible ${TARGET}" + } + + BOOTSTRAP_STATUS="$( + python3 - <<'PY' "$BOOTSTRAP_JSON" +import json, sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) +print(data.get("status", "error")) +PY + )" + + if [[ "$BOOTSTRAP_STATUS" != "success" ]]; then + cat "$BOOTSTRAP_JSON" + fail "bootstrap initial échoué pour la cible ${TARGET}" + fi + + log "Bootstrap initial terminé pour ${TARGET}" +else + log "Bootstrap initial désactivé pour ${TARGET}" +fi + SSH_OPTS=( -i "$TARGET_SSH_KEY" - -p "$TARGET_SSH_PORT" + -p "$TARGET_PORT" -o IdentitiesOnly=yes -o BatchMode=yes - -o ConnectTimeout="$TARGET_SSH_CONNECT_TIMEOUT" - -o StrictHostKeyChecking=yes + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=8 ) +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \ + || fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}" + +TARGET_CORE_SCRIPT="${TARGET_REPO_DIR}/rebuild-bdd-core.sh" + REMOTE_BOOTSTRAP_CMD=" set -euo pipefail @@ -291,9 +303,12 @@ PY if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then cat \"\$PRECHECK_JSON\" + rm -f \"\$PRECHECK_JSON\" exit 1 fi +rm -f \"\$PRECHECK_JSON\" + exec \"\$CORE_SCRIPT\" \ --env-file \"\$TARGET_ENV_FILE\" \ --db \"\$REQUESTED_DB\" \ @@ -304,4 +319,4 @@ exec \"\$CORE_SCRIPT\" \ --json-only " -exec ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" \ No newline at end of file +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" \ No newline at end of file diff --git a/RecetteScripts/backup-bdd-recette.sh b/RecetteScripts/backup-bdd-recette.sh old mode 100644 new mode 100755 diff --git a/RecetteScripts/check-statut-recette.sh b/RecetteScripts/check-statut-recette.sh old mode 100644 new mode 100755