74 Commits

Author SHA1 Message Date
e68c99a8b3 Merge pull request 'fix : changelog plus readme a jour' (#16) from fix/correctif into develop
Reviewed-on: #16
2026-03-18 20:25:58 +00:00
7b91691ef8 fix : changelog plus readme a jour 2026-03-18 21:24:30 +01:00
7261823806 Merge pull request 'fix : changelog plus readme a jour' (#15) from fix/correctif into develop
Reviewed-on: #15
2026-03-18 11:03:31 +00:00
fac2a5b47f fix : changelog plus readme a jour 2026-03-18 12:02:30 +01:00
3c91c3b5c1 Merge pull request 'fix/correctif' (#14) from fix/correctif into develop
Reviewed-on: #14
2026-03-18 10:54:05 +00:00
f3ebb4c011 Merge branch 'develop' into fix/correctif 2026-03-18 11:50:32 +01:00
83032ef5ab Merge pull request 'feat/script-redéploiement-BDD-utilisable-interface-web' (#13) from feat/script-redéploiement-BDD-utilisable-interface-web into develop
Reviewed-on: #13
2026-03-18 10:49:52 +00:00
863fee91a9 fix : fichier executable 2026-03-18 11:37:53 +01:00
0dddecd08f fix : correctifs 002 a 006 et de 008 a 019 2026-03-18 10:12:33 +01:00
66abdfca53 feat : correctif 2026-03-17 16:34:40 +01:00
26101f2112 feat : bug telechargement 2026-03-17 16:31:56 +01:00
0ee0c1328a feat : bug telechargement 2026-03-17 16:28:54 +01:00
f6e66e7bff feat : debbug more easy 2026-03-17 16:23:43 +01:00
41df83fe32 feat : debbug more easy 2026-03-17 16:21:46 +01:00
3faf8ab71d feat : debbug more easy 2026-03-17 16:19:20 +01:00
685a65e2d1 feat : correctif mineur 2026-03-17 16:12:46 +01:00
6a99a8115f feat : correctif mineur 2026-03-17 16:11:00 +01:00
f12c937b39 feat : correctif mineur 2026-03-17 16:09:00 +01:00
cb94e74414 feat : preparation de postgresql (WIP) 2026-03-17 16:05:26 +01:00
5b128bc81a feat : preparation de postgresql (WIP) 2026-03-17 16:03:19 +01:00
6c61f6e543 feat : preparation de postgresql (WIP) 2026-03-17 15:59:32 +01:00
447b04ce20 feat : preparation de postgresql (WIP) 2026-03-17 15:57:18 +01:00
29371b6529 feat : preparation de postgresql (WIP) 2026-03-17 15:55:33 +01:00
b3de87a452 feat : preparation de postgresql (WIP) 2026-03-17 15:53:43 +01:00
0ff3244c1d feat : repertorie bug (WIP) 2026-03-17 15:51:45 +01:00
12bbe6b1d9 feat : repertorie bug (WIP) 2026-03-17 15:49:44 +01:00
fbefe3fb03 feat : repertorie bug (WIP) 2026-03-17 15:45:25 +01:00
38b29796d3 feat : repertorie bug(WIP) 2026-03-17 15:41:29 +01:00
01ac392fa9 feat : repertorie bug(WIP) 2026-03-17 15:38:17 +01:00
e5b15426a1 feat : repertorie bug(WIP) 2026-03-17 15:35:40 +01:00
7974491e93 feat : sudoers bug (WIP) 2026-03-17 15:18:35 +01:00
b76b6613bf feat : sudoers bug (WIP) 2026-03-17 15:14:52 +01:00
122f53f804 feat : correction bug scp (WIP) 2026-03-17 13:55:25 +01:00
741fef225b feat : correction bug scp (WIP) 2026-03-17 13:49:14 +01:00
8ef81add14 feat : utilisation web disponible et simplification du deployement des scripts (WIP) 2026-03-17 13:43:34 +01:00
a1fb6f5504 feat : Utilisation web disponible et simplification du deployement des scripts (WIP) 2026-03-17 11:40:35 +01:00
0d4ffd9391 feat : web interface valide (WIP) 2026-03-17 10:19:20 +01:00
AkiNoKure
94537de551 feat : update web available rebuild bdd (WIP) 2026-03-17 10:11:00 +01:00
AkiNoKure
2971ef0ff9 fix: add executable bit on rebuild scripts 2026-03-17 09:21:38 +01:00
AkiNoKure
f0dfd6acb1 feat : update web available rebuild bdd (WIP) 2026-03-17 08:48:59 +01:00
AkiNoKure
858cad8269 feat : update web available rebuild bdd (WIP) 2026-03-16 11:10:10 +01:00
fb1aaac418 Actualiser RecetteScripts/check-statut-recette.sh 2026-03-13 09:20:58 +00:00
d7cb9b34c4 Actualiser CHANGELOG.md 2026-03-13 08:05:11 +00:00
643a0d9ec7 Actualiser CHANGELOG.md 2026-03-13 08:04:26 +00:00
df77d8be21 Merge pull request 'fix/427-correctifs' (#12) from fix/427-correctifs into develop
Reviewed-on: #12
2026-03-13 08:01:30 +00:00
038ddfe242 Actualiser BackupVaultWarden/README.md 2026-03-13 07:59:18 +00:00
AkiNoKure
a1ace94094 fix : correctifs 2026-03-13 08:55:11 +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
Matthieu
99072361c5 docs: ajout de la revue de code complète du dépôt
Revue couvrant les 4 scripts (backup-vaultwarden, check-storage,
backup-bdd-recette, check-statut-recette) avec identification des
problèmes de sécurité, qualité et suggestions d'amélioration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:20:13 +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
30df5ca8d6 Merge pull request 'feat/384-correctif' (#8) from feat/384-correctif into develop
Reviewed-on: MALIO-DEV/Scripts-Serveur#8
2026-03-09 12:23:15 +00:00
AkiNoKure
4b76e88853 Merge branch 'develop' into feat/384-correctif 2026-03-09 13:21:32 +01:00
AkiNoKure
99f8694250 fix : corrections orthographique 2026-03-09 13:19:32 +01:00
7f18e2f2e9 Merge pull request 'fix : review' (#7) from feat/384-correctif into develop
Reviewed-on: MALIO-DEV/Scripts-Serveur#7
2026-03-09 10:50:01 +00:00
AkiNoKure
d0ceea8bad fix : review 2026-03-09 11:48:21 +01:00
dd226592db Merge pull request 'feat/384-correctif' (#6) from feat/384-correctif into develop
Reviewed-on: MALIO-DEV/Scripts-Serveur#6
2026-03-09 10:35:47 +00:00
AkiNoKure
e81b953ac2 fix : discord message 2026-03-09 11:31:37 +01:00
AkiNoKure
c80a74adc5 fix : versionning 2026-03-09 11:03:28 +01:00
AkiNoKure
97eeffd9ea fix : correctifs multiple 2026-03-09 10:49:29 +01:00
AkiNoKure
14359b111f Merge branch 'refs/heads/develop' into feat/384-correctif 2026-03-09 10:48:21 +01:00
e860fd0f16 Merge pull request 'fix : correctif du script' (#5) from fix/372-script-storage into develop
Reviewed-on: MALIO-DEV/Scripts-Serveur#5
2026-03-09 09:32:44 +00:00
Lethary
fbbb68af88 fix : correctif du script 2026-03-09 10:30:06 +01:00
AkiNoKure
c2d1b716e0 fix : correctifs multiples 2026-03-09 09:08:44 +01:00
34 changed files with 5877 additions and 568 deletions

View File

@@ -1 +0,0 @@
WEBHOOK_URL=

45
.gitignore vendored
View File

@@ -1,9 +1,40 @@
# Secrets / environment
.env
.env.*
!.env.example
!.env.exemple
########################################
# Environment / secrets
########################################
.env
!.env.exemple
!.env.example
########################################
# Logs
########################################
*.log
logs/
var/log/
backup.log
########################################
# Temporary / system files
########################################
*.tmp
*.swp
*.swo
*~
########################################
# IDE / Editors
########################################
# IDE / editor
.idea/
.vscode/
.vscode/
*.iml
########################################
# OS files
########################################
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,55 @@
#############################################
# 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=
# Port SSH du serveur distant
BACKUP_REMOTE_SSH_PORT=22
# Timeout SSH en secondes
SSH_CONNECT_TIMEOUT=10
# Validation stricte des clés hôtes SSH (yes/no)
BACKUP_KNOWN_HOSTS_STRICT=yes
# Fichier known_hosts utilisé par ssh/scp
BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
#############################################
# ROTATION DES BACKUPS
#############################################
# Nombre de jours de conservation des sauvegardes
# BACKUP_RETENTION_DAYS=10

356
BackupVaultWarden/README.md Normal file
View File

@@ -0,0 +1,356 @@
# README — Mise en place du script de sauvegarde Vaultwarden
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. 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`
- `jq`
Installation sur Debian / Ubuntu :
```bash
sudo apt update
sudo apt install -y tar openssh-client curl cron jq
````
---
# 3. Emplacement du script
Le script est situé dans :
```bash
/home/<USER>/Malio-ops/BackupVaultWarden/
```
Structure recommandée :
```bash
/home/<USER>/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
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
REMOTE_USER=<USER>
REMOTE_HOST=<IP_SERVEUR>
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
DATA_DIR=/opt/vaultwarden/data
LOCAL_BACKUP=/var/backups/vaultwarden
REMOTE_DIR=/home/backup/backups/vaultwarden
# BACKUP_REMOTE_SSH_PORT=22
# SSH_CONNECT_TIMEOUT=10
# BACKUP_KNOWN_HOSTS_STRICT=yes
# BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
# BACKUP_RETENTION_DAYS=10
```
## Description des variables
| Variable | Description |
| --------------------- | -------------------------------------------------------------------- |
| DISCORD_WEBHOOK_URL | Webhook Discord pour les notifications |
| REMOTE_USER | Utilisateur du serveur distant |
| REMOTE_HOST | Adresse IP ou DNS du serveur de sauvegarde |
| SSH_KEY | Chemin vers la clé SSH utilisée pour le transfert |
| DATA_DIR | Dossier `data` de Vaultwarden |
| LOCAL_BACKUP | Dossier local où stocker temporairement l'archive |
| REMOTE_DIR | Dossier de stockage des backups sur le serveur distant |
| BACKUP_REMOTE_SSH_PORT | Port SSH du serveur distant, optionnel, défaut `22` |
| SSH_CONNECT_TIMEOUT | Timeout SSH en secondes, optionnel, défaut `10` |
| BACKUP_KNOWN_HOSTS_STRICT | Validation stricte des hôtes SSH (`yes`/`no`) |
| BACKUP_KNOWN_HOSTS_FILE | Fichier `known_hosts` utilisé par `ssh`/`scp` |
| BACKUP_RETENTION_DAYS | Nombre de jours de conservation distante, optionnel, défaut `10` |
---
# 5. Chargement des variables dans le script
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_backup
```
#### 2. Copie de la clé vers le serveur distant
```bash
ssh-copy-id -i ~/.ssh/id_ed25519_backup.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_backup -o StrictHostKeyChecking=yes <USER>@<IP_SERVEUR>
```
#### 4. Vérification des fichiers de clé
```bash
ls ~/.ssh/id_ed25519_backup*
```
Fichiers attendus :
```
~/.ssh/id_ed25519_backup
~/.ssh/id_ed25519_backup.pub
```
#### 5. Permissions SSH
Machine locale :
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_backup
chmod 644 ~/.ssh/id_ed25519_backup.pub
```
Machine distante :
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
```
#### 6. Provisionnement de `known_hosts`
Le script est prévu pour fonctionner avec validation stricte des hôtes SSH.
```bash
ssh-keyscan -H <IP_SERVEUR> >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
```
#### 7. Déclaration dans `.env`
```bash
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
```
Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour transférer les sauvegardes.
# 7. Sauvegarde des données Vaultwarden
Le script crée une archive compressée du dossier `data` :
```bash
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
```
Cela permet dobtenir une sauvegarde portable et compressée.
---
# 8. Transfert vers le serveur distant
Une fois larchive créée :
```bash
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$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'etat de la sauvegarde.
Construction du message :
```bash
msg="**${ping}Backup Vaultwarden ${icon}**\n"
msg+="Backup: ${BACKUP_NAME}\n"
msg+="Data transfer: ${status_line}\n"
[[ -n "$details" ]] && msg+="Détails: ${details}"
```
Envoi du message :
```bash
payload="$(jq -n --arg content "$msg" '{content: $content}')"
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL"
```
Le message indique :
* si la sauvegarde a réussi
* si elle a échoué
* le nom du backup
* les détails de lerreur
---
# 10. Rotation distante des sauvegardes
Le script supprime les archives distantes plus anciennes que la durée de retention configurée.
Configuration dans `.env` :
```bash
# BACKUP_RETENTION_DAYS=10
```
Commande utilisée :
```bash
find "$REMOTE_DIR" -type f -name 'vaultwarden-backup-*.tar.gz' -mtime +$RETENTION_DAYS -delete
```
Si la variable n'est pas définie, le script utilise `10` jours par défaut.
---
# 11. Planification avec cron
Le script est exécuté automatiquement tous les jours à 19h.
Ouvrir le crontab :
```bash
crontab -e
```
Ajouter :
```bash
0 19 * * * /home/<USER>/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 sexécute donc **tous les jours à 19h00**.
---
# 12. Nettoyage
Une fois la sauvegarde transférée :
```bash
rm -f "$LOCAL_BACKUP_FILE"
```
Cela évite de remplir le disque de la machine Vaultwarden.
---
# 13. Test manuel
Avant de mettre le script en cron, tester :
```bash
bash /home/<USER>/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
```
---
# 14. Vérification des logs
Logs :
```bash
cat /var/log/vaultwarden_backup.log
```
---
# 15. 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

@@ -1,11 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
#######################################
# 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")"
@@ -27,31 +28,83 @@ log() {
# Chargement du .env
#######################################
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
#######################################
# Variables obligatoires
#######################################
: "${WEBHOOK_URL:=}"
: "${DISCORD_WEBHOOK_URL:=}"
: "${DATA_DIR:?Variable DATA_DIR manquante dans .env}"
: "${LOCAL_BACKUP:?Variable LOCAL_BACKUP manquante dans .env}"
: "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}"
: "${REMOTE_HOST:?Variable REMOTE_HOST manquante dans .env}"
: "${REMOTE_DIR:?Variable REMOTE_DIR manquante dans .env}"
: "${SSH_KEY:?Variable SSH_KEY manquante dans .env}"
: "${BACKUP_REMOTE_SSH_PORT:=22}"
: "${SSH_CONNECT_TIMEOUT:=10}"
: "${BACKUP_KNOWN_HOSTS_STRICT:=yes}"
: "${BACKUP_KNOWN_HOSTS_FILE:=${HOME}/.ssh/known_hosts}"
#######################################
# Variables backup
#######################################
DATE="$(date +'%Y-%m-%d_%H-%M-%S')"
BACKUP_NAME="vaultwarden-backup-${DATE}.tar.gz"
LOCAL_BACKUP_DIR="$LOCAL_BACKUP"
LOCAL_BACKUP_FILE="${LOCAL_BACKUP_DIR}/${BACKUP_NAME}"
BACKUP_PREFIX="vaultwarden-backup"
BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
LOCAL_BACKUP_FILE="${LOCAL_BACKUP}/${BACKUP_NAME}"
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
echo "ERROR: Variable BACKUP_REMOTE_SSH_PORT invalide dans .env" >&2
exit 1
}
mkdir -p "$LOCAL_BACKUP_DIR"
[[ "$SSH_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
echo "ERROR: Variable SSH_CONNECT_TIMEOUT invalide dans .env" >&2
exit 1
}
[[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || {
echo "ERROR: Variable BACKUP_RETENTION_DAYS invalide dans .env" >&2
exit 1
}
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
*)
echo "ERROR: Variable BACKUP_KNOWN_HOSTS_STRICT invalide dans .env" >&2
exit 1
;;
esac
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
touch "$BACKUP_KNOWN_HOSTS_FILE"
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
SSH_OPTS=(
-i "$SSH_KEY"
-p "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
)
SCP_OPTS=(
-i "$SSH_KEY"
-P "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
)
mkdir -p "$LOCAL_BACKUP"
#######################################
# Notification Discord
@@ -60,27 +113,28 @@ discord_ping() {
local success="$1"
local details="${2:-}"
[[ -z "$WEBHOOK_URL" ]] && return 0
[[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0
local icon status_line
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}"
python3 - <<PY | curl -fsS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL" >/dev/null || true
import json
print(json.dumps({"content": """$msg"""}))
PY
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
}
#######################################
@@ -93,11 +147,22 @@ fail() {
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
}
#######################################
# Vérifications préalables
#######################################
[[ -d "$DATA_DIR" ]] || fail "Le dossier source n'existe pas : $DATA_DIR"
[[ -f "$SSH_KEY" ]] || fail "La clé SSH est introuvable : $SSH_KEY"
[[ -r "$SSH_KEY" ]] || fail "La clé SSH est non lisible : $SSH_KEY"
[[ ! -L "$SSH_KEY" ]] || fail "La clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
chmod 600 "$SSH_KEY" || true
for cmd in tar ssh scp jq curl find; do
require_cmd "$cmd"
done
log "Début du backup Vaultwarden"
log "Source : $DATA_DIR"
@@ -110,6 +175,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
#######################################
@@ -119,9 +186,20 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
#######################################
# Envoi du backup
#######################################
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
log "Backup envoyé sur $REMOTE_HOST:$REMOTE_DIR"
#######################################
# 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
#######################################

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
Ce projet suit le format [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
et applique le versionnement semantique.
## [Unreleased]
## [1.0.0] - 2026-03-18
### Added
- Ajout des scripts legacy de sauvegarde PostgreSQL, de verification de statut applicatif et de supervision du stockage.
- Ajout du script de sauvegarde Vaultwarden.
- Ajout des scripts de reconstruction de bases PostgreSQL dans `RebuildBdd/`.
- Ajout du bootstrap cible, du precheck et de l'orchestration de reconstruction.
- Ajout du support d'utilisation via une interface web avec sorties JSON.
- Ajout des fichiers d'exemple de configuration pour les scripts et les cibles.
### Changed
- Reorganisation des fichiers et des dossiers du projet.
- Variabilisation des scripts via des fichiers `.env`.
- Ajout de la rotation des sauvegardes.
- Harmonisation progressive de la documentation et des exemples de configuration.
- Ajout du bit executable sur les scripts qui doivent etre lancables directement.
### Fixed
- Correctifs multiples sur les scripts legacy et sur le flux `RebuildBdd`.
- Corrections de chemins de configuration et de telechargement des dumps.
- Corrections de messages Discord, de revue de code et d'orthographe dans la documentation.
- Correctifs sur la preparation PostgreSQL, `scp`, `sudoers` et le reperage des environnements cibles.

12
CheckStorage/.env.exemple Normal file
View File

@@ -0,0 +1,12 @@
#############################################
# DISCORD
#############################################
# Webhook Discord pour notifications
DISCORD_WEBHOOK_URL=
# Mention Discord en cas d'alerte
DISCORD_PING=@here
# Seuil d'alerte en pourcentage d'utilisation
STORAGE_ALERT_LIMIT=70

View File

@@ -1,43 +1,51 @@
# Scripts de vérification de l'espace de stockage
# CheckStorage
Ce projet contient des scripts pour vérifier l'espace de stockage
Script de vérification de lespace disque sur Ubuntu Server, avec notification Discord optionnelle.
## Préambule
Ce script est conçu pour vérifier l'espace de stockage disponible sur un serveur et envoyer une alerte
La vérification de l'espace de stockage ce fait sur la partition racine.
La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cette valeur dans le script selon vos besoins.
## Fonctionnement
## Installation du script
Le script :
1. Clonez le dépôt GitHub :
```bash
git clone https://gitea.malio.fr/MALIO-DEV/Scripts-Serveur.git
```
2. Accédez au répertoire du projet :
3. ```bash
cd Scripts-Serveur/CheckStorage
```
## Utilisation du script
1. Donnez les permissions d'exécution au script :
```bash
chmod +x check_storage.sh
```
2. Exécutez le script pour vérifier l'espace de stockage :
```bash
./check_storage.sh
```
1. charge `.env`
2. lit lutilisation de la partition `/`
3. compare le taux doccupation au seuil configuré
4. envoie une alerte Discord si le seuil est dépassé
## Initialisé un cron pour exécuter le script régulièrement
1. Ouvrez le crontab pour l'édition :
```bash
crontab -e
```
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
```bash
50 7 * * * /chemin/vers/le/script/check_storage.sh
```
## 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.
## Pré-requis
Installation recommandée sur Ubuntu Server :
```bash
sudo apt update
sudo apt install -y coreutils gawk jq curl
```
`jq` et `curl` ne sont nécessaires que si `DISCORD_WEBHOOK_URL` est renseigné.
## Configuration
```bash
cp .env.exemple .env
chmod 600 .env
```
Variables disponibles :
- `DISCORD_WEBHOOK_URL` : webhook Discord, optionnel
- `DISCORD_PING` : mention en cas dalerte, optionnel, défaut `@here`
- `STORAGE_ALERT_LIMIT` : seuil dalerte en pourcentage, défaut `70`
## Utilisation
```bash
chmod 700 check-storage.sh
./check-storage.sh
```
## Cron
Exemple quotidien à `07:50` :
```bash
50 7 * * * /chemin/vers/CheckStorage/check-storage.sh
```

98
CheckStorage/check-storage.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
###############################################################################
# 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
###############################################################################
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: commande requise absente : $1" >&2
exit 1
}
}
# Limite maximale d'utilisation du disque en pourcentage
limit="${STORAGE_ALERT_LIMIT:-70}"
DISCORD_PING="${DISCORD_PING:-@here}"
[[ "$limit" =~ ^[0-9]+$ ]] || {
echo "ERROR: STORAGE_ALERT_LIMIT invalide" >&2
exit 1
}
(( limit >= 1 && limit <= 99 )) || {
echo "ERROR: STORAGE_ALERT_LIMIT doit être compris entre 1 et 99" >&2
exit 1
}
require_cmd df
require_cmd awk
if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
require_cmd jq
require_cmd curl
fi
###############################################################################
# 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="${DISCORD_PING}\n**CHECK STOCKAGE :red_circle:**\nLimite autorisée : ${limit}%\nUtilisation actuelle : ${usage}%\nEspace restant : ${free}%\nUtilisé / total : ${used_gb} GB / ${total_gb} GB\nDisponible : ${avail_gb} GB\nHeure : $(date)"
if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
payload="$(jq -n --arg content "$msgLimit" '{content: $content}')"
curl -fsS \
-H "Accept: application/json" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$payload" \
"$DISCORD_WEBHOOK_URL" >/dev/null || true
fi
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}"

View File

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

188
README.md
View File

@@ -1,7 +1,187 @@
# Scripts Serveur MALIO
# MALIO-OPS
Ce projet contient des scripts pour la gestion et la maintenance des serveurs de MALIO.
Ce dépôt centralise lensemble des **scripts dexploitation, de maintenance et dautomatisation** utilisés dans linfrastructure MALIO.
Il constitue une base unique de **versionnement, standardisation et industrialisation** des opérations techniques (backup, supervision, PostgreSQL, reconstruction de bases).
---
# Objectif
Le dépôt permet de :
* assurer le **versionnement et la traçabilité** des scripts
* garantir des **exécutions reproductibles et fiables**
* mutualiser les **bonnes pratiques dexploitation**
* centraliser la **configuration et la documentation technique**
* faciliter le **déploiement automatisé des environnements**
---
# Structure du dépôt
Organisation par domaine fonctionnel :
## CheckStorage
* Surveillance de lespace disque
* Alerting via Discord
* Vérification proactive des capacités
## BackupVaultWarden
* Sauvegarde des données Vaultwarden
* Archivage et transfert distant sécurisé
* Gestion des logs et rétention
## RecetteScripts
* Scripts legacy pour environnement de recette
* Backup PostgreSQL
* Monitoring applicatif
* Rebuild simplifié
⚠️ Dossier en cours de transition vers `RebuildBdd`
## RebuildBdd
* Nouvelle architecture standardisée de reconstruction PostgreSQL
* Gestion multi-cibles
* Exécution non interactive (compatible web/API)
* Retour structuré (JSON + logs)
---
# Focus : RebuildBdd
Ce module constitue le **socle principal de reconstruction de bases PostgreSQL**.
## Scripts principaux
* `run-rebuild-bdd.sh`
→ Point dentrée (local / IA / interface web)
* `rebuild-bdd-core.sh`
→ Logique métier de restauration (dump + rôles + base)
* `bootstrap-target-host.sh`
→ Préparation automatique de la machine cible
* `create-target-config.sh`
→ Génération des configurations par environnement
## Sous-dossiers
* `Checkup/`
→ Vérification des prérequis (PostgreSQL, accès, rôles)
* `Config/`
→ Fichiers de configuration globaux et par cible
## Fonctionnalités clés
* Installation automatique PostgreSQL si absent
* Vérification et création des rôles
* Restauration complète (rôles + base)
* Gestion des dumps distants
* Mode sécurisé (validation des paramètres)
* Logs exploitables + sortie JSON (intégration web)
👉 Documentation détaillée : `RebuildBdd/README.md`
---
# Prérequis
Les scripts nécessitent :
## Outils de base
* bash
* jq
* curl
* ssh
* scp
## Outils optionnels (selon usage)
* PostgreSQL (`psql`, `pg_dump`, `pg_restore`)
* tar
* systemd (gestion services)
---
# Configuration
## Configuration globale
Un modèle est fourni :
```bash
global.env.exemple
```
Utilisation :
```bash
cp global.env.exemple global.env
```
## Configuration locale
* Chaque module peut contenir son propre `.env`
* Les variables sensibles doivent être définies localement
⚠️ Règles strictes :
* **Aucun secret en versionné**
* Utiliser `.gitignore`
* Cloisonner les accès (SSH, DB, webhooks)
---
# Sécurité
* Authentification SSH par clé obligatoire
* Validation des paramètres en entrée (scripts rebuild)
* Isolation des environnements (cibles distinctes)
* Logs sans données sensibles
* Validation stricte des hôtes SSH recommandée et utilisée par défaut sur les scripts durcis
* Permissions restrictives recommandées sur les `.env`, clés privées et `known_hosts`
## Déploiement Ubuntu Server
Le dépôt est maintenant pensé prioritairement pour des cibles **Ubuntu Server** :
* bootstrap `RebuildBdd` basé sur `apt`, `systemctl` et `sudo -n`
* clients SSH en mode batch avec `StrictHostKeyChecking=yes` quand le mode strict est actif
* exemples `.env` mis à jour pour expliciter les ports SSH, `known_hosts` et les timeouts
---
# Bonnes pratiques
* Scripts **idempotents** (relançables sans effet de bord)
* Logs systématiques
* Gestion des erreurs (`set -euo pipefail`)
* Centralisation des configurations
* Utilisation de formats structurés (JSON)
---
# Documentation
* Documentation détaillée par module (README locaux)
* Historique des évolutions :
```bash
CHANGELOG.md
```
---
# Positionnement actuel
* `RebuildBdd` = **standard cible**
* `RecetteScripts` = **legacy en cours de migration**
* Objectif : convergence vers une **chaîne unique, robuste et automatisable (web/API)**
## Scripts disponibles
* [CheckStorage] : Script de vérification de l'espace de stockage

View 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."

View File

@@ -0,0 +1,345 @@
#!/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_FILE=/dev/stderr
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >>"$LOG_FILE"
print_stdout "$msg"
}
fail() {
log "ERROR: $*"
print_json_and_exit "error" "$*" 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
}
to_bool_yes_no() {
local v="${1:-}"
v="${v,,}"
case "$v" in
yes|y|oui|o|true|1) echo "yes" ;;
no|n|non|false|0|"") echo "no" ;;
*) return 1 ;;
esac
}
require_env_vars() {
local missing=()
local var
for var in \
ENV_NAME PGHOST PGPORT PGUSER PGPASSWORD DBS \
BACKUP_REMOTE_USER BACKUP_REMOTE_HOST BACKUP_REMOTE_DIR \
SSH_KEY BACKUP_LOG_DIR
do
[[ -n "${!var:-}" ]] || missing+=("$var")
done
if (( ${#missing[@]} > 0 )); then
fail "variables .env manquantes : ${missing[*]}"
fi
}
validate_env_values() {
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME invalide"
[[ -n "$DBS" ]] || fail "DBS vide"
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
[[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide"
[[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide"
[[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide"
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT" >/dev/null || fail "BACKUP_KNOWN_HOSTS_STRICT invalide"
}
prepare_log_file() {
mkdir -p "$BACKUP_LOG_DIR" || {
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
exit 1
}
local ts safe_request_id
ts="$(date '+%Y-%m-%d_%H-%M-%S')"
safe_request_id="${REQUEST_ID:-manual}"
safe_request_id="${safe_request_id//[^a-zA-Z0-9_.-]/_}"
LOG_FILE="${BACKUP_LOG_DIR}/check_target_${ENV_NAME,,}_${safe_request_id}_${ts}.log"
touch "$LOG_FILE" || {
echo '{"status":"error","message":"impossible de créer le fichier de log"}'
exit 1
}
}
prepare_local_paths() {
local restore_base
restore_base="${LOCAL_RESTORE_BASE_DIR:-${REPO_DIR}/restore_tmp}"
mkdir -p "$BACKUP_LOG_DIR" || fail "création BACKUP_LOG_DIR impossible"
[[ -w "$BACKUP_LOG_DIR" ]] || fail "BACKUP_LOG_DIR non inscriptible"
mkdir -p "$restore_base" || fail "création LOCAL_RESTORE_BASE_DIR impossible"
[[ -w "$restore_base" ]] || fail "LOCAL_RESTORE_BASE_DIR non inscriptible"
log "Dossiers locaux prêts."
}
prepare_scripts_permissions() {
local core_script check_pg_script
core_script="${REPO_DIR}/rebuild-bdd-core.sh"
check_pg_script="${REPO_DIR}/Checkup/check-postgresql.sh"
[[ -f "$core_script" ]] || fail "script core introuvable : $core_script"
[[ -f "$check_pg_script" ]] || fail "script PostgreSQL introuvable : $check_pg_script"
chmod 700 "$core_script" || fail "chmod impossible sur $core_script"
chmod 700 "$check_pg_script" || fail "chmod impossible sur $check_pg_script"
log "Permissions scripts corrigées."
}
prepare_ssh_key() {
local key_dir
key_dir="$(dirname "$SSH_KEY")"
mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir"
chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir"
[[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY"
chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée"
[[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub"
[[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique"
log "Clé SSH prête."
}
prepare_known_hosts() {
local ssh_dir known_hosts
ssh_dir="$(dirname "$SSH_KEY")"
known_hosts="${ssh_dir}/known_hosts"
touch "$known_hosts" || fail "impossible de créer known_hosts"
chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts"
if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then
if [[ "$(to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT")" == "yes" ]]; then
fail "hôte ${BACKUP_REMOTE_HOST} absent de known_hosts en mode strict ; provisionner l'empreinte manuellement"
fi
log "Ajout non strict de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts"
ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \
fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}"
else
log "Host déjà présent dans known_hosts."
fi
}
test_backup_ssh() {
local ssh_timeout
ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}"
ssh \
-i "$SSH_KEY" \
-p "$BACKUP_REMOTE_SSH_PORT" \
-o IdentitiesOnly=yes \
-o BatchMode=yes \
-o ConnectTimeout="$ssh_timeout" \
-o StrictHostKeyChecking=yes \
"${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" \
"test -d '$BACKUP_REMOTE_DIR'" \
>>"$LOG_FILE" 2>&1 || \
fail "connexion SSH backup impossible, clé non autorisée, ou dossier distant absent"
log "Connexion SSH backup validée."
}
install_sudoers_if_allowed() {
local auto_configure sudoers_file tmp_file
auto_configure="$(to_bool_yes_no "${AUTO_CONFIGURE_SUDOERS:-no}")" || fail "AUTO_CONFIGURE_SUDOERS invalide"
if [[ "$auto_configure" != "yes" ]]; then
log "Installation sudoers automatique désactivée."
return 0
fi
if ! sudo true >/dev/null 2>&1; then
fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo n'est pas disponible ; configuration initiale manuelle requise"
fi
require_cmd visudo
sudoers_file="/etc/sudoers.d/rebuild-bdd-${USER}"
tmp_file="$(mktemp)"
cat >"$tmp_file" <<EOF
${USER} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
${USER} ALL=(postgres) NOPASSWD: /usr/bin/psql
EOF
chmod 440 "$tmp_file"
visudo -cf "$tmp_file" >/dev/null 2>&1 || {
rm -f "$tmp_file"
fail "fichier sudoers généré invalide"
}
sudo install -m 440 "$tmp_file" "$sudoers_file" || {
rm -f "$tmp_file"
fail "impossible d'installer $sudoers_file"
}
rm -f "$tmp_file"
log "Fichier sudoers installé : $sudoers_file"
}
check_sudo_non_interactive() {
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || \
fail "sudo indisponible pour systemctl"
log "sudo pour systemctl validé."
if command -v apt >/dev/null 2>&1; then
sudo /usr/bin/apt --version >/dev/null 2>&1 || \
fail "sudo indisponible pour apt"
log "sudo pour apt validé."
elif command -v apt-get >/dev/null 2>&1; then
sudo /usr/bin/apt-get --version >/dev/null 2>&1 || \
fail "sudo indisponible pour apt-get"
log "sudo pour apt-get validé."
else
fail "ni apt ni apt-get disponibles sur la cible"
fi
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
fail "sudo -u postgres indisponible pour psql"
log "sudo -u postgres pour psql validé."
}
run_postgresql_check() {
local check_script
check_script="${REPO_DIR}/Checkup/check-postgresql.sh"
[[ -x "$check_script" ]] || fail "script introuvable ou non exécutable : $check_script"
"$check_script" \
--env-file "$ENV_FILE" \
--request-id "$REQUEST_ID" \
--non-interactive \
>>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL"
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
fail "sudo -u postgres indisponible après préparation PostgreSQL"
log "Préparation PostgreSQL validée."
}
[[ -f "$ENV_FILE" ]] || {
echo '{"status":"error","message":"fichier .env introuvable"}'
exit 1
}
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
require_env_vars
validate_env_values
prepare_log_file
require_cmd bash
require_cmd python3
require_cmd ssh
require_cmd ssh-keygen
require_cmd ssh-keyscan
require_cmd sudo
prepare_local_paths
prepare_scripts_permissions
prepare_ssh_key
prepare_known_hosts
test_backup_ssh
install_sudoers_if_allowed
check_sudo_non_interactive
run_postgresql_check
log "Machine cible prête pour le rebuild."
print_json_and_exit "success" "machine cible prête" 0

View File

@@ -0,0 +1,38 @@
###############################################################################
# config/global.env.example
###############################################################################
# Defaults d'exécution
ALLOW_OVERWRITE=no
RESTORE_ROLES=yes
# Dépôt scripts
GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git
GLOBAL_REPO_BRANCH=main
# Backup central
GLOBAL_BACKUP_REMOTE_USER=backup
GLOBAL_BACKUP_REMOTE_HOST=192.168.1.60
GLOBAL_BACKUP_REMOTE_PORT=22
GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups
# Clé SSH de lecture backup copiée sur les cibles
GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub
GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes
# Defaults PostgreSQL
GLOBAL_PGHOST=127.0.0.1
GLOBAL_PGPORT=5432
# Defaults scripts
GLOBAL_REMOTE_ROLES_DIR_NAME=user
GLOBAL_EXCLUDED_RESTORE_ROLES="postgres"
# Defaults bootstrap / cible
GLOBAL_ENABLE_BOOTSTRAP=yes
GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
GLOBAL_AUTO_INSTALL_POSTGRES=yes
GLOBAL_AUTO_CREATE_PGUSER=yes
GLOBAL_PGUSER_SUPERUSER=no
GLOBAL_AUTO_CONFIGURE_SUDOERS=no

View File

@@ -0,0 +1,42 @@
###############################################################################
# config/targets/prod.env.exemple
###############################################################################
# SSH bootstrap cible
TARGET_HOST=192.168.1.60
TARGET_PORT=22
TARGET_BOOTSTRAP_USER=backup_liot
TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_prod
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=PROD
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-prod
# 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

View File

@@ -0,0 +1,42 @@
###############################################################################
# config/targets/test.env.exemple
###############################################################################
# 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

582
RebuildBdd/README.md Normal file
View File

@@ -0,0 +1,582 @@
# RebuildBdd
Orchestration de reconstruction de bases PostgreSQL à partir de dumps distants, avec préparation automatique des machines cibles, exécution non interactive et intégration web.
---
## Objectif
Ce projet permet de :
- préparer automatiquement une machine cible neuve ou partiellement configurée ;
- déployer et mettre à jour les scripts sur la cible ;
- préparer PostgreSQL localement sur la cible ;
- récupérer le dernier dump disponible depuis un serveur de backup ;
- restaurer une base PostgreSQL de manière non interactive ;
- exposer un flux exploitable depuis une interface web via des retours JSON.
---
## Fonctionnement global
Le flux standard est le suivant :
1. **Création ou mise à jour de la configuration dune cible**
2. **Bootstrap initial de la cible**
3. **Précheck de préparation**
4. **Rebuild de la base**
En pratique :
- `create-target-config.sh` crée un fichier de configuration cible ;
- `bootstrap-target-host.sh` prépare la machine cible ;
- `Checkup/check-target-readiness.sh` valide lenvironnement ;
- `rebuild-bdd-core.sh` exécute la restauration ;
- `run-rebuild-bdd.sh` orchestre lensemble.
---
## Architecture
### Configuration
Le projet utilise deux niveaux de configuration :
#### 1. Configuration globale
Fichier :
```bash
Config/global.env
````
Contient les paramètres stables, par exemple :
* dépôt Git des scripts ;
* serveur de backup ;
* clé SSH de lecture backup ;
* valeurs par défaut PostgreSQL ;
* options globales de bootstrap.
#### 2. Configuration par cible
Fichiers :
```bash
Config/Targets/<nom_cible>.env
```
Chaque fichier cible contient :
* accès SSH bootstrap ;
* répertoires locaux de la cible ;
* paramètres PostgreSQL ;
* sous-répertoire backup associé ;
* options spécifiques à la cible.
---
## Arborescence recommandée
```bash
RebuildBdd/
├── bootstrap-target-host.sh
├── create-target-config.sh
├── run-rebuild-bdd.sh
├── rebuild-bdd-core.sh
├── Config/
│ ├── global.env
│ └── Targets/
│ ├── test.env
│ └── prod.env
└── Checkup/
├── check-postgresql.sh
└── check-target-readiness.sh
```
---
## Scripts
### `create-target-config.sh`
Crée ou met à jour un fichier cible dans :
```bash
Config/Targets/<cible>.env
```
Usage :
```bash
./create-target-config.sh \
--target test \
--host 192.168.1.50 \
--port 22 \
--bootstrap-user backup_liot \
--bootstrap-key /home/user/.ssh/id_ed25519_target_test \
--runtime-user backup_liot \
--repo-dir /home/backup_liot/RebuildBdd \
--env-name RECETTE \
--pguser backup_liot \
--pgpassword secret \
--dbs "sirh inventory ferme" \
--backup-subdir bdd-recette
```
---
### `bootstrap-target-host.sh`
Prépare une machine cible neuve ou quasi neuve :
* connexion SSH bootstrap ;
* installation des paquets minimum ;
* création des dossiers ;
* génération du `.env` cible ;
* copie de la clé SSH backup ;
* préparation de `known_hosts` ;
* installation éventuelle dun `sudoers.d` minimal ;
* synchronisation du dépôt ;
* exécution de `check-postgresql.sh`.
Usage :
```bash
./bootstrap-target-host.sh --target test
```
Mode JSON :
```bash
./bootstrap-target-host.sh --target test --json-only
```
---
### `Checkup/check-postgresql.sh`
Prépare PostgreSQL localement sur la cible :
* installation si absent ;
* démarrage du service ;
* test de disponibilité ;
* création du rôle PostgreSQL cible si nécessaire.
Ce script est prévu pour fonctionner en non interactif avec `sudo -n`.
---
### `Checkup/check-target-readiness.sh`
Valide la préparation complète de la cible :
* lecture du `.env` cible ;
* vérification des chemins ;
* permissions locales ;
* permissions SSH ;
* `known_hosts` ;
* accès SSH au serveur de backup ;
* exécution de `check-postgresql.sh`.
Mode JSON disponible pour usage web.
---
### `rebuild-bdd-core.sh`
Script métier de reconstruction :
* validation des paramètres ;
* connexion au serveur de backup ;
* récupération du dernier dump ;
* récupération éventuelle du fichier des rôles ;
* suppression/recréation de la base si autorisé ;
* restauration des rôles ;
* restauration du dump PostgreSQL ;
* retour JSON final.
---
### `run-rebuild-bdd.sh`
Script orchestrateur principal.
Il peut :
* lancer le bootstrap si activé pour la cible ;
* synchroniser le dépôt distant ;
* lancer le précheck ;
* exécuter le rebuild.
Usage :
```bash
./run-rebuild-bdd.sh \
--target test \
--db sirh \
--overwrite yes \
--restore-roles yes \
--request-id web_001 \
--non-interactive
```
---
## Prérequis
### Machine de lancement
Doit disposer de :
* `bash`
* `ssh`
* `scp`
* `git`
* `python3`
* `jq` si vous consommez les JSON côté tooling
### Machine cible
Le bootstrap suppose :
* accès SSH fonctionnel ;
* utilisateur bootstrap existant ;
* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial ;
* `known_hosts` correctement provisionné si le mode strict SSH est activé vers le serveur de backup.
### Serveur de backup
Doit :
* être joignable en SSH depuis la cible ;
* accepter la clé de lecture backup ;
* contenir les dumps dans larborescence attendue.
## Sécurité / déploiement
### Clés hôtes SSH
Si `GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes`, lempreinte du serveur de backup doit déjà être présente dans le `known_hosts` de la cible. Le bootstrap et les checks ne font plus dajout aveugle en mode strict.
### Répertoires de clone
Les scripts refusent maintenant les chemins de clone manifestement dangereux comme `/`, `/root`, `/home` ou `/home/<user>` pour éviter un `rm -rf` destructeur dû à une mauvaise configuration.
### Ubuntu Server
Le flux de bootstrap est pensé pour des cibles Ubuntu Server avec `apt`, `systemctl` et `sudo -n`.
---
## Structure des backups attendue
Exemple :
```bash
/home/malio-b/backups/
├── bdd-recette/
│ ├── sirh/
│ │ ├── sirh_2026-03-16_19-00-01.dump
│ ├── inventory/
│ ├── ferme/
│ └── user/
│ ├── user_2026-03-16_19-00-01.sql
```
Le script recherche :
* le dernier dump dans :
```bash
<BACKUP_REMOTE_DIR>/<db>/<db>_*.dump
```
* le dernier fichier rôles dans :
```bash
<BACKUP_REMOTE_DIR>/<REMOTE_ROLES_DIR_NAME>/user_*.sql
```
---
## Configuration
### 1. Créer la configuration globale
Copier :
```bash
Config/.env.exemple
```
vers :
```bash
Config/global.env
```
Renseigner ensuite :
* dépôt Git ;
* serveur de backup ;
* clé SSH backup ;
* defaults globaux.
---
### 2. Créer une cible
Deux possibilités.
#### A. À la main
Créer un fichier :
```bash
Config/Targets/test.env
```
à partir de :
```bash
Config/Targets/test.env.exemple
```
#### B. Via script
Utiliser :
```bash
./create-target-config.sh ...
```
---
## Exécution locale
### Bootstrap seul
```bash
./bootstrap-target-host.sh --target test
```
### Rebuild complet
```bash
./run-rebuild-bdd.sh \
--target test \
--db sirh \
--overwrite yes \
--restore-roles yes \
--non-interactive
```
---
## Intégration web
Linterface web ne doit envoyer que les paramètres métier de lexécution :
```json
{
"target": "test",
"db": "sirh",
"overwrite": "yes",
"restore_roles": "yes",
"request_id": "web_20260317_001"
}
```
Le backend transforme cela en commande :
```bash
./run-rebuild-bdd.sh \
--target test \
--db sirh \
--overwrite yes \
--restore-roles yes \
--request-id web_20260317_001 \
--non-interactive
```
### Important
Le web ne doit pas transmettre directement :
* les clés SSH ;
* les mots de passe PostgreSQL ;
* les paramètres bas niveau de la cible ;
* les chemins système sensibles.
Ces informations doivent être stockées dans la configuration serveur.
---
## Ajouter une nouvelle machine depuis le web
Le flux recommandé est :
1. créer ou mettre à jour `Config/Targets/<cible>.env`
2. lancer `bootstrap-target-host.sh --target <cible>`
3. lancer ensuite `run-rebuild-bdd.sh --target <cible> ...`
Le bouton web **“Ajouter une machine”** doit donc :
* créer la configuration cible ;
* déclencher le bootstrap ;
* vérifier le retour ;
* rendre ensuite la cible disponible pour les rebuilds.
---
## Sorties JSON
### Succès
Exemple :
```json
{
"status": "success",
"message": "restauration terminée avec succès",
"request_id": "web_001",
"environment": "RECETTE",
"database": "sirh",
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
"log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
}
```
### Erreur
Exemple :
```json
{
"status": "error",
"message": "la base existe déjà et overwrite n'est pas autorisé",
"request_id": "web_001",
"environment": "RECETTE",
"database": "sirh",
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
"log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
}
```
---
## Sécurité
### Recommandations minimales
* utiliser des clés SSH dédiées ;
* limiter la clé backup à la lecture seule ;
* restreindre les permissions des fichiers de config ;
* exécuter les scripts avec un utilisateur dédié ;
* ne pas exposer les secrets dans linterface web ;
* valider strictement toutes les entrées côté backend.
### `sudoers`
Le bootstrap peut installer un `sudoers.d` minimal pour lutilisateur runtime :
```sudoers
<user> ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
<user> ALL=(postgres) NOPASSWD: /usr/bin/psql
```
Adapter si dautres commandes doivent être autorisées.
---
## Logs
Les logs de rebuild sont stockés dans :
```bash
TARGET_BACKUP_LOG_DIR
```
Exemple :
```bash
/home/backup_liot/logs/rebuild_bdd/
```
Le chemin du log est renvoyé dans le JSON final.
---
## Limites connues
* le bootstrap initial nécessite un accès SSH bootstrap valide ;
* le bootstrap ne remplace pas une mauvaise architecture réseau ;
* les secrets doivent être gérés proprement par la couche web/backend ;
* des verrous dexécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus.
---
## Recommandations de validation
Avant mise en production, tester au minimum :
1. bootstrap dune machine neuve ;
2. rebuild complet dune base ;
3. refus si la base existe et `overwrite=no` ;
4. relance complète une seconde fois sur la même cible ;
5. accès backup invalide ;
6. PostgreSQL absent au départ ;
7. `sudo -n` indisponible.
---
## Commandes utiles
### Créer une cible
```bash
./create-target-config.sh \
--target test \
--host 192.168.1.50 \
--port 22 \
--bootstrap-user backup_liot \
--bootstrap-key /home/matteo/.ssh/id_ed25519_target_test \
--runtime-user backup_liot \
--repo-dir /home/backup_liot/RebuildBdd \
--env-name RECETTE \
--pguser backup_liot \
--pgpassword secret \
--dbs "sirh inventory ferme" \
--backup-subdir bdd-recette
```
### Bootstrap
```bash
./bootstrap-target-host.sh --target test
```
### Rebuild
```bash
./run-rebuild-bdd.sh \
--target test \
--db sirh \
--overwrite yes \
--restore-roles yes \
--non-interactive
```
---
## État du projet
Le projet permet désormais une utilisation :
* locale ;
* automatisée ;
* intégrée au web ;
avec préparation des cibles, exécution non interactive et retour JSON.
```

View File

@@ -0,0 +1,596 @@
#!/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
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
done
[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME manquante"
[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST/GLOBAL_PGHOST manquant"
[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT/GLOBAL_PGPORT manquant"
[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER manquante"
[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD manquante"
[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS manquante"
[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_USER/TARGET_BACKUP_REMOTE_USER manquant"
[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_HOST/TARGET_BACKUP_REMOTE_HOST manquant"
[[ -n "$GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_BASE_DIR manquant"
[[ -n "$TARGET_BACKUP_SUBDIR_VALUE" ]] || fail "TARGET_BACKUP_SUBDIR manquante"
[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR manquante"
TARGET_BACKUP_REMOTE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE%/}/${TARGET_BACKUP_SUBDIR_VALUE}"
[[ -n "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "GLOBAL_BACKUP_SSH_PRIVATE_KEY/TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY manquant"
[[ -f "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE"
[[ -r "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE"
if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then
[[ -f "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE"
[[ -r "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE"
fi
[[ "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" =~ ^[0-9]+$ ]] || fail "port backup invalide"
to_bool_yes_no "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE" >/dev/null || fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide"
to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || fail "TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO invalide"
ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")"
require_cmd ssh
require_cmd python3
SSH_OPTS=(
-i "$BOOTSTRAP_SSH_KEY"
-p "$BOOTSTRAP_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o StrictHostKeyChecking=yes
-o ConnectTimeout=8
)
REMOTE="${BOOTSTRAP_USER}@${BOOTSTRAP_HOST}"
log "Test de connexion SSH bootstrap vers ${REMOTE}:${BOOTSTRAP_PORT}"
ssh "${SSH_OPTS[@]}" "$REMOTE" "exit 0" >/dev/null 2>&1 \
|| fail "connexion SSH bootstrap impossible vers ${REMOTE}"
REMOTE_SETUP_CMD="
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
run_root() {
if [ \"\$(id -u)\" -eq 0 ]; then
\"\$@\"
return 0
fi
if command -v sudo >/dev/null 2>&1; then
sudo \"\$@\" || {
echo 'sudo indisponible pour le bootstrap' >&2
exit 1
}
return 0
fi
echo 'ni root ni sudo disponible pour le bootstrap' >&2
exit 1
}
if ! command -v apt-get >/dev/null 2>&1; then
echo 'apt-get absent sur la cible' >&2
exit 1
fi
run_root apt-get update
run_root apt-get install -y bash git python3 sudo curl openssh-client ca-certificates postgresql-client
mkdir -p $(shell_quote "$(dirname "$TARGET_CLONE_DIR")")
mkdir -p $(shell_quote "$(dirname "$TARGET_SCRIPT_DIR")")
mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")")
mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")")
chmod 700 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") || true
touch $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts")
chmod 644 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") || true
"
log "Installation du socle minimal sur la cible"
if [[ "$JSON_ONLY" == "yes" ]]; then
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" >/dev/null \
|| fail "échec de préparation système distante"
else
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \
|| fail "échec de préparation système distante"
fi
TMP_ENV_FILE="$(mktemp)"
cat >"$TMP_ENV_FILE" <<EOF
ENV_NAME=$(shell_quote "$TARGET_ENV_NAME_VALUE")
PGHOST=$(shell_quote "$TARGET_PGHOST_VALUE")
PGPORT=$(shell_quote "$TARGET_PGPORT_VALUE")
PGUSER=$(shell_quote "$TARGET_PGUSER_VALUE")
PGPASSWORD=$(shell_quote "$TARGET_PGPASSWORD_VALUE")
DBS=$(shell_quote "$TARGET_DBS_VALUE")
BACKUP_REMOTE_USER=$(shell_quote "$TARGET_BACKUP_REMOTE_USER_VALUE")
BACKUP_REMOTE_HOST=$(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE")
BACKUP_REMOTE_DIR=$(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
BACKUP_REMOTE_SSH_PORT=$(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE")
BACKUP_LOG_DIR=$(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
LOCAL_RESTORE_BASE_DIR=$(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
REMOTE_ROLES_DIR_NAME=$(shell_quote "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE")
SSH_KEY=$(shell_quote "$TARGET_SSH_KEY_VALUE")
BACKUP_KNOWN_HOSTS_STRICT=$(shell_quote "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE")
AUTO_INSTALL_POSTGRES=$(shell_quote "$TARGET_AUTO_INSTALL_POSTGRES_VALUE")
AUTO_CREATE_PGUSER=$(shell_quote "$TARGET_AUTO_CREATE_PGUSER_VALUE")
PGUSER_SUPERUSER=$(shell_quote "$TARGET_PGUSER_SUPERUSER_VALUE")
AUTO_CONFIGURE_SUDOERS=$(shell_quote "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE")
EXCLUDED_RESTORE_ROLES=$(shell_quote "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE")
EOF
log "Copie du .env cible"
copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600"
REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")"
REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts"
log "Copie de la clé privée backup sur la cible"
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600"
if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then
log "Copie de la clé publique backup sur la cible"
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644"
fi
REMOTE_SSH_PERMS_CMD="
set -euo pipefail
chmod 700 $(shell_quote "$REMOTE_SSH_DIR")
chmod 600 $(shell_quote "$TARGET_SSH_KEY_VALUE")
if [[ -f $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") ]]; then
chmod 644 $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub")
fi
touch $(shell_quote "$REMOTE_KNOWN_HOSTS")
chmod 644 $(shell_quote "$REMOTE_KNOWN_HOSTS")
"
log "Correction des permissions SSH côté cible"
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SSH_PERMS_CMD" \
|| fail "échec de correction des permissions SSH sur la cible"
REMOTE_KNOWN_HOSTS_CMD="
set -euo pipefail
if ! command -v ssh-keyscan >/dev/null 2>&1; then
echo 'ssh-keyscan absent sur la cible' >&2
exit 1
fi
if ! ssh-keygen -F $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") -f $(shell_quote "$REMOTE_KNOWN_HOSTS") >/dev/null 2>&1; then
if [[ $(shell_quote "$STRICT_OPTION") == yes ]]; then
echo 'hôte backup absent de known_hosts en mode strict ; empreinte à provisionner manuellement' >&2
exit 1
fi
ssh-keyscan -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") -H $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") >> $(shell_quote "$REMOTE_KNOWN_HOSTS") 2>/dev/null
fi
"
log "Ajout du serveur de backup dans known_hosts côté cible"
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_KNOWN_HOSTS_CMD" \
|| fail "échec de préparation known_hosts sur la cible"
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
if [[ $(shell_quote "$TARGET_CLONE_DIR") == / || $(shell_quote "$TARGET_CLONE_DIR") == /root || $(shell_quote "$TARGET_CLONE_DIR") == /home || $(shell_quote "$TARGET_CLONE_DIR") =~ ^/home/[^/]+$ ]]; then
echo 'TARGET_CLONE_DIR dangereux refusé' >&2
exit 1
fi
rm -rf $(shell_quote "$TARGET_CLONE_DIR")
git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_CLONE_DIR")
else
git -C $(shell_quote "$TARGET_CLONE_DIR") fetch --prune origin
git -C $(shell_quote "$TARGET_CLONE_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH")
git -C $(shell_quote "$TARGET_CLONE_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH")
fi
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") 2>/dev/null || true
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") 2>/dev/null || true
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true
for required_file in \
$(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") \
$(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") \
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") \
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh"); do
if [[ ! -f \"\$required_file\" ]]; then
echo \"fichier requis absent après synchronisation du dépôt : \$required_file\" >&2
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2
exit 1
fi
done
"
log "Clone / mise à jour du dépôt distant"
if [[ "$JSON_ONLY" == "yes" ]]; then
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" >/dev/null \
|| fail "échec de synchronisation du dépôt sur la cible"
else
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \
|| fail "échec de synchronisation du dépôt sur la cible"
fi
REMOTE_VALIDATE_SUDO_ROOT_CMD="
set -euo pipefail
command -v sudo >/dev/null 2>&1 || {
echo 'sudo absent sur la cible' >&2
exit 1
}
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || {
echo 'sudo indisponible pour systemctl' >&2
exit 1
}
"
log "Validation initiale de sudo"
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \
|| fail "sudo invalide sur la cible"
REMOTE_RUN_CHECK_PG_CMD="
set -euo pipefail
CHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-postgresql.sh")
ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH")
[[ -f \"\$CHECK_SCRIPT\" ]] || {
echo \"script PostgreSQL introuvable : \$CHECK_SCRIPT\" >&2
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR") et TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR")\" >&2
exit 1
}
[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\"
\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive
"
log "Préparation PostgreSQL via check-postgresql.sh"
if [[ "$JSON_ONLY" == "yes" ]]; then
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" >/dev/null \
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
else
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
fi
REMOTE_VALIDATE_SUDO_POSTGRES_CMD="
set -euo pipefail
sudo -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || {
echo 'sudo -u postgres indisponible après préparation PostgreSQL' >&2
exit 1
}
"
log "Validation finale de sudo -u postgres"
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \
|| fail "sudo -u postgres invalide sur la cible"
success "bootstrap initial terminé pour ${TARGET_NAME}"

View File

@@ -0,0 +1,160 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${SCRIPT_DIR}/Config"
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
TARGET=""
HOST=""
PORT="22"
BOOTSTRAP_USER=""
BOOTSTRAP_SSH_KEY=""
RUNTIME_USER=""
REPO_DIR=""
ENV_FILE=""
ENV_NAME=""
PGHOST=""
PGPORT=""
PGUSER=""
PGPASSWORD=""
DBS=""
BACKUP_SUBDIR=""
BACKUP_LOG_DIR=""
LOCAL_RESTORE_BASE_DIR=""
SSH_KEY_TARGET_PATH=""
ENABLE_BOOTSTRAP="yes"
ALLOW_PASSWORDLESS_SUDO="yes"
AUTO_INSTALL_POSTGRES="yes"
AUTO_CREATE_PGUSER="yes"
PGUSER_SUPERUSER="no"
AUTO_CONFIGURE_SUDOERS="no"
REMOTE_ROLES_DIR_NAME="user"
EXCLUDED_RESTORE_ROLES="postgres"
FORCE="no"
while [[ $# -gt 0 ]]; do
case "$1" in
--targets-dir) TARGETS_DIR="$2"; shift 2 ;;
--target) TARGET="$2"; shift 2 ;;
--host) HOST="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--bootstrap-user) BOOTSTRAP_USER="$2"; shift 2 ;;
--bootstrap-key) BOOTSTRAP_SSH_KEY="$2"; shift 2 ;;
--runtime-user) RUNTIME_USER="$2"; shift 2 ;;
--repo-dir) REPO_DIR="$2"; shift 2 ;;
--env-file) ENV_FILE="$2"; shift 2 ;;
--env-name) ENV_NAME="$2"; shift 2 ;;
--pghost) PGHOST="$2"; shift 2 ;;
--pgport) PGPORT="$2"; shift 2 ;;
--pguser) PGUSER="$2"; shift 2 ;;
--pgpassword) PGPASSWORD="$2"; shift 2 ;;
--dbs) DBS="$2"; shift 2 ;;
--backup-subdir) BACKUP_SUBDIR="$2"; shift 2 ;;
--backup-log-dir) BACKUP_LOG_DIR="$2"; shift 2 ;;
--local-restore-base-dir) LOCAL_RESTORE_BASE_DIR="$2"; shift 2 ;;
--ssh-key-target-path) SSH_KEY_TARGET_PATH="$2"; shift 2 ;;
--enable-bootstrap) ENABLE_BOOTSTRAP="$2"; shift 2 ;;
--allow-passwordless-sudo) ALLOW_PASSWORDLESS_SUDO="$2"; shift 2 ;;
--auto-install-postgres) AUTO_INSTALL_POSTGRES="$2"; shift 2 ;;
--auto-create-pguser) AUTO_CREATE_PGUSER="$2"; shift 2 ;;
--pguser-superuser) PGUSER_SUPERUSER="$2"; shift 2 ;;
--auto-configure-sudoers) AUTO_CONFIGURE_SUDOERS="$2"; shift 2 ;;
--remote-roles-dir-name) REMOTE_ROLES_DIR_NAME="$2"; shift 2 ;;
--excluded-restore-roles) EXCLUDED_RESTORE_ROLES="$2"; shift 2 ;;
--force) FORCE="yes"; shift ;;
*) echo "Argument inconnu : $1" >&2; exit 1 ;;
esac
done
fail() {
echo "ERROR: $*" >&2
exit 1
}
to_bool_yes_no() {
local v="${1:-}"
v="${v,,}"
case "$v" in
yes|y|oui|o|true|1) echo "yes" ;;
no|n|non|false|0|"") echo "no" ;;
*) return 1 ;;
esac
}
[[ -n "$TARGET" ]] || fail "--target manquant"
[[ "$TARGET" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "target invalide"
[[ -n "$HOST" ]] || fail "--host manquant"
[[ -n "$BOOTSTRAP_USER" ]] || fail "--bootstrap-user manquant"
[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "--bootstrap-key manquant"
[[ -n "$REPO_DIR" ]] || fail "--repo-dir manquant"
[[ -n "$ENV_NAME" ]] || fail "--env-name manquant"
[[ -n "$PGUSER" ]] || fail "--pguser manquant"
[[ -n "$PGPASSWORD" ]] || fail "--pgpassword manquant"
[[ -n "$DBS" ]] || fail "--dbs manquant"
[[ -n "$BACKUP_SUBDIR" ]] || fail "--backup-subdir manquant"
[[ "$PORT" =~ ^[0-9]+$ ]] || fail "--port invalide"
[[ -n "$RUNTIME_USER" ]] || RUNTIME_USER="$BOOTSTRAP_USER"
[[ -n "$ENV_FILE" ]] || ENV_FILE="${REPO_DIR}/.env"
[[ -n "$PGHOST" ]] || PGHOST="127.0.0.1"
[[ -n "$PGPORT" ]] || PGPORT="5432"
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "--pgport invalide"
[[ -n "$BACKUP_LOG_DIR" ]] || BACKUP_LOG_DIR="/home/${RUNTIME_USER}/logs/rebuild_bdd"
[[ -n "$LOCAL_RESTORE_BASE_DIR" ]] || LOCAL_RESTORE_BASE_DIR="${REPO_DIR}/restore_tmp"
[[ -n "$SSH_KEY_TARGET_PATH" ]] || SSH_KEY_TARGET_PATH="/home/${RUNTIME_USER}/.ssh/id_ed25519_backup_readonly"
ENABLE_BOOTSTRAP="$(to_bool_yes_no "$ENABLE_BOOTSTRAP")" || fail "--enable-bootstrap invalide"
ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$ALLOW_PASSWORDLESS_SUDO")" || fail "--allow-passwordless-sudo invalide"
AUTO_INSTALL_POSTGRES="$(to_bool_yes_no "$AUTO_INSTALL_POSTGRES")" || fail "--auto-install-postgres invalide"
AUTO_CREATE_PGUSER="$(to_bool_yes_no "$AUTO_CREATE_PGUSER")" || fail "--auto-create-pguser invalide"
PGUSER_SUPERUSER="$(to_bool_yes_no "$PGUSER_SUPERUSER")" || fail "--pguser-superuser invalide"
AUTO_CONFIGURE_SUDOERS="$(to_bool_yes_no "$AUTO_CONFIGURE_SUDOERS")" || fail "--auto-configure-sudoers invalide"
mkdir -p "$TARGETS_DIR" || fail "impossible de créer $TARGETS_DIR"
TARGET_FILE="${TARGETS_DIR}/${TARGET}.env"
if [[ -f "$TARGET_FILE" && "$FORCE" != "yes" ]]; then
fail "fichier déjà existant : $TARGET_FILE (utiliser --force pour écraser)"
fi
cat >"$TARGET_FILE" <<EOF
TARGET_HOST=$(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}"

504
RebuildBdd/rebuild-bdd-core.sh Executable file
View File

@@ -0,0 +1,504 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
CLI_DB=""
CLI_OVERWRITE=""
CLI_RESTORE_ROLES=""
CLI_REQUEST_ID=""
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
JSON_ONLY="${JSON_ONLY:-no}"
while [[ $# -gt 0 ]]; do
case "$1" in
--env-file)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
ENV_FILE="$2"
shift 2
;;
--db)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
CLI_DB="$2"
shift 2
;;
--overwrite)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
CLI_OVERWRITE="$2"
shift 2
;;
--restore-roles)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
CLI_RESTORE_ROLES="$2"
shift 2
;;
--request-id)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
CLI_REQUEST_ID="$2"
shift 2
;;
--non-interactive)
NON_INTERACTIVE="yes"
shift
;;
--json-only)
JSON_ONLY="yes"
shift
;;
*)
echo "Argument inconnu : $1" >&2
exit 1
;;
esac
done
json_escape() {
python3 - <<'PY' "$1"
import json, sys
print(json.dumps(sys.argv[1]))
PY
}
print_json_and_exit() {
local status="$1"
local message="$2"
local exit_code="$3"
printf '{'
printf '"status":%s,' "$(json_escape "$status")"
printf '"message":%s,' "$(json_escape "$message")"
printf '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
printf '"database":%s,' "$(json_escape "${DB:-}")"
printf '"dump_file":%s,' "$(json_escape "${LAST_REMOTE_DB_DUMP:-}")"
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
printf '}\n'
exit "$exit_code"
}
print_stdout() {
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
}
LOG_FILE=/dev/stderr
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >>"$LOG_FILE"
print_stdout "$msg"
}
fail() {
log "ERROR: $*"
print_json_and_exit "error" "$*" 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1
}
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"
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME invalide"
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
mkdir -p "$BACKUP_LOG_DIR" || {
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
exit 1
}
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
SAFE_REQUEST_ID="${REQUEST_ID:-manual}"
SAFE_REQUEST_ID="${SAFE_REQUEST_ID//[^a-zA-Z0-9_.-]/_}"
LOG_FILE="${BACKUP_LOG_DIR}/restore_${ENV_NAME,,}_${SAFE_REQUEST_ID}_${TIMESTAMP}.log"
touch "$LOG_FILE" || {
echo '{"status":"error","message":"impossible de créer le log"}'
exit 1
}
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_BASE_DIR}/${SAFE_REQUEST_ID}_${TIMESTAMP}"
mkdir -p "$LOCAL_RESTORE_DIR" || fail "impossible de créer le dossier temporaire local"
EXCLUDED_ROLES_REGEX=""
if EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex "$EXCLUDED_RESTORE_ROLES")"; then
log "Rôles exclus de la restauration : $EXCLUDED_RESTORE_ROLES"
else
log "Aucun rôle exclu de la restauration."
fi
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename curl; do
require_cmd "$cmd" || 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

440
RebuildBdd/run-rebuild-bdd.sh Executable file
View File

@@ -0,0 +1,440 @@
#!/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
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
done
TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide"
BOOTSTRAP_SCRIPT_LOCAL="${SCRIPT_DIR}/bootstrap-target-host.sh"
[[ -f "$BOOTSTRAP_SCRIPT_LOCAL" ]] || fail "script bootstrap introuvable : $BOOTSTRAP_SCRIPT_LOCAL"
[[ -x "$BOOTSTRAP_SCRIPT_LOCAL" ]] || chmod 700 "$BOOTSTRAP_SCRIPT_LOCAL" || fail "chmod impossible sur $BOOTSTRAP_SCRIPT_LOCAL"
if [[ -z "$REQUESTED_DB" ]]; then
DBS_FOR_TARGET="${TARGET_DBS:-}"
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
fail "REQUESTED_DB manquante en mode non interactif"
fi
read -r -a DBS_ARRAY <<< "$DBS_FOR_TARGET"
[[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "TARGET_DBS vide"
if is_tty; then
echo "Bases disponibles :"
for i in "${!DBS_ARRAY[@]}"; do
echo " $((i + 1))) ${DBS_ARRAY[$i]}"
done
echo
read -r -p "Nom exact de la base à restaurer : " REQUESTED_DB
else
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
fi
fi
[[ "$REQUESTED_DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide"
if [[ "$TARGET_ENABLE_BOOTSTRAP" == "yes" ]]; then
log "Bootstrap initial activé pour la cible ${TARGET}"
BOOTSTRAP_JSON="/tmp/bootstrap_target_${REQUEST_ID}.json"
"$BOOTSTRAP_SCRIPT_LOCAL" \
--global-env-file "$GLOBAL_ENV_FILE" \
--targets-dir "$TARGETS_DIR" \
--target "$TARGET" \
--json-only >"$BOOTSTRAP_JSON" || {
cat "$BOOTSTRAP_JSON" 2>/dev/null || true
fail "échec du bootstrap initial de la cible ${TARGET}"
}
BOOTSTRAP_STATUS="$(
python3 - <<'PY' "$BOOTSTRAP_JSON"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
print(data.get("status", "error"))
PY
)"
if [[ "$BOOTSTRAP_STATUS" != "success" ]]; then
cat "$BOOTSTRAP_JSON"
fail "bootstrap initial échoué pour la cible ${TARGET}"
fi
log "Bootstrap initial terminé pour ${TARGET}"
else
log "Bootstrap initial désactivé pour ${TARGET}"
fi
SSH_OPTS=(
-i "$TARGET_SSH_KEY"
-p "$TARGET_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o StrictHostKeyChecking=yes
-o ConnectTimeout=8
)
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \
|| fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}"
TARGET_CORE_SCRIPT="${TARGET_SCRIPT_DIR}/rebuild-bdd-core.sh"
REMOTE_RESULT_JSON="/tmp/run_rebuild_bdd_${REQUEST_ID}.json"
REMOTE_BOOTSTRAP_CMD="
set -euo pipefail
CLONE_DIR=$(shell_quote "$TARGET_CLONE_DIR")
REPO_DIR=$(shell_quote "$TARGET_SCRIPT_DIR")
REPO_URL=$(shell_quote "$TARGET_REPO_URL")
REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")
CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT")
PRECHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-target-readiness.sh")
TARGET_ENV_FILE=$(shell_quote "$TARGET_ENV_FILE")
REQUESTED_DB=$(shell_quote "$REQUESTED_DB")
ALLOW_OVERWRITE=$(shell_quote "$ALLOW_OVERWRITE")
RESTORE_ROLES=$(shell_quote "$RESTORE_ROLES")
REQUEST_ID=$(shell_quote "$REQUEST_ID")
command -v git >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"git absent sur la cible\"}'; exit 1; }
command -v bash >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"bash absent sur la cible\"}'; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"python3 absent sur la cible\"}'; exit 1; }
mkdir -p \"\$(dirname \"\$CLONE_DIR\")\"
mkdir -p \"\$(dirname \"\$REPO_DIR\")\"
if [[ ! -d \"\$CLONE_DIR/.git\" ]]; then
if [[ \"\$CLONE_DIR\" == / || \"\$CLONE_DIR\" == /root || \"\$CLONE_DIR\" == /home || \"\$CLONE_DIR\" =~ ^/home/[^/]+$ ]]; then
echo '{\"status\":\"error\",\"message\":\"TARGET_CLONE_DIR dangereux refusé\"}'
exit 1
fi
rm -rf \"\$CLONE_DIR\"
git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_DIR\" >/dev/null 2>&1
else
git -C \"\$CLONE_DIR\" fetch --prune origin >/dev/null 2>&1
git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" >/dev/null 2>&1
git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" >/dev/null 2>&1
fi
[[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; }
[[ -f \"\$PRECHECK_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script précheck introuvable sur la cible\"}'; exit 1; }
chmod 700 \"\$CORE_SCRIPT\"
chmod 700 \"\$PRECHECK_SCRIPT\"
PRECHECK_JSON=\"/tmp/check_target_\${REQUEST_ID}.json\"
PRECHECK_STDERR=\"/tmp/check_target_\${REQUEST_ID}.stderr\"
CORE_JSON=\"/tmp/rebuild_target_\${REQUEST_ID}.json\"
CORE_STDERR=\"/tmp/rebuild_target_\${REQUEST_ID}.stderr\"
\"\$PRECHECK_SCRIPT\" \
--env-file \"\$TARGET_ENV_FILE\" \
--request-id \"\$REQUEST_ID\" \
--non-interactive \
--json-only >\"\$PRECHECK_JSON\" 2>\"\$PRECHECK_STDERR\" || {
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
}
PRECHECK_STATUS=\"\$(python3 - <<'PY' \"\$PRECHECK_JSON\"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
print(data.get('status', 'error'))
PY
)\" || {
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
}
if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
cat \"\$PRECHECK_JSON\"
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
fi
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\"
\"\$CORE_SCRIPT\" \
--env-file \"\$TARGET_ENV_FILE\" \
--db \"\$REQUESTED_DB\" \
--overwrite \"\$ALLOW_OVERWRITE\" \
--restore-roles \"\$RESTORE_ROLES\" \
--request-id \"\$REQUEST_ID\" \
--non-interactive \
--json-only >\"\$CORE_JSON\" 2>\"\$CORE_STDERR\" || {
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
cat \"\$CORE_JSON\" 2>/dev/null || true
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
}
CORE_STATUS=\"\$(python3 - <<'PY' \"\$CORE_JSON\"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
print(data.get('status', 'error'))
PY
)\" || {
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
cat \"\$CORE_JSON\" 2>/dev/null || true
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
}
if [[ \"\$CORE_STATUS\" != \"success\" ]]; then
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
cat \"\$CORE_JSON\"
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
exit 1
fi
cat \"\$CORE_JSON\"
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
"
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" 2>&1 \
|| {
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
fail "échec d'exécution distante sur la cible ${TARGET}"
}
REMOTE_STATUS="$(
python3 - <<'PY' "$REMOTE_RESULT_JSON"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
print(data.get("status", "error"))
PY
)" || {
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
fail "réponse JSON invalide renvoyée par la cible ${TARGET}"
}
if [[ "$REMOTE_STATUS" != "success" ]]; then
cat "$REMOTE_RESULT_JSON"
fail "restauration distante échouée pour la cible ${TARGET}"
fi
python3 - <<'PY' "$REMOTE_RESULT_JSON"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
message = data.get("message", "restauration terminée")
environment = data.get("environment") or "N/A"
database = data.get("database") or "N/A"
request_id = data.get("request_id") or "N/A"
dump_file = data.get("dump_file") or "N/A"
log_file = data.get("log_file") or "N/A"
print(f"[{request_id}] {message}")
print(f"Environnement : {environment}")
print(f"Base : {database}")
print(f"Dump : {dump_file}")
print(f"Log : {log_file}")
PY

390
RecetteScripts/README.md Normal file
View File

@@ -0,0 +1,390 @@
# RecetteScripts
Scripts Bash permettant dautomatiser la gestion dun environnement **PostgreSQL de recette**.
Ces scripts permettent :
* la **sauvegarde automatisée des bases**
* la **surveillance de la disponibilité des applications**
* la **reconstruction dune base à partir dun 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 dun 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 sur Ubuntu Server :
```
postgresql-client
curl
jq
openssh-client
```
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 -o StrictHostKeyChecking=yes backup@192.168.1.50
```
La connexion doit fonctionner **sans demander de mot de passe**.
Provisionner aussi `known_hosts` avant le premier run :
```bash
ssh-keyscan -H 192.168.1.50 >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
```
---
### Sécuriser les permissions
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 dexemple** :
```
backup.env.exemple
check-statut.env.exemple
rebuild.env.exemple
```
Pour utiliser les scripts :
```
cp backup.env.exemple .env
```
Puis modifier les variables.
Variables SSH supplémentaires désormais supportées :
```
BACKUP_REMOTE_SSH_PORT
BACKUP_KNOWN_HOSTS_STRICT
BACKUP_KNOWN_HOSTS_FILE
```
---
# 5. Script : backup-bdd-recette.sh
## 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 lexé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
```
Le script utilise maintenant des options SSH strictes et refuse les clés privées symboliques.
---
## Exécution
```
./backup-bdd-recette.sh
```
---
# 6. Script : check-statut-recette.sh
## 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 dun 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
Le script supporte désormais le port SSH distant, un fichier `known_hosts` dédié et lexclusion configurable des rôles via `EXCLUDED_RESTORE_ROLES`.
---
## 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
---

View File

@@ -0,0 +1,544 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
###############################################################################
# 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 lensemble 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 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 ;
# * 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"
validate_db_name() {
local db_name="$1"
[[ -n "$db_name" ]] || {
echo "ERROR: nom de base vide dans DBS" >&2
exit 1
}
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || {
echo "ERROR: nom de base invalide dans DBS : $db_name" >&2
exit 1
}
}
for DB in "${DBS_ARRAY[@]}"; do
validate_db_name "$DB"
done
IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
RETENTION_DAYS=10
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
BACKUP_KNOWN_HOSTS_FILE="${BACKUP_KNOWN_HOSTS_FILE:-${HOME}/.ssh/known_hosts}"
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
echo "ERROR: BACKUP_REMOTE_SSH_PORT invalide" >&2
exit 1
}
[[ "$PGPORT" =~ ^[0-9]+$ ]] || {
echo "ERROR: PGPORT invalide" >&2
exit 1
}
[[ "$SSH_TIMEOUT" =~ ^[0-9]+$ ]] || {
echo "ERROR: SSH_TIMEOUT invalide" >&2
exit 1
}
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
echo "ERROR: PGUSER invalide" >&2
exit 1
}
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
*)
echo "ERROR: BACKUP_KNOWN_HOSTS_STRICT invalide" >&2
exit 1
;;
esac
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
touch "$BACKUP_KNOWN_HOSTS_FILE"
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
SSH_OPTS=(
-i "$SSH_KEY"
-p "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="${SSH_TIMEOUT}"
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
)
SCP_OPTS=(
-i "$SSH_KEY"
-P "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="${SSH_TIMEOUT}"
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
)
LOG_DIR="${BACKUP_LOG_DIR}"
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="$(mktemp -d /tmp/pg_dump_XXXXXX)" || {
echo "ERROR: impossible de créer le dossier temporaire" >&2
exit 1
}
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
}
safe_remove_dir() {
local dir="${1:-}"
[[ -n "$dir" ]] || return 0
[[ "$dir" == /tmp/pg_dump_* ]] || {
log "WARNING: suppression refusée pour le chemin inattendu : $dir"
return 1
}
rm -rf -- "$dir"
}
export PGPASSWORD
#######################################
# Vérification dépendances minimales
#######################################
for cmd in ssh scp curl jq pg_dump pg_dumpall mktemp; do
require_cmd "$cmd" || {
echo "ERROR: commande manquante : $cmd" >&2
exit 1
}
done
[[ -f "$SSH_KEY" ]] || {
echo "ERROR: clé SSH introuvable : $SSH_KEY" >&2
exit 1
}
[[ -r "$SSH_KEY" ]] || {
echo "ERROR: clé SSH non lisible : $SSH_KEY" >&2
exit 1
}
[[ ! -L "$SSH_KEY" ]] || {
echo "ERROR: la clé SSH ne doit pas être un lien symbolique : $SSH_KEY" >&2
exit 1
}
chmod 600 "$SSH_KEY" || true
#######################################
# Configuration Discord
#######################################
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 dexé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
cleanup() {
rm -rf -- "$LOCK_DIR"
safe_remove_dir "$TMP_DIR" || true
}
trap cleanup EXIT
#######################################
# Préparation du dossier distant
#######################################
log "Creating remote directories"
MKDIR_CMD="mkdir -p '${BACKUP_REMOTE_DIR}/user'"
for DB in "${DBS_ARRAY[@]}"; do
MKDIR_CMD+=" '${BACKUP_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 "${SCP_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${BACKUP_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 "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_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 '${BACKUP_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 '${BACKUP_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
#######################################
safe_remove_dir "$TMP_DIR" || true
#######################################
# 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

View File

@@ -0,0 +1,74 @@
###############################################################################
# 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
# Port SSH du serveur de backup
BACKUP_REMOTE_SSH_PORT=22
# Timeout de connexion SSH (secondes)
SSH_TIMEOUT=10
# Validation stricte des clés hôtes SSH (yes/no)
BACKUP_KNOWN_HOSTS_STRICT=yes
# Fichier known_hosts utilisé par ssh/scp
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
###############################################################################
# LOGS
###############################################################################
# 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

View File

@@ -0,0 +1,222 @@
#!/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() {
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 "$@"

View 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

View File

@@ -0,0 +1,501 @@
#!/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}"
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
###############################################################################
# Préparation des dossiers locaux
###############################################################################
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
}
sql_escape_literal() {
local s="${1:-}"
s="${s//\'/\'\'}"
printf "%s" "$s"
}
validate_db_name() {
local db_name="$1"
[[ -n "$db_name" ]] || fail "nom de base vide"
[[ "$db_name" =~ ^[A-Za-z0-9_]+$ ]] || \
fail "nom de base invalide : seuls les lettres, chiffres et underscores sont autorisés"
}
build_excluded_roles_regex() {
local role regex=""
for role in $EXCLUDED_RESTORE_ROLES; do
[[ -z "$role" ]] && continue
[[ "$role" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || fail "rôle exclu invalide : ${role}"
if [[ -n "$regex" ]]; then
regex+="|"
fi
regex+="$role"
done
printf '%s' "$regex"
}
###############################################################################
# Envoi Discord
#
# Envoi simple d'un message texte via webhook Discord.
# Si WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
###############################################################################
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"
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
export PGPASSWORD
SSH_OPTS=(
-i "$SSH_KEY"
-p "$BACKUP_REMOTE_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
-o StrictHostKeyChecking=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}"
###############################################################################
# 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 '$(sql_escape_literal "$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
fi
validate_db_name "$DB"
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 "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du dump principal"
###############################################################################
# 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 "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du fichier des rôles"
else
log "La restauration des rôles sera ignorée."
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='$(sql_escape_literal "$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")"
EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex)"
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
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='$(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 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"

View File

@@ -0,0 +1,92 @@
###############################################################################
# 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
# Port SSH du serveur de backup
BACKUP_REMOTE_SSH_PORT=22
###############################################################################
# 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
# Validation stricte des clés hôtes SSH (yes/no)
BACKUP_KNOWN_HOSTS_STRICT=yes
# Fichier known_hosts utilisé par ssh/scp
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
###############################################################################
# LOGS
###############################################################################
# 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
# Rôles PostgreSQL à exclure lors de la restauration
EXCLUDED_RESTORE_ROLES="postgres"
###############################################################################
# DISCORD
###############################################################################
# Webhook Discord pour notifier le succès de la restauration
DISCORD_WEBHOOK_URL=

View File

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

View File

@@ -1,6 +0,0 @@
DATA_DIR=
LOCAL_BACKUP=
REMOTE_USER=
REMOTE_HOST=
REMOTE_DIR=
SSH_KEY=

View File

@@ -1,3 +0,0 @@
.env
backup.log

View File

@@ -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:
- damé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 lerreur 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 danalyser 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 davoir une sauvegarde quotidienne fiable et surveillée.

View File

@@ -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
View 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/<USER>/.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