feat : Utilisation web disponible et simplification du deployement des scripts (WIP)

This commit is contained in:
2026-03-17 11:40:35 +01:00
parent 0d4ffd9391
commit a1fb6f5504
14 changed files with 1287 additions and 257 deletions

0
BackupVaultWarden/backup-vaultwarden.sh Normal file → Executable file
View File

0
CheckStorage/check-storage.sh Normal file → Executable file
View File

View File

@@ -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

42
RebuildBdd/Checkup/check-target-readiness.sh Normal file → Executable file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

566
RebuildBdd/README.md Normal file
View File

@@ -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 dune 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 lenvironnement ;
- `rebuild-bdd-core.sh` exécute la restauration ;
- `run-rebuild-bdd.sh` orchestre lensemble.
---
## 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/<nom_cible>.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/<cible>.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 dun `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 larborescence 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
<BACKUP_REMOTE_DIR>/<db>/<db>_*.dump
```
* le dernier fichier rôles dans :
```bash
<BACKUP_REMOTE_DIR>/<REMOTE_ROLES_DIR_NAME>/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
Linterface web ne doit envoyer que les paramètres métier de lexé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/<cible>.env`
2. lancer `bootstrap-target-host.sh --target <cible>`
3. lancer ensuite `run-rebuild-bdd.sh --target <cible> ...`
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 linterface web ;
* valider strictement toutes les entrées côté backend.
### `sudoers`
Le bootstrap peut installer un `sudoers.d` minimal pour lutilisateur runtime :
```sudoers
<user> ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
<user> ALL=(postgres) NOPASSWD: /usr/bin/psql
```
Adapter si dautres 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 dexécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus.
---
## Recommandations de validation
Avant mise en production, tester au minimum :
1. bootstrap dune machine neuve ;
2. rebuild complet dune 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.
```

422
RebuildBdd/bootstrap-target-host.sh Normal file → Executable file
View File

@@ -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" <<EOF
ENV_NAME=$(printf '%s\n' "$TARGET_ENV_NAME_VALUE")
@@ -261,6 +294,132 @@ log "Copie du .env cible"
scp "${SSH_OPTS[@]}" "$TMP_ENV_FILE" "${REMOTE}:$(printf '%q' "$TARGET_ENV_FILE_PATH")" >/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\" <<EOF
${TARGET_RUNTIME_USER_VALUE} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
${TARGET_RUNTIME_USER_VALUE} ALL=(postgres) NOPASSWD: /usr/bin/psql
EOF
chmod 440 \"\$TMP_SUDOERS_FILE\"
visudo -cf \"\$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}"

View File

@@ -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" <<EOF
TARGET_HOST=${HOST}
TARGET_PORT=${PORT}
TARGET_BOOTSTRAP_USER=${BOOTSTRAP_USER}
TARGET_BOOTSTRAP_SSH_KEY=${BOOTSTRAP_SSH_KEY}
TARGET_RUNTIME_USER=${RUNTIME_USER}
TARGET_ENABLE_BOOTSTRAP=${ENABLE_BOOTSTRAP}
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=${ALLOW_PASSWORDLESS_SUDO}
TARGET_REPO_DIR=${REPO_DIR}
TARGET_ENV_FILE=${ENV_FILE}
TARGET_ENV_NAME=${ENV_NAME}
TARGET_PGHOST=${PGHOST}
TARGET_PGPORT=${PGPORT}
TARGET_PGUSER=${PGUSER}
TARGET_PGPASSWORD=${PGPASSWORD}
TARGET_DBS=${DBS}
TARGET_BACKUP_SUBDIR=${BACKUP_SUBDIR}
TARGET_BACKUP_LOG_DIR=${BACKUP_LOG_DIR}
TARGET_LOCAL_RESTORE_BASE_DIR=${LOCAL_RESTORE_BASE_DIR}
TARGET_SSH_KEY=${SSH_KEY_TARGET_PATH}
TARGET_REMOTE_ROLES_DIR_NAME=${REMOTE_ROLES_DIR_NAME}
TARGET_EXCLUDED_RESTORE_ROLES=${EXCLUDED_RESTORE_ROLES}
TARGET_AUTO_INSTALL_POSTGRES=${AUTO_INSTALL_POSTGRES}
TARGET_AUTO_CREATE_PGUSER=${AUTO_CREATE_PGUSER}
TARGET_PGUSER_SUPERUSER=${PGUSER_SUPERUSER}
TARGET_AUTO_CONFIGURE_SUDOERS=${AUTO_CONFIGURE_SUDOERS}
EOF
chmod 600 "$TARGET_FILE" || fail "chmod impossible sur $TARGET_FILE"
echo "OK: ${TARGET_FILE}"

View File

@@ -184,6 +184,7 @@ set +a
LOCAL_RESTORE_BASE_DIR="${LOCAL_RESTORE_BASE_DIR:-${SCRIPT_DIR}/restore_tmp}"
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
@@ -202,6 +203,9 @@ RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || {
exit 1
}
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
mkdir -p "$BACKUP_LOG_DIR" || {
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
exit 1
@@ -227,8 +231,8 @@ else
log "Aucun rôle exclu de la restauration."
fi
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename; do
require_cmd "$cmd" || true
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename curl; do
require_cmd "$cmd" || fail "commande requise absente : $cmd"
done
CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh"
@@ -245,6 +249,7 @@ export PGPASSWORD
SSH_OPTS=(
-i "$SSH_KEY"
-p "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
@@ -446,7 +451,6 @@ send_discord_message() {
local payload=""
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
require_cmd curl || return 0
payload="$(python3 -c 'import json,sys; print(json.dumps({"content": sys.argv[1]}))' "$message")" || return 0

View File

@@ -2,9 +2,12 @@
set -euo pipefail
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"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}"
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
CLI_TARGET=""
CLI_DB=""
@@ -12,13 +15,17 @@ CLI_OVERWRITE=""
CLI_RESTORE_ROLES=""
CLI_REQUEST_ID=""
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
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)
@@ -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"
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD"

0
RecetteScripts/backup-bdd-recette.sh Normal file → Executable file
View File

0
RecetteScripts/check-statut-recette.sh Normal file → Executable file
View File