Compare commits
69 Commits
master
...
f3ebb4c011
| Author | SHA1 | Date | |
|---|---|---|---|
| f3ebb4c011 | |||
| 83032ef5ab | |||
| 863fee91a9 | |||
| 0dddecd08f | |||
| 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 | ||
| fb1aaac418 | |||
| d7cb9b34c4 | |||
| 643a0d9ec7 | |||
| df77d8be21 | |||
| 038ddfe242 | |||
|
|
a1ace94094 | ||
| 210594b008 | |||
|
|
e221e82108 | ||
|
|
fabc9be4d4 | ||
|
|
9d4a5050e9 | ||
| 5729d0d484 | |||
|
|
89b1229efb | ||
|
|
f9b1d1da24 | ||
|
|
049574ffeb | ||
| c257270982 | |||
|
|
f72328e0ce | ||
|
|
29eff11b23 | ||
|
|
99072361c5 | ||
| 623424343e | |||
|
|
066ede6000 | ||
| 30df5ca8d6 | |||
|
|
4b76e88853 | ||
|
|
99f8694250 | ||
| 7f18e2f2e9 | |||
|
|
d0ceea8bad | ||
| dd226592db | |||
|
|
e81b953ac2 | ||
|
|
c80a74adc5 | ||
|
|
97eeffd9ea | ||
|
|
14359b111f | ||
| e860fd0f16 | |||
|
|
fbbb68af88 | ||
|
|
c2d1b716e0 |
@@ -1 +0,0 @@
|
|||||||
WEBHOOK_URL=
|
|
||||||
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,9 +1,40 @@
|
|||||||
# Secrets / environment
|
########################################
|
||||||
.env
|
# Environment / secrets
|
||||||
.env.*
|
########################################
|
||||||
!.env.example
|
|
||||||
!.env.exemple
|
.env
|
||||||
|
!.env.exemple
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Logs
|
||||||
|
########################################
|
||||||
|
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
var/log/
|
||||||
|
backup.log
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Temporary / system files
|
||||||
|
########################################
|
||||||
|
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# IDE / Editors
|
||||||
|
########################################
|
||||||
|
|
||||||
# IDE / editor
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# OS files
|
||||||
|
########################################
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
36
BackupVaultWarden/.env.exemple
Normal file
36
BackupVaultWarden/.env.exemple
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#############################################
|
||||||
|
# VAULTWARDEN BACKUP CONFIGURATION
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Webhook Discord pour notifications (optionnel)
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Répertoire contenant les données Vaultwarden
|
||||||
|
DATA_DIR=
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# BACKUP LOCAL
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Dossier local où seront stockées les archives
|
||||||
|
LOCAL_BACKUP=
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SERVEUR DE BACKUP DISTANT
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Utilisateur SSH du serveur distant
|
||||||
|
REMOTE_USER=
|
||||||
|
|
||||||
|
# Host ou IP du serveur distant
|
||||||
|
REMOTE_HOST=
|
||||||
|
|
||||||
|
# Répertoire distant de stockage des backups
|
||||||
|
REMOTE_DIR=
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# AUTHENTIFICATION SSH
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Chemin vers la clé privée SSH utilisée pour la connexion
|
||||||
|
SSH_KEY=
|
||||||
315
BackupVaultWarden/README.md
Normal file
315
BackupVaultWarden/README.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# README — Mise en place du script de sauvegarde Vaultwarden
|
||||||
|
|
||||||
|
Ce script permet d’automatiser la sauvegarde de Vaultwarden afin de conserver une copie du dossier `data`, de la transférer vers un serveur distant et d’envoyer une notification Discord en cas de succès ou d’échec.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Objectif du script
|
||||||
|
|
||||||
|
Le script de sauvegarde Vaultwarden permet de :
|
||||||
|
|
||||||
|
- sauvegarder les données de Vaultwarden ;
|
||||||
|
- compresser l’archive avec un nom daté ;
|
||||||
|
- transférer la sauvegarde vers un serveur distant ;
|
||||||
|
- envoyer une notification Discord ;
|
||||||
|
- automatiser l’exécution via `cron`.
|
||||||
|
|
||||||
|
Ce mécanisme permet de sécuriser les mots de passe, les utilisateurs et la configuration stockés dans le dossier `data` de Vaultwarden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Pré-requis
|
||||||
|
|
||||||
|
Avant de mettre en place le script, vérifier que les éléments suivants sont disponibles sur la machine Vaultwarden :
|
||||||
|
|
||||||
|
- `bash`
|
||||||
|
- `tar`
|
||||||
|
- `scp`
|
||||||
|
- `ssh`
|
||||||
|
- `curl`
|
||||||
|
- `cron`
|
||||||
|
|
||||||
|
Installation sur Debian / Ubuntu :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y tar openssh-client curl cron
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Emplacement du script
|
||||||
|
|
||||||
|
Le script est situé dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||||
|
```
|
||||||
|
|
||||||
|
Structure recommandée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||||
|
├── backup-vaultwarden.sh
|
||||||
|
├── .env
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Configuration sécurisée avec le fichier .env
|
||||||
|
|
||||||
|
Les informations sensibles ne doivent pas être stockées directement dans le script.
|
||||||
|
Elles doivent être placées dans un fichier `.env`.
|
||||||
|
|
||||||
|
## Exemple de fichier `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||||
|
REMOTE_USER=<USER>
|
||||||
|
REMOTE_HOST=<IP_SERVEUR>
|
||||||
|
SSH_KEY=/home/matt/.ssh/id_ed25519_vaultwarden_backup
|
||||||
|
DATA_DIR=/opt/vaultwarden/data
|
||||||
|
REMOTE_DIR=/home/backup/backups/vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description des variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ----------- | ------------------------------------------------------ |
|
||||||
|
| 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 |
|
||||||
|
| REMOTE_DIR | Dossier de stockage des backups sur le serveur distant |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Chargement des variables dans le script
|
||||||
|
|
||||||
|
Le script charge directement le fichier `.env` avec `source` et exporte automatiquement les variables pendant le chargement.
|
||||||
|
|
||||||
|
Mécanisme utilisé :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
```
|
||||||
|
|
||||||
|
Explication :
|
||||||
|
|
||||||
|
* `set -a` exporte automatiquement les variables définies ensuite
|
||||||
|
* `source "$ENV_FILE"` lit et exécute le contenu du fichier `.env` dans le shell courant
|
||||||
|
* `set +a` désactive ensuite l'export automatique
|
||||||
|
|
||||||
|
Cela permet :
|
||||||
|
|
||||||
|
* de charger toutes les variables du fichier `.env` en une seule fois
|
||||||
|
* de conserver la configuration en dehors du script
|
||||||
|
* de rester aligné avec le comportement réel du script
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Connexion au serveur de sauvegarde (Machine IA)
|
||||||
|
|
||||||
|
Le transfert des sauvegardes utilise une **clé SSH** afin de permettre une connexion automatique au serveur distant sans mot de passe.
|
||||||
|
|
||||||
|
#### 1. Génération de la clé
|
||||||
|
|
||||||
|
Sur la machine exécutant les scripts :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Copie de la clé vers le serveur distant
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-copy-id -i ~/.ssh/id_ed25519_bitwarden.pub <USER>@<IP_SERVEUR>
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande ajoute la clé dans :
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
sur la machine IA.
|
||||||
|
|
||||||
|
#### 3. Vérification de la connexion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/id_ed25519_bitwarden backup@192.168.0.179
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Vérification des fichiers de clé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls ~/.ssh/id_ed25519_bitwarden*
|
||||||
|
```
|
||||||
|
|
||||||
|
Fichiers attendus :
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.ssh/id_ed25519_bitwarden
|
||||||
|
~/.ssh/id_ed25519_bitwarden.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Permissions SSH
|
||||||
|
|
||||||
|
Machine locale :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
chmod 600 ~/.ssh/id_ed25519_bitwarden
|
||||||
|
chmod 644 ~/.ssh/id_ed25519_bitwarden.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
Machine distante :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
chmod 600 ~/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Déclaration dans `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SSH_KEY=/home/matt/.ssh/id_ed25519_bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour transférer les sauvegardes.
|
||||||
|
|
||||||
|
|
||||||
|
# 7. Sauvegarde des données Vaultwarden
|
||||||
|
|
||||||
|
Le script crée une archive compressée du dossier `data` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -czf "$LOCAL_BACKUP" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||||
|
```
|
||||||
|
|
||||||
|
Cela permet d’obtenir une sauvegarde portable et compressée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Transfert vers le serveur distant
|
||||||
|
|
||||||
|
Une fois l’archive créée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Construction du message :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
local msg="**@here Backup Vaultwarden $color**\n"
|
||||||
|
msg+="Backup: ${BACKUP_NAME}\n"
|
||||||
|
msg+="Data transfer: $dumps_display\n"
|
||||||
|
[[ -n "$details" ]] && msg+="Details: $details"
|
||||||
|
```
|
||||||
|
|
||||||
|
Envoi du message :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS -H "Content-Type: application/json" \
|
||||||
|
-d "{\"content\":\"$msg\"}" \
|
||||||
|
"$WEBHOOK_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
Le message indique :
|
||||||
|
|
||||||
|
* si la sauvegarde a réussi
|
||||||
|
* si elle a échoué
|
||||||
|
* le nom du backup
|
||||||
|
* les détails de l’erreur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Planification avec cron
|
||||||
|
|
||||||
|
Le script est exécuté automatiquement tous les jours à 19h.
|
||||||
|
|
||||||
|
Ouvrir le crontab :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
0 19 * * * /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh >> /var/log/vaultwarden_backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Signification :
|
||||||
|
|
||||||
|
| Champ | Valeur |
|
||||||
|
| ------------ | ------ |
|
||||||
|
| minute | 0 |
|
||||||
|
| heure | 19 |
|
||||||
|
| jour du mois | * |
|
||||||
|
| mois | * |
|
||||||
|
| jour semaine | * |
|
||||||
|
|
||||||
|
Le script s’exécute donc **tous les jours à 19h00**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Nettoyage
|
||||||
|
|
||||||
|
Une fois la sauvegarde transférée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -f "$LOCAL_BACKUP"
|
||||||
|
```
|
||||||
|
|
||||||
|
Cela évite de remplir le disque de la machine Vaultwarden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Test manuel
|
||||||
|
|
||||||
|
Avant de mettre le script en cron, tester :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Vérification des logs
|
||||||
|
|
||||||
|
Logs :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /var/log/vaultwarden_backup.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 14. Résumé
|
||||||
|
|
||||||
|
Le script automatise :
|
||||||
|
|
||||||
|
* la sauvegarde du dossier `data`
|
||||||
|
* la compression et la datation du backup
|
||||||
|
* le transfert sécurisé via SSH
|
||||||
|
* la notification Discord
|
||||||
|
* l’exécution automatique via cron
|
||||||
|
* la configuration sécurisée via `.env`
|
||||||
|
|
||||||
|
Ce système permet d’obtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
|
||||||
|
|
||||||
|
```
|
||||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
|||||||
# Chemins fixes du script
|
# Chemins fixes du script
|
||||||
#######################################
|
#######################################
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ENV_FILE="/home/matt/vaultwarden/scripts/Scripts-Serveur/backup_vaultwarden/.env"
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
LOG_FILE="/var/log/vaultwarden_backup.log"
|
LOG_FILE="/var/log/vaultwarden_backup.log"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
@@ -33,7 +33,7 @@ set +a
|
|||||||
#######################################
|
#######################################
|
||||||
# Variables obligatoires
|
# Variables obligatoires
|
||||||
#######################################
|
#######################################
|
||||||
: "${WEBHOOK_URL:=}"
|
: "${DISCORD_WEBHOOK_URL:=}"
|
||||||
: "${DATA_DIR:?Variable DATA_DIR manquante dans .env}"
|
: "${DATA_DIR:?Variable DATA_DIR manquante dans .env}"
|
||||||
: "${LOCAL_BACKUP:?Variable LOCAL_BACKUP manquante dans .env}"
|
: "${LOCAL_BACKUP:?Variable LOCAL_BACKUP manquante dans .env}"
|
||||||
: "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}"
|
: "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}"
|
||||||
@@ -45,13 +45,14 @@ set +a
|
|||||||
# Variables backup
|
# Variables backup
|
||||||
#######################################
|
#######################################
|
||||||
DATE="$(date +'%Y-%m-%d_%H-%M-%S')"
|
DATE="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||||
BACKUP_NAME="vaultwarden-backup-${DATE}.tar.gz"
|
BACKUP_PREFIX="vaultwarden-backup"
|
||||||
LOCAL_BACKUP_DIR="$LOCAL_BACKUP"
|
BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
|
||||||
LOCAL_BACKUP_FILE="${LOCAL_BACKUP_DIR}/${BACKUP_NAME}"
|
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)
|
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
||||||
|
|
||||||
mkdir -p "$LOCAL_BACKUP_DIR"
|
mkdir -p "$LOCAL_BACKUP"
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# Notification Discord
|
# Notification Discord
|
||||||
@@ -60,27 +61,28 @@ discord_ping() {
|
|||||||
local success="$1"
|
local success="$1"
|
||||||
local details="${2:-}"
|
local details="${2:-}"
|
||||||
|
|
||||||
[[ -z "$WEBHOOK_URL" ]] && return 0
|
[[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0
|
||||||
|
|
||||||
local icon status_line
|
local icon status_line
|
||||||
if [[ "$success" == "true" ]]; then
|
if [[ "$success" == "true" ]]; then
|
||||||
icon="🟢"
|
icon="🟢"
|
||||||
status_line="✅"
|
status_line="✅"
|
||||||
|
ping=""
|
||||||
else
|
else
|
||||||
icon="🔴"
|
icon="🔴"
|
||||||
status_line="❌"
|
status_line="❌"
|
||||||
|
ping="@here "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local msg
|
local msg
|
||||||
msg="**@here ${icon} Backup Vaultwarden**\n"
|
msg="**${ping}Backup Vaultwarden ${icon}**\n"
|
||||||
msg+="Backup: ${BACKUP_NAME}\n"
|
msg+="Backup: ${BACKUP_NAME}\n"
|
||||||
msg+="Data transfer: ${status_line}\n"
|
msg+="Data transfer: ${status_line}\n"
|
||||||
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||||
|
|
||||||
python3 - <<PY | curl -fsS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL" >/dev/null || true
|
local payload
|
||||||
import json
|
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||||
print(json.dumps({"content": """$msg"""}))
|
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||||
PY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
@@ -110,6 +112,8 @@ log "Destination distante : ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}"
|
|||||||
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" \
|
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" \
|
||||||
|| fail "Erreur lors de la compression du dossier $DATA_DIR"
|
|| fail "Erreur lors de la compression du dossier $DATA_DIR"
|
||||||
|
|
||||||
|
log "Backup local créé : $LOCAL_BACKUP_FILE"
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# Création dossier distant
|
# Création dossier distant
|
||||||
#######################################
|
#######################################
|
||||||
@@ -122,6 +126,17 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
|
|||||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
||||||
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
|
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
|
||||||
|
|
||||||
|
log "Backup envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Rotation distante - suppression > 10 jours
|
||||||
|
#######################################
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
|
"find '$REMOTE_DIR' -type f -name '${BACKUP_PREFIX}-*.tar.gz' -mtime +$RETENTION_DAYS -delete" \
|
||||||
|
|| fail "Erreur lors de la rotation distante des sauvegardes"
|
||||||
|
|
||||||
|
log "Rotation distante OK"
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# Nettoyage local
|
# Nettoyage local
|
||||||
#######################################
|
#######################################
|
||||||
@@ -132,4 +147,4 @@ rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOC
|
|||||||
#######################################
|
#######################################
|
||||||
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||||
discord_ping "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
discord_ping "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
||||||
echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||||
43
CHANGELOG.md
Normal file
43
CHANGELOG.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Liste des évolutions du projet Scripts Serveur
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### Fixed
|
||||||
6
CheckStorage/.env.exemple
Normal file
6
CheckStorage/.env.exemple
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#############################################
|
||||||
|
# DISCORD
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Webhook Discord pour notifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
@@ -18,15 +18,37 @@ La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cet
|
|||||||
3. ```bash
|
3. ```bash
|
||||||
cd Scripts-Serveur/CheckStorage
|
cd Scripts-Serveur/CheckStorage
|
||||||
```
|
```
|
||||||
|
### Génération de la clé SSH
|
||||||
|
|
||||||
|
Sur la machine exécutant les scripts :
|
||||||
|
|
||||||
|
```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
|
## Utilisation du script
|
||||||
|
0. Copiez le fichier d'environnement exemple et modifiez les variables selon votre configuration :
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
1. Donnez les permissions d'exécution au script :
|
1. Donnez les permissions d'exécution au script :
|
||||||
```bash
|
```bash
|
||||||
chmod +x check_storage.sh
|
chmod +x check-storage.sh
|
||||||
```
|
```
|
||||||
2. Exécutez le script pour vérifier l'espace de stockage :
|
2. Exécutez le script pour vérifier l'espace de stockage :
|
||||||
```bash
|
```bash
|
||||||
./check_storage.sh
|
./check-storage.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Initialisé un cron pour exécuter le script régulièrement
|
## Initialisé un cron pour exécuter le script régulièrement
|
||||||
@@ -36,8 +58,8 @@ La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cet
|
|||||||
```
|
```
|
||||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
|
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
|
||||||
```bash
|
```bash
|
||||||
50 7 * * * /chemin/vers/le/script/check_storage.sh
|
50 7 * * * /chemin/vers/le/script/check-storage.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Avertissement
|
## 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.
|
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.
|
||||||
|
|||||||
69
CheckStorage/check-storage.sh
Executable file
69
CheckStorage/check-storage.sh
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CHARGEMENT DU .env
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "ERROR: fichier .env introuvable : $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CONFIGURATION
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Limite maximale d'utilisation du disque en pourcentage
|
||||||
|
limit=70
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RÉCUPÉRATION DES INFORMATIONS DISQUE
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
read -r total_bytes used_bytes avail_bytes usage <<<"$(df -B1 / | awk 'NR==2 {gsub(/%/,"",$5); print $2, $3, $4, $5}')"
|
||||||
|
|
||||||
|
# Calcul du pourcentage d'espace libre
|
||||||
|
free=$((100 - usage))
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CONVERSION EN GIGAOCTETS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
used_gb=$(awk -v b="$used_bytes" 'BEGIN {printf "%.2f", b/1024/1024/1024}')
|
||||||
|
total_gb=$(awk -v b="$total_bytes" 'BEGIN {printf "%.2f", b/1024/1024/1024}')
|
||||||
|
avail_gb=$(awk -v b="$avail_bytes" 'BEGIN {printf "%.2f", b/1024/1024/1024}')
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# VÉRIFICATION DU SEUIL D'UTILISATION
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
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)"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# AFFICHAGE DES INFORMATIONS STOCKAGE
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
echo "Espace disponible : ${avail_gb} GB"
|
||||||
|
echo "Espace utilise / espace total : ${used_gb} GB / ${total_gb} GB"
|
||||||
|
echo "Name: ${HOSTNAME}"
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
limit=70
|
|
||||||
# Mettre le lien de votre webhook Discord dans un .env
|
|
||||||
WEBHOOK_URL=$(grep -E '^WEBHOOK_URL=' .env | cut -d '=' -f2-)
|
|
||||||
|
|
||||||
# Récupérer l'utilisation du disque en pourcentage
|
|
||||||
usage=$(df -h / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
|
|
||||||
# Calculer l'espace libre en pourcentage
|
|
||||||
free=$((100 - usage))
|
|
||||||
|
|
||||||
# Si l'utilisation dépasse la limite, envoyer une alerte sur Discord
|
|
||||||
if [ "$usage" -ge "$limit" ]; then
|
|
||||||
msgLimit="@here\n**CHECK STOCKAGE :red_circle:**\nLimite autorisé : ${limit}% \nUtilisation actuelle: ${usage}%\nEspace restant: ${free}%\nHeure: $(date)"
|
|
||||||
curl -X POST \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json; charset=utf-8" \
|
|
||||||
-d "{\"content\":\"$msgLimit\"}" \
|
|
||||||
"$WEBHOOK_URL"
|
|
||||||
# Log de l'alerte
|
|
||||||
echo "ALERTE >> ${usage}% d'utilisation, check fait le $(date)"
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
fi
|
|
||||||
57
README.md
57
README.md
@@ -1,7 +1,56 @@
|
|||||||
# Scripts Serveur MALIO
|
# Malio-Ops
|
||||||
|
|
||||||
Ce projet contient des scripts pour la gestion et la maintenance des serveurs de MALIO.
|
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.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
## Scripts disponibles
|
Le depot permet de :
|
||||||
* [CheckStorage] : Script de vérification de l'espace de stockage
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
## Structure du depot
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Focus RebuildBdd
|
||||||
|
|
||||||
|
Le dossier [RebuildBdd](RebuildBdd) regroupe la nouvelle chaine de reconstruction de base. Il contient notamment :
|
||||||
|
|
||||||
|
* [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
|
||||||
|
|
||||||
|
La documentation detaillee est disponible dans [RebuildBdd/README.md](/home/matte/Malio-ops/RebuildBdd/README.md).
|
||||||
|
|
||||||
|
## Prerequis
|
||||||
|
|
||||||
|
Les scripts du depot reposent principalement sur :
|
||||||
|
|
||||||
|
* `bash`
|
||||||
|
* `jq`
|
||||||
|
* `curl`
|
||||||
|
* `ssh`
|
||||||
|
* `scp`
|
||||||
|
|
||||||
|
Selon les scripts, d'autres outils peuvent etre necessaires, notamment PostgreSQL (`psql`, `pg_dump`, `pg_restore`) ou `tar`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Un modele commun est disponible dans [global.env.exemple](global.env.exemple). Il sert de base pour les variables partagees entre plusieurs scripts.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Les evolutions importantes sont suivies dans [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|||||||
221
RebuildBdd/Checkup/check-postgresql.sh
Executable file
221
RebuildBdd/Checkup/check-postgresql.sh
Executable file
@@ -0,0 +1,221 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
postgres_server_ready() {
|
||||||
|
require_cmd postgres || return 1
|
||||||
|
require_cmd pg_ctlcluster || return 1
|
||||||
|
require_cmd pg_lsclusters || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_postgres_cluster() {
|
||||||
|
if ! require_cmd pg_lsclusters || ! require_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" ]] && require_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 require_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 require_cmd pg_lsclusters; then
|
||||||
|
diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); "
|
||||||
|
fi
|
||||||
|
|
||||||
|
if require_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 require_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if require_cmd pg_lsclusters && require_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}"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
if ! require_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 ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_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_PACKAGE_LIST >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||||
|
POSTGRES_INSTALLED="yes"
|
||||||
|
log "Installation PostgreSQL terminée."
|
||||||
|
else
|
||||||
|
log "PostgreSQL déjà installé."
|
||||||
|
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..."
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
log "PostgreSQL répond correctement."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; 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."
|
||||||
336
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
336
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
@@ -0,0 +1,336 @@
|
|||||||
|
#!/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)
|
||||||
|
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() {
|
||||||
|
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" ;;
|
||||||
|
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"
|
||||||
|
[[ -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_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT 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
|
||||||
|
log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts"
|
||||||
|
ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \
|
||||||
|
fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}"
|
||||||
|
else
|
||||||
|
log "Host déjà présent dans known_hosts."
|
||||||
|
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=192.168.1.60
|
||||||
|
GLOBAL_BACKUP_REMOTE_PORT=22
|
||||||
|
GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups
|
||||||
|
|
||||||
|
# Clé SSH de lecture backup copiée sur les cibles
|
||||||
|
GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly
|
||||||
|
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub
|
||||||
|
GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||||
|
|
||||||
|
# Defaults PostgreSQL
|
||||||
|
GLOBAL_PGHOST=127.0.0.1
|
||||||
|
GLOBAL_PGPORT=5432
|
||||||
|
|
||||||
|
# Defaults scripts
|
||||||
|
GLOBAL_REMOTE_ROLES_DIR_NAME=user
|
||||||
|
GLOBAL_EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
|
||||||
|
# Defaults bootstrap / cible
|
||||||
|
GLOBAL_ENABLE_BOOTSTRAP=yes
|
||||||
|
GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||||
|
GLOBAL_AUTO_INSTALL_POSTGRES=yes
|
||||||
|
GLOBAL_AUTO_CREATE_PGUSER=yes
|
||||||
|
GLOBAL_PGUSER_SUPERUSER=no
|
||||||
|
GLOBAL_AUTO_CONFIGURE_SUDOERS=no
|
||||||
30
RebuildBdd/Config/Targets/prod.env.example
Normal file
30
RebuildBdd/Config/Targets/prod.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CIBLE : prod
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# TARGET_HOST_prod=10.0.0.20
|
||||||
|
# TARGET_PORT_prod=22
|
||||||
|
# TARGET_BOOTSTRAP_USER_prod=backup_liot
|
||||||
|
# TARGET_BOOTSTRAP_SSH_KEY_prod=/home/matteo/.ssh/id_ed25519_target_prod
|
||||||
|
# TARGET_RUNTIME_USER_prod=backup_liot
|
||||||
|
# TARGET_ENABLE_BOOTSTRAP_prod=yes
|
||||||
|
# TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_prod=yes
|
||||||
|
# TARGET_REPO_DIR_prod=/home/backup_liot/RebuildBdd
|
||||||
|
# TARGET_ENV_FILE_prod=/home/backup_liot/RebuildBdd/.env
|
||||||
|
# TARGET_ENV_NAME_prod=PROD
|
||||||
|
# TARGET_PGHOST_prod=127.0.0.1
|
||||||
|
# TARGET_PGPORT_prod=5432
|
||||||
|
# TARGET_PGUSER_prod=backup_liot
|
||||||
|
# TARGET_PGPASSWORD_prod=change_me_prod_password
|
||||||
|
# TARGET_DBS_prod="sirh inventory ferme"
|
||||||
|
# TARGET_BACKUP_SUBDIR_prod=bdd-prod
|
||||||
|
# TARGET_BACKUP_LOG_DIR_prod=/home/backup_liot/logs/rebuild_bdd
|
||||||
|
# TARGET_LOCAL_RESTORE_BASE_DIR_prod=/home/backup_liot/RebuildBdd/restore_tmp
|
||||||
|
# TARGET_SSH_KEY_prod=/home/backup_liot/.ssh/id_ed25519_backup_readonly
|
||||||
|
# TARGET_REMOTE_ROLES_DIR_NAME_prod=user
|
||||||
|
# TARGET_EXCLUDED_RESTORE_ROLES_prod="postgres"
|
||||||
|
# TARGET_AUTO_INSTALL_POSTGRES_prod=yes
|
||||||
|
# TARGET_AUTO_CREATE_PGUSER_prod=yes
|
||||||
|
# TARGET_PGUSER_SUPERUSER_prod=no
|
||||||
|
# TARGET_AUTO_CONFIGURE_SUDOERS_prod=no
|
||||||
42
RebuildBdd/Config/Targets/test.env.example
Normal file
42
RebuildBdd/Config/Targets/test.env.example
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# config/targets/test.env.example
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# SSH bootstrap cible
|
||||||
|
TARGET_HOST=192.168.1.50
|
||||||
|
TARGET_PORT=22
|
||||||
|
TARGET_BOOTSTRAP_USER=backup_liot
|
||||||
|
TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_test
|
||||||
|
TARGET_RUNTIME_USER=backup_liot
|
||||||
|
|
||||||
|
# Bootstrap
|
||||||
|
TARGET_ENABLE_BOOTSTRAP=yes
|
||||||
|
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||||
|
|
||||||
|
# Repo local cible
|
||||||
|
TARGET_REPO_DIR=/home/backup_liot/RebuildBdd
|
||||||
|
TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env
|
||||||
|
|
||||||
|
# PostgreSQL cible
|
||||||
|
TARGET_ENV_NAME=RECETTE
|
||||||
|
TARGET_PGHOST=127.0.0.1
|
||||||
|
TARGET_PGPORT=5432
|
||||||
|
TARGET_PGUSER=backup_liot
|
||||||
|
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/backup_liot/logs/rebuild_bdd
|
||||||
|
TARGET_LOCAL_RESTORE_BASE_DIR=/home/backup_liot/RebuildBdd/restore_tmp
|
||||||
|
TARGET_SSH_KEY=/home/backup_liot/.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
|
||||||
566
RebuildBdd/README.md
Normal file
566
RebuildBdd/README.md
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
# RebuildBdd
|
||||||
|
|
||||||
|
Orchestration de reconstruction de bases PostgreSQL à partir de dumps distants, avec préparation automatique des machines cibles, exécution non interactive et intégration web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Ce projet permet de :
|
||||||
|
|
||||||
|
- préparer automatiquement une machine cible neuve ou partiellement configurée ;
|
||||||
|
- déployer et mettre à jour les scripts sur la cible ;
|
||||||
|
- préparer PostgreSQL localement sur la cible ;
|
||||||
|
- récupérer le dernier dump disponible depuis un serveur de backup ;
|
||||||
|
- restaurer une base PostgreSQL de manière non interactive ;
|
||||||
|
- exposer un flux exploitable depuis une interface web via des retours JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnement global
|
||||||
|
|
||||||
|
Le flux standard est le suivant :
|
||||||
|
|
||||||
|
1. **Création ou mise à jour de la configuration d’une cible**
|
||||||
|
2. **Bootstrap initial de la cible**
|
||||||
|
3. **Précheck de préparation**
|
||||||
|
4. **Rebuild de la base**
|
||||||
|
|
||||||
|
En pratique :
|
||||||
|
|
||||||
|
- `create-target-config.sh` crée un fichier de configuration cible ;
|
||||||
|
- `bootstrap-target-host.sh` prépare la machine cible ;
|
||||||
|
- `Checkup/check-target-readiness.sh` valide l’environnement ;
|
||||||
|
- `rebuild-bdd-core.sh` exécute la restauration ;
|
||||||
|
- `run-rebuild-bdd.sh` orchestre l’ensemble.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Le projet utilise deux niveaux de configuration :
|
||||||
|
|
||||||
|
#### 1. Configuration globale
|
||||||
|
Fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/global.env
|
||||||
|
````
|
||||||
|
|
||||||
|
Contient les paramètres stables, par exemple :
|
||||||
|
|
||||||
|
* dépôt Git des scripts ;
|
||||||
|
* serveur de backup ;
|
||||||
|
* clé SSH de lecture backup ;
|
||||||
|
* valeurs par défaut PostgreSQL ;
|
||||||
|
* options globales de bootstrap.
|
||||||
|
|
||||||
|
#### 2. Configuration par cible
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/targets/<nom_cible>.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque fichier cible contient :
|
||||||
|
|
||||||
|
* accès SSH bootstrap ;
|
||||||
|
* répertoires locaux de la cible ;
|
||||||
|
* paramètres PostgreSQL ;
|
||||||
|
* sous-répertoire backup associé ;
|
||||||
|
* options spécifiques à la cible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arborescence recommandée
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RebuildBdd/
|
||||||
|
├── bootstrap-target-host.sh
|
||||||
|
├── create-target-config.sh
|
||||||
|
├── run-rebuild-bdd.sh
|
||||||
|
├── rebuild-bdd-core.sh
|
||||||
|
├── config/
|
||||||
|
│ ├── global.env
|
||||||
|
│ └── targets/
|
||||||
|
│ ├── test.env
|
||||||
|
│ └── prod.env
|
||||||
|
└── Checkup/
|
||||||
|
├── check-postgresql.sh
|
||||||
|
└── check-target-readiness.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### `create-target-config.sh`
|
||||||
|
|
||||||
|
Crée ou met à jour un fichier cible dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/targets/<cible>.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh \
|
||||||
|
--target test \
|
||||||
|
--host 192.168.1.50 \
|
||||||
|
--port 22 \
|
||||||
|
--bootstrap-user backup_liot \
|
||||||
|
--bootstrap-key /home/user/.ssh/id_ed25519_target_test \
|
||||||
|
--runtime-user backup_liot \
|
||||||
|
--repo-dir /home/backup_liot/RebuildBdd \
|
||||||
|
--env-name RECETTE \
|
||||||
|
--pguser backup_liot \
|
||||||
|
--pgpassword secret \
|
||||||
|
--dbs "sirh inventory ferme" \
|
||||||
|
--backup-subdir bdd-recette
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `bootstrap-target-host.sh`
|
||||||
|
|
||||||
|
Prépare une machine cible neuve ou quasi neuve :
|
||||||
|
|
||||||
|
* connexion SSH bootstrap ;
|
||||||
|
* installation des paquets minimum ;
|
||||||
|
* création des dossiers ;
|
||||||
|
* génération du `.env` cible ;
|
||||||
|
* copie de la clé SSH backup ;
|
||||||
|
* préparation de `known_hosts` ;
|
||||||
|
* installation éventuelle d’un `sudoers.d` minimal ;
|
||||||
|
* synchronisation du dépôt ;
|
||||||
|
* exécution de `check-postgresql.sh`.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
Mode JSON :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test --json-only
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Checkup/check-postgresql.sh`
|
||||||
|
|
||||||
|
Prépare PostgreSQL localement sur la cible :
|
||||||
|
|
||||||
|
* installation si absent ;
|
||||||
|
* démarrage du service ;
|
||||||
|
* test de disponibilité ;
|
||||||
|
* création du rôle PostgreSQL cible si nécessaire.
|
||||||
|
|
||||||
|
Ce script est prévu pour fonctionner en non interactif avec `sudo -n`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Checkup/check-target-readiness.sh`
|
||||||
|
|
||||||
|
Valide la préparation complète de la cible :
|
||||||
|
|
||||||
|
* lecture du `.env` cible ;
|
||||||
|
* vérification des chemins ;
|
||||||
|
* permissions locales ;
|
||||||
|
* permissions SSH ;
|
||||||
|
* `known_hosts` ;
|
||||||
|
* accès SSH au serveur de backup ;
|
||||||
|
* exécution de `check-postgresql.sh`.
|
||||||
|
|
||||||
|
Mode JSON disponible pour usage web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `rebuild-bdd-core.sh`
|
||||||
|
|
||||||
|
Script métier de reconstruction :
|
||||||
|
|
||||||
|
* validation des paramètres ;
|
||||||
|
* connexion au serveur de backup ;
|
||||||
|
* récupération du dernier dump ;
|
||||||
|
* récupération éventuelle du fichier des rôles ;
|
||||||
|
* suppression/recréation de la base si autorisé ;
|
||||||
|
* restauration des rôles ;
|
||||||
|
* restauration du dump PostgreSQL ;
|
||||||
|
* retour JSON final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `run-rebuild-bdd.sh`
|
||||||
|
|
||||||
|
Script orchestrateur principal.
|
||||||
|
|
||||||
|
Il peut :
|
||||||
|
|
||||||
|
* lancer le bootstrap si activé pour la cible ;
|
||||||
|
* synchroniser le dépôt distant ;
|
||||||
|
* lancer le précheck ;
|
||||||
|
* exécuter le rebuild.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--request-id web_001 \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Machine de lancement
|
||||||
|
|
||||||
|
Doit disposer de :
|
||||||
|
|
||||||
|
* `bash`
|
||||||
|
* `ssh`
|
||||||
|
* `scp`
|
||||||
|
* `git`
|
||||||
|
* `python3`
|
||||||
|
|
||||||
|
### Machine cible
|
||||||
|
|
||||||
|
Le bootstrap suppose :
|
||||||
|
|
||||||
|
* accès SSH fonctionnel ;
|
||||||
|
* utilisateur bootstrap existant ;
|
||||||
|
* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial.
|
||||||
|
|
||||||
|
### Serveur de backup
|
||||||
|
|
||||||
|
Doit :
|
||||||
|
|
||||||
|
* être joignable en SSH depuis la cible ;
|
||||||
|
* accepter la clé de lecture backup ;
|
||||||
|
* contenir les dumps dans l’arborescence attendue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure des backups attendue
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/malio-b/backups/
|
||||||
|
├── bdd-recette/
|
||||||
|
│ ├── sirh/
|
||||||
|
│ │ ├── sirh_2026-03-16_19-00-01.dump
|
||||||
|
│ ├── inventory/
|
||||||
|
│ ├── ferme/
|
||||||
|
│ └── user/
|
||||||
|
│ ├── user_2026-03-16_19-00-01.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script recherche :
|
||||||
|
|
||||||
|
* le dernier dump dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<BACKUP_REMOTE_DIR>/<db>/<db>_*.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
* le dernier fichier rôles dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<BACKUP_REMOTE_DIR>/<REMOTE_ROLES_DIR_NAME>/user_*.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Créer la configuration globale
|
||||||
|
|
||||||
|
Copier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/global.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
vers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/global.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Renseigner ensuite :
|
||||||
|
|
||||||
|
* dépôt Git ;
|
||||||
|
* serveur de backup ;
|
||||||
|
* clé SSH backup ;
|
||||||
|
* defaults globaux.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Créer une cible
|
||||||
|
|
||||||
|
Deux possibilités.
|
||||||
|
|
||||||
|
#### A. À la main
|
||||||
|
|
||||||
|
Créer un fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/targets/test.env
|
||||||
|
```
|
||||||
|
|
||||||
|
à partir de :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/targets/test.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Via script
|
||||||
|
|
||||||
|
Utiliser :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exécution locale
|
||||||
|
|
||||||
|
### Bootstrap seul
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild complet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intégration web
|
||||||
|
|
||||||
|
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/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "la base existe déjà et overwrite n'est pas autorisé",
|
||||||
|
"request_id": "web_001",
|
||||||
|
"environment": "RECETTE",
|
||||||
|
"database": "sirh",
|
||||||
|
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
|
||||||
|
"log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Recommandations minimales
|
||||||
|
|
||||||
|
* utiliser des clés SSH dédiées ;
|
||||||
|
* limiter la clé backup à la lecture seule ;
|
||||||
|
* restreindre les permissions des fichiers de config ;
|
||||||
|
* exécuter les scripts avec un utilisateur dédié ;
|
||||||
|
* ne pas exposer les secrets dans l’interface web ;
|
||||||
|
* valider strictement toutes les entrées côté backend.
|
||||||
|
|
||||||
|
### `sudoers`
|
||||||
|
|
||||||
|
Le bootstrap peut installer un `sudoers.d` minimal pour l’utilisateur runtime :
|
||||||
|
|
||||||
|
```sudoers
|
||||||
|
<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/backup_liot/logs/rebuild_bdd/
|
||||||
|
```
|
||||||
|
|
||||||
|
Le chemin du log est renvoyé dans le JSON final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limites connues
|
||||||
|
|
||||||
|
* le bootstrap initial nécessite un accès SSH bootstrap valide ;
|
||||||
|
* le bootstrap ne remplace pas une mauvaise architecture réseau ;
|
||||||
|
* les secrets doivent être gérés proprement par la couche web/backend ;
|
||||||
|
* des verrous d’exécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommandations de validation
|
||||||
|
|
||||||
|
Avant mise en production, tester au minimum :
|
||||||
|
|
||||||
|
1. bootstrap d’une machine neuve ;
|
||||||
|
2. rebuild complet d’une base ;
|
||||||
|
3. refus si la base existe et `overwrite=no` ;
|
||||||
|
4. relance complète une seconde fois sur la même cible ;
|
||||||
|
5. accès backup invalide ;
|
||||||
|
6. PostgreSQL absent au départ ;
|
||||||
|
7. `sudo -n` indisponible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
### Créer une cible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh \
|
||||||
|
--target test \
|
||||||
|
--host 192.168.1.50 \
|
||||||
|
--port 22 \
|
||||||
|
--bootstrap-user backup_liot \
|
||||||
|
--bootstrap-key /home/matteo/.ssh/id_ed25519_target_test \
|
||||||
|
--runtime-user backup_liot \
|
||||||
|
--repo-dir /home/backup_liot/RebuildBdd \
|
||||||
|
--env-name RECETTE \
|
||||||
|
--pguser backup_liot \
|
||||||
|
--pgpassword secret \
|
||||||
|
--dbs "sirh inventory ferme" \
|
||||||
|
--backup-subdir bdd-recette
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bootstrap
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État du projet
|
||||||
|
|
||||||
|
Le projet permet désormais une utilisation :
|
||||||
|
|
||||||
|
* locale ;
|
||||||
|
* automatisée ;
|
||||||
|
* intégrée au web ;
|
||||||
|
|
||||||
|
avec préparation des cibles, exécution non interactive et retour JSON.
|
||||||
|
|
||||||
|
```
|
||||||
579
RebuildBdd/bootstrap-target-host.sh
Executable file
579
RebuildBdd/bootstrap-target-host.sh
Executable file
@@ -0,0 +1,579 @@
|
|||||||
|
#!/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" ;;
|
||||||
|
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
|
||||||
|
|
||||||
|
[[ -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=accept-new
|
||||||
|
-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")
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
REMOTE_KNOWN_HOSTS_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v ssh-keyscan >/dev/null 2>&1; then
|
||||||
|
echo 'ssh-keyscan absent sur la cible' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ssh-keygen -F $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") -f $(shell_quote "$REMOTE_KNOWN_HOSTS") >/dev/null 2>&1; then
|
||||||
|
ssh-keyscan -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") -H $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") >> $(shell_quote "$REMOTE_KNOWN_HOSTS") 2>/dev/null
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Ajout du serveur de backup dans known_hosts côté cible"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_KNOWN_HOSTS_CMD" \
|
||||||
|
|| fail "échec de préparation known_hosts sur la cible"
|
||||||
|
|
||||||
|
STRICT_OPTION="yes"
|
||||||
|
case "${TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE,,}" in
|
||||||
|
yes|y|oui|o|true|1) STRICT_OPTION="yes" ;;
|
||||||
|
no|n|non|false|0) STRICT_OPTION="no" ;;
|
||||||
|
*) fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
REMOTE_BACKUP_TEST_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ssh \
|
||||||
|
-i $(shell_quote "$TARGET_SSH_KEY_VALUE") \
|
||||||
|
-p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") \
|
||||||
|
-o IdentitiesOnly=yes \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o ConnectTimeout=8 \
|
||||||
|
-o StrictHostKeyChecking=$(shell_quote "$STRICT_OPTION") \
|
||||||
|
$(shell_quote "${TARGET_BACKUP_REMOTE_USER_VALUE}@${TARGET_BACKUP_REMOTE_HOST_VALUE}") \
|
||||||
|
test -d $(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Test de la connexion SSH cible -> backup"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_BACKUP_TEST_CMD" \
|
||||||
|
|| fail "la cible ne peut pas accéder au serveur de backup avec la clé fournie"
|
||||||
|
|
||||||
|
if [[ "$ALLOW_PASSWORDLESS_SUDO" == "yes" ]]; then
|
||||||
|
REMOTE_SUDOERS_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
|
||||||
|
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}"
|
||||||
160
RebuildBdd/create-target-config.sh
Normal file
160
RebuildBdd/create-target-config.sh
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||||
|
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||||
|
|
||||||
|
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||||
|
|
||||||
|
TARGET=""
|
||||||
|
HOST=""
|
||||||
|
PORT="22"
|
||||||
|
BOOTSTRAP_USER=""
|
||||||
|
BOOTSTRAP_SSH_KEY=""
|
||||||
|
RUNTIME_USER=""
|
||||||
|
REPO_DIR=""
|
||||||
|
ENV_FILE=""
|
||||||
|
ENV_NAME=""
|
||||||
|
PGHOST=""
|
||||||
|
PGPORT=""
|
||||||
|
PGUSER=""
|
||||||
|
PGPASSWORD=""
|
||||||
|
DBS=""
|
||||||
|
BACKUP_SUBDIR=""
|
||||||
|
BACKUP_LOG_DIR=""
|
||||||
|
LOCAL_RESTORE_BASE_DIR=""
|
||||||
|
SSH_KEY_TARGET_PATH=""
|
||||||
|
ENABLE_BOOTSTRAP="yes"
|
||||||
|
ALLOW_PASSWORDLESS_SUDO="yes"
|
||||||
|
AUTO_INSTALL_POSTGRES="yes"
|
||||||
|
AUTO_CREATE_PGUSER="yes"
|
||||||
|
PGUSER_SUPERUSER="no"
|
||||||
|
AUTO_CONFIGURE_SUDOERS="no"
|
||||||
|
REMOTE_ROLES_DIR_NAME="user"
|
||||||
|
EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
FORCE="no"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--targets-dir) TARGETS_DIR="$2"; shift 2 ;;
|
||||||
|
--target) TARGET="$2"; shift 2 ;;
|
||||||
|
--host) HOST="$2"; shift 2 ;;
|
||||||
|
--port) PORT="$2"; shift 2 ;;
|
||||||
|
--bootstrap-user) BOOTSTRAP_USER="$2"; shift 2 ;;
|
||||||
|
--bootstrap-key) BOOTSTRAP_SSH_KEY="$2"; shift 2 ;;
|
||||||
|
--runtime-user) RUNTIME_USER="$2"; shift 2 ;;
|
||||||
|
--repo-dir) REPO_DIR="$2"; shift 2 ;;
|
||||||
|
--env-file) ENV_FILE="$2"; shift 2 ;;
|
||||||
|
--env-name) ENV_NAME="$2"; shift 2 ;;
|
||||||
|
--pghost) PGHOST="$2"; shift 2 ;;
|
||||||
|
--pgport) PGPORT="$2"; shift 2 ;;
|
||||||
|
--pguser) PGUSER="$2"; shift 2 ;;
|
||||||
|
--pgpassword) PGPASSWORD="$2"; shift 2 ;;
|
||||||
|
--dbs) DBS="$2"; shift 2 ;;
|
||||||
|
--backup-subdir) BACKUP_SUBDIR="$2"; shift 2 ;;
|
||||||
|
--backup-log-dir) BACKUP_LOG_DIR="$2"; shift 2 ;;
|
||||||
|
--local-restore-base-dir) LOCAL_RESTORE_BASE_DIR="$2"; shift 2 ;;
|
||||||
|
--ssh-key-target-path) SSH_KEY_TARGET_PATH="$2"; shift 2 ;;
|
||||||
|
--enable-bootstrap) ENABLE_BOOTSTRAP="$2"; shift 2 ;;
|
||||||
|
--allow-passwordless-sudo) ALLOW_PASSWORDLESS_SUDO="$2"; shift 2 ;;
|
||||||
|
--auto-install-postgres) AUTO_INSTALL_POSTGRES="$2"; shift 2 ;;
|
||||||
|
--auto-create-pguser) AUTO_CREATE_PGUSER="$2"; shift 2 ;;
|
||||||
|
--pguser-superuser) PGUSER_SUPERUSER="$2"; shift 2 ;;
|
||||||
|
--auto-configure-sudoers) AUTO_CONFIGURE_SUDOERS="$2"; shift 2 ;;
|
||||||
|
--remote-roles-dir-name) REMOTE_ROLES_DIR_NAME="$2"; shift 2 ;;
|
||||||
|
--excluded-restore-roles) EXCLUDED_RESTORE_ROLES="$2"; shift 2 ;;
|
||||||
|
--force) FORCE="yes"; shift ;;
|
||||||
|
*) echo "Argument inconnu : $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n "$TARGET" ]] || fail "--target manquant"
|
||||||
|
[[ "$TARGET" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "target invalide"
|
||||||
|
|
||||||
|
[[ -n "$HOST" ]] || fail "--host manquant"
|
||||||
|
[[ -n "$BOOTSTRAP_USER" ]] || fail "--bootstrap-user manquant"
|
||||||
|
[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "--bootstrap-key manquant"
|
||||||
|
[[ -n "$REPO_DIR" ]] || fail "--repo-dir manquant"
|
||||||
|
[[ -n "$ENV_NAME" ]] || fail "--env-name manquant"
|
||||||
|
[[ -n "$PGUSER" ]] || fail "--pguser manquant"
|
||||||
|
[[ -n "$PGPASSWORD" ]] || fail "--pgpassword manquant"
|
||||||
|
[[ -n "$DBS" ]] || fail "--dbs manquant"
|
||||||
|
[[ -n "$BACKUP_SUBDIR" ]] || fail "--backup-subdir manquant"
|
||||||
|
[[ "$PORT" =~ ^[0-9]+$ ]] || fail "--port invalide"
|
||||||
|
|
||||||
|
[[ -n "$RUNTIME_USER" ]] || RUNTIME_USER="$BOOTSTRAP_USER"
|
||||||
|
[[ -n "$ENV_FILE" ]] || ENV_FILE="${REPO_DIR}/.env"
|
||||||
|
[[ -n "$PGHOST" ]] || PGHOST="127.0.0.1"
|
||||||
|
[[ -n "$PGPORT" ]] || PGPORT="5432"
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "--pgport invalide"
|
||||||
|
[[ -n "$BACKUP_LOG_DIR" ]] || BACKUP_LOG_DIR="/home/${RUNTIME_USER}/logs/rebuild_bdd"
|
||||||
|
[[ -n "$LOCAL_RESTORE_BASE_DIR" ]] || LOCAL_RESTORE_BASE_DIR="${REPO_DIR}/restore_tmp"
|
||||||
|
[[ -n "$SSH_KEY_TARGET_PATH" ]] || SSH_KEY_TARGET_PATH="/home/${RUNTIME_USER}/.ssh/id_ed25519_backup_readonly"
|
||||||
|
|
||||||
|
ENABLE_BOOTSTRAP="$(to_bool_yes_no "$ENABLE_BOOTSTRAP")" || fail "--enable-bootstrap invalide"
|
||||||
|
ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$ALLOW_PASSWORDLESS_SUDO")" || fail "--allow-passwordless-sudo invalide"
|
||||||
|
AUTO_INSTALL_POSTGRES="$(to_bool_yes_no "$AUTO_INSTALL_POSTGRES")" || fail "--auto-install-postgres invalide"
|
||||||
|
AUTO_CREATE_PGUSER="$(to_bool_yes_no "$AUTO_CREATE_PGUSER")" || fail "--auto-create-pguser invalide"
|
||||||
|
PGUSER_SUPERUSER="$(to_bool_yes_no "$PGUSER_SUPERUSER")" || fail "--pguser-superuser invalide"
|
||||||
|
AUTO_CONFIGURE_SUDOERS="$(to_bool_yes_no "$AUTO_CONFIGURE_SUDOERS")" || fail "--auto-configure-sudoers invalide"
|
||||||
|
|
||||||
|
mkdir -p "$TARGETS_DIR" || fail "impossible de créer $TARGETS_DIR"
|
||||||
|
|
||||||
|
TARGET_FILE="${TARGETS_DIR}/${TARGET}.env"
|
||||||
|
if [[ -f "$TARGET_FILE" && "$FORCE" != "yes" ]]; then
|
||||||
|
fail "fichier déjà existant : $TARGET_FILE (utiliser --force pour écraser)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >"$TARGET_FILE" <<EOF
|
||||||
|
TARGET_HOST=$(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}"
|
||||||
501
RebuildBdd/rebuild-bdd-core.sh
Executable file
501
RebuildBdd/rebuild-bdd-core.sh
Executable file
@@ -0,0 +1,501 @@
|
|||||||
|
#!/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() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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" ;;
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
[[ "$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" || fail "commande requise absente : $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"
|
||||||
|
|
||||||
|
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"
|
||||||
|
[[ "$DB" =~ ^[a-zA-Z0-9_]+$ ]] || 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}"
|
||||||
|
|
||||||
|
send_discord_message() {
|
||||||
|
local message="$1"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||||
|
|
||||||
|
payload="$(python3 -c 'import json,sys; print(json.dumps({"content": sys.argv[1]}))' "$message")" || return 0
|
||||||
|
|
||||||
|
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
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_message "$SUCCESS_MESSAGE"
|
||||||
|
|
||||||
|
log "Restauration terminée avec succès pour ${DB}"
|
||||||
|
print_json_and_exit "success" "restauration terminée avec succès" 0
|
||||||
428
RebuildBdd/run-rebuild-bdd.sh
Executable file
428
RebuildBdd/run-rebuild-bdd.sh
Executable file
@@ -0,0 +1,428 @@
|
|||||||
|
#!/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" ;;
|
||||||
|
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}}"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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=accept-new
|
||||||
|
-o ConnectTimeout=8
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \
|
||||||
|
|| fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}"
|
||||||
|
|
||||||
|
TARGET_CORE_SCRIPT="${TARGET_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
|
||||||
|
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
|
||||||
378
RecetteScripts/README.md
Normal file
378
RecetteScripts/README.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# RecetteScripts
|
||||||
|
|
||||||
|
Scripts Bash permettant d’automatiser la gestion d’un environnement **PostgreSQL de recette**.
|
||||||
|
|
||||||
|
Ces scripts permettent :
|
||||||
|
|
||||||
|
* la **sauvegarde automatisée des bases**
|
||||||
|
* la **surveillance de la disponibilité des applications**
|
||||||
|
* la **reconstruction d’une base à partir d’un dump**
|
||||||
|
|
||||||
|
Chaque script possède son propre **fichier `.env` dédié** afin de séparer les configurations.(un global.env.exemple est disponible à la racine du projet)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0. Arborescence du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
RecetteScripts
|
||||||
|
│
|
||||||
|
├── backup-bdd-recette.sh # script de sauvegarde PostgreSQL
|
||||||
|
├── backup.env.exemple # exemple de configuration backup
|
||||||
|
│
|
||||||
|
├── check-statut-recette.sh # script de monitoring des applications
|
||||||
|
├── check-statut.env.exemple # exemple de configuration monitoring
|
||||||
|
│
|
||||||
|
├── rebuild-bdd-recette.sh # script de restauration PostgreSQL
|
||||||
|
├── rebuild.env.exemple # exemple de configuration restauration
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Principe général
|
||||||
|
|
||||||
|
Les scripts fonctionnent indépendamment mais utilisent le même principe :
|
||||||
|
|
||||||
|
1. chargement d’un fichier `.env`
|
||||||
|
2. vérification des variables obligatoires
|
||||||
|
3. exécution de la tâche principale
|
||||||
|
4. génération de logs
|
||||||
|
5. notification Discord (optionnelle)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Prérequis
|
||||||
|
|
||||||
|
Environnement Linux recommandé.
|
||||||
|
|
||||||
|
Packages nécessaires :
|
||||||
|
|
||||||
|
```
|
||||||
|
postgresql-client
|
||||||
|
curl
|
||||||
|
jq
|
||||||
|
ssh
|
||||||
|
scp
|
||||||
|
```
|
||||||
|
|
||||||
|
Commandes PostgreSQL requises :
|
||||||
|
|
||||||
|
```
|
||||||
|
pg_dump
|
||||||
|
pg_dumpall
|
||||||
|
pg_restore
|
||||||
|
psql
|
||||||
|
createdb
|
||||||
|
dropdb
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### 3 Connexion SSH
|
||||||
|
|
||||||
|
Une connexion SSH avec **clé privée** est nécessaire afin de permettre les transferts automatisés de fichiers vers le serveur distant (dump PostgreSQL, rôles, etc.).
|
||||||
|
|
||||||
|
### Génération de la clé SSH
|
||||||
|
|
||||||
|
Sur la machine exécutant les scripts :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -f ~/.ssh/id_backup_postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Explication :
|
||||||
|
|
||||||
|
* `-t ed25519` : algorithme recommandé
|
||||||
|
* `-f` : chemin de la clé
|
||||||
|
|
||||||
|
Deux fichiers seront créés :
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.ssh/id_backup_postgres
|
||||||
|
~/.ssh/id_backup_postgres.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Copier la clé sur le serveur distant
|
||||||
|
|
||||||
|
Méthode recommandée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-copy-id -i ~/.ssh/id_backup_postgres.pub user@serveur
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-copy-id -i ~/.ssh/id_backup_postgres.pub backup@192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Vérifier la connexion
|
||||||
|
|
||||||
|
Tester la connexion sans mot de passe :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/id_backup_postgres backup@192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
La connexion doit fonctionner **sans demander de mot de passe**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Sécuriser les permissions
|
||||||
|
|
||||||
|
Les permissions doivent être restreintes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
chmod 600 ~/.ssh/id_backup_postgres
|
||||||
|
chmod 644 ~/.ssh/id_backup_postgres.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Configuration
|
||||||
|
|
||||||
|
Chaque script possède un **fichier d’exemple** :
|
||||||
|
|
||||||
|
```
|
||||||
|
backup.env.exemple
|
||||||
|
check-statut.env.exemple
|
||||||
|
rebuild.env.exemple
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour utiliser les scripts :
|
||||||
|
|
||||||
|
```
|
||||||
|
cp backup.env.exemple .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis modifier les variables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Script : backup-bdd-recette.sh
|
||||||
|
|
||||||
|
Script :
|
||||||
|
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Sauvegarder plusieurs bases PostgreSQL et transférer les dumps vers un serveur distant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnement
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
|
||||||
|
1. charge la configuration `.env`
|
||||||
|
2. vérifie les dépendances
|
||||||
|
3. empêche l’exécution simultanée (lock)
|
||||||
|
4. exporte les rôles PostgreSQL
|
||||||
|
5. crée un dump de chaque base
|
||||||
|
6. transfère les dumps vers un serveur distant
|
||||||
|
7. applique une rotation des sauvegardes
|
||||||
|
8. envoie un résumé sur Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Format des fichiers
|
||||||
|
|
||||||
|
Dump base :
|
||||||
|
|
||||||
|
```
|
||||||
|
base_TIMESTAMP.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
Export utilisateurs :
|
||||||
|
|
||||||
|
```
|
||||||
|
user_TIMESTAMP.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation automatique
|
||||||
|
|
||||||
|
Suppression des sauvegardes plus anciennes que :
|
||||||
|
|
||||||
|
```
|
||||||
|
10 jours
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exécution
|
||||||
|
|
||||||
|
```
|
||||||
|
./backup-bdd-recette.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Script : check-statut-recette.sh
|
||||||
|
|
||||||
|
Script :
|
||||||
|
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Vérifier la disponibilité des applications web.
|
||||||
|
|
||||||
|
Ce script agit comme un **mini système de monitoring**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérifications
|
||||||
|
|
||||||
|
Pour chaque application :
|
||||||
|
|
||||||
|
1. résolution DNS
|
||||||
|
2. requête HTTP
|
||||||
|
3. analyse du code HTTP
|
||||||
|
|
||||||
|
Codes valides :
|
||||||
|
|
||||||
|
```
|
||||||
|
200 → 399
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemple de configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
APP_URLS="ferme.malio-dev.fr sirh.malio-dev.fr inventory.malio-dev.fr"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
Fichier généré :
|
||||||
|
|
||||||
|
```
|
||||||
|
app_health_YYYY-MM-DD.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Format :
|
||||||
|
|
||||||
|
```
|
||||||
|
date | statut | host | détail
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemple de notification Discord
|
||||||
|
|
||||||
|
```
|
||||||
|
CHECK APP RECETTE 🟢
|
||||||
|
|
||||||
|
✅ ferme.malio-dev.fr : OK
|
||||||
|
✅ sirh.malio-dev.fr : OK
|
||||||
|
✅ inventory.malio-dev.fr : OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Script : rebuild-bdd-recette.sh
|
||||||
|
|
||||||
|
Script :
|
||||||
|
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Restaurer une base PostgreSQL à partir d’un dump distant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnement
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
|
||||||
|
1. charge la configuration `.env`
|
||||||
|
2. installe PostgreSQL si nécessaire
|
||||||
|
3. démarre le service PostgreSQL
|
||||||
|
4. demande la base à restaurer
|
||||||
|
5. récupère le dernier dump sur le serveur distant
|
||||||
|
6. récupère le dernier export des rôles
|
||||||
|
7. crée les rôles manquants
|
||||||
|
8. supprime la base existante si nécessaire
|
||||||
|
9. restaure la base via `pg_restore`
|
||||||
|
10. envoie une notification Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sélection de la base
|
||||||
|
|
||||||
|
Les bases disponibles sont lues depuis :
|
||||||
|
|
||||||
|
```
|
||||||
|
DBS="sirh inventory ferme"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```
|
||||||
|
1) sirh
|
||||||
|
2) inventory
|
||||||
|
3) ferme
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commande utilisée pour la restauration
|
||||||
|
|
||||||
|
```
|
||||||
|
pg_restore
|
||||||
|
--clean
|
||||||
|
--if-exists
|
||||||
|
--no-owner
|
||||||
|
--no-privileges
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces options évitent les conflits entre environnements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Logs
|
||||||
|
|
||||||
|
Les scripts produisent des logs détaillés :
|
||||||
|
|
||||||
|
```
|
||||||
|
backup logs
|
||||||
|
restore logs
|
||||||
|
app health logs
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces logs permettent :
|
||||||
|
|
||||||
|
* diagnostic des erreurs
|
||||||
|
* audit des opérations
|
||||||
|
* suivi des backups
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Automatisation recommandée
|
||||||
|
|
||||||
|
### Backup et check quotidien
|
||||||
|
|
||||||
|
```
|
||||||
|
0 19 * * * /scripts/backup-bdd-recette.sh
|
||||||
|
0 19 * * * /scripts/check-statut-recette.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Bonnes pratiques
|
||||||
|
|
||||||
|
Recommandé :
|
||||||
|
|
||||||
|
* isoler le **serveur de stockage**
|
||||||
|
* vérifier régulièrement les restaurations
|
||||||
|
---
|
||||||
445
RecetteScripts/backup-bdd-recette.sh
Executable file
445
RecetteScripts/backup-bdd-recette.sh
Executable file
@@ -0,0 +1,445 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# backup-bdd-recette.sh
|
||||||
|
#
|
||||||
|
# Ce script réalise une sauvegarde logique de plusieurs bases PostgreSQL
|
||||||
|
# définies dans le fichier .env, exporte également la liste des rôles/users,
|
||||||
|
# puis transfère l’ensemble vers une machine distante de stockage.
|
||||||
|
#
|
||||||
|
# Fonctionnement global :
|
||||||
|
# 1. charge la configuration depuis le fichier .env ;
|
||||||
|
# 2. vérifie les dépendances nécessaires ;
|
||||||
|
# 3. prépare les chemins, logs et variables de connexion ;
|
||||||
|
# 4. empêche l’exécution simultanée grâce à un verrou ;
|
||||||
|
# 5. crée les dossiers de destination sur la machine distante ;
|
||||||
|
# 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 ;
|
||||||
|
# 10. envoie un bilan sur Discord :
|
||||||
|
# - 1 message global si tout est OK ;
|
||||||
|
# - en cas d’erreur partielle :
|
||||||
|
# * USERS OK -> message simple ;
|
||||||
|
# * USERS KO -> message détaillé ;
|
||||||
|
# * DB OK -> message simple ;
|
||||||
|
# * DB KO -> message détaillé.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Chargement du .env
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "ERROR: fichier .env introuvable : $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Vérification des variables requises
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
: "${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}"
|
||||||
|
: "${SSH_TIMEOUT:?Variable SSH_TIMEOUT manquante}"
|
||||||
|
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Configuration principale
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
read -r -a DBS_ARRAY <<< "$DBS"
|
||||||
|
|
||||||
|
IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||||
|
IA_BASE_DIR="${BACKUP_REMOTE_DIR}"
|
||||||
|
RETENTION_DAYS=10
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
LOG_DIR="${BACKUP_LOG_DIR}"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
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') ---- $*"; }
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Configuration Discord
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||||
|
|
||||||
|
discord_send() {
|
||||||
|
local msg="$1"
|
||||||
|
[[ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
curl -fsS \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Message global OK
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
discord_msg_global_ok() {
|
||||||
|
local msg
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**BACKUP BDD ${ENV_NAME} 🟢**
|
||||||
|
Name: ${BACKUP_DIR_NAME}
|
||||||
|
Dumps transfer: ✅
|
||||||
|
Users transfer: ✅
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
discord_send "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Messages USERS
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
discord_msg_users_ok_simple() {
|
||||||
|
local msg
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**BACKUP BDD ${ENV_NAME} 🟢**
|
||||||
|
Users backup validé
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
discord_send "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
discord_msg_users_error() {
|
||||||
|
local export_ok="$1"
|
||||||
|
local transfer_ok="$2"
|
||||||
|
local details="$3"
|
||||||
|
|
||||||
|
local export_disp transfer_disp
|
||||||
|
export_disp=$([[ -n "$export_ok" ]] && echo "✅" || echo "❌")
|
||||||
|
transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌")
|
||||||
|
|
||||||
|
local msg
|
||||||
|
if [[ -n "$details" ]]; then
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**
|
||||||
|
Name: ${BACKUP_DIR_NAME}
|
||||||
|
Users export: ${export_disp}
|
||||||
|
Users transfer: ${transfer_disp}
|
||||||
|
Details: ${details}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
else
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**
|
||||||
|
Name: ${BACKUP_DIR_NAME}
|
||||||
|
Users export: ${export_disp}
|
||||||
|
Users transfer: ${transfer_disp}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
discord_send "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Messages DB
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
discord_msg_db_ok_simple() {
|
||||||
|
local db="$1"
|
||||||
|
local msg
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**BACKUP BDD ${ENV_NAME} 🟢**
|
||||||
|
Backup validé : ${db}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
discord_send "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
discord_msg_db_error() {
|
||||||
|
local db="$1"
|
||||||
|
local dump_ok="$2"
|
||||||
|
local transfer_ok="$3"
|
||||||
|
local details="$4"
|
||||||
|
|
||||||
|
local dump_disp transfer_disp
|
||||||
|
dump_disp=$([[ -n "$dump_ok" ]] && echo "✅" || echo "❌")
|
||||||
|
transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌")
|
||||||
|
|
||||||
|
local msg
|
||||||
|
if [[ -n "$details" ]]; then
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**
|
||||||
|
Name: ${BACKUP_DIR_NAME}
|
||||||
|
Database: ${db}
|
||||||
|
Dump: ${dump_disp}
|
||||||
|
Transfer: ${transfer_disp}
|
||||||
|
Details: ${details}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
else
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**
|
||||||
|
Name: ${BACKUP_DIR_NAME}
|
||||||
|
Database: ${db}
|
||||||
|
Dump: ${dump_disp}
|
||||||
|
Transfer: ${transfer_disp}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
discord_send "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Variables de statut globales
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
DUMPS_OK=true
|
||||||
|
USERS_OK=true
|
||||||
|
|
||||||
|
USERS_EXPORT_OK=true
|
||||||
|
USERS_TRANSFER_OK=true
|
||||||
|
USERS_DETAILS=""
|
||||||
|
|
||||||
|
declare -A DB_DUMP_OK
|
||||||
|
declare -A DB_TRANSFER_OK
|
||||||
|
declare -A DB_DETAILS
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Verrou d’exécution
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
||||||
|
|
||||||
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||||
|
log "ERROR: Backup déjà en cours"
|
||||||
|
discord_msg_users_error "" "" "Lock already exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap 'rm -rf "$LOCK_DIR" "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Préparation du dossier distant
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
REMOTE_DIR="${IA_BASE_DIR}"
|
||||||
|
|
||||||
|
log "Creating remote directories"
|
||||||
|
|
||||||
|
MKDIR_CMD="mkdir -p '${REMOTE_DIR}/user'"
|
||||||
|
for DB in "${DBS_ARRAY[@]}"; do
|
||||||
|
MKDIR_CMD+=" '${REMOTE_DIR}/${DB}'"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "$MKDIR_CMD"; then
|
||||||
|
log "ERROR: remote mkdir failed"
|
||||||
|
discord_msg_users_error "" "" "Remote mkdir failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Export des rôles PostgreSQL
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
ROLES_FILE="${TMP_DIR}/user_${TS}.sql"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
log "Export des rôles PostgreSQL"
|
||||||
|
|
||||||
|
pg_dumpall \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
--globals-only \
|
||||||
|
> "$ROLES_FILE"
|
||||||
|
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
USERS_OK=
|
||||||
|
USERS_EXPORT_OK=
|
||||||
|
USERS_DETAILS="roles export failed"
|
||||||
|
else
|
||||||
|
log "Export des rôles OK : $ROLES_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
|
||||||
|
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/user/"
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
USERS_OK=
|
||||||
|
USERS_TRANSFER_OK=
|
||||||
|
if [[ -n "$USERS_DETAILS" ]]; then
|
||||||
|
USERS_DETAILS+=" | roles transfer failed"
|
||||||
|
else
|
||||||
|
USERS_DETAILS="roles transfer failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Transfert des rôles OK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Dump des bases
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
for DB in "${DBS_ARRAY[@]}"; do
|
||||||
|
FILE="${TMP_DIR}/${DB}_${TS}.dump"
|
||||||
|
|
||||||
|
DB_DUMP_OK["$DB"]=true
|
||||||
|
DB_TRANSFER_OK["$DB"]=true
|
||||||
|
DB_DETAILS["$DB"]=""
|
||||||
|
|
||||||
|
log "Dump $DB"
|
||||||
|
|
||||||
|
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
DUMPS_OK=
|
||||||
|
DB_DUMP_OK["$DB"]=
|
||||||
|
DB_TRANSFER_OK["$DB"]=
|
||||||
|
DB_DETAILS["$DB"]="dump failed"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/${DB}/"
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
DUMPS_OK=
|
||||||
|
DB_TRANSFER_OK["$DB"]=
|
||||||
|
DB_DETAILS["$DB"]="transfer failed"
|
||||||
|
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"
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
log "ERROR: remote rotation failed for users"
|
||||||
|
else
|
||||||
|
log "Remote rotation OK for users"
|
||||||
|
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"
|
||||||
|
RET=$?
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
log "ERROR: remote rotation failed for ${DB}"
|
||||||
|
else
|
||||||
|
log "Remote rotation OK for ${DB}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
log "Remote rotation finished"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Nettoyage local
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Bilan final Discord
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
MODE_KO=
|
||||||
|
|
||||||
|
[[ -z "${DUMPS_OK:-}" ]] && MODE_KO=true
|
||||||
|
[[ -z "${USERS_OK:-}" ]] && MODE_KO=true
|
||||||
|
|
||||||
|
if [[ -z "${MODE_KO:-}" ]]; then
|
||||||
|
discord_msg_global_ok
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${USERS_EXPORT_OK:-}" && -n "${USERS_TRANSFER_OK:-}" ]]; then
|
||||||
|
discord_msg_users_ok_simple
|
||||||
|
else
|
||||||
|
discord_msg_users_error "${USERS_EXPORT_OK:+true}" "${USERS_TRANSFER_OK:+true}" "$USERS_DETAILS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for DB in "${DBS_ARRAY[@]}"; do
|
||||||
|
if [[ -n "${DB_DUMP_OK[$DB]:-}" && -n "${DB_TRANSFER_OK[$DB]:-}" ]]; then
|
||||||
|
discord_msg_db_ok_simple "$DB"
|
||||||
|
else
|
||||||
|
discord_msg_db_error "$DB" "${DB_DUMP_OK[$DB]:+true}" "${DB_TRANSFER_OK[$DB]:+true}" "${DB_DETAILS[$DB]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 2
|
||||||
65
RecetteScripts/backup.env.exemple
Normal file
65
RecetteScripts/backup.env.exemple
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
###############################################################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Nom de l'environnement
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# POSTGRESQL
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Host du serveur PostgreSQL
|
||||||
|
PGHOST=localhost
|
||||||
|
|
||||||
|
# Port PostgreSQL
|
||||||
|
PGPORT=5432
|
||||||
|
|
||||||
|
# Utilisateur utilisé pour réaliser les dumps
|
||||||
|
PGUSER=
|
||||||
|
|
||||||
|
# Mot de passe PostgreSQL
|
||||||
|
PGPASSWORD=
|
||||||
|
|
||||||
|
# Bases de données à sauvegarder (séparées par des espaces)
|
||||||
|
DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# SERVEUR DISTANT DE BACKUP
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Utilisateur SSH du serveur de backup
|
||||||
|
BACKUP_REMOTE_USER=
|
||||||
|
|
||||||
|
# Host ou IP du serveur distant
|
||||||
|
BACKUP_REMOTE_HOST=
|
||||||
|
|
||||||
|
# Dossier distant où seront stockées les sauvegardes
|
||||||
|
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# SSH
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Clé SSH utilisée pour se connecter au serveur distant
|
||||||
|
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||||
|
|
||||||
|
# Timeout de connexion SSH (secondes)
|
||||||
|
SSH_TIMEOUT=10
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LOGS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Dossier où seront stockés les logs du script
|
||||||
|
BACKUP_LOG_DIR=/var/log/script/
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# DISCORD (optionnel)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Webhook Discord pour envoyer les notifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Mention envoyée en cas d'erreur
|
||||||
|
DISCORD_PING=@here
|
||||||
224
RecetteScripts/check-statut-recette.sh
Executable file
224
RecetteScripts/check-statut-recette.sh
Executable file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# -e omis volontairement : check_site retourne 1 pour les sites down
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# check-statut-recette.sh
|
||||||
|
#
|
||||||
|
# Ce script vérifie la disponibilité de plusieurs applications web définies
|
||||||
|
# dans le fichier .env.
|
||||||
|
#
|
||||||
|
# Fonctionnement global :
|
||||||
|
# 1. charge la configuration depuis le fichier .env ;
|
||||||
|
# 2. vérifie le DNS de chaque application ;
|
||||||
|
# 3. effectue une requête HTTP avec curl ;
|
||||||
|
# 4. écrit le résultat dans un fichier de log local ;
|
||||||
|
# 5. construit un message récapitulatif unique ;
|
||||||
|
# 6. envoie une seule notification Discord avec tous les statuts.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Chargement du .env
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "ERROR: fichier .env introuvable : $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Vérification des variables requises
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||||
|
: "${APP_LOG_DIR:?Variable APP_LOG_DIR manquante}"
|
||||||
|
: "${CHECK_CONNECT_TIMEOUT:?Variable CHECK_CONNECT_TIMEOUT manquante}"
|
||||||
|
: "${CHECK_MAX_TIME:?Variable CHECK_MAX_TIME manquante}"
|
||||||
|
: "${APP_URLS:?Variable APP_URLS manquante}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Sites à vérifier
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
read -r -a SITES <<< "$APP_URLS"
|
||||||
|
|
||||||
|
SCHEME="http"
|
||||||
|
CONNECT_TIMEOUT="${CHECK_CONNECT_TIMEOUT}"
|
||||||
|
MAX_TIME="${CHECK_MAX_TIME}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Logs
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
LOG_DIR="${APP_LOG_DIR}"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
LOG_FILE="${LOG_DIR}/app_health_$(date +'%Y-%m-%d').log"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Discord
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Variables globales de synthèse
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
SUMMARY_LINES=()
|
||||||
|
FAILURES=0
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Logging
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
log_line() {
|
||||||
|
printf "%s | %s | %s | %s\n" \
|
||||||
|
"$(date +'%Y-%m-%d %H:%M:%S')" "$1" "$2" "$3" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# DNS
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
dns_ok() {
|
||||||
|
getent hosts "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Ajout au résumé Discord
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
add_summary_line() {
|
||||||
|
local site="$1"
|
||||||
|
local status="$2"
|
||||||
|
local detail="$3"
|
||||||
|
|
||||||
|
local icon
|
||||||
|
if [[ "$status" == "OK" ]]; then
|
||||||
|
icon="✅"
|
||||||
|
else
|
||||||
|
icon="❌"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUMMARY_LINES+=("${icon} ${site} : ${detail}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Envoi du message Discord récapitulatif
|
||||||
|
#######################################
|
||||||
|
send_discord_summary() {
|
||||||
|
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||||
|
|
||||||
|
local header_icon ping_prefix=""
|
||||||
|
if [[ "$FAILURES" -eq 0 ]]; then
|
||||||
|
header_icon="🟢"
|
||||||
|
else
|
||||||
|
header_icon="🔴"
|
||||||
|
ping_prefix="${DISCORD_PING} "
|
||||||
|
fi
|
||||||
|
|
||||||
|
local msg="**${ping_prefix}CHECK APP ${ENV_NAME} ${header_icon}**"$'\n'
|
||||||
|
|
||||||
|
local line
|
||||||
|
for line in "${SUMMARY_LINES[@]}"; do
|
||||||
|
msg+="${line}"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Check application
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
check_site() {
|
||||||
|
local host="$1"
|
||||||
|
local url="${SCHEME}://${host}/"
|
||||||
|
|
||||||
|
if ! dns_ok "$host"; then
|
||||||
|
log_line "DOWN" "$host" "Résolution impossible (getent hosts)"
|
||||||
|
add_summary_line "$host" "DOWN" "DOWN - DNS"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local http_code curl_exit err
|
||||||
|
local stderr
|
||||||
|
stderr="$(mktemp)"
|
||||||
|
|
||||||
|
http_code="$(
|
||||||
|
curl -sS -o /dev/null \
|
||||||
|
-w '%{http_code}' \
|
||||||
|
--connect-timeout "$CONNECT_TIMEOUT" \
|
||||||
|
--max-time "$MAX_TIME" \
|
||||||
|
"$url" 2>"$stderr"
|
||||||
|
)"
|
||||||
|
curl_exit=$?
|
||||||
|
|
||||||
|
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"
|
||||||
|
add_summary_line "$host" "OK" "OK"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_line "DOWN" "$host" "HTTP $http_code (erreur appli)"
|
||||||
|
add_summary_line "$host" "DOWN" "DOWN - HTTP $http_code"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_line "DOWN" "$host" "Code HTTP inattendu: $http_code"
|
||||||
|
add_summary_line "$host" "DOWN" "DOWN - code HTTP invalide"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Main
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap '[[ -n "$STDERR_TMP" ]] && rm -f "$STDERR_TMP"' EXIT
|
||||||
|
|
||||||
|
local failures=0
|
||||||
|
|
||||||
|
for site in "${SITES[@]}"; do
|
||||||
|
if ! check_site "$site"; then
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
FAILURES="$failures"
|
||||||
|
send_discord_summary
|
||||||
|
|
||||||
|
if [[ "$failures" -gt 0 ]]; then
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
42
RecetteScripts/check-statut.env.exemple
Normal file
42
RecetteScripts/check-statut.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Nom de l'environnement surveillé
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LOGS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Dossier où seront stockés les logs du script
|
||||||
|
APP_LOG_DIR=/var/log/script
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# PARAMÈTRES DE VÉRIFICATION HTTP
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Timeout de connexion à l'application (secondes)
|
||||||
|
# Si le serveur ne répond pas dans ce délai, la connexion échoue
|
||||||
|
CHECK_CONNECT_TIMEOUT=5
|
||||||
|
|
||||||
|
# Temps maximum total autorisé pour la requête HTTP (secondes)
|
||||||
|
CHECK_MAX_TIME=10
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# APPLICATIONS À SURVEILLER
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Liste des applications à vérifier (séparées par des espaces)
|
||||||
|
|
||||||
|
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# DISCORD
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Webhook Discord pour envoyer le résumé des vérifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Mention Discord en cas de problème
|
||||||
|
DISCORD_PING=@here
|
||||||
451
RecetteScripts/rebuild-bdd-recette.sh
Normal file
451
RecetteScripts/rebuild-bdd-recette.sh
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# rebuild-bdd-recette.sh
|
||||||
|
#
|
||||||
|
# Script de reconstruction d'une base PostgreSQL à partir d'un dump distant.
|
||||||
|
#
|
||||||
|
# Fonctionnement global :
|
||||||
|
# 1. charge la configuration depuis le fichier .env ;
|
||||||
|
# 2. prépare les chemins, logs et options SSH ;
|
||||||
|
# 3. installe PostgreSQL si absent ;
|
||||||
|
# 4. démarre PostgreSQL si nécessaire ;
|
||||||
|
# 5. crée le rôle PGUSER uniquement si PostgreSQL vient d'être installé ;
|
||||||
|
# 6. propose à l'utilisateur de choisir une base à reconstruire ;
|
||||||
|
# 7. teste la connexion SSH au serveur distant ;
|
||||||
|
# 8. recherche le dernier dump distant de la base choisie ;
|
||||||
|
# 9. recherche le dernier fichier SQL des rôles dans le dossier "user" ;
|
||||||
|
# 10. télécharge les fichiers nécessaires ;
|
||||||
|
# 11. restaure les rôles via psql (avec filtrage des rôles sensibles) ;
|
||||||
|
# 12. supprime puis recrée la base cible ;
|
||||||
|
# 13. restaure la base choisie via pg_restore ;
|
||||||
|
# 14. envoie une notification Discord si tout s'est bien passé.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Chemins fixes du script
|
||||||
|
###############################################################################
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Vérification du fichier .env
|
||||||
|
###############################################################################
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "ERROR: fichier .env introuvable : $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Chargement du .env
|
||||||
|
###############################################################################
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Variables obligatoires
|
||||||
|
###############################################################################
|
||||||
|
: "${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}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Variables optionnelles
|
||||||
|
###############################################################################
|
||||||
|
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_DIR:-${SCRIPT_DIR}/restore_tmp}"
|
||||||
|
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
|
||||||
|
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Préparation des dossiers locaux
|
||||||
|
###############################################################################
|
||||||
|
mkdir -p "$BACKUP_LOG_DIR" || {
|
||||||
|
echo "ERROR: impossible de créer le dossier de logs : $BACKUP_LOG_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_RESTORE_DIR" || {
|
||||||
|
echo "ERROR: impossible de créer le dossier local de restauration : $LOCAL_RESTORE_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||||
|
LOG_FILE="${BACKUP_LOG_DIR}/restore_${ENV_NAME,,}_${TIMESTAMP}.log"
|
||||||
|
|
||||||
|
touch "$LOG_FILE" || {
|
||||||
|
echo "ERROR: impossible d'écrire dans le fichier de log : $LOG_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Fonctions utilitaires
|
||||||
|
###############################################################################
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f \
|
||||||
|
"${LOCAL_DB_DUMP_FILE:-}" \
|
||||||
|
"${LOCAL_ROLES_FILE:-}" \
|
||||||
|
"${FILTERED_ROLES_FILE:-}" \
|
||||||
|
"${ROLES_CREATE_LIST:-}" \
|
||||||
|
"${ROLES_APPLY_FILE:-}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Envoi Discord
|
||||||
|
#
|
||||||
|
# Envoi simple d'un message texte via webhook Discord.
|
||||||
|
# Si WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||||
|
###############################################################################
|
||||||
|
send_discord_message() {
|
||||||
|
local message="$1"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -n "$DISCORD_WEBHOOK_URL" ]] || {
|
||||||
|
log "WEBHOOK_URL non défini : notification Discord ignorée."
|
||||||
|
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}')" || {
|
||||||
|
log "Impossible de construire le payload JSON Discord."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
>/dev/null || log "Échec d'envoi de la notification Discord."
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Vérifications de base
|
||||||
|
###############################################################################
|
||||||
|
[[ -f "$SSH_KEY" ]] || fail "clé SSH introuvable : $SSH_KEY"
|
||||||
|
[[ -r "$SSH_KEY" ]] || fail "clé SSH non lisible : $SSH_KEY"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking=accept-new
|
||||||
|
)
|
||||||
|
|
||||||
|
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Installation PostgreSQL si absent
|
||||||
|
#
|
||||||
|
# Le rôle PGUSER est créé uniquement si PostgreSQL vient d'être installé.
|
||||||
|
###############################################################################
|
||||||
|
POSTGRES_INSTALLED=false
|
||||||
|
|
||||||
|
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; then
|
||||||
|
log "PostgreSQL absent : installation en cours..."
|
||||||
|
|
||||||
|
sudo apt update >>"$LOG_FILE" 2>&1 || fail "échec de apt update"
|
||||||
|
sudo apt install -y postgresql postgresql-client postgresql-contrib \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec de l'installation de PostgreSQL"
|
||||||
|
|
||||||
|
POSTGRES_INSTALLED=true
|
||||||
|
log "Installation PostgreSQL terminée."
|
||||||
|
else
|
||||||
|
log "PostgreSQL déjà installé."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Démarrage PostgreSQL
|
||||||
|
###############################################################################
|
||||||
|
if ! sudo systemctl is-active --quiet postgresql; then
|
||||||
|
log "Démarrage du service PostgreSQL..."
|
||||||
|
sudo systemctl start postgresql >>"$LOG_FILE" 2>&1 || fail "impossible de démarrer PostgreSQL"
|
||||||
|
else
|
||||||
|
log "Service PostgreSQL déjà actif."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Attente disponibilité PostgreSQL
|
||||||
|
###############################################################################
|
||||||
|
log "Vérification de la disponibilité de PostgreSQL..."
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
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
|
||||||
|
fail "PostgreSQL ne répond pas correctement"
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Création du rôle PGUSER uniquement si PostgreSQL vient d'être installé
|
||||||
|
###############################################################################
|
||||||
|
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}';" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec de création du rôle ${PGUSER}"
|
||||||
|
|
||||||
|
log "Rôle PostgreSQL ${PGUSER} créé."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Affichage des bases disponibles
|
||||||
|
###############################################################################
|
||||||
|
read -r -a DBS_ARRAY <<< "$DBS"
|
||||||
|
|
||||||
|
if [[ "${#DBS_ARRAY[@]}" -eq 0 ]]; then
|
||||||
|
fail "aucune base définie dans DBS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Bases disponibles dans le .env :"
|
||||||
|
for i in "${!DBS_ARRAY[@]}"; do
|
||||||
|
printf ' %d) %s\n' "$((i + 1))" "${DBS_ARRAY[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -r -p "Voulez-vous utiliser une base de cette liste ? (oui/non) : " USE_LIST
|
||||||
|
|
||||||
|
DB=""
|
||||||
|
|
||||||
|
if [[ "${USE_LIST,,}" == "oui" || "${USE_LIST,,}" == "o" ]]; then
|
||||||
|
read -r -p "Sélectionnez le numéro de la base à restaurer : " DB_INDEX
|
||||||
|
|
||||||
|
[[ "$DB_INDEX" =~ ^[0-9]+$ ]] || fail "numéro invalide"
|
||||||
|
(( DB_INDEX >= 1 && DB_INDEX <= ${#DBS_ARRAY[@]} )) || fail "numéro hors plage"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
log "Environnement : $ENV_NAME"
|
||||||
|
log "Base cible sélectionnée : $DB"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Test de connexion SSH
|
||||||
|
###############################################################################
|
||||||
|
log "Test de connexion SSH vers ${REMOTE_SSH}..."
|
||||||
|
|
||||||
|
SSH_TEST_OUTPUT=""
|
||||||
|
if ! SSH_TEST_OUTPUT="$(ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" "exit 0" 2>&1)"; then
|
||||||
|
echo "$SSH_TEST_OUTPUT" | tee -a "$LOG_FILE" >&2
|
||||||
|
fail "connexion SSH impossible vers ${REMOTE_SSH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Définition des chemins distants
|
||||||
|
###############################################################################
|
||||||
|
REMOTE_DB_DIR="${BACKUP_REMOTE_DIR}/${DB}"
|
||||||
|
REMOTE_ROLES_DIR="${BACKUP_REMOTE_DIR}/${REMOTE_ROLES_DIR_NAME}"
|
||||||
|
|
||||||
|
log "Recherche du dernier dump distant pour ${DB} dans : ${REMOTE_DB_DIR}"
|
||||||
|
log "Recherche du dernier fichier de rôles dans : ${REMOTE_ROLES_DIR}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Recherche du dernier dump de base
|
||||||
|
###############################################################################
|
||||||
|
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"
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$LAST_REMOTE_DB_DUMP" ]]; then
|
||||||
|
fail "aucun dump trouvé pour la base ${DB} dans ${REMOTE_DB_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Dernier dump distant sélectionné : ${LAST_REMOTE_DB_DUMP}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Recherche du dernier fichier SQL des rôles
|
||||||
|
###############################################################################
|
||||||
|
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 des rôles sélectionné : ${LAST_REMOTE_ROLES_FILE}"
|
||||||
|
else
|
||||||
|
log "Aucun fichier des rôles trouvé sur le serveur distant."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Téléchargement du dump principal
|
||||||
|
###############################################################################
|
||||||
|
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" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du dump principal"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Téléchargement du fichier des rôles si présent
|
||||||
|
###############################################################################
|
||||||
|
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" \
|
||||||
|
>>"$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."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Test de connexion PostgreSQL locale avec PGUSER
|
||||||
|
###############################################################################
|
||||||
|
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
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Demande d'écrasement si la base existe déjà
|
||||||
|
###############################################################################
|
||||||
|
DB_EXISTS="$(
|
||||||
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname='${DB}'" 2>>"$LOG_FILE" || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$DB_EXISTS" == "1" ]]; then
|
||||||
|
read -r -p "La base '${DB}' existe déjà. Voulez-vous l'écraser ? (oui/non) : " CONFIRM_OVERWRITE
|
||||||
|
if [[ "${CONFIRM_OVERWRITE,,}" != "oui" && "${CONFIRM_OVERWRITE,,}" != "o" ]]; then
|
||||||
|
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 de suppression de la base ${DB}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Restauration des rôles
|
||||||
|
###############################################################################
|
||||||
|
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")"
|
||||||
|
|
||||||
|
grep -viE '^(CREATE ROLE|ALTER ROLE) (backup_liot|postgres)\b' "$LOCAL_ROLES_FILE" \
|
||||||
|
> "$FILTERED_ROLES_FILE" || true
|
||||||
|
|
||||||
|
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
|
||||||
|
[[ -z "$role_name" ]] && continue
|
||||||
|
if [[ ! "$role_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
||||||
|
log "WARNING: nom de rôle suspect ignoré : ${role_name}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
)"
|
||||||
|
|
||||||
|
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 de création du rôle ${role_name}"
|
||||||
|
else
|
||||||
|
log "Rôle déjà présent, création ignorée : ${role_name}"
|
||||||
|
fi
|
||||||
|
done < "$ROLES_CREATE_LIST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -viE '^CREATE ROLE ' "$FILTERED_ROLES_FILE" > "$ROLES_APPLY_FILE" || true
|
||||||
|
|
||||||
|
log "Application des 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 de restauration des rôles via psql"
|
||||||
|
else
|
||||||
|
log "Aucune restauration des rôles effectuée."
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Création de la base
|
||||||
|
###############################################################################
|
||||||
|
log "Création de la base : ${DB}"
|
||||||
|
createdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$DB" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec de création de la base ${DB}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Restauration de la base principale
|
||||||
|
###############################################################################
|
||||||
|
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 de restauration de la base ${DB}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Fin
|
||||||
|
###############################################################################
|
||||||
|
log "Restauration terminée avec succès pour la base : ${DB}"
|
||||||
|
log "Fichier de log : ${LOG_FILE}"
|
||||||
|
|
||||||
|
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_message "$SUCCESS_MESSAGE"
|
||||||
80
RecetteScripts/rebuild.env.exemple
Normal file
80
RecetteScripts/rebuild.env.exemple
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
###############################################################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Nom de l'environnement
|
||||||
|
# Exemple : DEV / RECETTE / PROD
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# POSTGRESQL LOCAL
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Hôte PostgreSQL local sur lequel la restauration sera effectuée
|
||||||
|
PGHOST=localhost
|
||||||
|
|
||||||
|
# Port PostgreSQL local
|
||||||
|
PGPORT=5432
|
||||||
|
|
||||||
|
# Utilisateur PostgreSQL utilisé pour créer la base et lancer la restauration
|
||||||
|
PGUSER=
|
||||||
|
|
||||||
|
# Mot de passe
|
||||||
|
PGPASSWORD=
|
||||||
|
|
||||||
|
# Liste des bases proposées à la restauration (séparées par des espaces)
|
||||||
|
# L'utilisateur pourra en choisir une dans le script
|
||||||
|
DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# SERVEUR DISTANT DE BACKUP
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Utilisateur SSH du serveur distant contenant les dumps
|
||||||
|
BACKUP_REMOTE_USER=
|
||||||
|
|
||||||
|
# Hôte ou IP du serveur distant
|
||||||
|
BACKUP_REMOTE_HOST=
|
||||||
|
|
||||||
|
# Répertoire racine distant :
|
||||||
|
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# SSH
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Clé privée SSH utilisée pour se connecter au serveur distant
|
||||||
|
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||||
|
|
||||||
|
# Timeout de connexion SSH en secondes
|
||||||
|
# Variable optionnelle dans le script, mais utile ici comme valeur par défaut
|
||||||
|
SSH_CONNECT_TIMEOUT=8
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LOGS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Dossier local dans lequel seront écrits les logs de restauration
|
||||||
|
BACKUP_LOG_DIR=/var/log/pg_backup
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RESTAURATION LOCALE
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Dossier local temporaire pour télécharger les fichiers avant restauration
|
||||||
|
# Optionnel : si absent, le script utilise ./restore_tmp
|
||||||
|
LOCAL_RESTORE_DIR=/tmp/rebuild-bdd-recette
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RÔLES POSTGRESQL DISTANTS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Nom du dossier distant contenant les exports SQL des rôles
|
||||||
|
REMOTE_ROLES_DIR_NAME=user
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# DISCORD
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Webhook Discord pour notifier le succès de la restauration
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
210
backup_pg.sh
210
backup_pg.sh
@@ -1,210 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Configuration
|
|
||||||
#######################################
|
|
||||||
DBS=("sirh" "inventory" "ferme")
|
|
||||||
|
|
||||||
PGHOST="localhost"
|
|
||||||
PGPORT="5432"
|
|
||||||
PGUSER="backup_liot"
|
|
||||||
PGPASSWORD="backup_liot"
|
|
||||||
|
|
||||||
IA_SSH="malio-b@192.168.0.179"
|
|
||||||
IA_BASE_DIR="/home/malio-b/backups"
|
|
||||||
|
|
||||||
SSH_KEY="/home/malio/.ssh/id_ed25519_backup"
|
|
||||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
|
||||||
|
|
||||||
LOG_DIR="/var/log/pg_backup"
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
|
|
||||||
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') ---- $*"; }
|
|
||||||
|
|
||||||
export PGPASSWORD
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Discord (Webhook)
|
|
||||||
#######################################
|
|
||||||
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1478503102888935506/YCtJM09QZiKNMiCe5u7vCQb52VcLjHAd9wwEsKNltlJVcy7sKvoMTOJkvEKOOrk-Wpkh"
|
|
||||||
|
|
||||||
discord_ping() {
|
|
||||||
local details="${1:-}"
|
|
||||||
|
|
||||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
|
||||||
|
|
||||||
local color dumps_display users_display
|
|
||||||
if [[ -n "${DUMPS_OK:-}" && -n "${USERS_OK:-}" ]]; then
|
|
||||||
color="🟢"
|
|
||||||
else
|
|
||||||
color="🔴"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dumps_display=$([[ -n "${DUMPS_OK:-}" ]] && echo "✅" || echo "❌")
|
|
||||||
users_display=$([[ -n "${USERS_OK:-}" ]] && echo "✅" || echo "❌")
|
|
||||||
|
|
||||||
local msg="**@here BACKUP BDD RECETTE ${color}**\n"
|
|
||||||
msg+="Name: ${BACKUP_DIR_NAME}\n"
|
|
||||||
msg+="Dumps transfer: ${dumps_display}\n"
|
|
||||||
msg+="Users transfer: ${users_display}\n"
|
|
||||||
[[ -n "$details" ]] && msg+="Details: $details"
|
|
||||||
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Statuts init
|
|
||||||
#######################################
|
|
||||||
DUMPS_OK=true
|
|
||||||
USERS_OK=true
|
|
||||||
DUMP_ERRORS=""
|
|
||||||
USER_ERRORS=""
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Lock (évite 2 backups en même temps)
|
|
||||||
#######################################
|
|
||||||
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
|
||||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
||||||
log "ERROR: Backup déjà en cours (lock: $LOCK_DIR)"
|
|
||||||
DUMPS_OK=
|
|
||||||
USERS_OK=
|
|
||||||
discord_ping "Lock exists: $LOCK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Remote dir
|
|
||||||
#######################################
|
|
||||||
REMOTE_DIR="${IA_BASE_DIR}/${BACKUP_DIR_NAME}"
|
|
||||||
|
|
||||||
log "Starting backup process"
|
|
||||||
log "Remote directory: ${REMOTE_DIR}"
|
|
||||||
|
|
||||||
log "Creating remote directory"
|
|
||||||
if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "mkdir -p '${REMOTE_DIR}'"; then
|
|
||||||
log "ERROR: Création dossier distant impossible: ${REMOTE_DIR}"
|
|
||||||
DUMPS_OK=
|
|
||||||
USERS_OK=
|
|
||||||
discord_ping "Remote mkdir KO: ${REMOTE_DIR}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Export PostgreSQL roles (no passwords)
|
|
||||||
#######################################
|
|
||||||
ROLES_FILE="${TMP_DIR}/roles_${TS}.sql"
|
|
||||||
log "Exporting PostgreSQL roles"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -Atq <<'SQL' > "$ROLES_FILE"
|
|
||||||
SELECT
|
|
||||||
format(
|
|
||||||
'DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %L) THEN
|
|
||||||
CREATE ROLE %I;
|
|
||||||
END IF;
|
|
||||||
END $$;',
|
|
||||||
rolname, rolname
|
|
||||||
)
|
|
||||||
FROM pg_roles
|
|
||||||
WHERE rolname !~ '^pg_'
|
|
||||||
ORDER BY rolname;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
format(
|
|
||||||
'ALTER ROLE %I WITH %s%s%s%s%s%s%s%s;',
|
|
||||||
rolname,
|
|
||||||
CASE WHEN rolsuper THEN 'SUPERUSER ' ELSE 'NOSUPERUSER ' END,
|
|
||||||
CASE WHEN rolinherit THEN 'INHERIT ' ELSE 'NOINHERIT ' END,
|
|
||||||
CASE WHEN rolcreaterole THEN 'CREATEROLE ' ELSE 'NOCREATEROLE ' END,
|
|
||||||
CASE WHEN rolcreatedb THEN 'CREATEDB ' ELSE 'NOCREATEDB ' END,
|
|
||||||
CASE WHEN rolcanlogin THEN 'LOGIN ' ELSE 'NOLOGIN ' END,
|
|
||||||
CASE WHEN rolreplication THEN 'REPLICATION ' ELSE 'NOREPLICATION ' END,
|
|
||||||
CASE WHEN rolbypassrls THEN 'BYPASSRLS ' ELSE 'NOBYPASSRLS ' END,
|
|
||||||
'CONNECTION LIMIT ' || rolconnlimit
|
|
||||||
)
|
|
||||||
FROM pg_roles
|
|
||||||
WHERE rolname !~ '^pg_'
|
|
||||||
ORDER BY rolname;
|
|
||||||
SQL
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
USERS_OK=
|
|
||||||
USER_ERRORS+="roles_export "
|
|
||||||
log "ERROR: Users export failed"
|
|
||||||
else
|
|
||||||
log "Roles export completed: $ROLES_FILE"
|
|
||||||
fi
|
|
||||||
set -e
|
|
||||||
|
|
||||||
log "Sending roles file to IA server"
|
|
||||||
set +e
|
|
||||||
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
USERS_OK=
|
|
||||||
USER_ERRORS+="roles_scp "
|
|
||||||
log "ERROR: Users transfer failed (roles file)"
|
|
||||||
else
|
|
||||||
log "Roles transfer completed"
|
|
||||||
fi
|
|
||||||
set -e
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Dump des bases + transfert (continue même si KO)
|
|
||||||
#######################################
|
|
||||||
set +e
|
|
||||||
for DB in "${DBS[@]}"; do
|
|
||||||
FILE="${TMP_DIR}/${DB}_${TS}.dump"
|
|
||||||
|
|
||||||
log "Dumping database: $DB"
|
|
||||||
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc --no-owner --no-acl -d "$DB" -f "$FILE"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
DUMPS_OK=
|
|
||||||
DUMP_ERRORS+="${DB} "
|
|
||||||
log "ERROR: Dump failed for $DB"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
log "Dump completed: $FILE"
|
|
||||||
|
|
||||||
log "Sending dump to IA server"
|
|
||||||
scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
DUMPS_OK=
|
|
||||||
DUMP_ERRORS+="${DB}(scp) "
|
|
||||||
log "ERROR: Transfer failed for $DB"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
log "Transfer completed for $DB"
|
|
||||||
done
|
|
||||||
set -e
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Nettoyage
|
|
||||||
#######################################
|
|
||||||
log "Cleaning temporary files"
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Envoi message Discord (final)
|
|
||||||
#######################################
|
|
||||||
DETAILS=""
|
|
||||||
[[ -z "${DUMPS_OK:-}" ]] && DETAILS+="Dumps KO: ${DUMP_ERRORS} "
|
|
||||||
[[ -z "${USERS_OK:-}" ]] && DETAILS+="Users KO: ${USER_ERRORS} "
|
|
||||||
discord_ping "$DETAILS"
|
|
||||||
|
|
||||||
log "Backup finished"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
DATA_DIR=
|
|
||||||
LOCAL_BACKUP=
|
|
||||||
REMOTE_USER=
|
|
||||||
REMOTE_HOST=
|
|
||||||
REMOTE_DIR=
|
|
||||||
SSH_KEY=
|
|
||||||
3
backup_vaultwarden/.gitignore
vendored
3
backup_vaultwarden/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
.env
|
|
||||||
|
|
||||||
backup.log
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# FONCTIONNEMENT DU SCRIPT VAULTWARDEN
|
|
||||||
Le script de backup de vaultwarden permet une sauvegard périodique des mots de passe et utilisateurs de celui-ci.
|
|
||||||
|
|
||||||
## INITIALISATION DES VARIABLES DE MANIÈRE SÉCURISÉ
|
|
||||||
|
|
||||||
1. Les informations sensibles ne sont pas stockées directement dans le script. Elles sont placées dans un fichier .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
WEBHOOK_URL=...
|
|
||||||
REMOTE_USER=...
|
|
||||||
REMOTE_HOST=...
|
|
||||||
SSH_KEY=...
|
|
||||||
DATA_DIR=...
|
|
||||||
```
|
|
||||||
|
|
||||||
2. on recupere les varibales dans le script
|
|
||||||
```bash
|
|
||||||
REMOTE_USER=$(grep -E '^REMOTE_USER=' .env | cut -d '=' -f2-)
|
|
||||||
```
|
|
||||||
|
|
||||||
Explication:
|
|
||||||
|
|
||||||
- grep recherche la variable dans le fichier .env
|
|
||||||
- cut récupère uniquement la valeur après =
|
|
||||||
- REMOTE_USER="user" Le script récupère >> "user"
|
|
||||||
|
|
||||||
Cela permet:
|
|
||||||
|
|
||||||
- d’améliorer la sécurité
|
|
||||||
- d’éviter de modifier le script si un paramètre change
|
|
||||||
|
|
||||||
## RÉCUPÉRATION DES DONNÉES
|
|
||||||
|
|
||||||
1. Le dossier data de Vaultwarden est dupliqué puis compressé afin de créer une archive :
|
|
||||||
```bash
|
|
||||||
tar -czf "$LOCAL_BACKUP" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Transfer vers le serveur de backup
|
|
||||||
```bash
|
|
||||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
|
||||||
```
|
|
||||||
La sauvegarde est envoyée vers une machine dédiée grâce à SCP. Pour éviter de saisir un mot de passe à chaque fois, une clé SSH est utilisée.
|
|
||||||
|
|
||||||
Cette clé SSH est générée sur la machine de backup et autorisée sur la machine Vaultwarden.
|
|
||||||
|
|
||||||
## NOTIFICATION DISCORD
|
|
||||||
|
|
||||||
Le script envoie une notification sur un salon Discord pour informer de l’état de la sauvegarde. Cela se fait grâce à un webhook Discord.
|
|
||||||
|
|
||||||
1. on défini le message
|
|
||||||
```bash
|
|
||||||
local msg="**@here Backup Vaultwarden $color**\n"
|
|
||||||
msg+="Backup: ${BACKUP_NAME}\n"
|
|
||||||
msg+="Data transfer: $dumps_display\n"
|
|
||||||
[[ -n "$details" ]] && msg+="Details: $details"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. on envoie le message sur discord avec le message et le webhook
|
|
||||||
```bash
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
```
|
|
||||||
Le message indique:
|
|
||||||
|
|
||||||
- si la sauvegarde a réussi 🟢
|
|
||||||
- si elle a échoué 🔴
|
|
||||||
- le nom du backup
|
|
||||||
- les détails de l’erreur si nécessaire
|
|
||||||
|
|
||||||
## PLANIFICATION AVEC CRON
|
|
||||||
|
|
||||||
Le script est exécuté automatiquement chaque jour grâce à cron.
|
|
||||||
|
|
||||||
1. Ouvrez le crontab pour l'édition :
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
```
|
|
||||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 19h :
|
|
||||||
```bash
|
|
||||||
0 19 * * * /chemin/vers/le/script/check_storage.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Signification:
|
|
||||||
|
|
||||||
- 0 minute 0
|
|
||||||
- 19 19h
|
|
||||||
- * tous les jours du mois
|
|
||||||
- * tous les mois
|
|
||||||
- * tous les jours de la semaine
|
|
||||||
|
|
||||||
Tous les jours à 19h, le script est exécuté et les logs sont enregistrés dans backup.log ce qui permet d’analyser les erreurs si un problème survient.
|
|
||||||
|
|
||||||
## NETTOYAGE
|
|
||||||
|
|
||||||
Une fois la sauvegarde envoyée sur la machine distante, le fichier temporaire est supprimé :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -f "$LOCAL_BACKUP"
|
|
||||||
```
|
|
||||||
|
|
||||||
Cela permet de garder le serveur propre et éviter de remplir le disque.
|
|
||||||
|
|
||||||
## RÉSUMÉ
|
|
||||||
|
|
||||||
Le script automatise complètement les sauvegardes Vaultwarden :
|
|
||||||
|
|
||||||
- sauvegarde du dossier data
|
|
||||||
- compression et datation
|
|
||||||
- transfert sécurisé via SSH
|
|
||||||
- notification Discord
|
|
||||||
- exécution automatique avec cron
|
|
||||||
- sécurisation des paramètres via .env
|
|
||||||
|
|
||||||
Cela permet d’avoir une sauvegarde quotidienne fiable et surveillée.
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -u
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Sites à vérifier
|
|
||||||
#######################################
|
|
||||||
SITES=(
|
|
||||||
"ferme.malio-dev.fr"
|
|
||||||
"sirh.malio-dev.fr"
|
|
||||||
"inventory.malio-dev.fr"
|
|
||||||
)
|
|
||||||
|
|
||||||
SCHEME="http"
|
|
||||||
CONNECT_TIMEOUT=3
|
|
||||||
MAX_TIME=8
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Logs
|
|
||||||
#######################################
|
|
||||||
LOG_DIR="/var/log/app_health"
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
LOG_FILE="${LOG_DIR}/app_health_$(date +'%Y-%m-%d').log"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Discord
|
|
||||||
#######################################
|
|
||||||
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1478379245842600007/tSxi3G6PbCn89pOdeqK34LR7c-GhXfT-lSCPolwBywJXcpa3ihL8rN4QRwsTjF6SS3w0"
|
|
||||||
|
|
||||||
discord_ping() {
|
|
||||||
local site="$1"
|
|
||||||
local status="$2"
|
|
||||||
local detail="$3"
|
|
||||||
|
|
||||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
|
||||||
|
|
||||||
local color icon
|
|
||||||
|
|
||||||
if [[ "$status" == "OK" ]]; then
|
|
||||||
color="🟢"
|
|
||||||
icon="✅"
|
|
||||||
else
|
|
||||||
color="🔴"
|
|
||||||
icon="❌"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local msg="**CHECK APP RECETTE $color**\n"
|
|
||||||
msg+="Application: ${site}\n"
|
|
||||||
msg+="Status: ${icon}\n"
|
|
||||||
msg+="Details: ${detail}"
|
|
||||||
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Logging
|
|
||||||
#######################################
|
|
||||||
log_line() {
|
|
||||||
# 2026-03-04 14:12:33 | LEVEL | site | message
|
|
||||||
printf "%s | %s | %s | %s\n" \
|
|
||||||
"$(date +'%Y-%m-%d %H:%M:%S')" "$1" "$2" "$3" | tee -a "$LOG_FILE"
|
|
||||||
|
|
||||||
# Envoi Discord par application
|
|
||||||
discord_ping "$2" "$1" "$3"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# DNS
|
|
||||||
#######################################
|
|
||||||
dns_ok() {
|
|
||||||
getent hosts "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Check application
|
|
||||||
#######################################
|
|
||||||
check_site() {
|
|
||||||
|
|
||||||
local host="$1"
|
|
||||||
local url="${SCHEME}://${host}/"
|
|
||||||
|
|
||||||
if ! dns_ok "$host"; then
|
|
||||||
log_line "DOWN" "$host" "Résolution impossible (getent hosts)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local http_code curl_exit stderr
|
|
||||||
|
|
||||||
stderr="$(mktemp)"
|
|
||||||
|
|
||||||
http_code="$(
|
|
||||||
curl -sS -o /dev/null \
|
|
||||||
-w '%{http_code}' \
|
|
||||||
--connect-timeout "$CONNECT_TIMEOUT" \
|
|
||||||
--max-time "$MAX_TIME" \
|
|
||||||
"$url" 2>"$stderr"
|
|
||||||
)"
|
|
||||||
|
|
||||||
curl_exit=$?
|
|
||||||
|
|
||||||
if [ $curl_exit -ne 0 ]; then
|
|
||||||
local err
|
|
||||||
err="$(head -n 1 "$stderr" | tr -d '\r')"
|
|
||||||
rm -f "$stderr"
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "curl exit=$curl_exit : ${err:-"(aucun)"}"
|
|
||||||
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"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "HTTP $http_code (erreur appli)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "Code HTTP inattendu: $http_code"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Main
|
|
||||||
#######################################
|
|
||||||
main() {
|
|
||||||
|
|
||||||
local failures=0
|
|
||||||
|
|
||||||
for site in "${SITES[@]}"; do
|
|
||||||
if ! check_site "$site"; then
|
|
||||||
failures=$((failures + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$failures" -gt 0 ]; then
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
132
global.env.exemple
Normal file
132
global.env.exemple
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
###############################################################################
|
||||||
|
# FICHIER .env.example
|
||||||
|
#
|
||||||
|
# Ce fichier sert de modèle de configuration pour les scripts d'automatisation :
|
||||||
|
# - backup-bdd-recette.sh → sauvegarde PostgreSQL
|
||||||
|
# - rebuild-bdd-recette.sh → reconstruction d'une base PostgreSQL
|
||||||
|
# - check-statut-recette.sh → vérification disponibilité des applications
|
||||||
|
# - check-storage.sh → surveillance de l'espace disque
|
||||||
|
# - backup-vaultwarden.sh → sauvegarde du service Vaultwarden
|
||||||
|
#
|
||||||
|
# Copier ce fichier en .env puis remplir les valeurs.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Nom de l'environnement (ex : DEV / RECETTE / PROD)
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# DISCORD
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Webhook Discord utilisé pour envoyer les notifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# POSTGRESQL
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Adresse du serveur PostgreSQL
|
||||||
|
PGHOST=localhost
|
||||||
|
|
||||||
|
# Port PostgreSQL
|
||||||
|
PGPORT=5432
|
||||||
|
|
||||||
|
# Utilisateur utilisé pour les dumps
|
||||||
|
PGUSER=
|
||||||
|
|
||||||
|
# Mot de passe
|
||||||
|
PGPASSWORD=
|
||||||
|
|
||||||
|
# Bases de données à sauvegarder (séparées par espace)
|
||||||
|
# Utilisé par backup-bdd-recette.sh
|
||||||
|
DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# BACKUPS LOCAUX
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Dossier local où les dumps seront générés temporairement
|
||||||
|
BACKUP_LOCAL_DIR=/var/backups/postgresql
|
||||||
|
|
||||||
|
# Dossier des logs de sauvegarde
|
||||||
|
BACKUP_LOG_DIR=/var/log/script/...
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SERVEUR DISTANT DE STOCKAGE
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Utilisateur du serveur de backup distant
|
||||||
|
BACKUP_REMOTE_USER=
|
||||||
|
|
||||||
|
# Adresse IP ou hostname du serveur de stockage
|
||||||
|
BACKUP_REMOTE_HOST=
|
||||||
|
|
||||||
|
# Dossier distant où stocker les backups
|
||||||
|
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SSH
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Clé SSH utilisée pour se connecter au serveur distant
|
||||||
|
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||||
|
|
||||||
|
# Timeout SSH (secondes)
|
||||||
|
SSH_TIMEOUT=10
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# ROTATION DES BACKUPS
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Nombre de jours de conservation des sauvegardes
|
||||||
|
BACKUP_RETENTION_DAYS=10
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# APPLICATIONS À SURVEILLER
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Liste des applications à vérifier
|
||||||
|
APPS="
|
||||||
|
ferme.malio-dev.fr
|
||||||
|
inventory.malio-dev.fr
|
||||||
|
sirh.malio-dev.fr
|
||||||
|
"
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# VAULTWARDEN
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Dossier contenant les données Vaultwarden
|
||||||
|
VAULTWARDEN_DATA_DIR=/opt/vaultwarden/data
|
||||||
|
|
||||||
|
# Dossier local où stocker le backup
|
||||||
|
VAULTWARDEN_BACKUP_DIR=/var/backups/vaultwarden
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SERVEUR IA / STOCKAGE CENTRAL
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Utilisateur SSH du serveur distant
|
||||||
|
IA_SSH_USER=
|
||||||
|
|
||||||
|
# Host du serveur distant
|
||||||
|
IA_SSH_HOST=
|
||||||
|
|
||||||
|
# Dossier racine contenant les dumps PostgreSQL
|
||||||
|
IA_BASE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
# Dossier contenant les rôles PostgreSQL exportés
|
||||||
|
REMOTE_ROLES_NAME=user
|
||||||
Reference in New Issue
Block a user