Compare commits
45 Commits
0dddecd08f
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b223f514 | |||
| a9e492962c | |||
| 3bad5bad82 | |||
| 9af65f7739 | |||
| 11f69a9eda | |||
| e68c99a8b3 | |||
| 7b91691ef8 | |||
| 7261823806 | |||
| fac2a5b47f | |||
| 3c91c3b5c1 | |||
| f3ebb4c011 | |||
| 83032ef5ab | |||
| 863fee91a9 | |||
| 66abdfca53 | |||
| 26101f2112 | |||
| 0ee0c1328a | |||
| f6e66e7bff | |||
| 41df83fe32 | |||
| 3faf8ab71d | |||
| 685a65e2d1 | |||
| 6a99a8115f | |||
| f12c937b39 | |||
| cb94e74414 | |||
| 5b128bc81a | |||
| 6c61f6e543 | |||
| 447b04ce20 | |||
| 29371b6529 | |||
| b3de87a452 | |||
| 0ff3244c1d | |||
| 12bbe6b1d9 | |||
| fbefe3fb03 | |||
| 38b29796d3 | |||
| 01ac392fa9 | |||
| e5b15426a1 | |||
| 7974491e93 | |||
| b76b6613bf | |||
| 122f53f804 | |||
| 741fef225b | |||
| 8ef81add14 | |||
| a1fb6f5504 | |||
| 0d4ffd9391 | |||
|
|
94537de551 | ||
|
|
2971ef0ff9 | ||
|
|
f0dfd6acb1 | ||
|
|
858cad8269 |
@@ -34,3 +34,22 @@ REMOTE_DIR=
|
||||
|
||||
# Chemin vers la clé privée SSH utilisée pour la connexion
|
||||
SSH_KEY=
|
||||
|
||||
# Port SSH du serveur distant
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
# Timeout SSH en secondes
|
||||
SSH_CONNECT_TIMEOUT=10
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
|
||||
|
||||
#############################################
|
||||
# ROTATION DES BACKUPS
|
||||
#############################################
|
||||
|
||||
# Nombre de jours de conservation des sauvegardes
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
|
||||
@@ -28,12 +28,13 @@ Avant de mettre en place le script, vérifier que les éléments suivants sont d
|
||||
- `ssh`
|
||||
- `curl`
|
||||
- `cron`
|
||||
- `jq`
|
||||
|
||||
Installation sur Debian / Ubuntu :
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y tar openssh-client curl cron
|
||||
sudo apt install -y tar openssh-client curl cron jq
|
||||
````
|
||||
|
||||
---
|
||||
@@ -43,13 +44,13 @@ sudo apt install -y tar openssh-client curl cron
|
||||
Le script est situé dans :
|
||||
|
||||
```bash
|
||||
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||
/home/<USER>/Malio-ops/BackupVaultWarden/
|
||||
```
|
||||
|
||||
Structure recommandée :
|
||||
|
||||
```bash
|
||||
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||
/home/<USER>/Malio-ops/BackupVaultWarden/
|
||||
├── backup-vaultwarden.sh
|
||||
├── .env
|
||||
└── README.md
|
||||
@@ -65,29 +66,73 @@ Elles doivent être placées dans un fichier `.env`.
|
||||
## Exemple de fichier `.env`
|
||||
|
||||
```bash
|
||||
WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||
REMOTE_USER=<USER>
|
||||
REMOTE_HOST=<IP_SERVEUR>
|
||||
SSH_KEY=/home/matt/.ssh/id_ed25519_vaultwarden_backup
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
DATA_DIR=/opt/vaultwarden/data
|
||||
LOCAL_BACKUP=/var/backups/vaultwarden
|
||||
REMOTE_DIR=/home/backup/backups/vaultwarden
|
||||
# BACKUP_REMOTE_SSH_PORT=22
|
||||
# SSH_CONNECT_TIMEOUT=10
|
||||
# BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
# BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
```
|
||||
|
||||
## Description des variables
|
||||
|
||||
| Variable | Description |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| WEBHOOK_URL | Webhook Discord pour les notifications |
|
||||
| --------------------- | -------------------------------------------------------------------- |
|
||||
| DISCORD_WEBHOOK_URL | Webhook Discord pour les notifications |
|
||||
| REMOTE_USER | Utilisateur du serveur distant |
|
||||
| REMOTE_HOST | Adresse IP ou DNS du serveur de sauvegarde |
|
||||
| SSH_KEY | Chemin vers la clé SSH utilisée pour le transfert |
|
||||
| DATA_DIR | Dossier `data` de Vaultwarden |
|
||||
| LOCAL_BACKUP | Dossier local où stocker temporairement l'archive |
|
||||
| REMOTE_DIR | Dossier de stockage des backups sur le serveur distant |
|
||||
| BACKUP_REMOTE_SSH_PORT | Port SSH du serveur distant, optionnel, défaut `22` |
|
||||
| SSH_CONNECT_TIMEOUT | Timeout SSH en secondes, optionnel, défaut `10` |
|
||||
| BACKUP_KNOWN_HOSTS_STRICT | Validation stricte des hôtes SSH (`yes`/`no`) |
|
||||
| BACKUP_KNOWN_HOSTS_FILE | Fichier `known_hosts` utilisé par `ssh`/`scp` |
|
||||
| BACKUP_RETENTION_DAYS | Nombre de jours de conservation distante, optionnel, défaut `10` |
|
||||
|
||||
---
|
||||
|
||||
# 5. Chargement des variables dans le script
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 2</summary>
|
||||
|
||||
Quel format minimal faut-il donner a `printf` pour afficher une chaine brute ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 2</summary>
|
||||
|
||||
```text
|
||||
'%s'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 2</summary>
|
||||
|
||||
```text
|
||||
xlIHBldGl0IHN0YWdpYWlyZSBtYXR0ZW8gZHVu
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
Le script charge directement le fichier `.env` avec `source` et exporte automatiquement les variables pendant le chargement.
|
||||
|
||||
Mécanisme utilisé :
|
||||
@@ -121,13 +166,13 @@ Le transfert des sauvegardes utilise une **clé SSH** afin de permettre une conn
|
||||
Sur la machine exécutant les scripts :
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_bitwarden
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_backup
|
||||
```
|
||||
|
||||
#### 2. Copie de la clé vers le serveur distant
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519_bitwarden.pub <USER>@<IP_SERVEUR>
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519_backup.pub <USER>@<IP_SERVEUR>
|
||||
```
|
||||
|
||||
Cette commande ajoute la clé dans :
|
||||
@@ -141,20 +186,20 @@ sur la machine IA.
|
||||
#### 3. Vérification de la connexion
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_ed25519_bitwarden backup@192.168.0.179
|
||||
ssh -i ~/.ssh/id_ed25519_backup -o StrictHostKeyChecking=yes <USER>@<IP_SERVEUR>
|
||||
```
|
||||
|
||||
#### 4. Vérification des fichiers de clé
|
||||
|
||||
```bash
|
||||
ls ~/.ssh/id_ed25519_bitwarden*
|
||||
ls ~/.ssh/id_ed25519_backup*
|
||||
```
|
||||
|
||||
Fichiers attendus :
|
||||
|
||||
```
|
||||
~/.ssh/id_ed25519_bitwarden
|
||||
~/.ssh/id_ed25519_bitwarden.pub
|
||||
~/.ssh/id_ed25519_backup
|
||||
~/.ssh/id_ed25519_backup.pub
|
||||
```
|
||||
|
||||
#### 5. Permissions SSH
|
||||
@@ -163,8 +208,8 @@ Machine locale :
|
||||
|
||||
```bash
|
||||
chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/id_ed25519_bitwarden
|
||||
chmod 644 ~/.ssh/id_ed25519_bitwarden.pub
|
||||
chmod 600 ~/.ssh/id_ed25519_backup
|
||||
chmod 644 ~/.ssh/id_ed25519_backup.pub
|
||||
```
|
||||
|
||||
Machine distante :
|
||||
@@ -174,10 +219,19 @@ chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
#### 6. Déclaration dans `.env`
|
||||
#### 6. Provisionnement de `known_hosts`
|
||||
|
||||
Le script est prévu pour fonctionner avec validation stricte des hôtes SSH.
|
||||
|
||||
```bash
|
||||
SSH_KEY=/home/matt/.ssh/id_ed25519_bitwarden
|
||||
ssh-keyscan -H <IP_SERVEUR> >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
```
|
||||
|
||||
#### 7. Déclaration dans `.env`
|
||||
|
||||
```bash
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
```
|
||||
|
||||
Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour transférer les sauvegardes.
|
||||
@@ -188,7 +242,7 @@ Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour t
|
||||
Le script crée une archive compressée du dossier `data` :
|
||||
|
||||
```bash
|
||||
tar -czf "$LOCAL_BACKUP" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
```
|
||||
|
||||
Cela permet d’obtenir une sauvegarde portable et compressée.
|
||||
@@ -200,7 +254,7 @@ Cela permet d’obtenir une sauvegarde portable et compressée.
|
||||
Une fois l’archive créée :
|
||||
|
||||
```bash
|
||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
||||
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
||||
```
|
||||
|
||||
Le fichier est envoyé vers le serveur de sauvegarde via SCP.
|
||||
@@ -209,23 +263,22 @@ Le fichier est envoyé vers le serveur de sauvegarde via SCP.
|
||||
|
||||
# 9. Notification Discord
|
||||
|
||||
Le script envoie une notification Discord pour informer de l’état de la sauvegarde.
|
||||
Le script envoie une notification Discord pour informer de l'etat de la sauvegarde.
|
||||
|
||||
Construction du message :
|
||||
|
||||
```bash
|
||||
local msg="**@here Backup Vaultwarden $color**\n"
|
||||
msg="**${ping}Backup Vaultwarden ${icon}**\n"
|
||||
msg+="Backup: ${BACKUP_NAME}\n"
|
||||
msg+="Data transfer: $dumps_display\n"
|
||||
[[ -n "$details" ]] && msg+="Details: $details"
|
||||
msg+="Data transfer: ${status_line}\n"
|
||||
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||
```
|
||||
|
||||
Envoi du message :
|
||||
|
||||
```bash
|
||||
curl -fsS -H "Content-Type: application/json" \
|
||||
-d "{\"content\":\"$msg\"}" \
|
||||
"$WEBHOOK_URL"
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
Le message indique :
|
||||
@@ -237,7 +290,27 @@ Le message indique :
|
||||
|
||||
---
|
||||
|
||||
# 10. Planification avec cron
|
||||
# 10. Rotation distante des sauvegardes
|
||||
|
||||
Le script supprime les archives distantes plus anciennes que la durée de retention configurée.
|
||||
|
||||
Configuration dans `.env` :
|
||||
|
||||
```bash
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
```
|
||||
|
||||
Commande utilisée :
|
||||
|
||||
```bash
|
||||
find "$REMOTE_DIR" -type f -name 'vaultwarden-backup-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
```
|
||||
|
||||
Si la variable n'est pas définie, le script utilise `10` jours par défaut.
|
||||
|
||||
---
|
||||
|
||||
# 11. Planification avec cron
|
||||
|
||||
Le script est exécuté automatiquement tous les jours à 19h.
|
||||
|
||||
@@ -250,7 +323,7 @@ crontab -e
|
||||
Ajouter :
|
||||
|
||||
```bash
|
||||
0 19 * * * /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh >> /var/log/vaultwarden_backup.log 2>&1
|
||||
0 19 * * * /home/<USER>/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh 2>&1
|
||||
```
|
||||
|
||||
Signification :
|
||||
@@ -267,29 +340,29 @@ Le script s’exécute donc **tous les jours à 19h00**.
|
||||
|
||||
---
|
||||
|
||||
# 11. Nettoyage
|
||||
# 12. Nettoyage
|
||||
|
||||
Une fois la sauvegarde transférée :
|
||||
|
||||
```bash
|
||||
rm -f "$LOCAL_BACKUP"
|
||||
rm -f "$LOCAL_BACKUP_FILE"
|
||||
```
|
||||
|
||||
Cela évite de remplir le disque de la machine Vaultwarden.
|
||||
|
||||
---
|
||||
|
||||
# 12. Test manuel
|
||||
# 13. Test manuel
|
||||
|
||||
Avant de mettre le script en cron, tester :
|
||||
|
||||
```bash
|
||||
bash /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
|
||||
bash /home/<USER>/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 13. Vérification des logs
|
||||
# 14. Vérification des logs
|
||||
|
||||
Logs :
|
||||
|
||||
@@ -299,7 +372,7 @@ cat /var/log/vaultwarden_backup.log
|
||||
|
||||
---
|
||||
|
||||
# 14. Résumé
|
||||
# 15. Résumé
|
||||
|
||||
Le script automatise :
|
||||
|
||||
@@ -312,4 +385,3 @@ Le script automatise :
|
||||
|
||||
Ce système permet d’obtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
|
||||
|
||||
```
|
||||
|
||||
96
BackupVaultWarden/backup-vaultwarden.sh
Normal file → Executable file
96
BackupVaultWarden/backup-vaultwarden.sh
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
#######################################
|
||||
# Chemins fixes du script
|
||||
@@ -27,6 +28,7 @@ log() {
|
||||
# Chargement du .env
|
||||
#######################################
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
@@ -39,7 +41,15 @@ set +a
|
||||
: "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}"
|
||||
: "${REMOTE_HOST:?Variable REMOTE_HOST manquante dans .env}"
|
||||
: "${REMOTE_DIR:?Variable REMOTE_DIR manquante dans .env}"
|
||||
[[ "$REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||
echo "ERROR: Variable REMOTE_DIR invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${SSH_KEY:?Variable SSH_KEY manquante dans .env}"
|
||||
: "${BACKUP_REMOTE_SSH_PORT:=22}"
|
||||
: "${SSH_CONNECT_TIMEOUT:=10}"
|
||||
: "${BACKUP_KNOWN_HOSTS_STRICT:=yes}"
|
||||
: "${BACKUP_KNOWN_HOSTS_FILE:=${HOME}/.ssh/known_hosts}"
|
||||
|
||||
#######################################
|
||||
# Variables backup
|
||||
@@ -50,18 +60,67 @@ BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
|
||||
LOCAL_BACKUP_FILE="${LOCAL_BACKUP}/${BACKUP_NAME}"
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
|
||||
|
||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable BACKUP_REMOTE_SSH_PORT invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$SSH_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable SSH_CONNECT_TIMEOUT invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable BACKUP_RETENTION_DAYS invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||
*)
|
||||
echo "ERROR: Variable BACKUP_KNOWN_HOSTS_STRICT invalide dans .env" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||
)
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||
)
|
||||
|
||||
mkdir -p "$LOCAL_BACKUP"
|
||||
|
||||
#######################################
|
||||
# Notification Discord
|
||||
#######################################
|
||||
discord_ping() {
|
||||
send_discord() {
|
||||
local success="$1"
|
||||
local details="${2:-}"
|
||||
local payload=""
|
||||
|
||||
[[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0
|
||||
require_cmd jq || return 0
|
||||
require_cmd curl || return 0
|
||||
|
||||
local icon status_line
|
||||
if [[ "$success" == "true" ]]; then
|
||||
@@ -80,7 +139,6 @@ discord_ping() {
|
||||
msg+="Data transfer: ${status_line}\n"
|
||||
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||
}
|
||||
@@ -91,15 +149,41 @@ discord_ping() {
|
||||
fail() {
|
||||
local detail="$1"
|
||||
log "ERROR: $detail"
|
||||
discord_ping "false" "$detail"
|
||||
send_discord "false" "$detail"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Verrou d'execution
|
||||
#######################################
|
||||
LOCK_DIR="/tmp/vaultwarden_backup.lock.d"
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
fail "Backup deja en cours"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -f "${LOCAL_BACKUP_FILE:-}"
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Vérifications préalables
|
||||
#######################################
|
||||
[[ -d "$DATA_DIR" ]] || fail "Le dossier source n'existe pas : $DATA_DIR"
|
||||
[[ -f "$SSH_KEY" ]] || fail "La clé SSH est introuvable : $SSH_KEY"
|
||||
[[ -r "$SSH_KEY" ]] || fail "La clé SSH est non lisible : $SSH_KEY"
|
||||
[[ ! -L "$SSH_KEY" ]] || fail "La clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||
chmod 600 "$SSH_KEY" || true
|
||||
|
||||
for cmd in tar ssh scp jq curl find; do
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
|
||||
log "Début du backup Vaultwarden"
|
||||
log "Source : $DATA_DIR"
|
||||
@@ -123,7 +207,7 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
|
||||
#######################################
|
||||
# Envoi du backup
|
||||
#######################################
|
||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
||||
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
||||
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
|
||||
|
||||
log "Backup envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||
@@ -146,5 +230,5 @@ rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOC
|
||||
# Fin
|
||||
#######################################
|
||||
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||
discord_ping "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
||||
send_discord "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
||||
echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,43 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
Liste des évolutions du projet Scripts Serveur
|
||||
Ce projet suit le format [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
|
||||
et applique le versionnement semantique.
|
||||
|
||||
## [1.0.0]
|
||||
### Parameters
|
||||
Ajouter dans le fichier /RecetteScripts/.env
|
||||
SSH_TIMEOUT
|
||||
BACKUP_LOG_DIR
|
||||
APP_LOG_DIR
|
||||
DISCORD_WEBHOOK_URL
|
||||
DISCORD_PING
|
||||
CHECK_CONNECT_TIMEOUT
|
||||
CHECK_MAX_TIME
|
||||
APP_URLS
|
||||
## [Unreleased]
|
||||
|
||||
Ajouter dans le fichier /CheckStorage/.env
|
||||
WEBHOOK_URL
|
||||
|
||||
Ajouter dans le fichier /BackupVaultWarden/.env
|
||||
DATA_DIR
|
||||
LOCAL_BACKUP
|
||||
REMOTE_USER
|
||||
REMOTE_HOST
|
||||
REMOTE_DIR
|
||||
SSH_KEY
|
||||
|
||||
### Added
|
||||
* [#361] Script dump BDD
|
||||
* [#367] Avoir une notification discord quand les backup sont faites
|
||||
* [#368] Script pour check que les applis ne sont pas hors-ligne
|
||||
* first push
|
||||
* Reorganisation des fichiers et dossiers
|
||||
* [#372] Script de check si la machine a le stockage plein
|
||||
* [#378] Script Backup BDD Vaultwarden
|
||||
* [#381] Variabiliser tous les scripts
|
||||
* [#384] Fix Correctif
|
||||
* [#390] Rotation des script
|
||||
* [#392] Scripts de reconstruction des bases de données
|
||||
* [#427] Correctifs
|
||||
### Changed
|
||||
- Harmonisation des fonctions d'envoi Discord dans les scripts de sauvegarde, de supervision et de reconstruction.
|
||||
- Ajout d'un fichier de log dedie a l'orchestrateur `RebuildBdd/run-rebuild-bdd.sh`.
|
||||
- Renforcement de la journalisation et de la gestion des erreurs dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||
|
||||
### Fixed
|
||||
- Nettoyage du backup local temporaire dans `BackupVaultWarden/backup-vaultwarden.sh` en sortie de script.
|
||||
- Gestion plus robuste des dependances shell et des verifications de disponibilite PostgreSQL dans les scripts `RebuildBdd`.
|
||||
- Acceptation explicite du flag `--non-interactive` dans `RebuildBdd/Checkup/check-target-readiness.sh` pour compatibilite de workflow.
|
||||
- Validation des noms de base et refus des cles SSH symboliques dans les scripts de reconstruction.
|
||||
- Suppression des notifications Discord fragiles quand `jq` ou `curl` sont absents, avec envoi silencieux en secours.
|
||||
- Detection et nettoyage des verrous perimes dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||
- Filtrage des privileges `SUPERUSER` lors de la restauration des roles dans `RecetteScripts/rebuild-bdd-recette.sh`.
|
||||
|
||||
## [1.0.0] - 2026-03-18
|
||||
|
||||
### Added
|
||||
- Ajout des scripts legacy de sauvegarde PostgreSQL, de verification de statut applicatif et de supervision du stockage.
|
||||
- Ajout du script de sauvegarde Vaultwarden.
|
||||
- Ajout des scripts de reconstruction de bases PostgreSQL dans `RebuildBdd/`.
|
||||
- Ajout du bootstrap cible, du precheck et de l'orchestration de reconstruction.
|
||||
- Ajout du support d'utilisation via une interface web avec sorties JSON.
|
||||
- Ajout des fichiers d'exemple de configuration pour les scripts et les cibles.
|
||||
|
||||
### Changed
|
||||
- Reorganisation des fichiers et des dossiers du projet.
|
||||
- Variabilisation des scripts via des fichiers `.env`.
|
||||
- Ajout de la rotation des sauvegardes.
|
||||
- Harmonisation progressive de la documentation et des exemples de configuration.
|
||||
- Ajout du bit executable sur les scripts qui doivent etre lancables directement.
|
||||
|
||||
### Fixed
|
||||
- Correctifs multiples sur les scripts legacy et sur le flux `RebuildBdd`.
|
||||
- Corrections de chemins de configuration et de telechargement des dumps.
|
||||
- Corrections de messages Discord, de revue de code et d'orthographe dans la documentation.
|
||||
- Correctifs sur la preparation PostgreSQL, `scp`, `sudoers` et le reperage des environnements cibles.
|
||||
|
||||
@@ -4,3 +4,9 @@
|
||||
|
||||
# Webhook Discord pour notifications
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
# Mention Discord en cas d'alerte
|
||||
DISCORD_PING=@here
|
||||
|
||||
# Seuil d'alerte en pourcentage d'utilisation
|
||||
STORAGE_ALERT_LIMIT=70
|
||||
|
||||
@@ -1,65 +1,82 @@
|
||||
# Scripts de vérification de l'espace de stockage
|
||||
# CheckStorage
|
||||
|
||||
Ce projet contient des scripts pour vérifier l'espace de stockage
|
||||
Script de vérification de l’espace disque sur Ubuntu Server, avec notification Discord optionnelle.
|
||||
|
||||
## Préambule
|
||||
Ce script est conçu pour vérifier l'espace de stockage disponible sur un serveur et envoyer une alerte
|
||||
La vérification de l'espace de stockage ce fait sur la partition racine.
|
||||
La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cette valeur dans le script selon vos besoins.
|
||||
## Fonctionnement
|
||||
|
||||
## Installation du script
|
||||
Le script :
|
||||
|
||||
1. Clonez le dépôt GitHub :
|
||||
```bash
|
||||
git clone https://gitea.malio.fr/MALIO-DEV/Scripts-Serveur.git
|
||||
1. charge `.env`
|
||||
2. lit l’utilisation de la partition `/`
|
||||
3. compare le taux d’occupation au seuil configuré
|
||||
4. envoie une alerte Discord si le seuil est dépassé
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 3</summary>
|
||||
|
||||
Quel operateur shell permet d'envoyer la sortie d'une commande vers la suivante ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 3</summary>
|
||||
|
||||
```text
|
||||
|
|
||||
```
|
||||
|
||||
2. Accédez au répertoire du projet :
|
||||
3. ```bash
|
||||
cd Scripts-Serveur/CheckStorage
|
||||
```
|
||||
### Génération de la clé SSH
|
||||
</details>
|
||||
|
||||
Sur la machine exécutant les scripts :
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 3</summary>
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/check_storage_key
|
||||
```
|
||||
Copier la clé sur le serveur distant :
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/check_storage_key.pub user@serveur
|
||||
```
|
||||
Tester la connexion sans mot de passe :
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/check_storage_key <USER>@<HOST>
|
||||
```
|
||||
## Utilisation du script
|
||||
0. Copiez le fichier d'environnement exemple et modifiez les variables selon votre configuration :
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```text
|
||||
b3llciB2b2ljaSB1biBsaWVuIG1hZ2lxdWUgZW
|
||||
```
|
||||
|
||||
1. Donnez les permissions d'exécution au script :
|
||||
</details>
|
||||
|
||||
</details>
|
||||
## Pré-requis
|
||||
|
||||
Installation recommandée sur Ubuntu Server :
|
||||
|
||||
```bash
|
||||
chmod +x check-storage.sh
|
||||
sudo apt update
|
||||
sudo apt install -y coreutils gawk jq curl
|
||||
```
|
||||
2. Exécutez le script pour vérifier l'espace de stockage :
|
||||
|
||||
`jq` et `curl` ne sont nécessaires que si `DISCORD_WEBHOOK_URL` est renseigné.
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
cp .env.exemple .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
Variables disponibles :
|
||||
|
||||
- `DISCORD_WEBHOOK_URL` : webhook Discord, optionnel
|
||||
- `DISCORD_PING` : mention en cas d’alerte, optionnel, défaut `@here`
|
||||
- `STORAGE_ALERT_LIMIT` : seuil d’alerte en pourcentage, défaut `70`
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
chmod 700 check-storage.sh
|
||||
./check-storage.sh
|
||||
```
|
||||
|
||||
## Initialisé un cron pour exécuter le script régulièrement
|
||||
1. Ouvrez le crontab pour l'édition :
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
|
||||
```bash
|
||||
50 7 * * * /chemin/vers/le/script/check-storage.sh
|
||||
```
|
||||
## Cron
|
||||
|
||||
## Avertissement
|
||||
Assurez-vous de remplacer `/chemin/vers/le/script/check-storage.sh` par le chemin réel où se trouve le script sur votre système.
|
||||
Exemple quotidien à `07:50` :
|
||||
|
||||
```bash
|
||||
50 7 * * * /chemin/vers/CheckStorage/check-storage.sh
|
||||
```
|
||||
|
||||
55
CheckStorage/check-storage.sh
Normal file → Executable file
55
CheckStorage/check-storage.sh
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# CHARGEMENT DU .env
|
||||
@@ -22,8 +23,48 @@ set +a
|
||||
# CONFIGURATION
|
||||
###############################################################################
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "ERROR: commande requise absente : $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Limite maximale d'utilisation du disque en pourcentage
|
||||
limit=70
|
||||
limit="${STORAGE_ALERT_LIMIT:-70}"
|
||||
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
[[ "$limit" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: STORAGE_ALERT_LIMIT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
(( limit >= 1 && limit <= 99 )) || {
|
||||
echo "ERROR: STORAGE_ALERT_LIMIT doit être compris entre 1 et 99" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd df
|
||||
require_cmd awk
|
||||
|
||||
if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
|
||||
require_cmd jq
|
||||
require_cmd curl
|
||||
fi
|
||||
|
||||
send_discord() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "${DISCORD_WEBHOOK_URL:-}" ]] || return 0
|
||||
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||
|
||||
curl -fsS \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# RÉCUPÉRATION DES INFORMATIONS DISQUE
|
||||
@@ -48,15 +89,9 @@ avail_gb=$(awk -v b="$avail_bytes" 'BEGIN {printf "%.2f", b/1024/1024/1024}')
|
||||
|
||||
if [ "$usage" -ge "$limit" ]; then
|
||||
|
||||
msgLimit="@here\n**CHECK STOCKAGE :red_circle:**\nLimite autorisé : ${limit}%\nUtilisation actuelle: ${usage}%\nEspace restant: ${free}%\nUtilise / total: ${used_gb} GB / ${total_gb} GB\nDisponible: ${avail_gb} GB\nHeure: $(date)"
|
||||
msgLimit="${DISCORD_PING}\n**CHECK STOCKAGE :red_circle:**\nLimite autorisée : ${limit}%\nUtilisation actuelle : ${usage}%\nEspace restant : ${free}%\nUtilisé / total : ${used_gb} GB / ${total_gb} GB\nDisponible : ${avail_gb} GB\nHeure : $(date)"
|
||||
|
||||
payload="$(jq -n --arg content "$msgLimit" '{content: $content}')"
|
||||
|
||||
curl -X POST \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json; charset=utf-8" \
|
||||
-d "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
send_discord "$msgLimit"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
258
README.md
258
README.md
@@ -1,56 +1,242 @@
|
||||
# Malio-Ops
|
||||
# MALIO-OPS
|
||||
|
||||
Ce depot centralise les scripts d'exploitation et de maintenance utilises pour l'infrastructure MALIO. Il sert de base de versionnement pour les sauvegardes, la supervision, les operations PostgreSQL et la reconstruction de bases.
|
||||
Ce dépôt centralise l’ensemble des **scripts d’exploitation, de maintenance et d’automatisation** utilisés dans l’infrastructure MALIO.
|
||||
Il constitue une base unique de **versionnement, standardisation et industrialisation** des opérations techniques (backup, supervision, PostgreSQL, reconstruction de bases).
|
||||
|
||||
## Objectif
|
||||
---
|
||||
|
||||
Le depot permet de :
|
||||
# Objectif
|
||||
|
||||
* suivre les modifications des scripts dans le temps
|
||||
* conserver des versions stables et reproductibles
|
||||
* mutualiser la configuration et les bonnes pratiques d'exploitation
|
||||
* centraliser la documentation technique associee
|
||||
Le dépôt permet de :
|
||||
|
||||
## Structure du depot
|
||||
* assurer le **versionnement et la traçabilité** des scripts
|
||||
* garantir des **exécutions reproductibles et fiables**
|
||||
* mutualiser les **bonnes pratiques d’exploitation**
|
||||
* centraliser la **configuration et la documentation technique**
|
||||
* faciliter le **déploiement automatisé des environnements**
|
||||
|
||||
Le depot est organise par dossier fonctionnel :
|
||||
---
|
||||
|
||||
* [CheckStorage](CheckStorage) : surveillance de l'espace disque et alertes Discord
|
||||
* [BackupVaultWarden](BackupVaultWarden) : sauvegarde et transfert distant des donnees Vaultwarden
|
||||
* [RecetteScripts](RecetteScripts) : scripts historiques de backup, monitoring et rebuild pour l'environnement de recette
|
||||
* [RebuildBdd](RebuildBdd) : orchestration de reconstruction de bases PostgreSQL, bootstrap de cibles et checks de preparation
|
||||
# Structure du dépôt
|
||||
|
||||
## Focus RebuildBdd
|
||||
Organisation par domaine fonctionnel :
|
||||
|
||||
Le dossier [RebuildBdd](RebuildBdd) regroupe la nouvelle chaine de reconstruction de base. Il contient notamment :
|
||||
## CheckStorage
|
||||
|
||||
* [run-rebuild-bdd.sh](/home/matte/Malio-ops/RebuildBdd/run-rebuild-bdd.sh) : point d'entree principal
|
||||
* [rebuild-bdd-core.sh](/home/matte/Malio-ops/RebuildBdd/rebuild-bdd-core.sh) : logique de restauration
|
||||
* [bootstrap-target-host.sh](/home/matte/Malio-ops/RebuildBdd/bootstrap-target-host.sh) : preparation de la machine cible
|
||||
* [create-target-config.sh](/home/matte/Malio-ops/RebuildBdd/create-target-config.sh) : generation de configuration cible
|
||||
* [Checkup](RebuildBdd/Checkup) : scripts de verification prealable
|
||||
* [Config](RebuildBdd/Config) : fichiers d'exemple de configuration globale et par cible
|
||||
* Surveillance de l’espace disque
|
||||
* Alerting via Discord
|
||||
* Vérification proactive des capacités
|
||||
|
||||
La documentation detaillee est disponible dans [RebuildBdd/README.md](/home/matte/Malio-ops/RebuildBdd/README.md).
|
||||
## BackupVaultWarden
|
||||
|
||||
## Prerequis
|
||||
* Sauvegarde des données Vaultwarden
|
||||
* Archivage et transfert distant sécurisé
|
||||
* Gestion des logs et rétention
|
||||
|
||||
Les scripts du depot reposent principalement sur :
|
||||
## RecetteScripts
|
||||
|
||||
* `bash`
|
||||
* `jq`
|
||||
* `curl`
|
||||
* `ssh`
|
||||
* `scp`
|
||||
* Scripts legacy pour environnement de recette
|
||||
* Backup PostgreSQL
|
||||
* Monitoring applicatif
|
||||
* Rebuild simplifié
|
||||
|
||||
Selon les scripts, d'autres outils peuvent etre necessaires, notamment PostgreSQL (`psql`, `pg_dump`, `pg_restore`) ou `tar`.
|
||||
⚠️ Dossier en cours de transition vers `RebuildBdd`
|
||||
|
||||
## Configuration
|
||||
## RebuildBdd
|
||||
|
||||
Un modele commun est disponible dans [global.env.exemple](global.env.exemple). Il sert de base pour les variables partagees entre plusieurs scripts.
|
||||
* Nouvelle architecture standardisée de reconstruction PostgreSQL
|
||||
* Gestion multi-cibles
|
||||
* Exécution non interactive (compatible web/API)
|
||||
* Retour structuré (JSON + logs)
|
||||
|
||||
Chaque dossier peut aussi contenir son propre fichier `.env.exemple` ou ses propres fichiers de configuration. Les secrets et webhooks ne doivent jamais etre versionnes dans git et doivent rester dans des fichiers locaux ignores.
|
||||
---
|
||||
|
||||
## Documentation
|
||||
# Focus : RebuildBdd
|
||||
|
||||
Ce module constitue le **socle principal de reconstruction de bases PostgreSQL**.
|
||||
|
||||
## Scripts principaux
|
||||
|
||||
* `run-rebuild-bdd.sh`
|
||||
→ Point d’entrée (local / IA / interface web)
|
||||
|
||||
* `rebuild-bdd-core.sh`
|
||||
→ Logique métier de restauration (dump + rôles + base)
|
||||
|
||||
* `bootstrap-target-host.sh`
|
||||
→ Préparation automatique de la machine cible
|
||||
|
||||
* `create-target-config.sh`
|
||||
→ Génération des configurations par environnement
|
||||
|
||||
## Sous-dossiers
|
||||
|
||||
* `Checkup/`
|
||||
→ Vérification des prérequis (PostgreSQL, accès, rôles)
|
||||
|
||||
* `Config/`
|
||||
→ Fichiers de configuration globaux et par cible
|
||||
|
||||
## Fonctionnalités clés
|
||||
|
||||
* Installation automatique PostgreSQL si absent
|
||||
* Vérification et création des rôles
|
||||
* Restauration complète (rôles + base)
|
||||
* Gestion des dumps distants
|
||||
* Mode sécurisé (validation des paramètres)
|
||||
* Logs exploitables + sortie JSON (intégration web)
|
||||
|
||||
👉 Documentation détaillée : `RebuildBdd/README.md`
|
||||
|
||||
---
|
||||
|
||||
# Prérequis
|
||||
|
||||
Les scripts nécessitent :
|
||||
|
||||
## Outils de base
|
||||
|
||||
* bash
|
||||
* jq
|
||||
* curl
|
||||
* ssh
|
||||
* scp
|
||||
|
||||
## Outils optionnels (selon usage)
|
||||
|
||||
* PostgreSQL (`psql`, `pg_dump`, `pg_restore`)
|
||||
* tar
|
||||
* systemd (gestion services)
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Configuration globale
|
||||
|
||||
Un modèle est fourni :
|
||||
|
||||
```bash
|
||||
global.env.exemple
|
||||
```
|
||||
|
||||
Ce fichier concerne la configuration legacy de `RecetteScripts`.
|
||||
|
||||
Utilisation :
|
||||
|
||||
```bash
|
||||
cp global.env.exemple global.env
|
||||
```
|
||||
|
||||
Pour la configuration de `RebuildBdd`, voir la documentation dédiée :
|
||||
|
||||
```bash
|
||||
RebuildBdd/README.md
|
||||
```
|
||||
|
||||
## Configuration locale
|
||||
|
||||
* Chaque module peut contenir son propre `.env`
|
||||
* Les variables sensibles doivent être définies localement
|
||||
|
||||
⚠️ Règles strictes :
|
||||
|
||||
* **Aucun secret en versionné**
|
||||
* Utiliser `.gitignore`
|
||||
* Cloisonner les accès (SSH, DB, webhooks)
|
||||
|
||||
---
|
||||
|
||||
# Sécurité
|
||||
|
||||
* Authentification SSH par clé obligatoire
|
||||
* Validation des paramètres en entrée (scripts rebuild)
|
||||
* Isolation des environnements (cibles distinctes)
|
||||
* Logs sans données sensibles
|
||||
* Validation stricte des hôtes SSH recommandée et utilisée par défaut sur les scripts durcis
|
||||
* Permissions restrictives recommandées sur les `.env`, clés privées et `known_hosts`
|
||||
|
||||
## Déploiement Ubuntu Server
|
||||
|
||||
Le dépôt est maintenant pensé prioritairement pour des cibles **Ubuntu Server** :
|
||||
|
||||
* bootstrap `RebuildBdd` basé sur `apt`, `systemctl` et `sudo -n`
|
||||
* clients SSH en mode batch avec `StrictHostKeyChecking=yes` quand le mode strict est actif
|
||||
* exemples `.env` mis à jour pour expliciter les ports SSH, `known_hosts` et les timeouts
|
||||
|
||||
---
|
||||
|
||||
# Bonnes pratiques
|
||||
|
||||
* Scripts **idempotents** (relançables sans effet de bord)
|
||||
* Logs systématiques
|
||||
* Gestion des erreurs (`set -euo pipefail`)
|
||||
* Centralisation des configurations
|
||||
* Utilisation de formats structurés (JSON)
|
||||
|
||||
---
|
||||
|
||||
# Documentation
|
||||
|
||||
* Documentation détaillée par module (README locaux)
|
||||
* Historique des évolutions :
|
||||
|
||||
```bash
|
||||
CHANGELOG.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Positionnement actuel
|
||||
|
||||
* `RebuildBdd` = **standard cible**
|
||||
* `RecetteScripts` = **legacy en cours de migration**
|
||||
* Objectif : convergence vers une **chaîne unique, robuste et automatisable (web/API)**
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
Un message est disperse dans les `README` du depot.
|
||||
|
||||
Ordre de reconstruction :
|
||||
|
||||
1. `README.md`
|
||||
2. `BackupVaultWarden/README.md`
|
||||
3. `CheckStorage/README.md`
|
||||
4. `RebuildBdd/README.md`
|
||||
5. `RecetteScripts/README.md`
|
||||
|
||||
La commande de dechiffrement n'est pas donnee directement.
|
||||
Elle se reconstruit aussi via des questions cachees dans les `README`.
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 1</summary>
|
||||
|
||||
Quelle commande shell permet d'afficher exactement une chaine, sans interpretation particuliere, avant de la transmettre a une autre commande ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 1</summary>
|
||||
|
||||
```text
|
||||
printf
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 1</summary>
|
||||
|
||||
```text
|
||||
YmllbiB2dSB0dSBtJ2FzIHRyb3V2ZXIgbW9pIG
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
Les evolutions importantes sont suivies dans [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
226
RebuildBdd/Checkup/check-postgresql.sh
Executable file
226
RebuildBdd/Checkup/check-postgresql.sh
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
DEFAULT_ENV_FILE="${REPO_DIR}/.env"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||
CLI_REQUEST_ID=""
|
||||
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env-file)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--request-id)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||
CLI_REQUEST_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu : $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
postgres_server_ready() {
|
||||
has_cmd postgres || return 1
|
||||
has_cmd pg_ctlcluster || return 1
|
||||
has_cmd pg_lsclusters || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_postgres_cluster() {
|
||||
if ! has_cmd pg_lsclusters || ! has_cmd pg_createcluster; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if pg_lsclusters --no-header 2>/dev/null | grep -q .; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local version=""
|
||||
if [[ -d /etc/postgresql ]]; then
|
||||
version="$(find /etc/postgresql -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | LC_ALL=C sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -z "$version" ]] && has_cmd psql; then
|
||||
version="$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)"
|
||||
fi
|
||||
|
||||
[[ -n "$version" ]] || return 1
|
||||
|
||||
log "Aucun cluster PostgreSQL détecté, création de ${version}/main..."
|
||||
"$SUDO_BIN" pg_createcluster "$version" main --start >/dev/null 2>&1 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
collect_postgres_diagnostics() {
|
||||
local diagnostics=""
|
||||
|
||||
if "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager >/dev/null 2>&1; then
|
||||
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: OK; "
|
||||
elif has_cmd systemctl; then
|
||||
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: $( "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager 2>/dev/null | tail -n 5 | tr '\n' ' ' ); "
|
||||
fi
|
||||
|
||||
if has_cmd pg_lsclusters; then
|
||||
diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); "
|
||||
fi
|
||||
|
||||
if has_cmd journalctl; then
|
||||
diagnostics+="journalctl: $( "$SUDO_BIN" journalctl -u "$POSTGRES_SERVICE_NAME" -n 10 --no-pager 2>/dev/null | tr '\n' ' ' ); "
|
||||
fi
|
||||
|
||||
printf '%s' "${diagnostics% }"
|
||||
}
|
||||
|
||||
start_postgres_service() {
|
||||
if "$SUDO_BIN" systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_cmd pg_lsclusters && has_cmd pg_ctlcluster; then
|
||||
local version cluster
|
||||
while read -r version cluster _; do
|
||||
[[ -n "$version" && -n "$cluster" ]] || continue
|
||||
if "$SUDO_BIN" pg_ctlcluster "$version" "$cluster" start >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
done < <(pg_lsclusters --no-header 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
|
||||
|
||||
AUTO_INSTALL_POSTGRES="${AUTO_INSTALL_POSTGRES:-yes}"
|
||||
AUTO_CREATE_PGUSER="${AUTO_CREATE_PGUSER:-yes}"
|
||||
PGUSER_SUPERUSER="${PGUSER_SUPERUSER:-no}"
|
||||
POSTGRES_PACKAGE_LIST="${POSTGRES_PACKAGE_LIST:-postgresql postgresql-client postgresql-contrib}"
|
||||
POSTGRES_SERVICE_NAME="${POSTGRES_SERVICE_NAME:-postgresql}"
|
||||
SUDO_BIN="${SUDO_BIN:-sudo}"
|
||||
read -r -a POSTGRES_PACKAGES <<< "$POSTGRES_PACKAGE_LIST"
|
||||
|
||||
[[ "${#POSTGRES_PACKAGES[@]}" -gt 0 ]] || fail "POSTGRES_PACKAGE_LIST vide"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
if ! has_cmd "$SUDO_BIN"; then
|
||||
fail "sudo absent sur la cible"
|
||||
fi
|
||||
|
||||
if ! "$SUDO_BIN" /usr/bin/systemctl --version >/dev/null 2>&1; then
|
||||
fail "sudo indisponible pour systemctl"
|
||||
fi
|
||||
|
||||
if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then
|
||||
fail "PGPORT invalide : $PGPORT"
|
||||
fi
|
||||
|
||||
POSTGRES_INSTALLED="no"
|
||||
|
||||
if ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb || ! postgres_server_ready; then
|
||||
[[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no"
|
||||
|
||||
log "PostgreSQL absent : installation en cours..."
|
||||
"$SUDO_BIN" apt update >/dev/null 2>&1 || fail "échec de apt update"
|
||||
"$SUDO_BIN" apt install -y "${POSTGRES_PACKAGES[@]}" >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||
POSTGRES_INSTALLED="yes"
|
||||
log "Installation PostgreSQL terminée."
|
||||
else
|
||||
log "PostgreSQL déjà installé."
|
||||
fi
|
||||
|
||||
ensure_postgres_cluster || fail "aucun cluster PostgreSQL disponible et création automatique impossible"
|
||||
|
||||
if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then
|
||||
log "Démarrage du service PostgreSQL..."
|
||||
if ! start_postgres_service; then
|
||||
fail "impossible de démarrer PostgreSQL. $(collect_postgres_diagnostics)"
|
||||
fi
|
||||
else
|
||||
log "Service PostgreSQL déjà actif."
|
||||
fi
|
||||
|
||||
log "Vérification de la disponibilité de PostgreSQL..."
|
||||
PG_READY=false
|
||||
for _ in {1..20}; do
|
||||
if "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
PG_READY=true
|
||||
log "PostgreSQL répond correctement."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "$PG_READY" != true ]]; then
|
||||
fail "PostgreSQL ne répond pas correctement"
|
||||
fi
|
||||
|
||||
if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then
|
||||
ROLE_EXISTS="$(
|
||||
"$SUDO_BIN" -u postgres psql -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||
log "Création du rôle PostgreSQL ${PGUSER}..."
|
||||
|
||||
ROLE_ATTRIBUTES="LOGIN CREATEDB CREATEROLE"
|
||||
if [[ "${PGUSER_SUPERUSER,,}" == "yes" ]]; then
|
||||
ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE"
|
||||
fi
|
||||
|
||||
"$SUDO_BIN" -u postgres psql -d postgres -c \
|
||||
"CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} PASSWORD '${PGPASSWORD//\'/\'\'}';" \
|
||||
>/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}"
|
||||
|
||||
log "Rôle PostgreSQL ${PGUSER} créé."
|
||||
else
|
||||
log "Rôle PostgreSQL ${PGUSER} déjà présent."
|
||||
fi
|
||||
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
|
||||
|
||||
log "Check PostgreSQL terminé avec succès."
|
||||
347
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
347
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
###############################################################################
|
||||
# check-target-readiness.sh
|
||||
#
|
||||
# Prépare la machine cible pour permettre l'exécution non interactive du
|
||||
# script de rebuild depuis une interface web.
|
||||
###############################################################################
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
DEFAULT_ENV_FILE="${REPO_DIR}/.env"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||
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"
|
||||
shift 2
|
||||
;;
|
||||
--request-id)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||
CLI_REQUEST_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
# Flag accepté pour compatibilité avec les autres scripts du workflow.
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
--json-only)
|
||||
JSON_ONLY="yes"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu : $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
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 '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
|
||||
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
|
||||
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
|
||||
printf '}\n'
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
print_stdout() {
|
||||
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||
}
|
||||
|
||||
LOG_FILE=/dev/stderr
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
echo "$msg" >>"$LOG_FILE"
|
||||
print_stdout "$msg"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*"
|
||||
print_json_and_exit "error" "$*" 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
to_bool_yes_no() {
|
||||
local v="${1:-}"
|
||||
v="${v,,}"
|
||||
case "$v" in
|
||||
yes|y|oui|o|true|1) echo "yes" ;;
|
||||
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||
no|n|non|false|0|"") echo "no" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_env_vars() {
|
||||
local missing=()
|
||||
local var
|
||||
|
||||
for var in \
|
||||
ENV_NAME PGHOST PGPORT PGUSER PGPASSWORD DBS \
|
||||
BACKUP_REMOTE_USER BACKUP_REMOTE_HOST BACKUP_REMOTE_DIR \
|
||||
SSH_KEY BACKUP_LOG_DIR
|
||||
do
|
||||
[[ -n "${!var:-}" ]] || missing+=("$var")
|
||||
done
|
||||
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
fail "variables .env manquantes : ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_env_values() {
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME invalide"
|
||||
[[ -n "$DBS" ]] || fail "DBS vide"
|
||||
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
|
||||
[[ -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_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||
to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT" >/dev/null || fail "BACKUP_KNOWN_HOSTS_STRICT invalide"
|
||||
}
|
||||
|
||||
prepare_log_file() {
|
||||
mkdir -p "$BACKUP_LOG_DIR" || {
|
||||
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
local ts safe_request_id
|
||||
ts="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||
safe_request_id="${REQUEST_ID:-manual}"
|
||||
safe_request_id="${safe_request_id//[^a-zA-Z0-9_.-]/_}"
|
||||
|
||||
LOG_FILE="${BACKUP_LOG_DIR}/check_target_${ENV_NAME,,}_${safe_request_id}_${ts}.log"
|
||||
touch "$LOG_FILE" || {
|
||||
echo '{"status":"error","message":"impossible de créer le fichier de log"}'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
prepare_local_paths() {
|
||||
local restore_base
|
||||
restore_base="${LOCAL_RESTORE_BASE_DIR:-${REPO_DIR}/restore_tmp}"
|
||||
|
||||
mkdir -p "$BACKUP_LOG_DIR" || fail "création BACKUP_LOG_DIR impossible"
|
||||
[[ -w "$BACKUP_LOG_DIR" ]] || fail "BACKUP_LOG_DIR non inscriptible"
|
||||
|
||||
mkdir -p "$restore_base" || fail "création LOCAL_RESTORE_BASE_DIR impossible"
|
||||
[[ -w "$restore_base" ]] || fail "LOCAL_RESTORE_BASE_DIR non inscriptible"
|
||||
|
||||
log "Dossiers locaux prêts."
|
||||
}
|
||||
|
||||
prepare_scripts_permissions() {
|
||||
local core_script check_pg_script
|
||||
core_script="${REPO_DIR}/rebuild-bdd-core.sh"
|
||||
check_pg_script="${REPO_DIR}/Checkup/check-postgresql.sh"
|
||||
|
||||
[[ -f "$core_script" ]] || fail "script core introuvable : $core_script"
|
||||
[[ -f "$check_pg_script" ]] || fail "script PostgreSQL introuvable : $check_pg_script"
|
||||
|
||||
chmod 700 "$core_script" || fail "chmod impossible sur $core_script"
|
||||
chmod 700 "$check_pg_script" || fail "chmod impossible sur $check_pg_script"
|
||||
|
||||
log "Permissions scripts corrigées."
|
||||
}
|
||||
|
||||
prepare_ssh_key() {
|
||||
local key_dir
|
||||
key_dir="$(dirname "$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"
|
||||
|
||||
[[ -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" ]] || 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_dir="$(dirname "$SSH_KEY")"
|
||||
known_hosts="${ssh_dir}/known_hosts"
|
||||
|
||||
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
|
||||
if [[ "$(to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT")" == "yes" ]]; then
|
||||
fail "hôte ${BACKUP_REMOTE_HOST} absent de known_hosts en mode strict ; provisionner l'empreinte manuellement"
|
||||
fi
|
||||
|
||||
log "Ajout non strict 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."
|
||||
fi
|
||||
}
|
||||
|
||||
test_backup_ssh() {
|
||||
local ssh_timeout
|
||||
ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}"
|
||||
|
||||
ssh \
|
||||
-i "$SSH_KEY" \
|
||||
-p "$BACKUP_REMOTE_SSH_PORT" \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o BatchMode=yes \
|
||||
-o ConnectTimeout="$ssh_timeout" \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
"${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" \
|
||||
"test -d '$BACKUP_REMOTE_DIR'" \
|
||||
>>"$LOG_FILE" 2>&1 || \
|
||||
fail "connexion SSH backup impossible, clé non autorisée, ou dossier distant absent"
|
||||
|
||||
log "Connexion SSH backup validée."
|
||||
}
|
||||
|
||||
install_sudoers_if_allowed() {
|
||||
local auto_configure sudoers_file tmp_file
|
||||
auto_configure="$(to_bool_yes_no "${AUTO_CONFIGURE_SUDOERS:-no}")" || fail "AUTO_CONFIGURE_SUDOERS invalide"
|
||||
|
||||
if [[ "$auto_configure" != "yes" ]]; then
|
||||
log "Installation sudoers automatique désactivée."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! sudo true >/dev/null 2>&1; then
|
||||
fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo n'est pas disponible ; configuration initiale manuelle requise"
|
||||
fi
|
||||
|
||||
require_cmd visudo
|
||||
|
||||
sudoers_file="/etc/sudoers.d/rebuild-bdd-${USER}"
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
cat >"$tmp_file" <<EOF
|
||||
${USER} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||
${USER} ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||
EOF
|
||||
|
||||
chmod 440 "$tmp_file"
|
||||
|
||||
visudo -cf "$tmp_file" >/dev/null 2>&1 || {
|
||||
rm -f "$tmp_file"
|
||||
fail "fichier sudoers généré invalide"
|
||||
}
|
||||
|
||||
sudo install -m 440 "$tmp_file" "$sudoers_file" || {
|
||||
rm -f "$tmp_file"
|
||||
fail "impossible d'installer $sudoers_file"
|
||||
}
|
||||
|
||||
rm -f "$tmp_file"
|
||||
log "Fichier sudoers installé : $sudoers_file"
|
||||
}
|
||||
|
||||
check_sudo_non_interactive() {
|
||||
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || \
|
||||
fail "sudo indisponible pour systemctl"
|
||||
|
||||
log "sudo pour systemctl validé."
|
||||
|
||||
if command -v apt >/dev/null 2>&1; then
|
||||
sudo /usr/bin/apt --version >/dev/null 2>&1 || \
|
||||
fail "sudo indisponible pour apt"
|
||||
log "sudo pour apt validé."
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
sudo /usr/bin/apt-get --version >/dev/null 2>&1 || \
|
||||
fail "sudo indisponible pour apt-get"
|
||||
log "sudo pour apt-get validé."
|
||||
else
|
||||
fail "ni apt ni apt-get disponibles sur la cible"
|
||||
fi
|
||||
|
||||
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
|
||||
fail "sudo -u postgres indisponible pour psql"
|
||||
|
||||
log "sudo -u postgres pour psql validé."
|
||||
}
|
||||
|
||||
run_postgresql_check() {
|
||||
local check_script
|
||||
check_script="${REPO_DIR}/Checkup/check-postgresql.sh"
|
||||
|
||||
[[ -x "$check_script" ]] || fail "script introuvable ou non exécutable : $check_script"
|
||||
|
||||
"$check_script" \
|
||||
--env-file "$ENV_FILE" \
|
||||
--request-id "$REQUEST_ID" \
|
||||
--non-interactive \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL"
|
||||
|
||||
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
|
||||
fail "sudo -u postgres indisponible après préparation PostgreSQL"
|
||||
log "Préparation PostgreSQL validée."
|
||||
}
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || {
|
||||
echo '{"status":"error","message":"fichier .env introuvable"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
|
||||
|
||||
require_env_vars
|
||||
validate_env_values
|
||||
prepare_log_file
|
||||
|
||||
require_cmd bash
|
||||
require_cmd python3
|
||||
require_cmd ssh
|
||||
require_cmd ssh-keygen
|
||||
require_cmd ssh-keyscan
|
||||
require_cmd sudo
|
||||
|
||||
prepare_local_paths
|
||||
prepare_scripts_permissions
|
||||
prepare_ssh_key
|
||||
prepare_known_hosts
|
||||
test_backup_ssh
|
||||
install_sudoers_if_allowed
|
||||
check_sudo_non_interactive
|
||||
run_postgresql_check
|
||||
|
||||
log "Machine cible prête pour le rebuild."
|
||||
print_json_and_exit "success" "machine cible prête" 0
|
||||
38
RebuildBdd/Config/.env.exemple
Normal file
38
RebuildBdd/Config/.env.exemple
Normal 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=<BACKUP_HOST>
|
||||
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/<LOCAL_USER>/.ssh/id_ed25519_backup_readonly
|
||||
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/<LOCAL_USER>/.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
|
||||
42
RebuildBdd/Config/Targets/prod.env.exemple
Normal file
42
RebuildBdd/Config/Targets/prod.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
||||
###############################################################################
|
||||
# config/targets/prod.env.exemple
|
||||
###############################################################################
|
||||
|
||||
# SSH bootstrap cible
|
||||
TARGET_HOST=<TARGET_HOST>
|
||||
TARGET_PORT=22
|
||||
TARGET_BOOTSTRAP_USER=<BOOTSTRAP_USER>
|
||||
TARGET_BOOTSTRAP_SSH_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_target_prod
|
||||
TARGET_RUNTIME_USER=<RUNTIME_USER>
|
||||
|
||||
# Bootstrap
|
||||
TARGET_ENABLE_BOOTSTRAP=yes
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||
|
||||
# Repo local cible
|
||||
TARGET_REPO_DIR=/home/<RUNTIME_USER>/RebuildBdd
|
||||
TARGET_ENV_FILE=/home/<RUNTIME_USER>/RebuildBdd/.env
|
||||
|
||||
# PostgreSQL cible
|
||||
TARGET_ENV_NAME=PROD
|
||||
TARGET_PGHOST=127.0.0.1
|
||||
TARGET_PGPORT=5432
|
||||
TARGET_PGUSER=<PGUSER>
|
||||
TARGET_PGPASSWORD=change_me_pg_password
|
||||
TARGET_DBS="sirh inventory ferme"
|
||||
|
||||
# Backup cible
|
||||
TARGET_BACKUP_SUBDIR=bdd-prod
|
||||
|
||||
# Logs / tmp / ssh cible
|
||||
TARGET_BACKUP_LOG_DIR=/home/<RUNTIME_USER>/logs/rebuild_bdd
|
||||
TARGET_LOCAL_RESTORE_BASE_DIR=/home/<RUNTIME_USER>/RebuildBdd/restore_tmp
|
||||
TARGET_SSH_KEY=/home/<RUNTIME_USER>/.ssh/id_ed25519_backup_readonly
|
||||
|
||||
# Options cible
|
||||
TARGET_REMOTE_ROLES_DIR_NAME=user
|
||||
TARGET_EXCLUDED_RESTORE_ROLES="postgres"
|
||||
TARGET_AUTO_INSTALL_POSTGRES=yes
|
||||
TARGET_AUTO_CREATE_PGUSER=yes
|
||||
TARGET_PGUSER_SUPERUSER=no
|
||||
TARGET_AUTO_CONFIGURE_SUDOERS=no
|
||||
42
RebuildBdd/Config/Targets/test.env.exemple
Normal file
42
RebuildBdd/Config/Targets/test.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
||||
###############################################################################
|
||||
# config/targets/test.env.exemple
|
||||
###############################################################################
|
||||
|
||||
# SSH bootstrap cible
|
||||
TARGET_HOST=<TARGET_HOST>
|
||||
TARGET_PORT=22
|
||||
TARGET_BOOTSTRAP_USER=<BOOTSTRAP_USER>
|
||||
TARGET_BOOTSTRAP_SSH_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_target_test
|
||||
TARGET_RUNTIME_USER=<RUNTIME_USER>
|
||||
|
||||
# Bootstrap
|
||||
TARGET_ENABLE_BOOTSTRAP=yes
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||
|
||||
# Repo local cible
|
||||
TARGET_REPO_DIR=/home/<RUNTIME_USER>/RebuildBdd
|
||||
TARGET_ENV_FILE=/home/<RUNTIME_USER>/RebuildBdd/.env
|
||||
|
||||
# PostgreSQL cible
|
||||
TARGET_ENV_NAME=RECETTE
|
||||
TARGET_PGHOST=127.0.0.1
|
||||
TARGET_PGPORT=5432
|
||||
TARGET_PGUSER=<PGUSER>
|
||||
TARGET_PGPASSWORD=change_me_pg_password
|
||||
TARGET_DBS="sirh inventory ferme"
|
||||
|
||||
# Backup cible
|
||||
TARGET_BACKUP_SUBDIR=bdd-recette
|
||||
|
||||
# Logs / tmp / ssh cible
|
||||
TARGET_BACKUP_LOG_DIR=/home/<RUNTIME_USER>/logs/rebuild_bdd
|
||||
TARGET_LOCAL_RESTORE_BASE_DIR=/home/<RUNTIME_USER>/RebuildBdd/restore_tmp
|
||||
TARGET_SSH_KEY=/home/<RUNTIME_USER>/.ssh/id_ed25519_backup_readonly
|
||||
|
||||
# Options cible
|
||||
TARGET_REMOTE_ROLES_DIR_NAME=user
|
||||
TARGET_EXCLUDED_RESTORE_ROLES="postgres"
|
||||
TARGET_AUTO_INSTALL_POSTGRES=yes
|
||||
TARGET_AUTO_CREATE_PGUSER=yes
|
||||
TARGET_PGUSER_SUPERUSER=no
|
||||
TARGET_AUTO_CONFIGURE_SUDOERS=no
|
||||
612
RebuildBdd/README.md
Normal file
612
RebuildBdd/README.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 4</summary>
|
||||
|
||||
Quel utilitaire standard permet de decoder la chaine reconstituee ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 4</summary>
|
||||
|
||||
```text
|
||||
base64
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 4</summary>
|
||||
|
||||
```text
|
||||
4gcmVjb21wZW5zZSBodHRwczovL3d3dy55b3V0
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## 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 <TARGET_HOST> \
|
||||
--port 22 \
|
||||
--bootstrap-user <BOOTSTRAP_USER> \
|
||||
--bootstrap-key /home/user/.ssh/id_ed25519_target_test \
|
||||
--runtime-user <RUNTIME_USER> \
|
||||
--repo-dir /home/<RUNTIME_USER>/RebuildBdd \
|
||||
--env-name RECETTE \
|
||||
--pguser <PGUSER> \
|
||||
--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`
|
||||
* `jq` si vous consommez les JSON côté tooling
|
||||
|
||||
### Machine cible
|
||||
|
||||
Le bootstrap suppose :
|
||||
|
||||
* accès SSH fonctionnel ;
|
||||
* utilisateur bootstrap existant ;
|
||||
* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial ;
|
||||
* `known_hosts` correctement provisionné si le mode strict SSH est activé vers le serveur de backup.
|
||||
|
||||
### Serveur de backup
|
||||
|
||||
Doit :
|
||||
|
||||
* être joignable en SSH depuis la cible ;
|
||||
* accepter la clé de lecture backup ;
|
||||
* contenir les dumps dans l’arborescence attendue.
|
||||
|
||||
## Sécurité / déploiement
|
||||
|
||||
### Clés hôtes SSH
|
||||
|
||||
Si `GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes`, l’empreinte du serveur de backup doit déjà être présente dans le `known_hosts` de la cible. Le bootstrap et les checks ne font plus d’ajout aveugle en mode strict.
|
||||
|
||||
### Répertoires de clone
|
||||
|
||||
Les scripts refusent maintenant les chemins de clone manifestement dangereux comme `/`, `/root`, `/home` ou `/home/<user>` pour éviter un `rm -rf` destructeur dû à une mauvaise configuration.
|
||||
|
||||
### Ubuntu Server
|
||||
|
||||
Le flux de bootstrap est pensé pour des cibles Ubuntu Server avec `apt`, `systemctl` et `sudo -n`.
|
||||
|
||||
---
|
||||
|
||||
## 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/.env.exemple
|
||||
```
|
||||
|
||||
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.exemple
|
||||
```
|
||||
|
||||
#### 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/<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/<RUNTIME_USER>/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/<RUNTIME_USER>/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
|
||||
<user> ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||
<user> 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/<RUNTIME_USER>/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 <TARGET_HOST> \
|
||||
--port 22 \
|
||||
--bootstrap-user <BOOTSTRAP_USER> \
|
||||
--bootstrap-key /home/<LOCAL_USER>/.ssh/id_ed25519_target_test \
|
||||
--runtime-user <RUNTIME_USER> \
|
||||
--repo-dir /home/<RUNTIME_USER>/RebuildBdd \
|
||||
--env-name RECETTE \
|
||||
--pguser <PGUSER> \
|
||||
--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.
|
||||
597
RebuildBdd/bootstrap-target-host.sh
Executable file
597
RebuildBdd/bootstrap-target-host.sh
Executable file
@@ -0,0 +1,597 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||
GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env"
|
||||
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||
GIT_TOPLEVEL="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
LOCAL_REPO_SUBDIR_DEFAULT=""
|
||||
|
||||
if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then
|
||||
LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}"
|
||||
fi
|
||||
|
||||
GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}"
|
||||
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||
|
||||
TARGET_NAME="${TARGET_NAME:-}"
|
||||
CLI_TARGET=""
|
||||
JSON_ONLY="${JSON_ONLY:-no}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
|
||||
CLI_TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--json-only)
|
||||
JSON_ONLY="yes"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu : $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
json_escape() {
|
||||
python3 - <<'PY' "$1"
|
||||
import json, sys
|
||||
print(json.dumps(sys.argv[1]))
|
||||
PY
|
||||
}
|
||||
|
||||
print_stdout() {
|
||||
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||
}
|
||||
|
||||
log() {
|
||||
print_stdout "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local msg="$1"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
printf '{"status":%s,"message":%s}\n' \
|
||||
"$(json_escape "error")" \
|
||||
"$(json_escape "$msg")"
|
||||
else
|
||||
echo "ERROR: $msg" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
success() {
|
||||
local msg="$1"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
printf '{"status":%s,"message":%s}\n' \
|
||||
"$(json_escape "success")" \
|
||||
"$(json_escape "$msg")"
|
||||
else
|
||||
log "$msg"
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
to_bool_yes_no() {
|
||||
local v="${1:-}"
|
||||
v="${v,,}"
|
||||
case "$v" in
|
||||
yes|y|oui|o|true|1) echo "yes" ;;
|
||||
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||
no|n|non|false|0|"") echo "no" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "%q" "$1"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "${TMP_ENV_FILE:-}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
copy_file_to_remote_via_ssh() {
|
||||
local local_file="$1"
|
||||
local remote_final_path="$2"
|
||||
local remote_mode="$3"
|
||||
local remote_parent
|
||||
local remote_tmp
|
||||
|
||||
[[ -f "$local_file" ]] || fail "fichier source introuvable : $local_file"
|
||||
[[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file"
|
||||
|
||||
remote_parent="$(dirname "$remote_final_path")"
|
||||
remote_tmp="/tmp/bootstrap_copy.$$.$RANDOM.tmp"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||
set -euo pipefail
|
||||
mkdir -p $(shell_quote "$remote_parent")
|
||||
test -d $(shell_quote "$remote_parent")
|
||||
test -w $(shell_quote "$remote_parent")
|
||||
" >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||
set -euo pipefail
|
||||
cat > $(shell_quote "$remote_tmp")
|
||||
" < "$local_file" >/dev/null 2>&1 || fail "échec d'écriture temporaire distante : $remote_tmp"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||
set -euo pipefail
|
||||
install -m $(shell_quote "$remote_mode") $(shell_quote "$remote_tmp") $(shell_quote "$remote_final_path")
|
||||
rm -f $(shell_quote "$remote_tmp")
|
||||
" >/dev/null 2>&1 || fail "échec d'installation distante : $remote_final_path"
|
||||
}
|
||||
|
||||
TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}"
|
||||
[[ -n "$TARGET_NAME" ]] || fail "target manquante"
|
||||
|
||||
TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET_NAME}.env"
|
||||
|
||||
[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE"
|
||||
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$GLOBAL_ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
source "$TARGET_ENV_SOURCE"
|
||||
set +a
|
||||
|
||||
BOOTSTRAP_HOST="${TARGET_HOST:-}"
|
||||
BOOTSTRAP_PORT="${TARGET_PORT:-22}"
|
||||
BOOTSTRAP_USER="${TARGET_BOOTSTRAP_USER:-}"
|
||||
BOOTSTRAP_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}"
|
||||
|
||||
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_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}"
|
||||
TARGET_ENV_FILE_PATH="${TARGET_ENV_FILE:-}"
|
||||
|
||||
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:-}"
|
||||
|
||||
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 "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"
|
||||
|
||||
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}"
|
||||
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}"
|
||||
|
||||
TARGET_CLONE_DIR="$TARGET_REPO_DIR"
|
||||
TARGET_SCRIPT_DIR="$TARGET_REPO_DIR"
|
||||
if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||
if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then
|
||||
TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")"
|
||||
else
|
||||
TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}"
|
||||
fi
|
||||
fi
|
||||
|
||||
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
|
||||
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
|
||||
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
done
|
||||
|
||||
[[ -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 "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_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 python3
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$BOOTSTRAP_SSH_KEY"
|
||||
-p "$BOOTSTRAP_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o ConnectTimeout=8
|
||||
)
|
||||
|
||||
REMOTE="${BOOTSTRAP_USER}@${BOOTSTRAP_HOST}"
|
||||
|
||||
log "Test de connexion SSH bootstrap vers ${REMOTE}:${BOOTSTRAP_PORT}"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "exit 0" >/dev/null 2>&1 \
|
||||
|| fail "connexion SSH bootstrap impossible vers ${REMOTE}"
|
||||
|
||||
REMOTE_SETUP_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
run_root() {
|
||||
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||
\"\$@\"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo \"\$@\" || {
|
||||
echo 'sudo 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_CLONE_DIR")")
|
||||
mkdir -p $(shell_quote "$(dirname "$TARGET_SCRIPT_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"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" >/dev/null \
|
||||
|| fail "échec de préparation système distante"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \
|
||||
|| fail "échec de préparation système distante"
|
||||
fi
|
||||
|
||||
TMP_ENV_FILE="$(mktemp)"
|
||||
|
||||
cat >"$TMP_ENV_FILE" <<EOF
|
||||
ENV_NAME=$(shell_quote "$TARGET_ENV_NAME_VALUE")
|
||||
PGHOST=$(shell_quote "$TARGET_PGHOST_VALUE")
|
||||
PGPORT=$(shell_quote "$TARGET_PGPORT_VALUE")
|
||||
PGUSER=$(shell_quote "$TARGET_PGUSER_VALUE")
|
||||
PGPASSWORD=$(shell_quote "$TARGET_PGPASSWORD_VALUE")
|
||||
DBS=$(shell_quote "$TARGET_DBS_VALUE")
|
||||
|
||||
BACKUP_REMOTE_USER=$(shell_quote "$TARGET_BACKUP_REMOTE_USER_VALUE")
|
||||
BACKUP_REMOTE_HOST=$(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE")
|
||||
BACKUP_REMOTE_DIR=$(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
||||
BACKUP_REMOTE_SSH_PORT=$(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE")
|
||||
|
||||
BACKUP_LOG_DIR=$(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
|
||||
LOCAL_RESTORE_BASE_DIR=$(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
||||
REMOTE_ROLES_DIR_NAME=$(shell_quote "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE")
|
||||
SSH_KEY=$(shell_quote "$TARGET_SSH_KEY_VALUE")
|
||||
BACKUP_KNOWN_HOSTS_STRICT=$(shell_quote "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE")
|
||||
|
||||
AUTO_INSTALL_POSTGRES=$(shell_quote "$TARGET_AUTO_INSTALL_POSTGRES_VALUE")
|
||||
AUTO_CREATE_PGUSER=$(shell_quote "$TARGET_AUTO_CREATE_PGUSER_VALUE")
|
||||
PGUSER_SUPERUSER=$(shell_quote "$TARGET_PGUSER_SUPERUSER_VALUE")
|
||||
AUTO_CONFIGURE_SUDOERS=$(shell_quote "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE")
|
||||
EXCLUDED_RESTORE_ROLES=$(shell_quote "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE")
|
||||
EOF
|
||||
|
||||
log "Copie du .env cible"
|
||||
copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600"
|
||||
|
||||
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"
|
||||
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600"
|
||||
|
||||
if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then
|
||||
log "Copie de la clé publique backup sur la cible"
|
||||
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644"
|
||||
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"
|
||||
|
||||
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_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
|
||||
if [[ $(shell_quote "$STRICT_OPTION") == yes ]]; then
|
||||
echo 'hôte backup absent de known_hosts en mode strict ; empreinte à provisionner manuellement' >&2
|
||||
exit 1
|
||||
fi
|
||||
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"
|
||||
|
||||
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_PRECHECK_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
command -v sudo >/dev/null 2>&1 || exit 1
|
||||
sudo true </dev/null >/dev/null 2>&1
|
||||
"
|
||||
|
||||
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 \"\$@\" || {
|
||||
echo 'sudo 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 minimal"
|
||||
if ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_PRECHECK_CMD" >/dev/null 2>&1; then
|
||||
log "Installation du sudoers ignorée : élévation de privilèges indisponible sans interaction."
|
||||
elif ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" >/dev/null 2>&1; then
|
||||
log "Installation du sudoers ignorée : privilèges root/sudo insuffisants pour cette étape."
|
||||
fi
|
||||
else
|
||||
log "Installation du sudoers minimal désactivée."
|
||||
fi
|
||||
|
||||
REMOTE_REPO_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! -d $(shell_quote "${TARGET_CLONE_DIR}/.git") ]]; then
|
||||
if [[ $(shell_quote "$TARGET_CLONE_DIR") == / || $(shell_quote "$TARGET_CLONE_DIR") == /root || $(shell_quote "$TARGET_CLONE_DIR") == /home || $(shell_quote "$TARGET_CLONE_DIR") =~ ^/home/[^/]+$ ]]; then
|
||||
echo 'TARGET_CLONE_DIR dangereux refusé' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf $(shell_quote "$TARGET_CLONE_DIR")
|
||||
git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_CLONE_DIR")
|
||||
else
|
||||
git -C $(shell_quote "$TARGET_CLONE_DIR") fetch --prune origin
|
||||
git -C $(shell_quote "$TARGET_CLONE_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH")
|
||||
git -C $(shell_quote "$TARGET_CLONE_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH")
|
||||
fi
|
||||
|
||||
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") 2>/dev/null || true
|
||||
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") 2>/dev/null || true
|
||||
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true
|
||||
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true
|
||||
|
||||
for required_file in \
|
||||
$(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") \
|
||||
$(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") \
|
||||
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") \
|
||||
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh"); do
|
||||
if [[ ! -f \"\$required_file\" ]]; then
|
||||
echo \"fichier requis absent après synchronisation du dépôt : \$required_file\" >&2
|
||||
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
"
|
||||
|
||||
log "Clone / mise à jour du dépôt distant"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" >/dev/null \
|
||||
|| fail "échec de synchronisation du dépôt sur la cible"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \
|
||||
|| fail "échec de synchronisation du dépôt sur la cible"
|
||||
fi
|
||||
|
||||
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 /usr/bin/systemctl --version >/dev/null 2>&1 || {
|
||||
echo 'sudo indisponible pour systemctl' >&2
|
||||
exit 1
|
||||
}
|
||||
"
|
||||
|
||||
log "Validation initiale de sudo"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \
|
||||
|| fail "sudo invalide sur la cible"
|
||||
|
||||
REMOTE_RUN_CHECK_PG_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
CHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-postgresql.sh")
|
||||
ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH")
|
||||
|
||||
[[ -f \"\$CHECK_SCRIPT\" ]] || {
|
||||
echo \"script PostgreSQL introuvable : \$CHECK_SCRIPT\" >&2
|
||||
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR") et TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR")\" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\"
|
||||
|
||||
\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive
|
||||
"
|
||||
|
||||
log "Préparation PostgreSQL via check-postgresql.sh"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" >/dev/null \
|
||||
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \
|
||||
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
|
||||
fi
|
||||
|
||||
REMOTE_VALIDATE_SUDO_POSTGRES_CMD="
|
||||
set -euo pipefail
|
||||
sudo -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || {
|
||||
echo 'sudo -u postgres indisponible après préparation PostgreSQL' >&2
|
||||
exit 1
|
||||
}
|
||||
"
|
||||
|
||||
log "Validation finale de sudo -u postgres"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \
|
||||
|| fail "sudo -u postgres invalide sur la cible"
|
||||
|
||||
success "bootstrap initial terminé pour ${TARGET_NAME}"
|
||||
161
RebuildBdd/create-target-config.sh
Normal file
161
RebuildBdd/create-target-config.sh
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/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" ;;
|
||||
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||
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=$(printf '%q' "$HOST")
|
||||
TARGET_PORT=$(printf '%q' "$PORT")
|
||||
TARGET_BOOTSTRAP_USER=$(printf '%q' "$BOOTSTRAP_USER")
|
||||
TARGET_BOOTSTRAP_SSH_KEY=$(printf '%q' "$BOOTSTRAP_SSH_KEY")
|
||||
TARGET_RUNTIME_USER=$(printf '%q' "$RUNTIME_USER")
|
||||
|
||||
TARGET_ENABLE_BOOTSTRAP=$(printf '%q' "$ENABLE_BOOTSTRAP")
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=$(printf '%q' "$ALLOW_PASSWORDLESS_SUDO")
|
||||
|
||||
TARGET_REPO_DIR=$(printf '%q' "$REPO_DIR")
|
||||
TARGET_ENV_FILE=$(printf '%q' "$ENV_FILE")
|
||||
|
||||
TARGET_ENV_NAME=$(printf '%q' "$ENV_NAME")
|
||||
TARGET_PGHOST=$(printf '%q' "$PGHOST")
|
||||
TARGET_PGPORT=$(printf '%q' "$PGPORT")
|
||||
TARGET_PGUSER=$(printf '%q' "$PGUSER")
|
||||
TARGET_PGPASSWORD=$(printf '%q' "$PGPASSWORD")
|
||||
TARGET_DBS=$(printf '%q' "$DBS")
|
||||
|
||||
TARGET_BACKUP_SUBDIR=$(printf '%q' "$BACKUP_SUBDIR")
|
||||
|
||||
TARGET_BACKUP_LOG_DIR=$(printf '%q' "$BACKUP_LOG_DIR")
|
||||
TARGET_LOCAL_RESTORE_BASE_DIR=$(printf '%q' "$LOCAL_RESTORE_BASE_DIR")
|
||||
TARGET_SSH_KEY=$(printf '%q' "$SSH_KEY_TARGET_PATH")
|
||||
|
||||
TARGET_REMOTE_ROLES_DIR_NAME=$(printf '%q' "$REMOTE_ROLES_DIR_NAME")
|
||||
TARGET_EXCLUDED_RESTORE_ROLES=$(printf '%q' "$EXCLUDED_RESTORE_ROLES")
|
||||
TARGET_AUTO_INSTALL_POSTGRES=$(printf '%q' "$AUTO_INSTALL_POSTGRES")
|
||||
TARGET_AUTO_CREATE_PGUSER=$(printf '%q' "$AUTO_CREATE_PGUSER")
|
||||
TARGET_PGUSER_SUPERUSER=$(printf '%q' "$PGUSER_SUPERUSER")
|
||||
TARGET_AUTO_CONFIGURE_SUDOERS=$(printf '%q' "$AUTO_CONFIGURE_SUDOERS")
|
||||
EOF
|
||||
|
||||
chmod 600 "$TARGET_FILE" || fail "chmod impossible sur $TARGET_FILE"
|
||||
|
||||
echo "OK: ${TARGET_FILE}"
|
||||
519
RebuildBdd/rebuild-bdd-core.sh
Executable file
519
RebuildBdd/rebuild-bdd-core.sh
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||
|
||||
CLI_DB=""
|
||||
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"
|
||||
shift 2
|
||||
;;
|
||||
--db)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
|
||||
CLI_DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--overwrite)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
|
||||
CLI_OVERWRITE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--restore-roles)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
|
||||
CLI_RESTORE_ROLES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--request-id)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||
CLI_REQUEST_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
--json-only)
|
||||
JSON_ONLY="yes"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu : $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
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 '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
|
||||
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
|
||||
printf '"database":%s,' "$(json_escape "${DB:-}")"
|
||||
printf '"dump_file":%s,' "$(json_escape "${LAST_REMOTE_DB_DUMP:-}")"
|
||||
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
|
||||
printf '}\n'
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
print_stdout() {
|
||||
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||
}
|
||||
|
||||
LOG_FILE=/dev/stderr
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
echo "$msg" >>"$LOG_FILE"
|
||||
print_stdout "$msg"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*"
|
||||
print_json_and_exit "error" "$*" 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
download_remote_file() {
|
||||
local remote_path="$1"
|
||||
local local_path="$2"
|
||||
local local_dir
|
||||
|
||||
local_dir="$(dirname "$local_path")"
|
||||
mkdir -p "$local_dir" || fail "impossible de créer le dossier local de restauration : $local_dir"
|
||||
|
||||
if scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Téléchargement scp standard échoué, tentative avec scp -O"
|
||||
scp -O "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1
|
||||
}
|
||||
|
||||
to_bool_yes_no() {
|
||||
local v="${1:-}"
|
||||
v="${v,,}"
|
||||
case "$v" in
|
||||
yes|y|oui|o|true|1) echo "yes" ;;
|
||||
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||
no|n|non|false|0|"") echo "no" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_tty() {
|
||||
[[ -t 0 && -t 1 ]]
|
||||
}
|
||||
|
||||
sql_escape_literal() {
|
||||
local s="${1:-}"
|
||||
s="${s//\'/\'\'}"
|
||||
printf "%s" "$s"
|
||||
}
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="${1:-}"
|
||||
|
||||
[[ -n "$db_name" ]] || return 1
|
||||
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
|
||||
}
|
||||
|
||||
build_excluded_roles_regex() {
|
||||
local roles_string="${1:-}"
|
||||
local role
|
||||
local -a escaped_roles=()
|
||||
|
||||
read -r -a roles_array <<< "$roles_string"
|
||||
|
||||
for role in "${roles_array[@]}"; do
|
||||
[[ -n "$role" ]] || continue
|
||||
[[ "$role" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || continue
|
||||
escaped_roles+=("$role")
|
||||
done
|
||||
|
||||
if [[ "${#escaped_roles[@]}" -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local joined=""
|
||||
local first="yes"
|
||||
for role in "${escaped_roles[@]}"; do
|
||||
if [[ "$first" == "yes" ]]; then
|
||||
joined="$role"
|
||||
first="no"
|
||||
else
|
||||
joined+="|$role"
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s' "$joined"
|
||||
}
|
||||
|
||||
send_discord() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||
has_cmd jq || return 0
|
||||
has_cmd curl || return 0
|
||||
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||
|
||||
curl -fsS "$DISCORD_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f \
|
||||
"${LOCAL_DB_DUMP_FILE:-}" \
|
||||
"${LOCAL_ROLES_FILE:-}" \
|
||||
"${FILTERED_ROLES_FILE:-}" \
|
||||
"${ROLES_CREATE_LIST:-}" \
|
||||
"${ROLES_APPLY_FILE:-}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || {
|
||||
echo '{"status":"error","message":"fichier .env cible introuvable"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
|
||||
: "${DBS:?Variable DBS manquante}"
|
||||
: "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}"
|
||||
: "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}"
|
||||
: "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}"
|
||||
: "${SSH_KEY:?Variable SSH_KEY manquante}"
|
||||
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||
|
||||
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}"
|
||||
|
||||
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
|
||||
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
|
||||
ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}"
|
||||
RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}"
|
||||
|
||||
ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || {
|
||||
echo '{"status":"error","message":"ALLOW_OVERWRITE invalide"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || {
|
||||
echo '{"status":"error","message":"RESTORE_ROLES invalide"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME 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
|
||||
}
|
||||
|
||||
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||
SAFE_REQUEST_ID="${REQUEST_ID:-manual}"
|
||||
SAFE_REQUEST_ID="${SAFE_REQUEST_ID//[^a-zA-Z0-9_.-]/_}"
|
||||
|
||||
LOG_FILE="${BACKUP_LOG_DIR}/restore_${ENV_NAME,,}_${SAFE_REQUEST_ID}_${TIMESTAMP}.log"
|
||||
touch "$LOG_FILE" || {
|
||||
echo '{"status":"error","message":"impossible de créer le log"}'
|
||||
exit 1
|
||||
}
|
||||
|
||||
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_BASE_DIR}/${SAFE_REQUEST_ID}_${TIMESTAMP}"
|
||||
mkdir -p "$LOCAL_RESTORE_DIR" || fail "impossible de créer le dossier temporaire local"
|
||||
|
||||
EXCLUDED_ROLES_REGEX=""
|
||||
if EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex "$EXCLUDED_RESTORE_ROLES")"; then
|
||||
log "Rôles exclus de la restauration : $EXCLUDED_RESTORE_ROLES"
|
||||
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 curl; do
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
|
||||
CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh"
|
||||
if [[ -x "$CHECK_SCRIPT" ]]; then
|
||||
log "Précheck PostgreSQL déjà effectué par check-target-readiness.sh"
|
||||
else
|
||||
fail "script introuvable ou non exécutable : $CHECK_SCRIPT"
|
||||
fi
|
||||
|
||||
[[ -f "$SSH_KEY" ]] || fail "clé SSH source backup introuvable : $SSH_KEY"
|
||||
[[ -r "$SSH_KEY" ]] || fail "clé SSH source backup non lisible : $SSH_KEY"
|
||||
[[ ! -L "$SSH_KEY" ]] || fail "clé SSH source backup ne doit pas être un lien symbolique : $SSH_KEY"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
|
||||
read -r -a DBS_ARRAY <<< "$DBS"
|
||||
[[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "aucune base définie dans DBS"
|
||||
|
||||
if [[ -z "$REQUESTED_DB" ]]; then
|
||||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||
fail "REQUESTED_DB manquante en mode non interactif"
|
||||
fi
|
||||
|
||||
if is_tty; then
|
||||
print_stdout "Bases disponibles :"
|
||||
for i in "${!DBS_ARRAY[@]}"; do
|
||||
print_stdout " $((i + 1))) ${DBS_ARRAY[$i]}"
|
||||
done
|
||||
echo
|
||||
read -r -p "Sélectionnez le numéro de la base à restaurer : " DB_INDEX
|
||||
[[ "$DB_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de base invalide"
|
||||
(( DB_INDEX >= 1 && DB_INDEX <= ${#DBS_ARRAY[@]} )) || fail "numéro hors plage"
|
||||
REQUESTED_DB="${DBS_ARRAY[$((DB_INDEX - 1))]}"
|
||||
else
|
||||
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
|
||||
fi
|
||||
fi
|
||||
|
||||
DB=""
|
||||
for candidate in "${DBS_ARRAY[@]}"; do
|
||||
if [[ "$candidate" == "$REQUESTED_DB" ]]; then
|
||||
DB="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -n "$DB" ]] || fail "base refusée : non présente dans DBS"
|
||||
validate_db_name "$DB" || fail "nom de base invalide"
|
||||
|
||||
log "Environnement : $ENV_NAME"
|
||||
log "Base cible : $DB"
|
||||
log "Request ID : ${REQUEST_ID:-N/A}"
|
||||
log "Overwrite : $ALLOW_OVERWRITE"
|
||||
log "Restore roles : $RESTORE_ROLES"
|
||||
|
||||
if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" \
|
||||
>>"$LOG_FILE" 2>&1; then
|
||||
fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}"
|
||||
fi
|
||||
|
||||
log "Test SSH vers ${REMOTE_SSH}"
|
||||
if ! ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" "exit 0" >>"$LOG_FILE" 2>&1; then
|
||||
fail "connexion SSH impossible vers ${REMOTE_SSH}"
|
||||
fi
|
||||
|
||||
REMOTE_DB_DIR="${BACKUP_REMOTE_DIR}/${DB}"
|
||||
REMOTE_ROLES_DIR="${BACKUP_REMOTE_DIR}/${REMOTE_ROLES_DIR_NAME}"
|
||||
|
||||
LAST_REMOTE_DB_DUMP="$(
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
|
||||
"find '${REMOTE_DB_DIR}' -maxdepth 1 -type f -name '${DB}_*.dump' | LC_ALL=C sort | tail -n 1"
|
||||
)"
|
||||
|
||||
[[ -n "$LAST_REMOTE_DB_DUMP" ]] || fail "aucun dump trouvé pour ${DB} dans ${REMOTE_DB_DIR}"
|
||||
log "Dernier dump sélectionné : ${LAST_REMOTE_DB_DUMP}"
|
||||
|
||||
LAST_REMOTE_ROLES_FILE=""
|
||||
if [[ "$RESTORE_ROLES" == "yes" ]]; then
|
||||
LAST_REMOTE_ROLES_FILE="$(
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
|
||||
"find '${REMOTE_ROLES_DIR}' -maxdepth 1 -type f -name 'user_*.sql' | LC_ALL=C sort | tail -n 1"
|
||||
)"
|
||||
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||
log "Dernier fichier rôles sélectionné : ${LAST_REMOTE_ROLES_FILE}"
|
||||
else
|
||||
log "Aucun fichier rôles trouvé ; la restauration des rôles sera ignorée."
|
||||
fi
|
||||
else
|
||||
log "Restauration des rôles désactivée."
|
||||
fi
|
||||
|
||||
LOCAL_DB_DUMP_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_DB_DUMP")"
|
||||
LOCAL_ROLES_FILE=""
|
||||
|
||||
log "Téléchargement du dump principal"
|
||||
download_remote_file "$LAST_REMOTE_DB_DUMP" "$LOCAL_DB_DUMP_FILE" \
|
||||
|| fail "échec téléchargement du dump principal"
|
||||
|
||||
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||
LOCAL_ROLES_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_ROLES_FILE")"
|
||||
log "Téléchargement du fichier des rôles"
|
||||
download_remote_file "$LAST_REMOTE_ROLES_FILE" "$LOCAL_ROLES_FILE" \
|
||||
|| fail "échec téléchargement du fichier des rôles"
|
||||
fi
|
||||
|
||||
DB_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='$(sql_escape_literal "$DB")'" \
|
||||
2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$DB_EXISTS" == "1" ]]; then
|
||||
if [[ "$ALLOW_OVERWRITE" != "yes" ]]; then
|
||||
if [[ "$NON_INTERACTIVE" == "yes" || ! -t 0 ]]; then
|
||||
fail "la base existe déjà et overwrite n'est pas autorisé"
|
||||
fi
|
||||
|
||||
read -r -p "La base '${DB}' existe déjà. Voulez-vous l'écraser ? (oui/non) : " CONFIRM_OVERWRITE
|
||||
CONFIRM_OVERWRITE="$(to_bool_yes_no "$CONFIRM_OVERWRITE")" || fail "réponse overwrite invalide"
|
||||
[[ "$CONFIRM_OVERWRITE" == "yes" ]] || fail "restauration annulée par l'utilisateur"
|
||||
fi
|
||||
|
||||
log "Suppression de la base existante : ${DB}"
|
||||
dropdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" --if-exists "$DB" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec suppression base ${DB}"
|
||||
fi
|
||||
|
||||
if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||
log "Restauration des rôles depuis : ${LOCAL_ROLES_FILE}"
|
||||
|
||||
FILTERED_ROLES_FILE="${LOCAL_RESTORE_DIR}/filtered_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_CREATE_LIST="${LOCAL_RESTORE_DIR}/roles_to_create_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_APPLY_FILE="${LOCAL_RESTORE_DIR}/roles_apply_$(basename "$LOCAL_ROLES_FILE")"
|
||||
|
||||
if [[ -n "$EXCLUDED_ROLES_REGEX" ]]; then
|
||||
grep -viE "^(CREATE ROLE|ALTER ROLE) (${EXCLUDED_ROLES_REGEX})\\b" "$LOCAL_ROLES_FILE" \
|
||||
> "$FILTERED_ROLES_FILE" || true
|
||||
else
|
||||
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
|
||||
fi
|
||||
|
||||
# Une exécution sous un rôle non superuser ne peut pas restaurer l'attribut
|
||||
# SUPERUSER ; on ignore donc ces lignes pour laisser passer le reste.
|
||||
sed -i -E '/^ALTER ROLE .* (NO)?SUPERUSER\b/d' "$FILTERED_ROLES_FILE"
|
||||
|
||||
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
|
||||
|
||||
sed -nE 's/^CREATE ROLE "?([^" ;]+)"?;$/\1/p' "$FILTERED_ROLES_FILE" \
|
||||
> "$ROLES_CREATE_LIST" || true
|
||||
|
||||
if [[ -s "$ROLES_CREATE_LIST" ]]; then
|
||||
while IFS= read -r role_name; do
|
||||
[[ -n "$role_name" ]] || continue
|
||||
[[ "$role_name" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
|
||||
log "Rôle ignoré car non conforme : ${role_name}"
|
||||
continue
|
||||
}
|
||||
|
||||
ROLE_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='$(sql_escape_literal "$role_name")'" \
|
||||
2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||
log "Création du rôle manquant : ${role_name}"
|
||||
psql -v ON_ERROR_STOP=1 \
|
||||
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres \
|
||||
-c "CREATE ROLE \"${role_name}\";" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec création rôle ${role_name}"
|
||||
else
|
||||
log "Rôle déjà présent : ${role_name}"
|
||||
fi
|
||||
done < "$ROLES_CREATE_LIST"
|
||||
fi
|
||||
|
||||
grep -viE '^CREATE ROLE ' "$FILTERED_ROLES_FILE" > "$ROLES_APPLY_FILE" || true
|
||||
|
||||
log "Application ALTER ROLE / privilèges / memberships"
|
||||
psql -v ON_ERROR_STOP=1 \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
-d postgres \
|
||||
-f "$ROLES_APPLY_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec restauration rôles"
|
||||
else
|
||||
log "Aucune restauration des rôles effectuée."
|
||||
fi
|
||||
|
||||
log "Création de la base : ${DB}"
|
||||
createdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$DB" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec création base ${DB}"
|
||||
|
||||
log "Restauration de la base ${DB}"
|
||||
pg_restore \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
-d "$DB" \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
"$LOCAL_DB_DUMP_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec restauration base ${DB}"
|
||||
|
||||
SUCCESS_MESSAGE="✅ REBUILD BDD ${ENV_NAME}
|
||||
Base restaurée : ${DB}
|
||||
Hôte PostgreSQL : ${PGHOST}:${PGPORT}
|
||||
Dump utilisé : $(basename "$LAST_REMOTE_DB_DUMP")
|
||||
Log : ${LOG_FILE}"
|
||||
|
||||
send_discord "$SUCCESS_MESSAGE"
|
||||
|
||||
log "Restauration terminée avec succès pour ${DB}"
|
||||
print_json_and_exit "success" "restauration terminée avec succès" 0
|
||||
445
RebuildBdd/run-rebuild-bdd.sh
Executable file
445
RebuildBdd/run-rebuild-bdd.sh
Executable file
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||
GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env"
|
||||
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||
GIT_TOPLEVEL="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
LOCAL_REPO_SUBDIR_DEFAULT=""
|
||||
|
||||
if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then
|
||||
LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}"
|
||||
fi
|
||||
|
||||
GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}"
|
||||
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||
|
||||
CLI_TARGET=""
|
||||
CLI_DB=""
|
||||
CLI_OVERWRITE=""
|
||||
CLI_RESTORE_ROLES=""
|
||||
CLI_REQUEST_ID=""
|
||||
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
|
||||
CLI_TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--db)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
|
||||
CLI_DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--overwrite)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
|
||||
CLI_OVERWRITE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--restore-roles)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
|
||||
CLI_RESTORE_ROLES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--request-id)
|
||||
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||
CLI_REQUEST_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu : $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
to_bool_yes_no() {
|
||||
local v="${1:-}"
|
||||
v="${v,,}"
|
||||
case "$v" in
|
||||
yes|y|oui|o|true|1) echo "yes" ;;
|
||||
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||
no|n|non|false|0|"") echo "no" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_tty() {
|
||||
[[ -t 0 && -t 1 ]]
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "%q" "$1"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "${BOOTSTRAP_JSON:-}" "${REMOTE_RESULT_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 "$GLOBAL_ENV_FILE"
|
||||
set +a
|
||||
|
||||
require_cmd ssh
|
||||
require_cmd git
|
||||
require_cmd python3
|
||||
|
||||
TARGET="${CLI_TARGET:-${TARGET:-}}"
|
||||
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
|
||||
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}}"
|
||||
LOG_DIR="${RUN_REBUILD_BDD_LOG_DIR:-${SCRIPT_DIR}/logs}"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="${LOG_DIR}/run_rebuild_bdd_${REQUEST_ID}.log"
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
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 <= ${#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_ENV_SOURCE="${TARGETS_DIR}/${TARGET}.env"
|
||||
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$TARGET_ENV_SOURCE"
|
||||
set +a
|
||||
|
||||
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_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}"
|
||||
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"
|
||||
|
||||
[[ -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_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}"
|
||||
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}"
|
||||
|
||||
TARGET_CLONE_DIR="$TARGET_REPO_DIR"
|
||||
TARGET_SCRIPT_DIR="$TARGET_REPO_DIR"
|
||||
if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||
if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then
|
||||
TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")"
|
||||
else
|
||||
TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}"
|
||||
fi
|
||||
fi
|
||||
|
||||
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
|
||||
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
|
||||
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||
done
|
||||
|
||||
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
|
||||
else
|
||||
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
|
||||
fi
|
||||
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_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-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_SCRIPT_DIR}/rebuild-bdd-core.sh"
|
||||
REMOTE_RESULT_JSON="/tmp/run_rebuild_bdd_${REQUEST_ID}.json"
|
||||
|
||||
REMOTE_BOOTSTRAP_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
CLONE_DIR=$(shell_quote "$TARGET_CLONE_DIR")
|
||||
REPO_DIR=$(shell_quote "$TARGET_SCRIPT_DIR")
|
||||
REPO_URL=$(shell_quote "$TARGET_REPO_URL")
|
||||
REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")
|
||||
CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT")
|
||||
PRECHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-target-readiness.sh")
|
||||
TARGET_ENV_FILE=$(shell_quote "$TARGET_ENV_FILE")
|
||||
REQUESTED_DB=$(shell_quote "$REQUESTED_DB")
|
||||
ALLOW_OVERWRITE=$(shell_quote "$ALLOW_OVERWRITE")
|
||||
RESTORE_ROLES=$(shell_quote "$RESTORE_ROLES")
|
||||
REQUEST_ID=$(shell_quote "$REQUEST_ID")
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"git absent sur la cible\"}'; exit 1; }
|
||||
command -v bash >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"bash absent sur la cible\"}'; exit 1; }
|
||||
command -v python3 >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"python3 absent sur la cible\"}'; exit 1; }
|
||||
|
||||
mkdir -p \"\$(dirname \"\$CLONE_DIR\")\"
|
||||
mkdir -p \"\$(dirname \"\$REPO_DIR\")\"
|
||||
|
||||
if [[ ! -d \"\$CLONE_DIR/.git\" ]]; then
|
||||
if [[ \"\$CLONE_DIR\" == / || \"\$CLONE_DIR\" == /root || \"\$CLONE_DIR\" == /home || \"\$CLONE_DIR\" =~ ^/home/[^/]+$ ]]; then
|
||||
echo '{\"status\":\"error\",\"message\":\"TARGET_CLONE_DIR dangereux refusé\"}'
|
||||
exit 1
|
||||
fi
|
||||
rm -rf \"\$CLONE_DIR\"
|
||||
git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_DIR\" >/dev/null 2>&1
|
||||
else
|
||||
git -C \"\$CLONE_DIR\" fetch --prune origin >/dev/null 2>&1
|
||||
git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" >/dev/null 2>&1
|
||||
git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
[[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; }
|
||||
[[ -f \"\$PRECHECK_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script précheck introuvable sur la cible\"}'; exit 1; }
|
||||
|
||||
chmod 700 \"\$CORE_SCRIPT\"
|
||||
chmod 700 \"\$PRECHECK_SCRIPT\"
|
||||
|
||||
PRECHECK_JSON=\"/tmp/check_target_\${REQUEST_ID}.json\"
|
||||
PRECHECK_STDERR=\"/tmp/check_target_\${REQUEST_ID}.stderr\"
|
||||
CORE_JSON=\"/tmp/rebuild_target_\${REQUEST_ID}.json\"
|
||||
CORE_STDERR=\"/tmp/rebuild_target_\${REQUEST_ID}.stderr\"
|
||||
|
||||
\"\$PRECHECK_SCRIPT\" \
|
||||
--env-file \"\$TARGET_ENV_FILE\" \
|
||||
--request-id \"\$REQUEST_ID\" \
|
||||
--non-interactive \
|
||||
--json-only >\"\$PRECHECK_JSON\" 2>\"\$PRECHECK_STDERR\" || {
|
||||
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
|
||||
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
PRECHECK_STATUS=\"\$(python3 - <<'PY' \"\$PRECHECK_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
|
||||
)\" || {
|
||||
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
|
||||
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then
|
||||
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$PRECHECK_JSON\"
|
||||
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\"
|
||||
|
||||
\"\$CORE_SCRIPT\" \
|
||||
--env-file \"\$TARGET_ENV_FILE\" \
|
||||
--db \"\$REQUESTED_DB\" \
|
||||
--overwrite \"\$ALLOW_OVERWRITE\" \
|
||||
--restore-roles \"\$RESTORE_ROLES\" \
|
||||
--request-id \"\$REQUEST_ID\" \
|
||||
--non-interactive \
|
||||
--json-only >\"\$CORE_JSON\" 2>\"\$CORE_STDERR\" || {
|
||||
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$CORE_JSON\" 2>/dev/null || true
|
||||
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
CORE_STATUS=\"\$(python3 - <<'PY' \"\$CORE_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
|
||||
)\" || {
|
||||
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$CORE_JSON\" 2>/dev/null || true
|
||||
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ \"\$CORE_STATUS\" != \"success\" ]]; then
|
||||
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||
cat \"\$CORE_JSON\"
|
||||
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat \"\$CORE_JSON\"
|
||||
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" 2>&1 \
|
||||
|| {
|
||||
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
|
||||
fail "échec d'exécution distante sur la cible ${TARGET}"
|
||||
}
|
||||
|
||||
REMOTE_STATUS="$(
|
||||
python3 - <<'PY' "$REMOTE_RESULT_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
|
||||
)" || {
|
||||
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
|
||||
fail "réponse JSON invalide renvoyée par la cible ${TARGET}"
|
||||
}
|
||||
|
||||
if [[ "$REMOTE_STATUS" != "success" ]]; then
|
||||
cat "$REMOTE_RESULT_JSON"
|
||||
fail "restauration distante échouée pour la cible ${TARGET}"
|
||||
fi
|
||||
|
||||
python3 - <<'PY' "$REMOTE_RESULT_JSON"
|
||||
import json, sys
|
||||
|
||||
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
message = data.get("message", "restauration terminée")
|
||||
environment = data.get("environment") or "N/A"
|
||||
database = data.get("database") or "N/A"
|
||||
request_id = data.get("request_id") or "N/A"
|
||||
dump_file = data.get("dump_file") or "N/A"
|
||||
log_file = data.get("log_file") or "N/A"
|
||||
|
||||
print(f"[{request_id}] {message}")
|
||||
print(f"Environnement : {environment}")
|
||||
print(f"Base : {database}")
|
||||
print(f"Dump : {dump_file}")
|
||||
print(f"Log : {log_file}")
|
||||
PY
|
||||
@@ -47,14 +47,13 @@ Les scripts fonctionnent indépendamment mais utilisent le même principe :
|
||||
|
||||
Environnement Linux recommandé.
|
||||
|
||||
Packages nécessaires :
|
||||
Packages nécessaires sur Ubuntu Server :
|
||||
|
||||
```
|
||||
postgresql-client
|
||||
curl
|
||||
jq
|
||||
ssh
|
||||
scp
|
||||
openssh-client
|
||||
```
|
||||
|
||||
Commandes PostgreSQL requises :
|
||||
@@ -116,11 +115,18 @@ ssh-copy-id -i ~/.ssh/id_backup_postgres.pub backup@192.168.1.50
|
||||
Tester la connexion sans mot de passe :
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_backup_postgres backup@192.168.1.50
|
||||
ssh -i ~/.ssh/id_backup_postgres -o StrictHostKeyChecking=yes backup@192.168.1.50
|
||||
```
|
||||
|
||||
La connexion doit fonctionner **sans demander de mot de passe**.
|
||||
|
||||
Provisionner aussi `known_hosts` avant le premier run :
|
||||
|
||||
```bash
|
||||
ssh-keyscan -H 192.168.1.50 >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sécuriser les permissions
|
||||
@@ -153,13 +159,18 @@ cp backup.env.exemple .env
|
||||
|
||||
Puis modifier les variables.
|
||||
|
||||
Variables SSH supplémentaires désormais supportées :
|
||||
|
||||
```
|
||||
BACKUP_REMOTE_SSH_PORT
|
||||
BACKUP_KNOWN_HOSTS_STRICT
|
||||
BACKUP_KNOWN_HOSTS_FILE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. Script : backup-bdd-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Sauvegarder plusieurs bases PostgreSQL et transférer les dumps vers un serveur distant.
|
||||
@@ -205,6 +216,8 @@ Suppression des sauvegardes plus anciennes que :
|
||||
10 jours
|
||||
```
|
||||
|
||||
Le script utilise maintenant des options SSH strictes et refuse les clés privées symboliques.
|
||||
|
||||
---
|
||||
|
||||
## Exécution
|
||||
@@ -217,9 +230,6 @@ Suppression des sauvegardes plus anciennes que :
|
||||
|
||||
# 6. Script : check-statut-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Vérifier la disponibilité des applications web.
|
||||
@@ -279,12 +289,40 @@ CHECK APP RECETTE 🟢
|
||||
```
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 5</summary>
|
||||
|
||||
Quelle option demande explicitement un decodage plutot qu'un encodage ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 5</summary>
|
||||
|
||||
```text
|
||||
-d
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 5</summary>
|
||||
|
||||
```text
|
||||
dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNR
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
# 7. Script : rebuild-bdd-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Restaurer une base PostgreSQL à partir d’un dump distant.
|
||||
@@ -306,6 +344,8 @@ Le script :
|
||||
9. restaure la base via `pg_restore`
|
||||
10. envoie une notification Discord
|
||||
|
||||
Le script supporte désormais le port SSH distant, un fichier `known_hosts` dédié et l’exclusion configurable des rôles via `EXCLUDED_RESTORE_ROLES`.
|
||||
|
||||
---
|
||||
|
||||
## Sélection de la base
|
||||
|
||||
236
RecetteScripts/backup-bdd-recette.sh
Normal file → Executable file
236
RecetteScripts/backup-bdd-recette.sh
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# backup-bdd-recette.sh
|
||||
@@ -17,7 +18,8 @@ set -euo pipefail
|
||||
# 6. exporte les rôles PostgreSQL ;
|
||||
# 7. dump chaque base au format personnalisé PostgreSQL ;
|
||||
# 8. transfère chaque fichier vers le serveur distant ;
|
||||
# 9. applique une rotation distante sur 10 jours ;
|
||||
# 9. applique une rotation distante selon BACKUP_RETENTION_DAYS
|
||||
# (10 jours par défaut) ;
|
||||
# 10. envoie un bilan sur Discord :
|
||||
# - 1 message global si tout est OK ;
|
||||
# - en cas d’erreur partielle :
|
||||
@@ -49,6 +51,10 @@ set +a
|
||||
#######################################
|
||||
|
||||
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
@@ -57,6 +63,10 @@ set +a
|
||||
: "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}"
|
||||
: "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}"
|
||||
: "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}"
|
||||
[[ "$BACKUP_REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||
echo "Variable BACKUP_REMOTE_DIR invalide : $BACKUP_REMOTE_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${SSH_KEY:?Variable SSH_KEY manquante}"
|
||||
: "${SSH_TIMEOUT:?Variable SSH_TIMEOUT manquante}"
|
||||
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||
@@ -67,15 +77,82 @@ set +a
|
||||
|
||||
read -r -a DBS_ARRAY <<< "$DBS"
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="$1"
|
||||
|
||||
[[ -n "$db_name" ]] || {
|
||||
echo "ERROR: nom de base vide dans DBS" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || {
|
||||
echo "ERROR: nom de base invalide dans DBS : $db_name" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
validate_db_name "$DB"
|
||||
done
|
||||
|
||||
IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
IA_BASE_DIR="${BACKUP_REMOTE_DIR}"
|
||||
RETENTION_DAYS=10
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
|
||||
BACKUP_KNOWN_HOSTS_FILE="${BACKUP_KNOWN_HOSTS_FILE:-${HOME}/.ssh/known_hosts}"
|
||||
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: BACKUP_REMOTE_SSH_PORT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: PGPORT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$SSH_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: SSH_TIMEOUT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
|
||||
echo "ERROR: PGUSER invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||
*)
|
||||
echo "ERROR: BACKUP_KNOWN_HOSTS_STRICT invalide" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
LOG_DIR="${BACKUP_LOG_DIR}"
|
||||
@@ -85,15 +162,32 @@ TS="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||
BACKUP_DIR_NAME="backup_${TS}"
|
||||
LOG_FILE="${LOG_DIR}/${BACKUP_DIR_NAME}.log"
|
||||
|
||||
TMP_DIR="/tmp/pg_dump_${BACKUP_DIR_NAME}"
|
||||
mkdir -p "$TMP_DIR"
|
||||
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
log() { echo "---- $(date +'%Y-%m-%d %H:%M:%S') ---- $*"; }
|
||||
TMP_DIR="$(mktemp -d /tmp/pg_dump_XXXXXX)" || {
|
||||
echo "ERROR: impossible de créer le dossier temporaire" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande manquante : $1"
|
||||
}
|
||||
|
||||
safe_remove_dir() {
|
||||
local dir="${1:-}"
|
||||
[[ -n "$dir" ]] || return 0
|
||||
[[ "$dir" == /tmp/pg_dump_* ]] || {
|
||||
log "WARNING: suppression refusée pour le chemin inattendu : $dir"
|
||||
return 1
|
||||
}
|
||||
rm -rf -- "$dir"
|
||||
}
|
||||
|
||||
export PGPASSWORD
|
||||
@@ -102,12 +196,26 @@ export PGPASSWORD
|
||||
# Vérification dépendances minimales
|
||||
#######################################
|
||||
|
||||
for cmd in ssh scp curl jq pg_dump pg_dumpall; do
|
||||
require_cmd "$cmd" || {
|
||||
echo "ERROR: commande manquante : $cmd" >&2
|
||||
for cmd in ssh scp curl jq pg_dump pg_dumpall mktemp; do
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
|
||||
[[ -f "$SSH_KEY" ]] || {
|
||||
echo "ERROR: clé SSH introuvable : $SSH_KEY" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
[[ -r "$SSH_KEY" ]] || {
|
||||
echo "ERROR: clé SSH non lisible : $SSH_KEY" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ ! -L "$SSH_KEY" ]] || {
|
||||
echo "ERROR: la clé SSH ne doit pas être un lien symbolique : $SSH_KEY" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
chmod 600 "$SSH_KEY" || true
|
||||
|
||||
#######################################
|
||||
# Configuration Discord
|
||||
@@ -116,14 +224,14 @@ done
|
||||
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
discord_send() {
|
||||
send_discord() {
|
||||
local msg="$1"
|
||||
local payload
|
||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')" || {
|
||||
log "ERROR: impossible de construire le payload JSON Discord"
|
||||
return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
curl -fsS \
|
||||
@@ -145,7 +253,7 @@ Dumps transfer: ✅
|
||||
Users transfer: ✅
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -159,7 +267,7 @@ discord_msg_users_ok_simple() {
|
||||
Users backup validé
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
discord_msg_users_error() {
|
||||
@@ -191,7 +299,7 @@ EOF
|
||||
)"
|
||||
fi
|
||||
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -206,7 +314,7 @@ discord_msg_db_ok_simple() {
|
||||
Backup validé : ${db}
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
discord_msg_db_error() {
|
||||
@@ -241,7 +349,7 @@ EOF
|
||||
)"
|
||||
fi
|
||||
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -264,26 +372,53 @@ declare -A DB_DETAILS
|
||||
#######################################
|
||||
|
||||
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
||||
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
log "ERROR: Backup déjà en cours"
|
||||
discord_msg_users_error "" "" "Lock already exists"
|
||||
stale_lock="no"
|
||||
existing_pid=""
|
||||
|
||||
if [[ -f "$LOCK_PID_FILE" ]]; then
|
||||
existing_pid="$(<"$LOCK_PID_FILE")"
|
||||
fi
|
||||
|
||||
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
||||
log "ERROR: Backup déjà en cours (PID ${existing_pid})"
|
||||
discord_msg_users_error "" "" "Lock already exists (PID ${existing_pid})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trap 'rm -rf "$LOCK_DIR" "$TMP_DIR"' EXIT
|
||||
stale_lock="yes"
|
||||
log "WARNING: lock périmé détecté, nettoyage en cours"
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || fail "impossible de recréer le lock après nettoyage"
|
||||
fi
|
||||
|
||||
echo $$ > "$LOCK_PID_FILE" || {
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
fail "impossible d'écrire le PID du lock"
|
||||
}
|
||||
|
||||
if [[ "${stale_lock:-no}" == "yes" ]]; then
|
||||
log "Lock périmé nettoyé."
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
safe_remove_dir "$TMP_DIR" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Préparation du dossier distant
|
||||
#######################################
|
||||
|
||||
REMOTE_DIR="${IA_BASE_DIR}"
|
||||
|
||||
log "Creating remote directories"
|
||||
|
||||
MKDIR_CMD="mkdir -p '${REMOTE_DIR}/user'"
|
||||
MKDIR_CMD="mkdir -p '${BACKUP_REMOTE_DIR}/user'"
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
MKDIR_CMD+=" '${REMOTE_DIR}/${DB}'"
|
||||
MKDIR_CMD+=" '${BACKUP_REMOTE_DIR}/${DB}'"
|
||||
done
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "$MKDIR_CMD"; then
|
||||
@@ -298,18 +433,18 @@ fi
|
||||
|
||||
ROLES_FILE="${TMP_DIR}/user_${TS}.sql"
|
||||
|
||||
set +e
|
||||
|
||||
log "Export des rôles PostgreSQL"
|
||||
|
||||
pg_dumpall \
|
||||
if pg_dumpall \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
--globals-only \
|
||||
> "$ROLES_FILE"
|
||||
|
||||
> "$ROLES_FILE"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
USERS_OK=
|
||||
@@ -320,8 +455,11 @@ else
|
||||
fi
|
||||
|
||||
if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
|
||||
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/user/"
|
||||
if scp "${SCP_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/user/"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
USERS_OK=
|
||||
@@ -336,14 +474,10 @@ if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
#######################################
|
||||
# Dump des bases
|
||||
#######################################
|
||||
|
||||
set +e
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
FILE="${TMP_DIR}/${DB}_${TS}.dump"
|
||||
|
||||
@@ -353,8 +487,11 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
|
||||
log "Dump $DB"
|
||||
|
||||
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"
|
||||
if pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
DUMPS_OK=
|
||||
@@ -364,8 +501,11 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/${DB}/"
|
||||
if scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/${DB}/"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
DUMPS_OK=
|
||||
@@ -374,18 +514,17 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
#######################################
|
||||
# Rotation distante
|
||||
#######################################
|
||||
|
||||
log "Starting remote rotation: delete backups older than ${RETENTION_DAYS} days"
|
||||
|
||||
set +e
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"
|
||||
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
log "ERROR: remote rotation failed for users"
|
||||
@@ -394,8 +533,11 @@ else
|
||||
fi
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"
|
||||
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
log "ERROR: remote rotation failed for ${DB}"
|
||||
@@ -404,15 +546,13 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
log "Remote rotation finished"
|
||||
|
||||
#######################################
|
||||
# Nettoyage local
|
||||
#######################################
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
safe_remove_dir "$TMP_DIR" || true
|
||||
|
||||
#######################################
|
||||
# Bilan final Discord
|
||||
|
||||
@@ -44,9 +44,18 @@ BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
# Clé SSH utilisée pour se connecter au serveur distant
|
||||
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
|
||||
# Port SSH du serveur de backup
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
# Timeout de connexion SSH (secondes)
|
||||
SSH_TIMEOUT=10
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
|
||||
|
||||
###############################################################################
|
||||
# LOGS
|
||||
###############################################################################
|
||||
|
||||
47
RecetteScripts/check-statut-recette.sh
Normal file → Executable file
47
RecetteScripts/check-statut-recette.sh
Normal file → Executable file
@@ -44,13 +44,23 @@ set +a
|
||||
: "${CHECK_MAX_TIME:?Variable CHECK_MAX_TIME manquante}"
|
||||
: "${APP_URLS:?Variable APP_URLS manquante}"
|
||||
|
||||
[[ "$CHECK_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable CHECK_CONNECT_TIMEOUT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$CHECK_MAX_TIME" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable CHECK_MAX_TIME invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Sites à vérifier
|
||||
#######################################
|
||||
|
||||
read -r -a SITES <<< "$APP_URLS"
|
||||
|
||||
SCHEME="http"
|
||||
SCHEME="${APP_SCHEME:-http}"
|
||||
CONNECT_TIMEOUT="${CHECK_CONNECT_TIMEOUT}"
|
||||
MAX_TIME="${CHECK_MAX_TIME}"
|
||||
|
||||
@@ -75,6 +85,16 @@ DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
SUMMARY_LINES=()
|
||||
FAILURES=0
|
||||
TMPFILES=()
|
||||
|
||||
cleanup() {
|
||||
local tmpfile
|
||||
for tmpfile in "${TMPFILES[@]}"; do
|
||||
[[ -n "$tmpfile" ]] || continue
|
||||
rm -f -- "$tmpfile"
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Logging
|
||||
@@ -115,8 +135,21 @@ add_summary_line() {
|
||||
#######################################
|
||||
# Envoi du message Discord récapitulatif
|
||||
#######################################
|
||||
send_discord_summary() {
|
||||
should_send_discord() {
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local current_hour current_minute
|
||||
current_hour="$(date +'%H')"
|
||||
current_minute="$(date +'%M')"
|
||||
|
||||
[[ "$current_hour" == "19" && "$current_minute" -ge 0 && "$current_minute" -le 4 ]]
|
||||
}
|
||||
|
||||
send_discord() {
|
||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||
should_send_discord || return 0
|
||||
|
||||
local header_icon ping_prefix=""
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
@@ -134,7 +167,7 @@ send_discord_summary() {
|
||||
done
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')" || return 0
|
||||
|
||||
curl -fsS -H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
@@ -158,6 +191,7 @@ check_site() {
|
||||
local http_code curl_exit err
|
||||
local stderr
|
||||
stderr="$(mktemp)"
|
||||
TMPFILES+=("$stderr")
|
||||
|
||||
http_code="$(
|
||||
curl -sS -o /dev/null \
|
||||
@@ -170,15 +204,12 @@ check_site() {
|
||||
|
||||
if [[ "$curl_exit" -ne 0 ]]; then
|
||||
err="$(head -n 1 "$stderr" | tr -d '\r')"
|
||||
rm -f "$stderr"
|
||||
|
||||
log_line "DOWN" "$host" "curl exit=$curl_exit : ${err:-"(aucun)"}"
|
||||
add_summary_line "$host" "DOWN" "DOWN - curl"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$stderr"
|
||||
|
||||
if [[ "$http_code" =~ ^[0-9]{3}$ ]]; then
|
||||
if [[ "$http_code" -ge 200 && "$http_code" -le 399 ]]; then
|
||||
log_line "OK" "$host" "HTTP $http_code"
|
||||
@@ -201,8 +232,6 @@ check_site() {
|
||||
#######################################
|
||||
|
||||
main() {
|
||||
trap '[[ -n "$STDERR_TMP" ]] && rm -f "$STDERR_TMP"' EXIT
|
||||
|
||||
local failures=0
|
||||
|
||||
for site in "${SITES[@]}"; do
|
||||
@@ -212,7 +241,7 @@ main() {
|
||||
done
|
||||
|
||||
FAILURES="$failures"
|
||||
send_discord_summary
|
||||
send_discord
|
||||
|
||||
if [[ "$failures" -gt 0 ]]; then
|
||||
exit 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# rebuild-bdd-recette.sh
|
||||
@@ -49,6 +50,10 @@ set +a
|
||||
# Variables obligatoires
|
||||
###############################################################################
|
||||
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
@@ -65,8 +70,10 @@ set +a
|
||||
###############################################################################
|
||||
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_DIR:-${SCRIPT_DIR}/restore_tmp}"
|
||||
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
|
||||
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
|
||||
|
||||
###############################################################################
|
||||
# Préparation des dossiers locaux
|
||||
@@ -108,42 +115,66 @@ cleanup() {
|
||||
"${FILTERED_ROLES_FILE:-}" \
|
||||
"${ROLES_CREATE_LIST:-}" \
|
||||
"${ROLES_APPLY_FILE:-}"
|
||||
rm -rf "${LOCAL_RESTORE_DIR:-}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
sql_escape_literal() {
|
||||
local s="${1:-}"
|
||||
s="${s//\'/\'\'}"
|
||||
printf "%s" "$s"
|
||||
}
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="${1:-}"
|
||||
|
||||
[[ -n "$db_name" ]] || return 1
|
||||
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
|
||||
}
|
||||
|
||||
build_excluded_roles_regex() {
|
||||
local role regex=""
|
||||
|
||||
for role in $EXCLUDED_RESTORE_ROLES; do
|
||||
[[ -z "$role" ]] && continue
|
||||
[[ "$role" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || fail "rôle exclu invalide : ${role}"
|
||||
if [[ -n "$regex" ]]; then
|
||||
regex+="|"
|
||||
fi
|
||||
regex+="$role"
|
||||
done
|
||||
|
||||
printf '%s' "$regex"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Envoi Discord
|
||||
#
|
||||
# Envoi simple d'un message texte via webhook Discord.
|
||||
# Si WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||
# Si DISCORD_WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||
###############################################################################
|
||||
send_discord_message() {
|
||||
send_discord() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || {
|
||||
log "WEBHOOK_URL non défini : notification Discord ignorée."
|
||||
return 0
|
||||
}
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||
has_cmd jq || return 0
|
||||
has_cmd curl || return 0
|
||||
|
||||
if ! require_cmd curl; then
|
||||
log "curl absent : notification Discord ignorée."
|
||||
return 0
|
||||
fi
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || {
|
||||
log "Impossible de construire le payload JSON Discord."
|
||||
return 0
|
||||
}
|
||||
|
||||
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
|
||||
curl -fsS "$DISCORD_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null || log "Échec d'envoi de la notification Discord."
|
||||
>/dev/null || true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
@@ -151,15 +182,29 @@ send_discord_message() {
|
||||
###############################################################################
|
||||
[[ -f "$SSH_KEY" ]] || fail "clé SSH introuvable : $SSH_KEY"
|
||||
[[ -r "$SSH_KEY" ]] || fail "clé SSH non lisible : $SSH_KEY"
|
||||
[[ ! -L "$SSH_KEY" ]] || fail "clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
@@ -171,7 +216,7 @@ REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
###############################################################################
|
||||
POSTGRES_INSTALLED=false
|
||||
|
||||
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; then
|
||||
if ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb; then
|
||||
log "PostgreSQL absent : installation en cours..."
|
||||
|
||||
sudo apt update >>"$LOG_FILE" 2>&1 || fail "échec de apt update"
|
||||
@@ -198,15 +243,17 @@ fi
|
||||
# Attente disponibilité PostgreSQL
|
||||
###############################################################################
|
||||
log "Vérification de la disponibilité de PostgreSQL..."
|
||||
PG_READY=false
|
||||
for _ in {1..20}; do
|
||||
if sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
PG_READY=true
|
||||
log "PostgreSQL répond correctement."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
if [[ "$PG_READY" != true ]]; then
|
||||
fail "PostgreSQL ne répond pas correctement"
|
||||
fi
|
||||
|
||||
@@ -217,7 +264,7 @@ if [[ "$POSTGRES_INSTALLED" == "true" ]]; then
|
||||
log "Création du rôle PostgreSQL ${PGUSER} suite à une installation neuve..."
|
||||
|
||||
sudo -u postgres psql -d postgres -c \
|
||||
"CREATE ROLE \"${PGUSER}\" WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${PGPASSWORD}';" \
|
||||
"CREATE ROLE \"${PGUSER}\" WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '$(sql_escape_literal "$PGPASSWORD")';" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec de création du rôle ${PGUSER}"
|
||||
|
||||
log "Rôle PostgreSQL ${PGUSER} créé."
|
||||
@@ -251,9 +298,10 @@ if [[ "${USE_LIST,,}" == "oui" || "${USE_LIST,,}" == "o" ]]; then
|
||||
DB="${DBS_ARRAY[$((DB_INDEX - 1))]}"
|
||||
else
|
||||
read -r -p "Nom exact de la base à restaurer : " DB
|
||||
[[ -n "$DB" ]] || fail "nom de base vide"
|
||||
fi
|
||||
|
||||
validate_db_name "$DB" || fail "nom de base invalide"
|
||||
|
||||
log "Environnement : $ENV_NAME"
|
||||
log "Base cible sélectionnée : $DB"
|
||||
|
||||
@@ -312,7 +360,7 @@ LOCAL_DB_DUMP_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_DB_DUMP")"
|
||||
LOCAL_ROLES_FILE=""
|
||||
|
||||
log "Téléchargement du dump..."
|
||||
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
|
||||
scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du dump principal"
|
||||
|
||||
###############################################################################
|
||||
@@ -322,7 +370,7 @@ if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||
LOCAL_ROLES_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_ROLES_FILE")"
|
||||
|
||||
log "Téléchargement du fichier des rôles..."
|
||||
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
|
||||
scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du fichier des rôles"
|
||||
else
|
||||
log "La restauration des rôles sera ignorée."
|
||||
@@ -341,7 +389,7 @@ fi
|
||||
###############################################################################
|
||||
DB_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='${DB}'" 2>>"$LOG_FILE" || true
|
||||
"SELECT 1 FROM pg_database WHERE datname='$(sql_escape_literal "$DB")'" 2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$DB_EXISTS" == "1" ]]; then
|
||||
@@ -364,9 +412,16 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||
FILTERED_ROLES_FILE="${LOCAL_RESTORE_DIR}/filtered_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_CREATE_LIST="${LOCAL_RESTORE_DIR}/roles_to_create_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_APPLY_FILE="${LOCAL_RESTORE_DIR}/roles_apply_$(basename "$LOCAL_ROLES_FILE")"
|
||||
EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex)"
|
||||
|
||||
grep -viE '^(CREATE ROLE|ALTER ROLE) (backup_liot|postgres)\b' "$LOCAL_ROLES_FILE" \
|
||||
if [[ -n "$EXCLUDED_ROLES_REGEX" ]]; then
|
||||
grep -viE "^(CREATE ROLE|ALTER ROLE) (${EXCLUDED_ROLES_REGEX})\\b" "$LOCAL_ROLES_FILE" \
|
||||
> "$FILTERED_ROLES_FILE" || true
|
||||
else
|
||||
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
|
||||
fi
|
||||
|
||||
sed -i -E '/^ALTER ROLE .* (NO)?SUPERUSER\b/d' "$FILTERED_ROLES_FILE"
|
||||
|
||||
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
|
||||
|
||||
@@ -383,7 +438,7 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||
|
||||
ROLE_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='${role_name}'" 2>>"$LOG_FILE" || true
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='$(sql_escape_literal "$role_name")'" 2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||
@@ -448,4 +503,4 @@ Hôte PostgreSQL : ${PGHOST}:${PGPORT}
|
||||
Dump utilisé : $(basename "$LAST_REMOTE_DB_DUMP")
|
||||
Log : ${LOG_FILE}"
|
||||
|
||||
send_discord_message "$SUCCESS_MESSAGE"
|
||||
send_discord "$SUCCESS_MESSAGE"
|
||||
|
||||
@@ -39,6 +39,9 @@ BACKUP_REMOTE_HOST=
|
||||
# Répertoire racine distant :
|
||||
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
|
||||
# Port SSH du serveur de backup
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
###############################################################################
|
||||
# SSH
|
||||
###############################################################################
|
||||
@@ -50,6 +53,12 @@ SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
# Variable optionnelle dans le script, mais utile ici comme valeur par défaut
|
||||
SSH_CONNECT_TIMEOUT=8
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
|
||||
|
||||
###############################################################################
|
||||
# LOGS
|
||||
###############################################################################
|
||||
@@ -72,6 +81,9 @@ LOCAL_RESTORE_DIR=/tmp/rebuild-bdd-recette
|
||||
# Nom du dossier distant contenant les exports SQL des rôles
|
||||
REMOTE_ROLES_DIR_NAME=user
|
||||
|
||||
# Rôles PostgreSQL à exclure lors de la restauration
|
||||
EXCLUDED_RESTORE_ROLES="postgres"
|
||||
|
||||
###############################################################################
|
||||
# DISCORD
|
||||
###############################################################################
|
||||
|
||||
@@ -78,7 +78,7 @@ BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
#############################################
|
||||
|
||||
# Clé SSH utilisée pour se connecter au serveur distant
|
||||
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
|
||||
# Timeout SSH (secondes)
|
||||
SSH_TIMEOUT=10
|
||||
@@ -89,6 +89,7 @@ SSH_TIMEOUT=10
|
||||
#############################################
|
||||
|
||||
# Nombre de jours de conservation des sauvegardes
|
||||
# Utilisé par backup-bdd-recette.sh et backup-vaultwarden.sh
|
||||
BACKUP_RETENTION_DAYS=10
|
||||
|
||||
|
||||
@@ -96,12 +97,11 @@ BACKUP_RETENTION_DAYS=10
|
||||
# APPLICATIONS À SURVEILLER
|
||||
#############################################
|
||||
|
||||
# Liste des applications à vérifier
|
||||
APPS="
|
||||
ferme.malio-dev.fr
|
||||
inventory.malio-dev.fr
|
||||
sirh.malio-dev.fr
|
||||
"
|
||||
# Liste des applications à vérifier (séparées par espace)
|
||||
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||
|
||||
# Schéma utilisé pour les applications surveillées
|
||||
APP_SCHEME="http"
|
||||
|
||||
|
||||
#############################################
|
||||
|
||||
Reference in New Issue
Block a user