Compare commits

14 Commits

Author SHA1 Message Date
AkiNoKure
37fe2f5239 feat : script deploiement de script 2026-03-13 10:22:29 +01:00
210594b008 Merge pull request 'feat/392-script-reconstruction-bdd' (#11) from feat/392-script-reconstruction-bdd into develop
Reviewed-on: #11
2026-03-12 08:53:22 +00:00
AkiNoKure
e221e82108 feat : script de reconstruction de bdd 2026-03-12 09:49:35 +01:00
AkiNoKure
fabc9be4d4 fix : pb chemin env 2026-03-11 17:11:30 +01:00
AkiNoKure
9d4a5050e9 feat : rebuild-bdd-recette 2026-03-11 11:14:44 +01:00
5729d0d484 Merge pull request 'fix/code-review' (#10) from fix/code-review into develop
Reviewed-on: #10
2026-03-10 15:17:56 +00:00
AkiNoKure
89b1229efb fix : code review 2026-03-10 16:17:11 +01:00
AkiNoKure
f9b1d1da24 Merge remote-tracking branch 'origin/fix/code-review' into fix/code-review
# Conflicts:
#	CODE_REVIEW.md
2026-03-10 16:16:51 +01:00
AkiNoKure
049574ffeb fix : code review 2026-03-10 16:15:21 +01:00
c257270982 Actualiser CODE_REVIEW.md 2026-03-10 15:14:25 +00:00
AkiNoKure
f72328e0ce fix : code review 2026-03-10 15:54:22 +01:00
AkiNoKure
29eff11b23 Merge remote-tracking branch 'refs/remotes/origin/fix/code-review' into fix/code-review 2026-03-10 09:09:18 +01:00
623424343e Merge pull request 'feat : ajout de la rotation pour les scripts de backup' (#9) from feat/384-correctif into develop
Reviewed-on: #9
2026-03-09 15:51:09 +00:00
AkiNoKure
066ede6000 feat : ajout de la rotation pour les scripts de backup 2026-03-09 16:46:50 +01:00
12 changed files with 1983 additions and 394 deletions

15
.gitignore vendored
View File

@@ -2,22 +2,9 @@
# Environment / secrets
########################################
# Fichiers .env réels (contiennent des secrets)
.env
.env.*
!.env.example
!.env.exemple
# Sous-dossiers
RecetteScripts/.env
CheckStorage/.env
BackupVaultWarden/.env
# Garder les fichiers exemple
!*.env.exemple
!*.env.example
!CheckStorage/.env.exemple
!RecetteScripts/.env.exemple
!BackupVaultWarden/.env.exemple
!.env.example
########################################
# Logs

View File

@@ -1,54 +1,259 @@
# FONCTIONNEMENT DU SCRIPT VAULTWARDEN
Le script de backup de vaultwarden permet une sauvegarde périodique des mots de passe et utilisateurs de celui-ci.
markdown
# README — Mise en place du script de sauvegarde Vaultwarden
## INITIALISATION DES VARIABLES DE MANIÈRE SÉCURISÉ
Ce script permet dautomatiser la sauvegarde de Vaultwarden afin de conserver une copie du dossier `data`, de la transférer vers un serveur distant et denvoyer une notification Discord en cas de succès ou déchec.
1. Les informations sensibles ne sont pas stockées directement dans le script. Elles sont placées dans un fichier .env
---
# 1. Objectif du script
Le script de sauvegarde Vaultwarden permet de :
- sauvegarder les données de Vaultwarden ;
- compresser larchive avec un nom daté ;
- transférer la sauvegarde vers un serveur distant ;
- envoyer une notification Discord ;
- automatiser lexé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
WEBHOOK_URL=...
REMOTE_USER=...
REMOTE_HOST=...
SSH_KEY=...
DATA_DIR=...
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/
```
2. on récupère les variables dans le script
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=backup
REMOTE_HOST=192.168.1.50
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 récupère les variables du fichier `.env`.
Exemple :
```bash
REMOTE_USER=$(grep -E '^REMOTE_USER=' .env | cut -d '=' -f2-)
```
Explication:
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"
* `grep` recherche la variable dans `.env`
* `cut` récupère uniquement la valeur après `=`
* la variable shell reçoit la valeur correspondante
Cela permet:
Cela permet :
- daméliorer la sécurité
- déviter de modifier le script si un paramètre change
* d'améliorer la sécurité
* de modifier la configuration sans toucher au script
## RÉCUPÉRATION DES DONNÉES
---
# 6. Connexion au serveur de sauvegarde (Machine IA)
Le transfert des sauvegardes vers la machine IA repose sur une **authentification par clé SSH**.
Cette méthode permet au script de se connecter automatiquement au serveur distant sans mot de passe.
La clé utilisée pour ce script est :
```
~/.ssh/id_ed25519_bitwarden
````
---
## 6.1 Vérifier la présence de la clé SSH
Sur la machine exécutant le script, vérifier que la clé existe :
```bash
ls ~/.ssh/id_ed25519_bitwarden*
````
Les fichiers attendus sont :
```
~/.ssh/id_ed25519_bitwarden
~/.ssh/id_ed25519_bitwarden.pub
```
* `id_ed25519_bitwarden` → clé privée utilisée par le script
* `id_ed25519_bitwarden.pub` → clé publique autorisée sur la machine IA
---
## 6.2 Copier la clé publique sur la machine IA
Envoyer la clé publique vers la machine IA :
```bash
ssh-copy-id -i ~/.ssh/id_ed25519_bitwarden.pub backup@192.168.0.179
```
Cette commande ajoute automatiquement la clé dans :
```
~/.ssh/authorized_keys
```
sur la machine IA.
---
## 6.3 Ajout manuel de la clé (si ssh-copy-id n'est pas disponible)
Afficher la clé publique :
```bash
cat ~/.ssh/id_ed25519_bitwarden.pub
```
Copier son contenu puis lajouter sur la machine IA dans :
```
~/.ssh/authorized_keys
```
---
## 6.4 Vérifier les permissions SSH
Sur la machine locale :
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_bitwarden
chmod 644 ~/.ssh/id_ed25519_bitwarden.pub
```
Sur la machine IA :
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
```
---
## 6.5 Tester la connexion
Tester la connexion SSH avec la clé :
```bash
ssh -i ~/.ssh/id_ed25519_bitwarden backup@192.168.0.179
```
Si la configuration est correcte :
* la connexion se fait **sans mot de passe**
* la machine IA accepte la clé SSH
* le script pourra envoyer les sauvegardes automatiquement
---
## 6.6 Déclaration dans le fichier `.env`
La clé utilisée par le script doit être déclarée dans `.env` :
```bash
SSH_KEY=/home/matt/.ssh/id_ed25519_bitwarden
```
Cette clé sera utilisée automatiquement par `scp` lors du transfert des sauvegardes.
# 7. Sauvegarde des données Vaultwarden
Le script crée une archive compressée du dossier `data` :
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
Cela permet dobtenir une sauvegarde portable et compressée.
---
# 8. Transfert vers le serveur distant
Une fois larchive créée :
```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.
Le fichier est envoyé vers le serveur de sauvegarde via SCP.
## 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.
# 9. Notification Discord
Le script envoie une notification Discord pour informer de létat de la sauvegarde.
Construction du message :
1. on défini le message
```bash
local msg="**@here Backup Vaultwarden $color**\n"
msg+="Backup: ${BACKUP_NAME}\n"
@@ -56,61 +261,96 @@ msg+="Data transfer: $dumps_display\n"
[[ -n "$details" ]] && msg+="Details: $details"
```
2. on envoie le message sur discord avec le message et le webhook
Envoi du message :
```bash
curl -fsS -H "Content-Type: application/json" \
-d "{\"content\":\"$msg\"}" \
"$DISCORD_WEBHOOK_URL"
"$WEBHOOK_URL"
```
Le message indique:
- si la sauvegarde a réussi 🟢
- si elle a échoué 🔴
- le nom du backup
- les détails de lerreur si nécessaire
Le message indique :
## PLANIFICATION AVEC CRON
* si la sauvegarde a réussi
* si elle a échoué
* le nom du backup
* les détails de lerreur
Le script est exécuté automatiquement chaque jour grâce à cron.
---
# 10. Planification avec cron
Le script est exécuté automatiquement tous les jours à 19h.
Ouvrir le crontab :
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
crontab -e
```
Signification:
Ajouter :
- 0 minute 0
- 19 19h
- * tous les jours du mois
- * tous les mois
- * tous les jours de la semaine
```bash
0 19 * * * /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh >> /var/log/vaultwarden_backup.log 2>&1
```
Tous les jours à 19h, le script est exécuté et les logs sont enregistrés dans backup.log ce qui permet danalyser les erreurs si un problème survient.
Signification :
## NETTOYAGE
| Champ | Valeur |
| ------------ | ------ |
| minute | 0 |
| heure | 19 |
| jour du mois | * |
| mois | * |
| jour semaine | * |
Une fois la sauvegarde envoyée sur la machine distante, le fichier temporaire est supprimé :
Le script sexécute donc **tous les jours à 19h00**.
---
# 11. Nettoyage
Une fois la sauvegarde transférée :
```bash
rm -f "$LOCAL_BACKUP"
```
Cela permet de garder le serveur propre et éviter de remplir le disque.
Cela évite de remplir le disque de la machine Vaultwarden.
## RÉSUMÉ
---
Le script automatise complètement les sauvegardes Vaultwarden :
# 12. Test manuel
- 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
Avant de mettre le script en cron, tester :
Cela permet davoir une sauvegarde quotidienne fiable et surveillée.
```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
* lexécution automatique via cron
* la configuration sécurisée via `.env`
Ce système permet dobtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
```

View File

@@ -5,7 +5,7 @@ set -euo pipefail
# Chemins fixes du script
#######################################
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"
mkdir -p "$(dirname "$LOG_FILE")"
@@ -45,9 +45,11 @@ set +a
# Variables backup
#######################################
DATE="$(date +'%Y-%m-%d_%H-%M-%S')"
BACKUP_NAME="vaultwarden-backup-${DATE}.tar.gz"
BACKUP_PREFIX="vaultwarden-backup"
BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
LOCAL_BACKUP_DIR="$LOCAL_BACKUP"
LOCAL_BACKUP_FILE="${LOCAL_BACKUP_DIR}/${BACKUP_NAME}"
RETENTION_DAYS=10
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
@@ -66,13 +68,15 @@ discord_ping() {
if [[ "$success" == "true" ]]; then
icon="🟢"
status_line="✅"
ping=""
else
icon="🔴"
status_line="❌"
ping="@here "
fi
local msg
msg="**@here ${icon} Backup Vaultwarden**\n"
msg="**${ping}Backup Vaultwarden ${icon}**\n"
msg+="Backup: ${BACKUP_NAME}\n"
msg+="Data transfer: ${status_line}\n"
[[ -n "$details" ]] && msg+="Détails: ${details}"
@@ -110,6 +114,8 @@ log "Destination distante : ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}"
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" \
|| fail "Erreur lors de la compression du dossier $DATA_DIR"
log "Backup local créé : $LOCAL_BACKUP_FILE"
#######################################
# Création dossier distant
#######################################
@@ -122,6 +128,17 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$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"
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
#######################################
@@ -132,4 +149,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"
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"

View File

@@ -35,6 +35,7 @@ SSH_KEY
* [#378] Script Backup BDD Vaultwarden
* [#381] Variabiliser tous les scripts
* [#384] Fix Correctif
* [#391] Script Déploiement de Scripts
### Changed
### Fixed

View File

@@ -1,212 +0,0 @@
# Code Review - Scripts Serveur (MALIO)
**Date** : 2026-03-09
**Reviewer** : Claude (Opus 4.6)
**Scope** : Revue complète de tout le code du dépôt
---
**Note** : Le fichier `CheckStorage/.env` contient un webhook Discord en local mais n'est **pas commité** dans le dépôt (correctement ignoré par le `.gitignore`). Pas de fuite de secret.
---
## 1. BackupVaultWarden/backup-vaultwarden.sh
### Qualite globale : Bonne
Le script est bien structuré, utilise `set -euo pipefail`, et gère correctement les erreurs.
### Problemes
| Severite | Ligne | Description |
|----------|-------|-------------|
| **CRITIQUE** | 8 | Chemin `.env` en dur (`/home/matt/vaultwarden/scripts/...`) au lieu d'utiliser `$SCRIPT_DIR`. Ce script ne fonctionnera **que sur la machine de matt**. |
| **HAUTE** | 80-83 | **Injection de code** dans le message Discord. La variable `$msg` est injectée directement dans du code Python via un heredoc. Si le contenu du backup contient des guillemets triples `"""` ou du code Python, il sera exécuté. |
| **MOYENNE** | 7 | `SCRIPT_DIR` est calculé mais jamais utilisé (variable morte). |
| **BASSE** | 36 | `WEBHOOK_URL` utilise `:=` (valeur par défaut vide) au lieu de `:-`. Ce n'est pas un bug mais c'est incohérent avec les autres variables qui utilisent `:?`. |
### Suggestions
- **Ligne 8** : Remplacer par `ENV_FILE="${SCRIPT_DIR}/.env"` comme dans les autres scripts.
- **Ligne 80-83** : Utiliser `jq` ou `python3 -c` avec passage par argument au lieu d'interpoler dans un heredoc Python :
```bash
printf '%s' "$msg" | python3 -c 'import sys,json; print(json.dumps({"content": sys.stdin.read()}))' | curl ...
```
- Ajouter une rotation des backups distants (pas de purge des anciens backups actuellement).
---
## 2. CheckStorage/check-storage.sh
### Qualite globale : Faible
Ce script est le moins mature du dépôt. Pas de `set -euo pipefail`, pas de gestion d'erreurs, et des pratiques fragiles.
### Problemes
| Severite | Ligne | Description |
|----------|-------|-------------|
| **HAUTE** | 1 | Pas de `set -euo pipefail`. Le script continue silencieusement en cas d'erreur. |
| **HAUTE** | 10 | Lecture du `.env` avec `grep | cut` au lieu de `source`. Fragile : ne supporte pas les valeurs avec `=`, les espaces, ou les guillemets. |
| **HAUTE** | 39-45 | **Injection JSON** : le message Discord est construit par interpolation directe dans du JSON. Si une variable contient `"` ou `\`, le JSON sera invalide ou pire. |
| **MOYENNE** | 10 | Chemin `.env` relatif sans `cd` vers le répertoire du script. Le script cassera s'il est exécuté depuis un autre répertoire (ex: via cron). |
| **MOYENNE** | 37 | Pas de notification quand tout va bien (aucun message si usage < limite). Impossible de savoir si le cron fonctionne. |
| **BASSE** | - | Pas de shebang `#!/usr/bin/env bash`, utilise `#!/bin/bash` directement (moins portable). |
| **BASSE** | - | Pas de logging dans un fichier, seulement `echo` sur stdout. |
### Suggestions
- Aligner sur le pattern des scripts RecetteScripts : `set -euo pipefail`, `source .env`, `SCRIPT_DIR`, gestion d'erreurs.
- Utiliser `jq` ou Python pour construire le JSON Discord de maniere sure.
- Ajouter un chemin absolu vers le `.env` base sur `$SCRIPT_DIR`.
- Ajouter un mode `--verbose` ou un log file.
---
## 3. RecetteScripts/backup-bdd-recette.sh
### Qualite globale : Tres bonne
C'est le script le plus mature du dépôt. Bien structure, bonne gestion d'erreurs, verrou d'execution, messages Discord granulaires.
### Problemes
| Severite | Ligne | Description |
|----------|-------|-------------|
| **HAUTE** | 92 | `export PGPASSWORD` : le mot de passe PostgreSQL est expose dans l'environnement. Tout process fils peut le lire (visible dans `/proc/*/environ`). Preferer un fichier `.pgpass` avec permissions 600. |
| **HAUTE** | 106-107 | **Injection JSON** dans `discord_send()` : la variable `$msg` est interpolee directement dans le JSON curl. Meme probleme que les autres scripts. |
| **MOYENNE** | 223 | Les noms de dossiers distants (`ferme`, `sirh`, `inventory`, `user`) sont en dur au lieu d'etre derives de `$DBS`. Si on ajoute une base dans `.env`, le dossier distant ne sera pas cree. |
| **MOYENNE** | 237-239 | L'export des "roles" fait en realite un simple `SELECT rolname` : ca ne sauvegarde que les **noms** des roles, pas leurs privileges, mots de passe, ou attributs. Utiliser `pg_dumpall --roles-only` pour un vrai backup des roles. |
| **BASSE** | 336 | `exit 2` en fin de script en cas d'erreur partielle : c'est bien, mais pas documente dans le README. |
| **BASSE** | 85 | Le `TMP_DIR` est sous `/tmp` : sur un systeme multi-utilisateur, un autre user pourrait pre-creer le dossier (race condition / symlink attack). Utiliser `mktemp -d`. |
### Suggestions
- Remplacer `export PGPASSWORD` par un fichier `.pgpass`.
- Deriver les dossiers distants de `$DBS_ARRAY` au lieu de les hardcoder.
- Utiliser `pg_dumpall --roles-only` pour un vrai export des roles.
- Utiliser `mktemp -d` pour le repertoire temporaire.
---
## 4. RecetteScripts/check-statut-recette.sh
### Qualite globale : Bonne
Script bien ecrit, bonne separation des responsabilites, gestion propre des erreurs curl.
### Problemes
| Severite | Ligne | Description |
|----------|-------|-------------|
| **HAUTE** | 92-94 | **Injection JSON** dans Discord (meme pattern que partout). |
| **MOYENNE** | 1 | `set -u` sans `set -e`. Les erreurs non gerees ne stopperont pas le script. C'est voulu (le script doit continuer pour checker tous les sites), mais `set -o pipefail` manque. |
| **MOYENNE** | 52 | Schema HTTP en dur (`http`). Pas de support HTTPS. Si les apps passent en HTTPS, le script retournera des redirections 301/302 au lieu de verifier le vrai endpoint. |
| **BASSE** | 134 | Le `mktemp` pour stderr n'est pas nettoye en cas d'interruption (pas de `trap`). Fuite mineure de fichiers temp. |
### Suggestions
- Ajouter le support HTTPS (configurable par URL dans le `.env`, ou suivre les redirections avec `-L`).
- Ajouter `set -o pipefail`.
- Nettoyer le fichier `stderr` temporaire dans un `trap`.
---
## 5. Problemes transversaux
### 5.1 Injection JSON dans les notifications Discord
**Tous les scripts** construisent le payload JSON Discord par interpolation de chaines. C'est le probleme le plus repandu.
**Pattern actuel (dangereux)** :
```bash
curl -d "{\"content\":\"$msg\"}" "$WEBHOOK_URL"
```
**Pattern corrige** :
```bash
jq -n --arg msg "$msg" '{content: $msg}' | curl -d @- ...
```
Ou si `jq` n'est pas disponible :
```bash
python3 -c "import sys,json; print(json.dumps({'content': sys.argv[1]}))" "$msg" | curl -d @- ...
```
### 5.2 Pas de rotation des sauvegardes
Aucun script ne gere la purge des anciens backups sur le serveur distant. L'espace disque distant finira par se remplir.
**Suggestion** : ajouter une commande SSH pour supprimer les backups de plus de N jours :
```bash
ssh "$REMOTE" "find '$REMOTE_DIR' -name '*.dump' -mtime +30 -delete"
```
### 5.3 Incoherence de style entre les scripts
| Aspect | check-storage | backup-vaultwarden | backup-bdd-recette | check-statut-recette |
|--------|--------------|--------------------|--------------------|---------------------|
| `set -euo pipefail` | Non | Oui | Oui | Partiel (`set -u`) |
| Chargement .env | `grep\|cut` | `source` (chemin dur) | `source` (SCRIPT_DIR) | `source` (SCRIPT_DIR) |
| Logging | `echo` | `tee` + fichier | `tee` + fichier | `tee` + fichier |
| Gestion erreurs | Aucune | `fail()` | Granulaire | `log_line()` |
| Shebang | `#!/bin/bash` | `#!/usr/bin/env bash` | `#!/usr/bin/env bash` | `#!/usr/bin/env bash` |
`check-storage.sh` est clairement en retard sur les conventions adoptees dans les autres scripts.
### 5.4 README du BackupVaultWarden desynchronise
Le README de BackupVaultWarden decrit l'ancien code (lecture `.env` avec `grep | cut`) alors que le script actuel utilise `source`. La documentation ne correspond plus au code.
---
## 6. .gitignore
### Problemes
Le `.gitignore` est **trop complexe** et redondant. Les regles se contredisent :
```gitignore
.env
.env.*
!.env.example
!.env.exemple
RecetteScripts/.env # redondant avec .env
CheckStorage/.env # redondant avec .env
```
Les regles fonctionnent correctement (les fichiers `.env` ne sont pas commites), mais la redondance rend le fichier difficile a maintenir.
**Suggestion** : simplifier le `.gitignore` :
```gitignore
# Secrets
.env
!.env.exemple
!.env.example
```
---
## 7. Resume et priorites
### Actions immediates (a faire maintenant)
1. Corriger le chemin `.env` en dur dans `backup-vaultwarden.sh` (ligne 8)
2. Corriger l'injection JSON dans **tous** les scripts (utiliser `jq`)
### Actions a court terme
3. Remonter `check-storage.sh` au niveau des autres scripts (set -euo, source .env, SCRIPT_DIR, logging)
4. Remplacer `export PGPASSWORD` par `.pgpass` dans `backup-bdd-recette.sh`
5. Utiliser `pg_dumpall --roles-only` au lieu du simple `SELECT rolname`
### Actions a moyen terme
8. Ajouter la rotation des backups distants
9. Ajouter le support HTTPS dans `check-statut-recette.sh`
10. Mettre a jour le README de BackupVaultWarden
11. Simplifier le `.gitignore`
---
*Revue generee par Claude (Opus 4.6) - Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>*

View File

@@ -1,4 +1,23 @@
#!/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
###############################################################################
@@ -6,14 +25,10 @@
# Limite maximale d'utilisation du disque en pourcentage
limit=70
# Récupération du webhook Discord depuis le fichier .env
WEBHOOK_URL=$(grep -E '^WEBHOOK_URL=' .env | cut -d '=' -f2-)
###############################################################################
# RÉCUPÉRATION DES INFORMATIONS DISQUE
###############################################################################
# extraction des informations
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
@@ -23,25 +38,24 @@ free=$((100 - usage))
# CONVERSION EN GIGAOCTETS
###############################################################################
# Conversion bytes → gigaoctets pour un affichage plus lisible
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
###############################################################################
# Si l'utilisation dépasse la limite définie, une alerte est envoyée sur Discord
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 "{\"content\":\"$msgLimit\"}" \
-d "$payload" \
"$WEBHOOK_URL"
fi
@@ -50,9 +64,6 @@ fi
# AFFICHAGE DES INFORMATIONS STOCKAGE
###############################################################################
# Affichage des informations disque dans la console
echo "Espace disponible : ${avail_gb} GB"
echo "Espace utilise / espace total : ${used_gb} GB / ${total_gb} GB"
# Nom de la machine exécutant le script
echo "Name: ${HOSTNAME}"

964
Deployment/Deployment.sh Normal file
View File

@@ -0,0 +1,964 @@
#!/usr/bin/env bash
set -Eeuo pipefail
###############################################################################
# bootstrap-backup-env.sh
#
# Prépare un environnement de déploiement pour :
# - backup-vaultwarden.sh
# - check-storage.sh
# - check-statut-recette.sh
# - backup-bdd-recette.sh
# - rebuild-bdd-recette.sh
#
# Fonctionnalités :
# - idempotent : relançable sans erreur ;
# - installation / mise à jour des dépendances ;
# - création des dossiers ;
# - permissions ;
# - génération des clés SSH si absentes ;
# - récupération depuis un dépôt Git privé volumineux via sparse-checkout ;
# - mise à jour du .env ;
# - injection des valeurs sensibles dans le .env si fournies ;
# - ajout automatique de la clé publique backup sur le serveur distant
# si un accès SSH bootstrap est disponible ;
# - questions interactives si lancé en local et que des variables obligatoires
# sont absentes ;
# - génération d'un fichier scripts.json pour un futur affichage web ;
# - exécutable en local ou envoyé via SSH sur un serveur distant.
###############################################################################
#######################################
# Valeurs par défaut
#######################################
REPO_URL=""
REPO_BRANCH="main"
REPO_SUBDIR=""
INSTALL_DIR="/opt/malio-backup"
DEPLOY_USER="${SUDO_USER:-${USER}}"
DEPLOY_GROUP=""
ENV_FILE_NAME=".env"
GIT_DIR_NAME="repo"
SCRIPTS_DIR_NAME="scripts"
CONFIG_DIR_NAME="config"
LOG_DIR_NAME="logs"
DATA_DIR_NAME="data"
TMP_DIR_NAME="tmp"
SSH_DIR_NAME="ssh"
BACKUP_SSH_KEY_NAME="id_ed25519_backup"
REPO_SSH_KEY_NAME="id_ed25519_repo"
FORCE_CHOWN="false"
NON_INTERACTIVE="false"
# Paramètres fonctionnels
ENV_NAME="RECETTE"
PGHOST="localhost"
PGPORT="5432"
PGUSER_VALUE=""
PGPASSWORD_VALUE=""
DBS_VALUE="sirh inventory ferme"
BACKUP_REMOTE_USER="backup"
BACKUP_REMOTE_HOST=""
BACKUP_REMOTE_DIR="/home/backup/backups/bdd-recette"
SSH_CONNECT_TIMEOUT="10"
RETENTION_DAYS="10"
DISCORD_WEBHOOK_URL_VALUE=""
DISCORD_PING_VALUE=""
WEBHOOK_URL_VALUE=""
VAULTWARDEN_DATA_DIR_VALUE="/var/lib/vaultwarden"
CHECK_STORAGE_PATHS_VALUE="/ /var /home"
APP_1_NAME_VALUE="ferme"
APP_1_URL_VALUE="https://ferme.malio-dev.fr"
APP_2_NAME_VALUE="sirh"
APP_2_URL_VALUE="https://sirh.malio-dev.fr"
APP_3_NAME_VALUE="inventory"
APP_3_URL_VALUE="https://inventory.malio-dev.fr"
# Bootstrap SSH vers le serveur de destination
BOOTSTRAP_SSH_USER=""
BOOTSTRAP_SSH_PORT="22"
BOOTSTRAP_SSH_KEY=""
BOOTSTRAP_SSH_STRICT="accept-new"
INSTALL_BACKUP_KEY_ON_REMOTE="true"
#######################################
# Scripts attendus
#######################################
EXPECTED_SCRIPTS=(
"backup-vaultwarden.sh"
"check-storage.sh"
"check-statut-recette.sh"
"backup-bdd-recette.sh"
"rebuild-bdd-recette.sh"
)
#######################################
# Journalisation
#######################################
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
echo "[$(timestamp)] [INFO] $*"
}
warn() {
echo "[$(timestamp)] [WARN] $*" >&2
}
err() {
echo "[$(timestamp)] [ERROR] $*" >&2
}
die() {
err "$*"
exit 1
}
#######################################
# Gestion erreurs
#######################################
on_error() {
local exit_code=$?
err "Échec ligne ${BASH_LINENO[0]} : ${BASH_COMMAND}"
exit "$exit_code"
}
trap on_error ERR
#######################################
# Aide
#######################################
usage() {
cat <<'EOF'
Usage:
bootstrap-backup-env.sh [options]
Options dépôt :
--repo-url URL
--repo-branch BRANCH
--repo-subdir PATH
Options installation :
--install-dir PATH
--deploy-user USER
--deploy-group GROUP
--env-file-name NAME
--force-chown true|false
--non-interactive true|false
Options configuration applicative :
--env-name NAME
--pghost HOST
--pgport PORT
--pguser USER
--pgpassword PASSWORD
--dbs "sirh inventory ferme"
--backup-remote-user USER
--backup-remote-host HOST
--backup-remote-dir PATH
--ssh-connect-timeout SECONDS
--retention-days DAYS
--discord-webhook-url URL
--discord-ping VALUE
--webhook-url URL
--vaultwarden-data-dir PATH
--check-storage-paths "/ /var /home"
--app-1-name NAME
--app-1-url URL
--app-2-name NAME
--app-2-url URL
--app-3-name NAME
--app-3-url URL
Options bootstrap SSH distant :
--bootstrap-ssh-user USER
--bootstrap-ssh-port PORT
--bootstrap-ssh-key PATH
--bootstrap-ssh-strict accept-new|yes|no
--install-backup-key-on-remote true|false
Divers :
--help
Notes :
- si le script est lancé localement en mode interactif, il posera les
questions nécessaires pour compléter les champs obligatoires ;
- la clé publique backup peut être installée automatiquement sur le serveur
distant uniquement si un accès SSH bootstrap existe déjà ;
- le .env peut être rempli automatiquement si les valeurs sont passées en
arguments ou via variables d'environnement avant exécution.
EOF
}
#######################################
# Parsing arguments
#######################################
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-url) REPO_URL="${2:-}"; shift 2 ;;
--repo-branch) REPO_BRANCH="${2:-}"; shift 2 ;;
--repo-subdir) REPO_SUBDIR="${2:-}"; shift 2 ;;
--install-dir) INSTALL_DIR="${2:-}"; shift 2 ;;
--deploy-user) DEPLOY_USER="${2:-}"; shift 2 ;;
--deploy-group) DEPLOY_GROUP="${2:-}"; shift 2 ;;
--env-file-name) ENV_FILE_NAME="${2:-}"; shift 2 ;;
--force-chown) FORCE_CHOWN="${2:-}"; shift 2 ;;
--non-interactive) NON_INTERACTIVE="${2:-}"; shift 2 ;;
--env-name) ENV_NAME="${2:-}"; shift 2 ;;
--pghost) PGHOST="${2:-}"; shift 2 ;;
--pgport) PGPORT="${2:-}"; shift 2 ;;
--pguser) PGUSER_VALUE="${2:-}"; shift 2 ;;
--pgpassword) PGPASSWORD_VALUE="${2:-}"; shift 2 ;;
--dbs) DBS_VALUE="${2:-}"; shift 2 ;;
--backup-remote-user) BACKUP_REMOTE_USER="${2:-}"; shift 2 ;;
--backup-remote-host) BACKUP_REMOTE_HOST="${2:-}"; shift 2 ;;
--backup-remote-dir) BACKUP_REMOTE_DIR="${2:-}"; shift 2 ;;
--ssh-connect-timeout) SSH_CONNECT_TIMEOUT="${2:-}"; shift 2 ;;
--retention-days) RETENTION_DAYS="${2:-}"; shift 2 ;;
--discord-webhook-url) DISCORD_WEBHOOK_URL_VALUE="${2:-}"; shift 2 ;;
--discord-ping) DISCORD_PING_VALUE="${2:-}"; shift 2 ;;
--webhook-url) WEBHOOK_URL_VALUE="${2:-}"; shift 2 ;;
--vaultwarden-data-dir) VAULTWARDEN_DATA_DIR_VALUE="${2:-}"; shift 2 ;;
--check-storage-paths) CHECK_STORAGE_PATHS_VALUE="${2:-}"; shift 2 ;;
--app-1-name) APP_1_NAME_VALUE="${2:-}"; shift 2 ;;
--app-1-url) APP_1_URL_VALUE="${2:-}"; shift 2 ;;
--app-2-name) APP_2_NAME_VALUE="${2:-}"; shift 2 ;;
--app-2-url) APP_2_URL_VALUE="${2:-}"; shift 2 ;;
--app-3-name) APP_3_NAME_VALUE="${2:-}"; shift 2 ;;
--app-3-url) APP_3_URL_VALUE="${2:-}"; shift 2 ;;
--bootstrap-ssh-user) BOOTSTRAP_SSH_USER="${2:-}"; shift 2 ;;
--bootstrap-ssh-port) BOOTSTRAP_SSH_PORT="${2:-}"; shift 2 ;;
--bootstrap-ssh-key) BOOTSTRAP_SSH_KEY="${2:-}"; shift 2 ;;
--bootstrap-ssh-strict) BOOTSTRAP_SSH_STRICT="${2:-}"; shift 2 ;;
--install-backup-key-on-remote) INSTALL_BACKUP_KEY_ON_REMOTE="${2:-}"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) die "Option inconnue : $1" ;;
esac
done
#######################################
# Surcharge par variables d'environnement
#######################################
PGPASSWORD_VALUE="${PGPASSWORD_VALUE:-${PGPASSWORD:-}}"
DISCORD_WEBHOOK_URL_VALUE="${DISCORD_WEBHOOK_URL_VALUE:-${DISCORD_WEBHOOK_URL:-}}"
DISCORD_PING_VALUE="${DISCORD_PING_VALUE:-${DISCORD_PING:-}}"
WEBHOOK_URL_VALUE="${WEBHOOK_URL_VALUE:-${WEBHOOK_URL:-}}"
#######################################
# Détection mode interactif local
#######################################
is_interactive() {
[[ -t 0 && -t 1 && "${NON_INTERACTIVE}" != "true" ]]
}
#######################################
# Questions interactives
#######################################
prompt_value() {
local var_name="$1"
local prompt_label="$2"
local default_value="${3:-}"
local secret="${4:-false}"
local required="${5:-false}"
local current_value
current_value="${!var_name:-}"
if [[ -n "$current_value" ]]; then
return 0
fi
if ! is_interactive; then
if [[ "$required" == "true" ]]; then
die "Valeur obligatoire manquante : ${var_name}. Fournissez-la en argument ou variable d'environnement."
fi
return 0
fi
local input=""
while true; do
if [[ "$secret" == "true" ]]; then
if [[ -n "$default_value" ]]; then
read -r -s -p "${prompt_label} [valeur masquée, Entrée pour conserver la valeur par défaut] : " input
else
read -r -s -p "${prompt_label} : " input
fi
echo
else
if [[ -n "$default_value" ]]; then
read -r -p "${prompt_label} [${default_value}] : " input
else
read -r -p "${prompt_label} : " input
fi
fi
if [[ -z "$input" && -n "$default_value" ]]; then
input="$default_value"
fi
if [[ "$required" == "true" && -z "$input" ]]; then
warn "Cette valeur est obligatoire."
continue
fi
printf -v "$var_name" '%s' "$input"
break
done
}
ask_required_local_configuration() {
if ! is_interactive; then
return 0
fi
log "Mode interactif local détecté : collecte des données obligatoires."
prompt_value REPO_URL "URL SSH du dépôt Git privé" "$REPO_URL" false true
prompt_value REPO_BRANCH "Branche Git" "$REPO_BRANCH" false true
prompt_value REPO_SUBDIR "Sous-dossier du dépôt contenant les scripts" "$REPO_SUBDIR" false true
prompt_value INSTALL_DIR "Répertoire d'installation" "$INSTALL_DIR" false true
prompt_value DEPLOY_USER "Utilisateur propriétaire du déploiement" "$DEPLOY_USER" false true
if [[ -z "$DEPLOY_GROUP" ]]; then
local deploy_group_default=""
if id "$DEPLOY_USER" >/dev/null 2>&1; then
deploy_group_default="$(id -gn "$DEPLOY_USER")"
fi
prompt_value DEPLOY_GROUP "Groupe propriétaire" "$deploy_group_default" false true
fi
prompt_value ENV_NAME "Nom de l'environnement" "$ENV_NAME" false true
prompt_value PGHOST "Host PostgreSQL" "$PGHOST" false true
prompt_value PGPORT "Port PostgreSQL" "$PGPORT" false true
if [[ -z "$PGUSER_VALUE" ]]; then
prompt_value PGUSER_VALUE "Utilisateur PostgreSQL" "$DEPLOY_USER" false true
fi
prompt_value PGPASSWORD_VALUE "Mot de passe PostgreSQL (PGPASSWORD)" "" true true
prompt_value DBS_VALUE "Bases PostgreSQL à gérer (séparées par des espaces)" "$DBS_VALUE" false true
prompt_value BACKUP_REMOTE_USER "Utilisateur du serveur de sauvegarde" "$BACKUP_REMOTE_USER" false true
prompt_value BACKUP_REMOTE_HOST "Host/IP du serveur de sauvegarde" "$BACKUP_REMOTE_HOST" false true
prompt_value BACKUP_REMOTE_DIR "Répertoire distant de sauvegarde" "$BACKUP_REMOTE_DIR" false true
prompt_value SSH_CONNECT_TIMEOUT "Timeout SSH en secondes" "$SSH_CONNECT_TIMEOUT" false true
prompt_value RETENTION_DAYS "Rétention en jours" "$RETENTION_DAYS" false true
prompt_value VAULTWARDEN_DATA_DIR_VALUE "Répertoire des données Vaultwarden" "$VAULTWARDEN_DATA_DIR_VALUE" false true
prompt_value CHECK_STORAGE_PATHS_VALUE "Chemins à surveiller pour le stockage" "$CHECK_STORAGE_PATHS_VALUE" false true
prompt_value APP_1_NAME_VALUE "Nom application 1" "$APP_1_NAME_VALUE" false true
prompt_value APP_1_URL_VALUE "URL application 1" "$APP_1_URL_VALUE" false true
prompt_value APP_2_NAME_VALUE "Nom application 2" "$APP_2_NAME_VALUE" false true
prompt_value APP_2_URL_VALUE "URL application 2" "$APP_2_URL_VALUE" false true
prompt_value APP_3_NAME_VALUE "Nom application 3" "$APP_3_NAME_VALUE" false true
prompt_value APP_3_URL_VALUE "URL application 3" "$APP_3_URL_VALUE" false true
prompt_value DISCORD_WEBHOOK_URL_VALUE "Discord webhook URL (optionnel)" "$DISCORD_WEBHOOK_URL_VALUE" true false
prompt_value DISCORD_PING_VALUE "Discord ping (optionnel)" "$DISCORD_PING_VALUE" false false
prompt_value WEBHOOK_URL_VALUE "Webhook URL générique (optionnel)" "$WEBHOOK_URL_VALUE" true false
if [[ "$INSTALL_BACKUP_KEY_ON_REMOTE" == "true" ]]; then
prompt_value BOOTSTRAP_SSH_USER "Utilisateur SSH bootstrap pour installer la clé distante" "${BOOTSTRAP_SSH_USER:-$BACKUP_REMOTE_USER}" false true
prompt_value BOOTSTRAP_SSH_PORT "Port SSH bootstrap" "$BOOTSTRAP_SSH_PORT" false true
prompt_value BOOTSTRAP_SSH_KEY "Chemin de la clé SSH bootstrap (optionnel si agent SSH ou accès existant)" "$BOOTSTRAP_SSH_KEY" false false
fi
}
#######################################
# Vérifications initiales
#######################################
validate_required_values() {
[[ -n "$REPO_URL" ]] || die "L'option --repo-url est obligatoire."
[[ -n "$REPO_SUBDIR" ]] || die "L'option --repo-subdir est obligatoire."
[[ -n "$BACKUP_REMOTE_HOST" ]] || die "L'option --backup-remote-host est obligatoire."
[[ -n "$DEPLOY_USER" ]] || die "L'utilisateur de déploiement est obligatoire."
if ! id "$DEPLOY_USER" >/dev/null 2>&1; then
die "Utilisateur inexistant : $DEPLOY_USER"
fi
if [[ -z "$DEPLOY_GROUP" ]]; then
DEPLOY_GROUP="$(id -gn "$DEPLOY_USER")"
fi
if [[ -z "$PGUSER_VALUE" ]]; then
PGUSER_VALUE="$DEPLOY_USER"
fi
}
#######################################
# Chemins calculés
#######################################
compute_paths() {
BASE_DIR="$INSTALL_DIR"
REPO_DIR="$BASE_DIR/$GIT_DIR_NAME"
APP_SCRIPTS_DIR="$BASE_DIR/$SCRIPTS_DIR_NAME"
CONFIG_DIR="$BASE_DIR/$CONFIG_DIR_NAME"
LOG_DIR="$BASE_DIR/$LOG_DIR_NAME"
DATA_DIR="$BASE_DIR/$DATA_DIR_NAME"
TMP_DIR="$BASE_DIR/$TMP_DIR_NAME"
APP_SSH_DIR="$BASE_DIR/$SSH_DIR_NAME"
ENV_FILE="$BASE_DIR/$ENV_FILE_NAME"
SCRIPTS_JSON="$CONFIG_DIR/scripts.json"
BACKUP_SSH_KEY="$APP_SSH_DIR/$BACKUP_SSH_KEY_NAME"
REPO_SSH_KEY="$APP_SSH_DIR/$REPO_SSH_KEY_NAME"
}
#######################################
# Wrapper sudo
#######################################
run_root() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
else
sudo "$@"
fi
}
run_as_deploy_user() {
if [[ "$(id -un)" == "$DEPLOY_USER" ]]; then
"$@"
else
run_root sudo -u "$DEPLOY_USER" -H "$@"
fi
}
#######################################
# Détection package manager
#######################################
detect_pkg_manager() {
if command -v apt-get >/dev/null 2>&1; then
echo "apt"
elif command -v dnf >/dev/null 2>&1; then
echo "dnf"
elif command -v yum >/dev/null 2>&1; then
echo "yum"
elif command -v apk >/dev/null 2>&1; then
echo "apk"
elif command -v pacman >/dev/null 2>&1; then
echo "pacman"
else
echo ""
fi
}
install_packages() {
local packages=("$@")
[[ ${#packages[@]} -gt 0 ]] || return 0
case "$PKG_MANAGER" in
apt)
run_root apt-get update -y
run_root apt-get install -y "${packages[@]}"
;;
dnf)
run_root dnf install -y "${packages[@]}"
;;
yum)
run_root yum install -y "${packages[@]}"
;;
apk)
run_root apk add --no-cache "${packages[@]}"
;;
pacman)
run_root pacman -Sy --noconfirm "${packages[@]}"
;;
*)
die "Package manager non géré : $PKG_MANAGER"
;;
esac
}
ensure_cmd() {
local cmd="$1"
shift
local packages=("$@")
if command -v "$cmd" >/dev/null 2>&1; then
log "Dépendance OK : $cmd"
else
warn "Dépendance absente : $cmd"
install_packages "${packages[@]}"
fi
}
install_dependencies() {
case "$PKG_MANAGER" in
apt)
ensure_cmd git git
ensure_cmd ssh openssh-client
ensure_cmd ssh-keygen openssh-client
ensure_cmd rsync rsync
ensure_cmd curl curl
ensure_cmd jq jq
ensure_cmd psql postgresql-client
install_packages ca-certificates
;;
dnf|yum)
ensure_cmd git git
ensure_cmd ssh openssh-clients
ensure_cmd ssh-keygen openssh-clients
ensure_cmd rsync rsync
ensure_cmd curl curl
ensure_cmd jq jq
ensure_cmd psql postgresql
install_packages ca-certificates
;;
apk)
ensure_cmd git git
ensure_cmd ssh openssh-client
ensure_cmd ssh-keygen openssh-keygen
ensure_cmd rsync rsync
ensure_cmd curl curl
ensure_cmd jq jq
ensure_cmd psql postgresql-client
install_packages ca-certificates
;;
pacman)
ensure_cmd git git
ensure_cmd ssh openssh
ensure_cmd ssh-keygen openssh
ensure_cmd rsync rsync
ensure_cmd curl curl
ensure_cmd jq jq
ensure_cmd psql postgresql-libs
install_packages ca-certificates
;;
esac
}
#######################################
# Création dossiers
#######################################
ensure_dir() {
local dir="$1"
local mode="$2"
run_root mkdir -p "$dir"
run_root chmod "$mode" "$dir"
}
prepare_directories() {
ensure_dir "$BASE_DIR" 0755
ensure_dir "$REPO_DIR" 0755
ensure_dir "$APP_SCRIPTS_DIR" 0750
ensure_dir "$CONFIG_DIR" 0750
ensure_dir "$LOG_DIR" 0750
ensure_dir "$DATA_DIR" 0750
ensure_dir "$TMP_DIR" 0750
ensure_dir "$APP_SSH_DIR" 0700
}
#######################################
# Permissions
#######################################
apply_ownership() {
if [[ "$FORCE_CHOWN" == "true" ]]; then
run_root chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "$BASE_DIR"
else
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" \
"$BASE_DIR" "$REPO_DIR" "$APP_SCRIPTS_DIR" "$CONFIG_DIR" \
"$LOG_DIR" "$DATA_DIR" "$TMP_DIR" "$APP_SSH_DIR"
fi
}
#######################################
# SSH
#######################################
ensure_ssh_keypair() {
local private_key="$1"
local comment="$2"
if [[ -f "$private_key" && -f "${private_key}.pub" ]]; then
log "Clé SSH déjà présente : $private_key"
run_root chmod 600 "$private_key"
run_root chmod 644 "${private_key}.pub"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$private_key" "${private_key}.pub"
return 0
fi
log "Génération de la clé SSH : $private_key"
run_root ssh-keygen -t ed25519 -N "" -C "$comment" -f "$private_key" >/dev/null
run_root chmod 600 "$private_key"
run_root chmod 644 "${private_key}.pub"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$private_key" "${private_key}.pub"
}
build_bootstrap_ssh_cmd() {
local target="$1"
local -a cmd=(ssh -p "$BOOTSTRAP_SSH_PORT" -o "StrictHostKeyChecking=$BOOTSTRAP_SSH_STRICT")
if [[ -n "$BOOTSTRAP_SSH_KEY" ]]; then
cmd+=(-i "$BOOTSTRAP_SSH_KEY" -o IdentitiesOnly=yes)
fi
cmd+=("$target")
printf '%q ' "${cmd[@]}"
}
install_backup_key_on_remote() {
[[ "$INSTALL_BACKUP_KEY_ON_REMOTE" == "true" ]] || {
log "Installation de la clé backup sur le serveur distant désactivée."
return 0
}
[[ -n "$BACKUP_REMOTE_HOST" ]] || {
warn "BACKUP_REMOTE_HOST vide, installation de la clé distante ignorée."
return 0
}
local bootstrap_user="${BOOTSTRAP_SSH_USER:-$BACKUP_REMOTE_USER}"
local remote_target="${bootstrap_user}@${BACKUP_REMOTE_HOST}"
local remote_auth_user="$BACKUP_REMOTE_USER"
local pubkey
pubkey="$(run_root cat "${BACKUP_SSH_KEY}.pub")"
log "Tentative d'installation de la clé publique backup sur ${remote_auth_user}@${BACKUP_REMOTE_HOST}"
local ssh_cmd
ssh_cmd="$(build_bootstrap_ssh_cmd "$remote_target")"
if ! eval "$ssh_cmd" "true" >/dev/null 2>&1; then
warn "Accès SSH bootstrap indisponible vers ${remote_target}. Clé backup non installée automatiquement."
return 0
fi
local escaped_pubkey
escaped_pubkey="$(printf '%q' "$pubkey")"
eval "$ssh_cmd" "sudo -u '$remote_auth_user' sh -c '
set -eu
umask 077
mkdir -p ~/.ssh
touch ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
grep -qxF $escaped_pubkey ~/.ssh/authorized_keys || printf \"%s\n\" $escaped_pubkey >> ~/.ssh/authorized_keys
'" >/dev/null
log "Clé publique backup installée / vérifiée sur ${remote_auth_user}@${BACKUP_REMOTE_HOST}"
}
#######################################
# .env
#######################################
ensure_env_file() {
if [[ ! -f "$ENV_FILE" ]]; then
log "Création du fichier : $ENV_FILE"
run_root touch "$ENV_FILE"
fi
run_root chmod 0640 "$ENV_FILE"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$ENV_FILE"
}
set_env_value() {
local key="$1"
local value="$2"
local escaped
escaped="$(printf '%s' "$value" | sed 's/[\/&]/\\&/g')"
if run_root grep -qE "^${key}=" "$ENV_FILE"; then
run_root sed -i "s/^${key}=.*/${key}=${escaped}/" "$ENV_FILE"
else
printf '%s=%s\n' "$key" "$value" | run_root tee -a "$ENV_FILE" >/dev/null
fi
}
update_env_defaults() {
set_env_value "ENV_NAME" "$ENV_NAME"
set_env_value "BASE_DIR" "$BASE_DIR"
set_env_value "SCRIPTS_DIR" "$APP_SCRIPTS_DIR"
set_env_value "CONFIG_DIR" "$CONFIG_DIR"
set_env_value "LOG_DIR" "$LOG_DIR"
set_env_value "DATA_DIR" "$DATA_DIR"
set_env_value "TMP_DIR" "$TMP_DIR"
set_env_value "SSH_DIR" "$APP_SSH_DIR"
set_env_value "SSH_KEY" "$BACKUP_SSH_KEY"
set_env_value "REPO_SSH_KEY" "$REPO_SSH_KEY"
set_env_value "PGHOST" "$PGHOST"
set_env_value "PGPORT" "$PGPORT"
set_env_value "PGUSER" "$PGUSER_VALUE"
set_env_value "PGPASSWORD" "${PGPASSWORD_VALUE:-change_me}"
set_env_value "DBS" "\"$DBS_VALUE\""
set_env_value "BACKUP_REMOTE_USER" "$BACKUP_REMOTE_USER"
set_env_value "BACKUP_REMOTE_HOST" "$BACKUP_REMOTE_HOST"
set_env_value "BACKUP_REMOTE_DIR" "$BACKUP_REMOTE_DIR"
set_env_value "SSH_CONNECT_TIMEOUT" "$SSH_CONNECT_TIMEOUT"
set_env_value "RETENTION_DAYS" "$RETENTION_DAYS"
set_env_value "DISCORD_WEBHOOK_URL" "$DISCORD_WEBHOOK_URL_VALUE"
set_env_value "DISCORD_PING" "$DISCORD_PING_VALUE"
set_env_value "WEBHOOK_URL" "$WEBHOOK_URL_VALUE"
set_env_value "VAULTWARDEN_DATA_DIR" "$VAULTWARDEN_DATA_DIR_VALUE"
set_env_value "CHECK_STORAGE_PATHS" "\"$CHECK_STORAGE_PATHS_VALUE\""
set_env_value "APP_1_NAME" "$APP_1_NAME_VALUE"
set_env_value "APP_1_URL" "$APP_1_URL_VALUE"
set_env_value "APP_2_NAME" "$APP_2_NAME_VALUE"
set_env_value "APP_2_URL" "$APP_2_URL_VALUE"
set_env_value "APP_3_NAME" "$APP_3_NAME_VALUE"
set_env_value "APP_3_URL" "$APP_3_URL_VALUE"
}
#######################################
# Git privé + sparse checkout
#######################################
write_git_ssh_wrapper() {
local wrapper="$TMP_DIR/git_ssh_wrapper.sh"
cat > /tmp/.git_ssh_wrapper.$$ <<EOF
#!/usr/bin/env bash
exec ssh -i "$REPO_SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new "\$@"
EOF
run_root mv /tmp/.git_ssh_wrapper.$$ "$wrapper"
run_root chmod 0700 "$wrapper"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$wrapper"
echo "$wrapper"
}
sync_repo() {
local wrapper
wrapper="$(write_git_ssh_wrapper)"
if [[ ! -d "$REPO_DIR/.git" ]]; then
log "Clone initial du dépôt"
run_root rm -rf "$REPO_DIR"
run_root mkdir -p "$REPO_DIR"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$REPO_DIR"
run_as_deploy_user env GIT_SSH_COMMAND="$wrapper" \
git clone \
--filter=blob:none \
--no-checkout \
--branch "$REPO_BRANCH" \
"$REPO_URL" \
"$REPO_DIR"
run_as_deploy_user git -C "$REPO_DIR" sparse-checkout init --cone
run_as_deploy_user git -C "$REPO_DIR" sparse-checkout set "$REPO_SUBDIR"
run_as_deploy_user git -C "$REPO_DIR" checkout "$REPO_BRANCH"
else
log "Mise à jour du dépôt existant"
run_as_deploy_user git -C "$REPO_DIR" remote set-url origin "$REPO_URL"
run_as_deploy_user git -C "$REPO_DIR" sparse-checkout init --cone || true
run_as_deploy_user git -C "$REPO_DIR" sparse-checkout set "$REPO_SUBDIR"
run_as_deploy_user env GIT_SSH_COMMAND="$wrapper" \
git -C "$REPO_DIR" fetch origin "$REPO_BRANCH" --depth=1
run_as_deploy_user git -C "$REPO_DIR" checkout "$REPO_BRANCH"
run_as_deploy_user git -C "$REPO_DIR" reset --hard "origin/$REPO_BRANCH"
run_as_deploy_user git -C "$REPO_DIR" clean -fd
fi
}
#######################################
# Déploiement scripts
#######################################
deploy_scripts() {
local source_dir="$REPO_DIR/$REPO_SUBDIR"
[[ -d "$source_dir" ]] || die "Sous-dossier introuvable : $source_dir"
local script
for script in "${EXPECTED_SCRIPTS[@]}"; do
[[ -f "$source_dir/$script" ]] || die "Script manquant dans le dépôt : $source_dir/$script"
done
for script in "${EXPECTED_SCRIPTS[@]}"; do
log "Déploiement : $script"
run_root install -m 0750 -o "$DEPLOY_USER" -g "$DEPLOY_GROUP" \
"$source_dir/$script" "$APP_SCRIPTS_DIR/$script"
done
}
verify_scripts() {
local script
for script in "${EXPECTED_SCRIPTS[@]}"; do
[[ -f "$APP_SCRIPTS_DIR/$script" ]] || die "Script absent après déploiement : $APP_SCRIPTS_DIR/$script"
[[ -x "$APP_SCRIPTS_DIR/$script" ]] || die "Script non exécutable : $APP_SCRIPTS_DIR/$script"
done
}
#######################################
# Configuration web
#######################################
generate_scripts_json() {
local tmp_json
tmp_json="$(mktemp)"
cat > "$tmp_json" <<EOF
{
"generated_at": "$(date -Iseconds)",
"base_dir": "$BASE_DIR",
"env_file": "$ENV_FILE",
"scripts": [
{
"id": "backup-vaultwarden",
"label": "Backup Vaultwarden",
"path": "$APP_SCRIPTS_DIR/backup-vaultwarden.sh",
"web_enabled": true,
"params": [
{ "name": "env_file", "type": "string", "required": false, "default": "$ENV_FILE" },
{ "name": "dry_run", "type": "boolean", "required": false, "default": false },
{ "name": "json", "type": "boolean", "required": false, "default": true }
]
},
{
"id": "check-storage",
"label": "Vérification stockage",
"path": "$APP_SCRIPTS_DIR/check-storage.sh",
"web_enabled": true,
"params": [
{ "name": "env_file", "type": "string", "required": false, "default": "$ENV_FILE" },
{ "name": "verbose", "type": "boolean", "required": false, "default": false },
{ "name": "json", "type": "boolean", "required": false, "default": true }
]
},
{
"id": "check-statut-recette",
"label": "Vérification statut recette",
"path": "$APP_SCRIPTS_DIR/check-statut-recette.sh",
"web_enabled": true,
"params": [
{ "name": "env_file", "type": "string", "required": false, "default": "$ENV_FILE" },
{ "name": "timeout", "type": "integer", "required": false, "default": 10 },
{ "name": "json", "type": "boolean", "required": false, "default": true }
]
},
{
"id": "backup-bdd-recette",
"label": "Backup BDD recette",
"path": "$APP_SCRIPTS_DIR/backup-bdd-recette.sh",
"web_enabled": true,
"params": [
{ "name": "env_file", "type": "string", "required": false, "default": "$ENV_FILE" },
{ "name": "db", "type": "select", "required": false, "values": ["sirh", "inventory", "ferme"] },
{ "name": "dry_run", "type": "boolean", "required": false, "default": false },
{ "name": "json", "type": "boolean", "required": false, "default": true }
]
},
{
"id": "rebuild-bdd-recette",
"label": "Rebuild BDD recette",
"path": "$APP_SCRIPTS_DIR/rebuild-bdd-recette.sh",
"web_enabled": true,
"params": [
{ "name": "env_file", "type": "string", "required": false, "default": "$ENV_FILE" },
{ "name": "db", "type": "select", "required": true, "values": ["sirh", "inventory", "ferme"] },
{ "name": "source", "type": "string", "required": false, "default": "latest" },
{ "name": "force", "type": "boolean", "required": false, "default": false },
{ "name": "json", "type": "boolean", "required": false, "default": true }
]
}
]
}
EOF
run_root mv "$tmp_json" "$SCRIPTS_JSON"
run_root chmod 0640 "$SCRIPTS_JSON"
run_root chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "$SCRIPTS_JSON"
}
#######################################
# Résumé
#######################################
print_summary() {
cat <<EOF
========================================================================
Bootstrap terminé
========================================================================
Utilisateur : $DEPLOY_USER:$DEPLOY_GROUP
Base : $BASE_DIR
Scripts : $APP_SCRIPTS_DIR
Config : $CONFIG_DIR
.env : $ENV_FILE
scripts.json : $SCRIPTS_JSON
Repo : $REPO_DIR
Clé SSH backup : $BACKUP_SSH_KEY
Clé SSH repo : $REPO_SSH_KEY
Remote backup : ${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_DIR}
Clé publique repo :
$(run_root cat "${REPO_SSH_KEY}.pub")
Clé publique backup :
$(run_root cat "${BACKUP_SSH_KEY}.pub")
Vérifiez :
- la clé repo doit être ajoutée comme deploy key sur le dépôt Git privé ;
- la valeur PGPASSWORD dans $ENV_FILE ;
- les webhooks renseignés dans $ENV_FILE si nécessaires.
========================================================================
EOF
}
#######################################
# Main
#######################################
main() {
ask_required_local_configuration
validate_required_values
compute_paths
PKG_MANAGER="$(detect_pkg_manager)"
[[ -n "$PKG_MANAGER" ]] || die "Gestionnaire de paquets non supporté."
log "Vérification / installation des dépendances"
install_dependencies
log "Création des répertoires"
prepare_directories
log "Préparation du .env"
ensure_env_file
update_env_defaults
log "Préparation des clés SSH"
ensure_ssh_keypair "$BACKUP_SSH_KEY" "${DEPLOY_USER}@backup-runtime"
ensure_ssh_keypair "$REPO_SSH_KEY" "${DEPLOY_USER}@repo-deploy"
log "Installation de la clé publique backup sur le serveur distant"
install_backup_key_on_remote
log "Synchronisation du dépôt Git privé"
sync_repo
log "Déploiement des scripts"
deploy_scripts
verify_scripts
log "Génération de la configuration web scripts.json"
generate_scripts_json
log "Application des permissions"
apply_ownership
print_summary
}
main "$@"

View File

@@ -1,4 +1,4 @@
# Scripts Serveur MALIO
# Malio-Ops MALIO
Ce dépôt sert au **versionnement des scripts utilisés dans linfrastructure du projet Ferme**.
Lobjectif est de conserver un historique clair des scripts, suivre les évolutions et permettre leur amélioration progressive.

View File

@@ -16,7 +16,7 @@ PGHOST=localhost
PGPORT=5432
# Utilisateur utilisé pour les dumps
PGUSER=backup_user
PGUSER=nom_de_user
# Mot de passe PostgreSQL
PGPASSWORD=change_me_secure_password
@@ -29,20 +29,20 @@ DBS="sirh inventory ferme"
#############################################
# Utilisateur du serveur distant
BACKUP_REMOTE_USER=backup
BACKUP_REMOTE_USER=nom_de_user
# Host ou IP du serveur distant
BACKUP_REMOTE_HOST=192.168.1.50
# Dossier distant pour stocker les backups
BACKUP_REMOTE_DIR=/home/backup/backups
BACKUP_REMOTE_DIR=/home/nom_de_user/backups/bdd-recette
#############################################
# SSH
#############################################
# Clé SSH utilisée pour envoyer les dumps
SSH_KEY=/home/backup/.ssh/id_ed25519_backup
SSH_KEY=/home/nom_de_user/.ssh/id_ed25519_backup
# Timeout SSH (secondes)
SSH_TIMEOUT=10

View File

@@ -10,13 +10,15 @@ set -euo pipefail
#
# Fonctionnement global :
# 1. charge la configuration depuis le fichier .env ;
# 2. prépare les chemins, logs et variables de connexion ;
# 3. empêche lexécution simultanée grâce à un verrou ;
# 4. crée les dossiers de destination sur la machine distante ;
# 5. exporte les rôles PostgreSQL ;
# 6. dump chaque base au format personnalisé PostgreSQL ;
# 7. transfère chaque fichier vers le serveur distant ;
# 8. envoie un bilan sur Discord :
# 2. vérifie les dépendances nécessaires ;
# 3. prépare les chemins, logs et variables de connexion ;
# 4. empêche lexé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 derreur partielle :
# * USERS OK -> message simple ;
@@ -67,6 +69,7 @@ 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"
@@ -89,8 +92,23 @@ 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
#######################################
@@ -100,10 +118,17 @@ DISCORD_PING="${DISCORD_PING:-@here}"
discord_send() {
local msg="$1"
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
curl -fsS -H "Content-Type: application/json" \
-d "{\"content\":\"$msg\"}" \
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
}
@@ -112,11 +137,14 @@ discord_send() {
#######################################
discord_msg_global_ok() {
local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n"
msg+="Name: ${BACKUP_DIR_NAME}\n"
msg+="Dumps transfer: ✅\n"
msg+="Users transfer: ✅"
local msg
msg="$(cat <<EOF
**BACKUP BDD ${ENV_NAME} 🟢**
Name: ${BACKUP_DIR_NAME}
Dumps transfer: ✅
Users transfer: ✅
EOF
)"
discord_send "$msg"
}
@@ -125,9 +153,12 @@ discord_msg_global_ok() {
#######################################
discord_msg_users_ok_simple() {
local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n"
msg+="Users backup validé"
local msg
msg="$(cat <<EOF
**BACKUP BDD ${ENV_NAME} 🟢**
Users backup validé
EOF
)"
discord_send "$msg"
}
@@ -140,12 +171,25 @@ discord_msg_users_error() {
export_disp=$([[ -n "$export_ok" ]] && echo "✅" || echo "❌")
transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌")
local msg="**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**\n"
msg+="Name: ${BACKUP_DIR_NAME}\n"
msg+="Users export: ${export_disp}\n"
msg+="Users transfer: ${transfer_disp}"
[[ -n "$details" ]] && msg+="\nDetails: ${details}"
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"
}
@@ -156,9 +200,12 @@ discord_msg_users_error() {
discord_msg_db_ok_simple() {
local db="$1"
local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n"
msg+="Backup validé : ${db}"
local msg
msg="$(cat <<EOF
**BACKUP BDD ${ENV_NAME} 🟢**
Backup validé : ${db}
EOF
)"
discord_send "$msg"
}
@@ -172,13 +219,27 @@ discord_msg_db_error() {
dump_disp=$([[ -n "$dump_ok" ]] && echo "✅" || echo "❌")
transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌")
local msg="**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**\n"
msg+="Name: ${BACKUP_DIR_NAME}\n"
msg+="Database: ${db}\n"
msg+="Dump: ${dump_disp}\n"
msg+="Transfer: ${transfer_disp}"
[[ -n "$details" ]] && msg+="\nDetails: ${details}"
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"
}
@@ -210,7 +271,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
exit 1
fi
trap 'rm -rf "$LOCK_DIR"' EXIT
trap 'rm -rf "$LOCK_DIR" "$TMP_DIR"' EXIT
#######################################
# Préparation du dossier distant
@@ -230,13 +291,18 @@ fi
# Export des rôles PostgreSQL
#######################################
ROLES_FILE="${TMP_DIR}/user_${TS}.dump"
ROLES_FILE="${TMP_DIR}/user_${TS}.sql"
set +e
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -Atq <<'SQL' > "$ROLES_FILE"
SELECT rolname FROM pg_roles WHERE rolname !~ '^pg_';
SQL
log "Export des rôles PostgreSQL"
pg_dumpall \
-h "$PGHOST" \
-p "$PGPORT" \
-U "$PGUSER" \
--globals-only \
> "$ROLES_FILE"
RET=$?
@@ -244,18 +310,24 @@ 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
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/user/"
RET=$?
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"
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
USERS_DETAILS="roles transfer failed"
log "Transfert des rôles OK"
fi
fi
@@ -299,6 +371,38 @@ 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
#######################################

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -u
set -uo pipefail
###############################################################################
# check-statut-recette.sh
@@ -9,11 +9,11 @@ set -u
#
# Fonctionnement global :
# 1. charge la configuration depuis le fichier .env ;
# 2. vérifie que le DNS du site est résolu ;
# 2. vérifie le DNS de chaque application ;
# 3. effectue une requête HTTP avec curl ;
# 4. analyse le code HTTP retourné ;
# 5. écrit le résultat dans un fichier de log local ;
# 6. envoie une notification Discord avec létat du service.
# 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.
###############################################################################
#######################################
@@ -68,43 +68,20 @@ LOG_FILE="${LOG_DIR}/app_health_$(date +'%Y-%m-%d').log"
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
DISCORD_PING="${DISCORD_PING:-@here}"
discord_ping() {
local site="$1"
local status="$2"
local detail="$3"
#######################################
# Variables globales de synthèse
#######################################
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
local color icon ping_prefix=""
if [[ "$status" == "OK" ]]; then
color="🟢"
icon="✅"
else
color="🔴"
icon="❌"
ping_prefix="${DISCORD_PING} "
fi
local msg="**${ping_prefix}CHECK APP ${ENV_NAME} $color**\n"
msg+="Application: ${site}\n"
msg+="Details: ${detail}"
curl -fsS -H "Content-Type: application/json" \
-d "{\"content\":\"$msg\"}" \
"$DISCORD_WEBHOOK_URL" >/dev/null || true
}
SUMMARY_LINES=()
FAILURES=0
#######################################
# 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"
}
#######################################
@@ -115,22 +92,71 @@ 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 stderr
local http_code curl_exit err
local stderr
stderr="$(mktemp)"
http_code="$(
@@ -140,31 +166,33 @@ check_site() {
--max-time "$MAX_TIME" \
"$url" 2>"$stderr"
)"
curl_exit=$?
if [ $curl_exit -ne 0 ]; then
local err
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
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
}
@@ -173,7 +201,6 @@ check_site() {
#######################################
main() {
local failures=0
for site in "${SITES[@]}"; do
@@ -182,7 +209,10 @@ main() {
fi
done
if [ "$failures" -gt 0 ]; then
FAILURES="$failures"
send_discord_summary
if [[ "$failures" -gt 0 ]]; then
exit 2
fi

View File

@@ -0,0 +1,447 @@
#!/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="$(python3 -c 'import json,sys; print(json.dumps({"content": sys.argv[1]}))' "$message")" || {
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
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"