Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b223f514 | |||
| a9e492962c | |||
| 3bad5bad82 | |||
| 9af65f7739 | |||
| 11f69a9eda | |||
| e68c99a8b3 | |||
| 7b91691ef8 | |||
| 7261823806 | |||
| fac2a5b47f | |||
| 3c91c3b5c1 | |||
| f3ebb4c011 | |||
| 83032ef5ab | |||
| 863fee91a9 | |||
| 0dddecd08f | |||
| 66abdfca53 | |||
| 26101f2112 | |||
| 0ee0c1328a | |||
| f6e66e7bff | |||
| 41df83fe32 | |||
| 3faf8ab71d | |||
| 685a65e2d1 | |||
| 6a99a8115f | |||
| f12c937b39 | |||
| cb94e74414 | |||
| 5b128bc81a | |||
| 6c61f6e543 | |||
| 447b04ce20 | |||
| 29371b6529 | |||
| b3de87a452 | |||
| 0ff3244c1d | |||
| 12bbe6b1d9 | |||
| fbefe3fb03 | |||
| 38b29796d3 | |||
| 01ac392fa9 | |||
| e5b15426a1 | |||
| 7974491e93 | |||
| b76b6613bf | |||
| 122f53f804 | |||
| 741fef225b | |||
| 8ef81add14 | |||
| a1fb6f5504 | |||
| 0d4ffd9391 | |||
|
|
94537de551 | ||
|
|
2971ef0ff9 | ||
|
|
f0dfd6acb1 | ||
|
|
858cad8269 | ||
| fb1aaac418 | |||
| d7cb9b34c4 | |||
| 643a0d9ec7 | |||
| df77d8be21 | |||
| 038ddfe242 | |||
|
|
a1ace94094 | ||
| 210594b008 | |||
|
|
e221e82108 | ||
|
|
fabc9be4d4 | ||
|
|
9d4a5050e9 | ||
| 5729d0d484 | |||
|
|
89b1229efb | ||
|
|
f9b1d1da24 | ||
|
|
049574ffeb | ||
| c257270982 | |||
|
|
f72328e0ce | ||
|
|
29eff11b23 | ||
|
|
99072361c5 | ||
| 623424343e | |||
|
|
066ede6000 | ||
| 30df5ca8d6 | |||
|
|
4b76e88853 | ||
|
|
99f8694250 | ||
| 7f18e2f2e9 | |||
|
|
d0ceea8bad | ||
| dd226592db | |||
|
|
e81b953ac2 | ||
|
|
c80a74adc5 | ||
|
|
97eeffd9ea | ||
|
|
14359b111f | ||
| e860fd0f16 | |||
|
|
fbbb68af88 | ||
|
|
c2d1b716e0 |
@@ -1 +0,0 @@
|
|||||||
WEBHOOK_URL=
|
|
||||||
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,9 +1,40 @@
|
|||||||
# Secrets / environment
|
########################################
|
||||||
.env
|
# Environment / secrets
|
||||||
.env.*
|
########################################
|
||||||
!.env.example
|
|
||||||
!.env.exemple
|
.env
|
||||||
|
!.env.exemple
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Logs
|
||||||
|
########################################
|
||||||
|
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
var/log/
|
||||||
|
backup.log
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Temporary / system files
|
||||||
|
########################################
|
||||||
|
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# IDE / Editors
|
||||||
|
########################################
|
||||||
|
|
||||||
# IDE / editor
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# OS files
|
||||||
|
########################################
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
55
BackupVaultWarden/.env.exemple
Normal file
55
BackupVaultWarden/.env.exemple
Normal 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
|
||||||
387
BackupVaultWarden/README.md
Normal file
387
BackupVaultWarden/README.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# README — Mise en place du script de sauvegarde Vaultwarden
|
||||||
|
|
||||||
|
Ce script permet d’automatiser la sauvegarde de Vaultwarden afin de conserver une copie du dossier `data`, de la transférer vers un serveur distant et d’envoyer une notification Discord en cas de succès ou d’échec.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Objectif du script
|
||||||
|
|
||||||
|
Le script de sauvegarde Vaultwarden permet de :
|
||||||
|
|
||||||
|
- sauvegarder les données de Vaultwarden ;
|
||||||
|
- compresser l’archive avec un nom daté ;
|
||||||
|
- transférer la sauvegarde vers un serveur distant ;
|
||||||
|
- envoyer une notification Discord ;
|
||||||
|
- automatiser l’exécution via `cron`.
|
||||||
|
|
||||||
|
Ce mécanisme permet de sécuriser les mots de passe, les utilisateurs et la configuration stockés dans le dossier `data` de Vaultwarden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Pré-requis
|
||||||
|
|
||||||
|
Avant de mettre en place le script, vérifier que les éléments suivants sont disponibles sur la machine Vaultwarden :
|
||||||
|
|
||||||
|
- `bash`
|
||||||
|
- `tar`
|
||||||
|
- `scp`
|
||||||
|
- `ssh`
|
||||||
|
- `curl`
|
||||||
|
- `cron`
|
||||||
|
- `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
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">
|
||||||
|
<strong>EggMaster</strong>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Question 2</summary>
|
||||||
|
|
||||||
|
Quel format minimal faut-il donner a `printf` pour afficher une chaine brute ?
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Indice commande 2</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
'%s'
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Fragment 2</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
xlIHBldGl0IHN0YWdpYWlyZSBtYXR0ZW8gZHVu
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Le script charge directement le fichier `.env` avec `source` et exporte automatiquement les variables pendant le chargement.
|
||||||
|
|
||||||
|
Mécanisme utilisé :
|
||||||
|
|
||||||
|
```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 d’obtenir une sauvegarde portable et compressée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Transfert vers le serveur distant
|
||||||
|
|
||||||
|
Une fois l’archive 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 l’erreur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Signification :
|
||||||
|
|
||||||
|
| Champ | Valeur |
|
||||||
|
| ------------ | ------ |
|
||||||
|
| minute | 0 |
|
||||||
|
| heure | 19 |
|
||||||
|
| jour du mois | * |
|
||||||
|
| mois | * |
|
||||||
|
| jour semaine | * |
|
||||||
|
|
||||||
|
Le script s’exécute donc **tous les jours à 19h00**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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
|
||||||
|
* l’exécution automatique via cron
|
||||||
|
* la configuration sécurisée via `.env`
|
||||||
|
|
||||||
|
Ce système permet d’obtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
|
||||||
|
|
||||||
234
BackupVaultWarden/backup-vaultwarden.sh
Executable file
234
BackupVaultWarden/backup-vaultwarden.sh
Executable file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Chemins fixes du script
|
||||||
|
#######################################
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
LOG_FILE="/var/log/vaultwarden_backup.log"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
touch "$LOG_FILE"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Vérification fichier .env
|
||||||
|
#######################################
|
||||||
|
[[ -f "$ENV_FILE" ]] || {
|
||||||
|
echo "ERROR: Fichier .env introuvable : $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Chargement du .env
|
||||||
|
#######################################
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Variables obligatoires
|
||||||
|
#######################################
|
||||||
|
: "${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}"
|
||||||
|
[[ "$REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||||
|
echo "ERROR: Variable REMOTE_DIR invalide dans .env" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
: "${SSH_KEY:?Variable SSH_KEY manquante dans .env}"
|
||||||
|
: "${BACKUP_REMOTE_SSH_PORT:=22}"
|
||||||
|
: "${SSH_CONNECT_TIMEOUT:=10}"
|
||||||
|
: "${BACKUP_KNOWN_HOSTS_STRICT:=yes}"
|
||||||
|
: "${BACKUP_KNOWN_HOSTS_FILE:=${HOME}/.ssh/known_hosts}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Variables backup
|
||||||
|
#######################################
|
||||||
|
DATE="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||||
|
BACKUP_PREFIX="vaultwarden-backup"
|
||||||
|
BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
|
||||||
|
LOCAL_BACKUP_FILE="${LOCAL_BACKUP}/${BACKUP_NAME}"
|
||||||
|
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
|
||||||
|
|
||||||
|
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: Variable BACKUP_REMOTE_SSH_PORT invalide dans .env" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$SSH_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: Variable SSH_CONNECT_TIMEOUT invalide dans .env" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: Variable BACKUP_RETENTION_DAYS invalide dans .env" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||||
|
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||||
|
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Variable BACKUP_KNOWN_HOSTS_STRICT invalide dans .env" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||||
|
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||||
|
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||||
|
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||||
|
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||||
|
)
|
||||||
|
SCP_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||||
|
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_BACKUP"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Notification Discord
|
||||||
|
#######################################
|
||||||
|
send_discord() {
|
||||||
|
local success="$1"
|
||||||
|
local details="${2:-}"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0
|
||||||
|
require_cmd jq || return 0
|
||||||
|
require_cmd curl || return 0
|
||||||
|
|
||||||
|
local icon status_line
|
||||||
|
if [[ "$success" == "true" ]]; then
|
||||||
|
icon="🟢"
|
||||||
|
status_line="✅"
|
||||||
|
ping=""
|
||||||
|
else
|
||||||
|
icon="🔴"
|
||||||
|
status_line="❌"
|
||||||
|
ping="@here "
|
||||||
|
fi
|
||||||
|
|
||||||
|
local msg
|
||||||
|
msg="**${ping}Backup Vaultwarden ${icon}**\n"
|
||||||
|
msg+="Backup: ${BACKUP_NAME}\n"
|
||||||
|
msg+="Data transfer: ${status_line}\n"
|
||||||
|
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||||
|
|
||||||
|
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||||
|
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Fonction erreur
|
||||||
|
#######################################
|
||||||
|
fail() {
|
||||||
|
local detail="$1"
|
||||||
|
log "ERROR: $detail"
|
||||||
|
send_discord "false" "$detail"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Verrou d'execution
|
||||||
|
#######################################
|
||||||
|
LOCK_DIR="/tmp/vaultwarden_backup.lock.d"
|
||||||
|
|
||||||
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||||
|
fail "Backup deja en cours"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "${LOCAL_BACKUP_FILE:-}"
|
||||||
|
rm -rf -- "$LOCK_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Vérifications préalables
|
||||||
|
#######################################
|
||||||
|
[[ -d "$DATA_DIR" ]] || fail "Le dossier source n'existe pas : $DATA_DIR"
|
||||||
|
[[ -f "$SSH_KEY" ]] || fail "La clé SSH est introuvable : $SSH_KEY"
|
||||||
|
[[ -r "$SSH_KEY" ]] || fail "La clé SSH est non lisible : $SSH_KEY"
|
||||||
|
[[ ! -L "$SSH_KEY" ]] || fail "La clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||||
|
chmod 600 "$SSH_KEY" || true
|
||||||
|
|
||||||
|
for cmd in tar ssh scp jq curl find; do
|
||||||
|
require_cmd "$cmd"
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Début du backup Vaultwarden"
|
||||||
|
log "Source : $DATA_DIR"
|
||||||
|
log "Archive locale : $LOCAL_BACKUP_FILE"
|
||||||
|
log "Destination distante : ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Création du backup
|
||||||
|
#######################################
|
||||||
|
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
|
||||||
|
#######################################
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
|
||||||
|
|| fail "Impossible de créer le dossier distant $REMOTE_DIR"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Envoi du backup
|
||||||
|
#######################################
|
||||||
|
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
|
||||||
|
#######################################
|
||||||
|
rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOCAL_BACKUP_FILE"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Fin
|
||||||
|
#######################################
|
||||||
|
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||||
|
send_discord "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
||||||
|
echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||||
43
CHANGELOG.md
Normal file
43
CHANGELOG.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Ce projet suit le format [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
|
||||||
|
et applique le versionnement semantique.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Harmonisation des fonctions d'envoi Discord dans les scripts de sauvegarde, de supervision et de reconstruction.
|
||||||
|
- Ajout d'un fichier de log dedie a l'orchestrateur `RebuildBdd/run-rebuild-bdd.sh`.
|
||||||
|
- Renforcement de la journalisation et de la gestion des erreurs dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Nettoyage du backup local temporaire dans `BackupVaultWarden/backup-vaultwarden.sh` en sortie de script.
|
||||||
|
- Gestion plus robuste des dependances shell et des verifications de disponibilite PostgreSQL dans les scripts `RebuildBdd`.
|
||||||
|
- Acceptation explicite du flag `--non-interactive` dans `RebuildBdd/Checkup/check-target-readiness.sh` pour compatibilite de workflow.
|
||||||
|
- Validation des noms de base et refus des cles SSH symboliques dans les scripts de reconstruction.
|
||||||
|
- Suppression des notifications Discord fragiles quand `jq` ou `curl` sont absents, avec envoi silencieux en secours.
|
||||||
|
- Detection et nettoyage des verrous perimes dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||||
|
- Filtrage des privileges `SUPERUSER` lors de la restauration des roles dans `RecetteScripts/rebuild-bdd-recette.sh`.
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Ajout des scripts legacy de sauvegarde PostgreSQL, de verification de statut applicatif et de supervision du stockage.
|
||||||
|
- Ajout du script de sauvegarde Vaultwarden.
|
||||||
|
- Ajout des scripts de reconstruction de bases PostgreSQL dans `RebuildBdd/`.
|
||||||
|
- Ajout du bootstrap cible, du precheck et de l'orchestration de reconstruction.
|
||||||
|
- Ajout du support d'utilisation via une interface web avec sorties JSON.
|
||||||
|
- Ajout des fichiers d'exemple de configuration pour les scripts et les cibles.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Reorganisation des fichiers et des dossiers du projet.
|
||||||
|
- Variabilisation des scripts via des fichiers `.env`.
|
||||||
|
- Ajout de la rotation des sauvegardes.
|
||||||
|
- Harmonisation progressive de la documentation et des exemples de configuration.
|
||||||
|
- Ajout du bit executable sur les scripts qui doivent etre lancables directement.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Correctifs multiples sur les scripts legacy et sur le flux `RebuildBdd`.
|
||||||
|
- Corrections de chemins de configuration et de telechargement des dumps.
|
||||||
|
- Corrections de messages Discord, de revue de code et d'orthographe dans la documentation.
|
||||||
|
- Correctifs sur la preparation PostgreSQL, `scp`, `sudoers` et le reperage des environnements cibles.
|
||||||
12
CheckStorage/.env.exemple
Normal file
12
CheckStorage/.env.exemple
Normal 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
|
||||||
@@ -1,43 +1,82 @@
|
|||||||
# Scripts de vérification de l'espace de stockage
|
# CheckStorage
|
||||||
|
|
||||||
Ce projet contient des scripts pour vérifier l'espace de stockage
|
Script de vérification de l’espace disque sur Ubuntu Server, avec notification Discord optionnelle.
|
||||||
|
|
||||||
## Préambule
|
## Fonctionnement
|
||||||
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.
|
|
||||||
|
|
||||||
## Installation du script
|
Le script :
|
||||||
|
|
||||||
1. Clonez le dépôt GitHub :
|
1. charge `.env`
|
||||||
```bash
|
2. lit l’utilisation de la partition `/`
|
||||||
git clone https://gitea.malio.fr/MALIO-DEV/Scripts-Serveur.git
|
3. compare le taux d’occupation au seuil configuré
|
||||||
```
|
4. envoie une alerte Discord si le seuil est dépassé
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Initialisé un cron pour exécuter le script régulièrement
|
<details>
|
||||||
1. Ouvrez le crontab pour l'édition :
|
<summary style="list-style: none; cursor: pointer;">
|
||||||
```bash
|
<strong>EggMaster</strong>
|
||||||
crontab -e
|
</summary>
|
||||||
```
|
|
||||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
|
<details>
|
||||||
```bash
|
<summary style="list-style: none; cursor: pointer;">Question 3</summary>
|
||||||
50 7 * * * /chemin/vers/le/script/check_storage.sh
|
|
||||||
```
|
Quel operateur shell permet d'envoyer la sortie d'une commande vers la suivante ?
|
||||||
|
|
||||||
## Avertissement
|
</details>
|
||||||
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.
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Indice commande 3</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Fragment 3</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
b3llciB2b2ljaSB1biBsaWVuIG1hZ2lxdWUgZW
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
## 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 d’alerte, optionnel, défaut `@here`
|
||||||
|
- `STORAGE_ALERT_LIMIT` : seuil d’alerte en pourcentage, défaut `70`
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 700 check-storage.sh
|
||||||
|
./check-storage.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cron
|
||||||
|
|
||||||
|
Exemple quotidien à `07:50` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
50 7 * * * /chemin/vers/CheckStorage/check-storage.sh
|
||||||
|
```
|
||||||
|
|||||||
104
CheckStorage/check-storage.sh
Executable file
104
CheckStorage/check-storage.sh
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
send_discord() {
|
||||||
|
local message="$1"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -n "${DISCORD_WEBHOOK_URL:-}" ]] || return 0
|
||||||
|
|
||||||
|
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||||
|
|
||||||
|
curl -fsS \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RÉCUPÉRATION DES INFORMATIONS DISQUE
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
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)"
|
||||||
|
|
||||||
|
send_discord "$msgLimit"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# AFFICHAGE DES INFORMATIONS STOCKAGE
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
echo "Espace disponible : ${avail_gb} GB"
|
||||||
|
echo "Espace utilise / espace total : ${used_gb} GB / ${total_gb} GB"
|
||||||
|
echo "Name: ${HOSTNAME}"
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
limit=70
|
|
||||||
# Mettre le lien de votre webhook Discord dans un .env
|
|
||||||
WEBHOOK_URL=$(grep -E '^WEBHOOK_URL=' .env | cut -d '=' -f2-)
|
|
||||||
|
|
||||||
# Récupérer l'utilisation du disque en pourcentage
|
|
||||||
usage=$(df -h / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
|
|
||||||
# Calculer l'espace libre en pourcentage
|
|
||||||
free=$((100 - usage))
|
|
||||||
|
|
||||||
# Si l'utilisation dépasse la limite, envoyer une alerte sur Discord
|
|
||||||
if [ "$usage" -ge "$limit" ]; then
|
|
||||||
msgLimit="@here\n**CHECK STOCKAGE :red_circle:**\nLimite autorisé : ${limit}% \nUtilisation actuelle: ${usage}%\nEspace restant: ${free}%\nHeure: $(date)"
|
|
||||||
curl -X POST \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json; charset=utf-8" \
|
|
||||||
-d "{\"content\":\"$msgLimit\"}" \
|
|
||||||
"$WEBHOOK_URL"
|
|
||||||
# Log de l'alerte
|
|
||||||
echo "ALERTE >> ${usage}% d'utilisation, check fait le $(date)"
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
fi
|
|
||||||
243
README.md
243
README.md
@@ -1,7 +1,242 @@
|
|||||||
# 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 l’ensemble des **scripts d’exploitation, de maintenance et d’automatisation** utilisés dans l’infrastructure MALIO.
|
||||||
|
Il constitue une base unique de **versionnement, standardisation et industrialisation** des opérations techniques (backup, supervision, PostgreSQL, reconstruction de bases).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Objectif
|
||||||
|
|
||||||
|
Le 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 d’exploitation**
|
||||||
|
* 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 l’espace 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 d’entrée (local / IA / interface web)
|
||||||
|
|
||||||
|
* `rebuild-bdd-core.sh`
|
||||||
|
→ Logique métier de restauration (dump + rôles + base)
|
||||||
|
|
||||||
|
* `bootstrap-target-host.sh`
|
||||||
|
→ Préparation automatique de la machine cible
|
||||||
|
|
||||||
|
* `create-target-config.sh`
|
||||||
|
→ Génération des configurations par environnement
|
||||||
|
|
||||||
|
## Sous-dossiers
|
||||||
|
|
||||||
|
* `Checkup/`
|
||||||
|
→ Vérification des prérequis (PostgreSQL, accès, rôles)
|
||||||
|
|
||||||
|
* `Config/`
|
||||||
|
→ Fichiers de configuration globaux et par cible
|
||||||
|
|
||||||
|
## Fonctionnalités clés
|
||||||
|
|
||||||
|
* Installation automatique PostgreSQL si absent
|
||||||
|
* Vérification et création des rôles
|
||||||
|
* Restauration complète (rôles + base)
|
||||||
|
* Gestion des dumps distants
|
||||||
|
* Mode sécurisé (validation des paramètres)
|
||||||
|
* Logs exploitables + sortie JSON (intégration web)
|
||||||
|
|
||||||
|
👉 Documentation détaillée : `RebuildBdd/README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prérequis
|
||||||
|
|
||||||
|
Les scripts nécessitent :
|
||||||
|
|
||||||
|
## Outils de base
|
||||||
|
|
||||||
|
* bash
|
||||||
|
* jq
|
||||||
|
* curl
|
||||||
|
* ssh
|
||||||
|
* scp
|
||||||
|
|
||||||
|
## Outils optionnels (selon usage)
|
||||||
|
|
||||||
|
* PostgreSQL (`psql`, `pg_dump`, `pg_restore`)
|
||||||
|
* tar
|
||||||
|
* systemd (gestion services)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Configuration globale
|
||||||
|
|
||||||
|
Un modèle est fourni :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
global.env.exemple
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce fichier concerne la configuration legacy de `RecetteScripts`.
|
||||||
|
|
||||||
|
Utilisation :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp global.env.exemple global.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour la configuration de `RebuildBdd`, voir la documentation dédiée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RebuildBdd/README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration locale
|
||||||
|
|
||||||
|
* Chaque module peut contenir son propre `.env`
|
||||||
|
* Les variables sensibles doivent être définies localement
|
||||||
|
|
||||||
|
⚠️ Règles strictes :
|
||||||
|
|
||||||
|
* **Aucun secret en versionné**
|
||||||
|
* Utiliser `.gitignore`
|
||||||
|
* Cloisonner les accès (SSH, DB, webhooks)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sécurité
|
||||||
|
|
||||||
|
* Authentification SSH par clé obligatoire
|
||||||
|
* Validation des paramètres en entrée (scripts rebuild)
|
||||||
|
* Isolation des environnements (cibles distinctes)
|
||||||
|
* Logs sans données sensibles
|
||||||
|
* Validation stricte des hôtes SSH recommandée et utilisée par défaut sur les scripts durcis
|
||||||
|
* Permissions restrictives recommandées sur les `.env`, clés privées et `known_hosts`
|
||||||
|
|
||||||
|
## Déploiement Ubuntu Server
|
||||||
|
|
||||||
|
Le dépôt est maintenant pensé prioritairement pour des cibles **Ubuntu Server** :
|
||||||
|
|
||||||
|
* bootstrap `RebuildBdd` basé sur `apt`, `systemctl` et `sudo -n`
|
||||||
|
* clients SSH en mode batch avec `StrictHostKeyChecking=yes` quand le mode strict est actif
|
||||||
|
* exemples `.env` mis à jour pour expliciter les ports SSH, `known_hosts` et les timeouts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bonnes pratiques
|
||||||
|
|
||||||
|
* Scripts **idempotents** (relançables sans effet de bord)
|
||||||
|
* Logs systématiques
|
||||||
|
* Gestion des erreurs (`set -euo pipefail`)
|
||||||
|
* Centralisation des configurations
|
||||||
|
* Utilisation de formats structurés (JSON)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
|
||||||
|
* Documentation détaillée par module (README locaux)
|
||||||
|
* Historique des évolutions :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CHANGELOG.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Positionnement actuel
|
||||||
|
|
||||||
|
* `RebuildBdd` = **standard cible**
|
||||||
|
* `RecetteScripts` = **legacy en cours de migration**
|
||||||
|
* Objectif : convergence vers une **chaîne unique, robuste et automatisable (web/API)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">
|
||||||
|
<strong>EggMaster</strong>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
Un message est disperse dans les `README` du depot.
|
||||||
|
|
||||||
|
Ordre de reconstruction :
|
||||||
|
|
||||||
|
1. `README.md`
|
||||||
|
2. `BackupVaultWarden/README.md`
|
||||||
|
3. `CheckStorage/README.md`
|
||||||
|
4. `RebuildBdd/README.md`
|
||||||
|
5. `RecetteScripts/README.md`
|
||||||
|
|
||||||
|
La commande de dechiffrement n'est pas donnee directement.
|
||||||
|
Elle se reconstruit aussi via des questions cachees dans les `README`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Question 1</summary>
|
||||||
|
|
||||||
|
Quelle commande shell permet d'afficher exactement une chaine, sans interpretation particuliere, avant de la transmettre a une autre commande ?
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Indice commande 1</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
printf
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Fragment 1</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
YmllbiB2dSB0dSBtJ2FzIHRyb3V2ZXIgbW9pIG
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Scripts disponibles
|
|
||||||
* [CheckStorage] : Script de vérification de l'espace de stockage
|
|
||||||
226
RebuildBdd/Checkup/check-postgresql.sh
Executable file
226
RebuildBdd/Checkup/check-postgresql.sh
Executable file
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
DEFAULT_ENV_FILE="${REPO_DIR}/.env"
|
||||||
|
|
||||||
|
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||||
|
CLI_REQUEST_ID=""
|
||||||
|
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--env-file)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
|
||||||
|
ENV_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--request-id)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||||
|
CLI_REQUEST_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
NON_INTERACTIVE="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu : $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
has_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
postgres_server_ready() {
|
||||||
|
has_cmd postgres || return 1
|
||||||
|
has_cmd pg_ctlcluster || return 1
|
||||||
|
has_cmd pg_lsclusters || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_postgres_cluster() {
|
||||||
|
if ! has_cmd pg_lsclusters || ! has_cmd pg_createcluster; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pg_lsclusters --no-header 2>/dev/null | grep -q .; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version=""
|
||||||
|
if [[ -d /etc/postgresql ]]; then
|
||||||
|
version="$(find /etc/postgresql -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | LC_ALL=C sort -V | tail -n 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$version" ]] && has_cmd psql; then
|
||||||
|
version="$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$version" ]] || return 1
|
||||||
|
|
||||||
|
log "Aucun cluster PostgreSQL détecté, création de ${version}/main..."
|
||||||
|
"$SUDO_BIN" pg_createcluster "$version" main --start >/dev/null 2>&1 || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_postgres_diagnostics() {
|
||||||
|
local diagnostics=""
|
||||||
|
|
||||||
|
if "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager >/dev/null 2>&1; then
|
||||||
|
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: OK; "
|
||||||
|
elif has_cmd systemctl; then
|
||||||
|
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: $( "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager 2>/dev/null | tail -n 5 | tr '\n' ' ' ); "
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_cmd pg_lsclusters; then
|
||||||
|
diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); "
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_cmd journalctl; then
|
||||||
|
diagnostics+="journalctl: $( "$SUDO_BIN" journalctl -u "$POSTGRES_SERVICE_NAME" -n 10 --no-pager 2>/dev/null | tr '\n' ' ' ); "
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "${diagnostics% }"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_postgres_service() {
|
||||||
|
if "$SUDO_BIN" systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_cmd pg_lsclusters && has_cmd pg_ctlcluster; then
|
||||||
|
local version cluster
|
||||||
|
while read -r version cluster _; do
|
||||||
|
[[ -n "$version" && -n "$cluster" ]] || continue
|
||||||
|
if "$SUDO_BIN" pg_ctlcluster "$version" "$cluster" start >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(pg_lsclusters --no-header 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${PGHOST:?Variable PGHOST manquante}"
|
||||||
|
: "${PGPORT:?Variable PGPORT manquante}"
|
||||||
|
: "${PGUSER:?Variable PGUSER manquante}"
|
||||||
|
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
|
||||||
|
|
||||||
|
AUTO_INSTALL_POSTGRES="${AUTO_INSTALL_POSTGRES:-yes}"
|
||||||
|
AUTO_CREATE_PGUSER="${AUTO_CREATE_PGUSER:-yes}"
|
||||||
|
PGUSER_SUPERUSER="${PGUSER_SUPERUSER:-no}"
|
||||||
|
POSTGRES_PACKAGE_LIST="${POSTGRES_PACKAGE_LIST:-postgresql postgresql-client postgresql-contrib}"
|
||||||
|
POSTGRES_SERVICE_NAME="${POSTGRES_SERVICE_NAME:-postgresql}"
|
||||||
|
SUDO_BIN="${SUDO_BIN:-sudo}"
|
||||||
|
read -r -a POSTGRES_PACKAGES <<< "$POSTGRES_PACKAGE_LIST"
|
||||||
|
|
||||||
|
[[ "${#POSTGRES_PACKAGES[@]}" -gt 0 ]] || fail "POSTGRES_PACKAGE_LIST vide"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
if ! has_cmd "$SUDO_BIN"; then
|
||||||
|
fail "sudo absent sur la cible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$SUDO_BIN" /usr/bin/systemctl --version >/dev/null 2>&1; then
|
||||||
|
fail "sudo indisponible pour systemctl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then
|
||||||
|
fail "PGPORT invalide : $PGPORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
POSTGRES_INSTALLED="no"
|
||||||
|
|
||||||
|
if ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb || ! postgres_server_ready; then
|
||||||
|
[[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no"
|
||||||
|
|
||||||
|
log "PostgreSQL absent : installation en cours..."
|
||||||
|
"$SUDO_BIN" apt update >/dev/null 2>&1 || fail "échec de apt update"
|
||||||
|
"$SUDO_BIN" apt install -y "${POSTGRES_PACKAGES[@]}" >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||||
|
POSTGRES_INSTALLED="yes"
|
||||||
|
log "Installation PostgreSQL terminée."
|
||||||
|
else
|
||||||
|
log "PostgreSQL déjà installé."
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_postgres_cluster || fail "aucun cluster PostgreSQL disponible et création automatique impossible"
|
||||||
|
|
||||||
|
if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then
|
||||||
|
log "Démarrage du service PostgreSQL..."
|
||||||
|
if ! start_postgres_service; then
|
||||||
|
fail "impossible de démarrer PostgreSQL. $(collect_postgres_diagnostics)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Service PostgreSQL déjà actif."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Vérification de la disponibilité de PostgreSQL..."
|
||||||
|
PG_READY=false
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
PG_READY=true
|
||||||
|
log "PostgreSQL répond correctement."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$PG_READY" != true ]]; then
|
||||||
|
fail "PostgreSQL ne répond pas correctement"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then
|
||||||
|
ROLE_EXISTS="$(
|
||||||
|
"$SUDO_BIN" -u postgres psql -d postgres -tAc \
|
||||||
|
"SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||||
|
log "Création du rôle PostgreSQL ${PGUSER}..."
|
||||||
|
|
||||||
|
ROLE_ATTRIBUTES="LOGIN CREATEDB CREATEROLE"
|
||||||
|
if [[ "${PGUSER_SUPERUSER,,}" == "yes" ]]; then
|
||||||
|
ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SUDO_BIN" -u postgres psql -d postgres -c \
|
||||||
|
"CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} PASSWORD '${PGPASSWORD//\'/\'\'}';" \
|
||||||
|
>/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}"
|
||||||
|
|
||||||
|
log "Rôle PostgreSQL ${PGUSER} créé."
|
||||||
|
else
|
||||||
|
log "Rôle PostgreSQL ${PGUSER} déjà présent."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Check PostgreSQL terminé avec succès."
|
||||||
347
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
347
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
@@ -0,0 +1,347 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# check-target-readiness.sh
|
||||||
|
#
|
||||||
|
# Prépare la machine cible pour permettre l'exécution non interactive du
|
||||||
|
# script de rebuild depuis une interface web.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
DEFAULT_ENV_FILE="${REPO_DIR}/.env"
|
||||||
|
|
||||||
|
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||||
|
CLI_REQUEST_ID=""
|
||||||
|
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||||
|
JSON_ONLY="${JSON_ONLY:-no}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--env-file)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
|
||||||
|
ENV_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--request-id)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||||
|
CLI_REQUEST_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
# Flag accepté pour compatibilité avec les autres scripts du workflow.
|
||||||
|
NON_INTERACTIVE="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--json-only)
|
||||||
|
JSON_ONLY="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu : $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
python3 - <<'PY' "$1"
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps(sys.argv[1]))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
print_json_and_exit() {
|
||||||
|
local status="$1"
|
||||||
|
local message="$2"
|
||||||
|
local exit_code="$3"
|
||||||
|
|
||||||
|
printf '{'
|
||||||
|
printf '"status":%s,' "$(json_escape "$status")"
|
||||||
|
printf '"message":%s,' "$(json_escape "$message")"
|
||||||
|
printf '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
|
||||||
|
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
|
||||||
|
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
|
||||||
|
printf '}\n'
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_stdout() {
|
||||||
|
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_FILE=/dev/stderr
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
echo "$msg" >>"$LOG_FILE"
|
||||||
|
print_stdout "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
print_json_and_exit "error" "$*" 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env_vars() {
|
||||||
|
local missing=()
|
||||||
|
local var
|
||||||
|
|
||||||
|
for var in \
|
||||||
|
ENV_NAME PGHOST PGPORT PGUSER PGPASSWORD DBS \
|
||||||
|
BACKUP_REMOTE_USER BACKUP_REMOTE_HOST BACKUP_REMOTE_DIR \
|
||||||
|
SSH_KEY BACKUP_LOG_DIR
|
||||||
|
do
|
||||||
|
[[ -n "${!var:-}" ]] || missing+=("$var")
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#missing[@]} > 0 )); then
|
||||||
|
fail "variables .env manquantes : ${missing[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_env_values() {
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||||
|
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME invalide"
|
||||||
|
[[ -n "$DBS" ]] || fail "DBS vide"
|
||||||
|
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
|
||||||
|
[[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide"
|
||||||
|
[[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide"
|
||||||
|
[[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide"
|
||||||
|
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||||
|
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
|
||||||
|
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||||
|
to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT" >/dev/null || fail "BACKUP_KNOWN_HOSTS_STRICT invalide"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_log_file() {
|
||||||
|
mkdir -p "$BACKUP_LOG_DIR" || {
|
||||||
|
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local ts safe_request_id
|
||||||
|
ts="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||||
|
safe_request_id="${REQUEST_ID:-manual}"
|
||||||
|
safe_request_id="${safe_request_id//[^a-zA-Z0-9_.-]/_}"
|
||||||
|
|
||||||
|
LOG_FILE="${BACKUP_LOG_DIR}/check_target_${ENV_NAME,,}_${safe_request_id}_${ts}.log"
|
||||||
|
touch "$LOG_FILE" || {
|
||||||
|
echo '{"status":"error","message":"impossible de créer le fichier de log"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_local_paths() {
|
||||||
|
local restore_base
|
||||||
|
restore_base="${LOCAL_RESTORE_BASE_DIR:-${REPO_DIR}/restore_tmp}"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_LOG_DIR" || fail "création BACKUP_LOG_DIR impossible"
|
||||||
|
[[ -w "$BACKUP_LOG_DIR" ]] || fail "BACKUP_LOG_DIR non inscriptible"
|
||||||
|
|
||||||
|
mkdir -p "$restore_base" || fail "création LOCAL_RESTORE_BASE_DIR impossible"
|
||||||
|
[[ -w "$restore_base" ]] || fail "LOCAL_RESTORE_BASE_DIR non inscriptible"
|
||||||
|
|
||||||
|
log "Dossiers locaux prêts."
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_scripts_permissions() {
|
||||||
|
local core_script check_pg_script
|
||||||
|
core_script="${REPO_DIR}/rebuild-bdd-core.sh"
|
||||||
|
check_pg_script="${REPO_DIR}/Checkup/check-postgresql.sh"
|
||||||
|
|
||||||
|
[[ -f "$core_script" ]] || fail "script core introuvable : $core_script"
|
||||||
|
[[ -f "$check_pg_script" ]] || fail "script PostgreSQL introuvable : $check_pg_script"
|
||||||
|
|
||||||
|
chmod 700 "$core_script" || fail "chmod impossible sur $core_script"
|
||||||
|
chmod 700 "$check_pg_script" || fail "chmod impossible sur $check_pg_script"
|
||||||
|
|
||||||
|
log "Permissions scripts corrigées."
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_ssh_key() {
|
||||||
|
local key_dir
|
||||||
|
key_dir="$(dirname "$SSH_KEY")"
|
||||||
|
|
||||||
|
mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir"
|
||||||
|
chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir"
|
||||||
|
|
||||||
|
[[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY"
|
||||||
|
chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée"
|
||||||
|
|
||||||
|
[[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub"
|
||||||
|
[[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique"
|
||||||
|
|
||||||
|
log "Clé SSH prête."
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_known_hosts() {
|
||||||
|
local ssh_dir known_hosts
|
||||||
|
ssh_dir="$(dirname "$SSH_KEY")"
|
||||||
|
known_hosts="${ssh_dir}/known_hosts"
|
||||||
|
|
||||||
|
touch "$known_hosts" || fail "impossible de créer known_hosts"
|
||||||
|
chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts"
|
||||||
|
|
||||||
|
if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then
|
||||||
|
if [[ "$(to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT")" == "yes" ]]; then
|
||||||
|
fail "hôte ${BACKUP_REMOTE_HOST} absent de known_hosts en mode strict ; provisionner l'empreinte manuellement"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Ajout non strict de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts"
|
||||||
|
ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \
|
||||||
|
fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}"
|
||||||
|
else
|
||||||
|
log "Host déjà présent dans known_hosts."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_backup_ssh() {
|
||||||
|
local ssh_timeout
|
||||||
|
ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}"
|
||||||
|
|
||||||
|
ssh \
|
||||||
|
-i "$SSH_KEY" \
|
||||||
|
-p "$BACKUP_REMOTE_SSH_PORT" \
|
||||||
|
-o IdentitiesOnly=yes \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o ConnectTimeout="$ssh_timeout" \
|
||||||
|
-o StrictHostKeyChecking=yes \
|
||||||
|
"${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" \
|
||||||
|
"test -d '$BACKUP_REMOTE_DIR'" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || \
|
||||||
|
fail "connexion SSH backup impossible, clé non autorisée, ou dossier distant absent"
|
||||||
|
|
||||||
|
log "Connexion SSH backup validée."
|
||||||
|
}
|
||||||
|
|
||||||
|
install_sudoers_if_allowed() {
|
||||||
|
local auto_configure sudoers_file tmp_file
|
||||||
|
auto_configure="$(to_bool_yes_no "${AUTO_CONFIGURE_SUDOERS:-no}")" || fail "AUTO_CONFIGURE_SUDOERS invalide"
|
||||||
|
|
||||||
|
if [[ "$auto_configure" != "yes" ]]; then
|
||||||
|
log "Installation sudoers automatique désactivée."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! sudo true >/dev/null 2>&1; then
|
||||||
|
fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo n'est pas disponible ; configuration initiale manuelle requise"
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd visudo
|
||||||
|
|
||||||
|
sudoers_file="/etc/sudoers.d/rebuild-bdd-${USER}"
|
||||||
|
tmp_file="$(mktemp)"
|
||||||
|
|
||||||
|
cat >"$tmp_file" <<EOF
|
||||||
|
${USER} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||||
|
${USER} ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 440 "$tmp_file"
|
||||||
|
|
||||||
|
visudo -cf "$tmp_file" >/dev/null 2>&1 || {
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
fail "fichier sudoers généré invalide"
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo install -m 440 "$tmp_file" "$sudoers_file" || {
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
fail "impossible d'installer $sudoers_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
log "Fichier sudoers installé : $sudoers_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_sudo_non_interactive() {
|
||||||
|
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || \
|
||||||
|
fail "sudo indisponible pour systemctl"
|
||||||
|
|
||||||
|
log "sudo pour systemctl validé."
|
||||||
|
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
sudo /usr/bin/apt --version >/dev/null 2>&1 || \
|
||||||
|
fail "sudo indisponible pour apt"
|
||||||
|
log "sudo pour apt validé."
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
sudo /usr/bin/apt-get --version >/dev/null 2>&1 || \
|
||||||
|
fail "sudo indisponible pour apt-get"
|
||||||
|
log "sudo pour apt-get validé."
|
||||||
|
else
|
||||||
|
fail "ni apt ni apt-get disponibles sur la cible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
|
||||||
|
fail "sudo -u postgres indisponible pour psql"
|
||||||
|
|
||||||
|
log "sudo -u postgres pour psql validé."
|
||||||
|
}
|
||||||
|
|
||||||
|
run_postgresql_check() {
|
||||||
|
local check_script
|
||||||
|
check_script="${REPO_DIR}/Checkup/check-postgresql.sh"
|
||||||
|
|
||||||
|
[[ -x "$check_script" ]] || fail "script introuvable ou non exécutable : $check_script"
|
||||||
|
|
||||||
|
"$check_script" \
|
||||||
|
--env-file "$ENV_FILE" \
|
||||||
|
--request-id "$REQUEST_ID" \
|
||||||
|
--non-interactive \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL"
|
||||||
|
|
||||||
|
sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \
|
||||||
|
fail "sudo -u postgres indisponible après préparation PostgreSQL"
|
||||||
|
log "Préparation PostgreSQL validée."
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -f "$ENV_FILE" ]] || {
|
||||||
|
echo '{"status":"error","message":"fichier .env introuvable"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
|
||||||
|
|
||||||
|
require_env_vars
|
||||||
|
validate_env_values
|
||||||
|
prepare_log_file
|
||||||
|
|
||||||
|
require_cmd bash
|
||||||
|
require_cmd python3
|
||||||
|
require_cmd ssh
|
||||||
|
require_cmd ssh-keygen
|
||||||
|
require_cmd ssh-keyscan
|
||||||
|
require_cmd sudo
|
||||||
|
|
||||||
|
prepare_local_paths
|
||||||
|
prepare_scripts_permissions
|
||||||
|
prepare_ssh_key
|
||||||
|
prepare_known_hosts
|
||||||
|
test_backup_ssh
|
||||||
|
install_sudoers_if_allowed
|
||||||
|
check_sudo_non_interactive
|
||||||
|
run_postgresql_check
|
||||||
|
|
||||||
|
log "Machine cible prête pour le rebuild."
|
||||||
|
print_json_and_exit "success" "machine cible prête" 0
|
||||||
38
RebuildBdd/Config/.env.exemple
Normal file
38
RebuildBdd/Config/.env.exemple
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
###############################################################################
|
||||||
|
# config/global.env.example
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Defaults d'exécution
|
||||||
|
ALLOW_OVERWRITE=no
|
||||||
|
RESTORE_ROLES=yes
|
||||||
|
|
||||||
|
# Dépôt scripts
|
||||||
|
GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git
|
||||||
|
GLOBAL_REPO_BRANCH=main
|
||||||
|
|
||||||
|
# Backup central
|
||||||
|
GLOBAL_BACKUP_REMOTE_USER=backup
|
||||||
|
GLOBAL_BACKUP_REMOTE_HOST=<BACKUP_HOST>
|
||||||
|
GLOBAL_BACKUP_REMOTE_PORT=22
|
||||||
|
GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups
|
||||||
|
|
||||||
|
# Clé SSH de lecture backup copiée sur les cibles
|
||||||
|
GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_backup_readonly
|
||||||
|
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_backup_readonly.pub
|
||||||
|
GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||||
|
|
||||||
|
# Defaults PostgreSQL
|
||||||
|
GLOBAL_PGHOST=127.0.0.1
|
||||||
|
GLOBAL_PGPORT=5432
|
||||||
|
|
||||||
|
# Defaults scripts
|
||||||
|
GLOBAL_REMOTE_ROLES_DIR_NAME=user
|
||||||
|
GLOBAL_EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
|
||||||
|
# Defaults bootstrap / cible
|
||||||
|
GLOBAL_ENABLE_BOOTSTRAP=yes
|
||||||
|
GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||||
|
GLOBAL_AUTO_INSTALL_POSTGRES=yes
|
||||||
|
GLOBAL_AUTO_CREATE_PGUSER=yes
|
||||||
|
GLOBAL_PGUSER_SUPERUSER=no
|
||||||
|
GLOBAL_AUTO_CONFIGURE_SUDOERS=no
|
||||||
42
RebuildBdd/Config/Targets/prod.env.exemple
Normal file
42
RebuildBdd/Config/Targets/prod.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# config/targets/prod.env.exemple
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# SSH bootstrap cible
|
||||||
|
TARGET_HOST=<TARGET_HOST>
|
||||||
|
TARGET_PORT=22
|
||||||
|
TARGET_BOOTSTRAP_USER=<BOOTSTRAP_USER>
|
||||||
|
TARGET_BOOTSTRAP_SSH_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_target_prod
|
||||||
|
TARGET_RUNTIME_USER=<RUNTIME_USER>
|
||||||
|
|
||||||
|
# Bootstrap
|
||||||
|
TARGET_ENABLE_BOOTSTRAP=yes
|
||||||
|
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||||
|
|
||||||
|
# Repo local cible
|
||||||
|
TARGET_REPO_DIR=/home/<RUNTIME_USER>/RebuildBdd
|
||||||
|
TARGET_ENV_FILE=/home/<RUNTIME_USER>/RebuildBdd/.env
|
||||||
|
|
||||||
|
# PostgreSQL cible
|
||||||
|
TARGET_ENV_NAME=PROD
|
||||||
|
TARGET_PGHOST=127.0.0.1
|
||||||
|
TARGET_PGPORT=5432
|
||||||
|
TARGET_PGUSER=<PGUSER>
|
||||||
|
TARGET_PGPASSWORD=change_me_pg_password
|
||||||
|
TARGET_DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
# Backup cible
|
||||||
|
TARGET_BACKUP_SUBDIR=bdd-prod
|
||||||
|
|
||||||
|
# Logs / tmp / ssh cible
|
||||||
|
TARGET_BACKUP_LOG_DIR=/home/<RUNTIME_USER>/logs/rebuild_bdd
|
||||||
|
TARGET_LOCAL_RESTORE_BASE_DIR=/home/<RUNTIME_USER>/RebuildBdd/restore_tmp
|
||||||
|
TARGET_SSH_KEY=/home/<RUNTIME_USER>/.ssh/id_ed25519_backup_readonly
|
||||||
|
|
||||||
|
# Options cible
|
||||||
|
TARGET_REMOTE_ROLES_DIR_NAME=user
|
||||||
|
TARGET_EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
TARGET_AUTO_INSTALL_POSTGRES=yes
|
||||||
|
TARGET_AUTO_CREATE_PGUSER=yes
|
||||||
|
TARGET_PGUSER_SUPERUSER=no
|
||||||
|
TARGET_AUTO_CONFIGURE_SUDOERS=no
|
||||||
42
RebuildBdd/Config/Targets/test.env.exemple
Normal file
42
RebuildBdd/Config/Targets/test.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# config/targets/test.env.exemple
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# SSH bootstrap cible
|
||||||
|
TARGET_HOST=<TARGET_HOST>
|
||||||
|
TARGET_PORT=22
|
||||||
|
TARGET_BOOTSTRAP_USER=<BOOTSTRAP_USER>
|
||||||
|
TARGET_BOOTSTRAP_SSH_KEY=/home/<LOCAL_USER>/.ssh/id_ed25519_target_test
|
||||||
|
TARGET_RUNTIME_USER=<RUNTIME_USER>
|
||||||
|
|
||||||
|
# Bootstrap
|
||||||
|
TARGET_ENABLE_BOOTSTRAP=yes
|
||||||
|
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||||
|
|
||||||
|
# Repo local cible
|
||||||
|
TARGET_REPO_DIR=/home/<RUNTIME_USER>/RebuildBdd
|
||||||
|
TARGET_ENV_FILE=/home/<RUNTIME_USER>/RebuildBdd/.env
|
||||||
|
|
||||||
|
# PostgreSQL cible
|
||||||
|
TARGET_ENV_NAME=RECETTE
|
||||||
|
TARGET_PGHOST=127.0.0.1
|
||||||
|
TARGET_PGPORT=5432
|
||||||
|
TARGET_PGUSER=<PGUSER>
|
||||||
|
TARGET_PGPASSWORD=change_me_pg_password
|
||||||
|
TARGET_DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
# Backup cible
|
||||||
|
TARGET_BACKUP_SUBDIR=bdd-recette
|
||||||
|
|
||||||
|
# Logs / tmp / ssh cible
|
||||||
|
TARGET_BACKUP_LOG_DIR=/home/<RUNTIME_USER>/logs/rebuild_bdd
|
||||||
|
TARGET_LOCAL_RESTORE_BASE_DIR=/home/<RUNTIME_USER>/RebuildBdd/restore_tmp
|
||||||
|
TARGET_SSH_KEY=/home/<RUNTIME_USER>/.ssh/id_ed25519_backup_readonly
|
||||||
|
|
||||||
|
# Options cible
|
||||||
|
TARGET_REMOTE_ROLES_DIR_NAME=user
|
||||||
|
TARGET_EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
TARGET_AUTO_INSTALL_POSTGRES=yes
|
||||||
|
TARGET_AUTO_CREATE_PGUSER=yes
|
||||||
|
TARGET_PGUSER_SUPERUSER=no
|
||||||
|
TARGET_AUTO_CONFIGURE_SUDOERS=no
|
||||||
612
RebuildBdd/README.md
Normal file
612
RebuildBdd/README.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# RebuildBdd
|
||||||
|
|
||||||
|
Orchestration de reconstruction de bases PostgreSQL à partir de dumps distants, avec préparation automatique des machines cibles, exécution non interactive et intégration web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Ce projet permet de :
|
||||||
|
|
||||||
|
- préparer automatiquement une machine cible neuve ou partiellement configurée ;
|
||||||
|
- déployer et mettre à jour les scripts sur la cible ;
|
||||||
|
- préparer PostgreSQL localement sur la cible ;
|
||||||
|
- récupérer le dernier dump disponible depuis un serveur de backup ;
|
||||||
|
- restaurer une base PostgreSQL de manière non interactive ;
|
||||||
|
- exposer un flux exploitable depuis une interface web via des retours JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnement global
|
||||||
|
|
||||||
|
Le flux standard est le suivant :
|
||||||
|
|
||||||
|
1. **Création ou mise à jour de la configuration d’une cible**
|
||||||
|
2. **Bootstrap initial de la cible**
|
||||||
|
3. **Précheck de préparation**
|
||||||
|
4. **Rebuild de la base**
|
||||||
|
|
||||||
|
En pratique :
|
||||||
|
|
||||||
|
- `create-target-config.sh` crée un fichier de configuration cible ;
|
||||||
|
- `bootstrap-target-host.sh` prépare la machine cible ;
|
||||||
|
- `Checkup/check-target-readiness.sh` valide l’environnement ;
|
||||||
|
- `rebuild-bdd-core.sh` exécute la restauration ;
|
||||||
|
- `run-rebuild-bdd.sh` orchestre l’ensemble.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">
|
||||||
|
<strong>EggMaster</strong>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Question 4</summary>
|
||||||
|
|
||||||
|
Quel utilitaire standard permet de decoder la chaine reconstituee ?
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Indice commande 4</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
base64
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Fragment 4</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
4gcmVjb21wZW5zZSBodHRwczovL3d3dy55b3V0
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Le projet utilise deux niveaux de configuration :
|
||||||
|
|
||||||
|
#### 1. Configuration globale
|
||||||
|
Fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/global.env
|
||||||
|
````
|
||||||
|
|
||||||
|
Contient les paramètres stables, par exemple :
|
||||||
|
|
||||||
|
* dépôt Git des scripts ;
|
||||||
|
* serveur de backup ;
|
||||||
|
* clé SSH de lecture backup ;
|
||||||
|
* valeurs par défaut PostgreSQL ;
|
||||||
|
* options globales de bootstrap.
|
||||||
|
|
||||||
|
#### 2. Configuration par cible
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/Targets/<nom_cible>.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque fichier cible contient :
|
||||||
|
|
||||||
|
* accès SSH bootstrap ;
|
||||||
|
* répertoires locaux de la cible ;
|
||||||
|
* paramètres PostgreSQL ;
|
||||||
|
* sous-répertoire backup associé ;
|
||||||
|
* options spécifiques à la cible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arborescence recommandée
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RebuildBdd/
|
||||||
|
├── bootstrap-target-host.sh
|
||||||
|
├── create-target-config.sh
|
||||||
|
├── run-rebuild-bdd.sh
|
||||||
|
├── rebuild-bdd-core.sh
|
||||||
|
├── Config/
|
||||||
|
│ ├── global.env
|
||||||
|
│ └── Targets/
|
||||||
|
│ ├── test.env
|
||||||
|
│ └── prod.env
|
||||||
|
└── Checkup/
|
||||||
|
├── check-postgresql.sh
|
||||||
|
└── check-target-readiness.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### `create-target-config.sh`
|
||||||
|
|
||||||
|
Crée ou met à jour un fichier cible dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/Targets/<cible>.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh \
|
||||||
|
--target test \
|
||||||
|
--host <TARGET_HOST> \
|
||||||
|
--port 22 \
|
||||||
|
--bootstrap-user <BOOTSTRAP_USER> \
|
||||||
|
--bootstrap-key /home/user/.ssh/id_ed25519_target_test \
|
||||||
|
--runtime-user <RUNTIME_USER> \
|
||||||
|
--repo-dir /home/<RUNTIME_USER>/RebuildBdd \
|
||||||
|
--env-name RECETTE \
|
||||||
|
--pguser <PGUSER> \
|
||||||
|
--pgpassword secret \
|
||||||
|
--dbs "sirh inventory ferme" \
|
||||||
|
--backup-subdir bdd-recette
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `bootstrap-target-host.sh`
|
||||||
|
|
||||||
|
Prépare une machine cible neuve ou quasi neuve :
|
||||||
|
|
||||||
|
* connexion SSH bootstrap ;
|
||||||
|
* installation des paquets minimum ;
|
||||||
|
* création des dossiers ;
|
||||||
|
* génération du `.env` cible ;
|
||||||
|
* copie de la clé SSH backup ;
|
||||||
|
* préparation de `known_hosts` ;
|
||||||
|
* installation éventuelle d’un `sudoers.d` minimal ;
|
||||||
|
* synchronisation du dépôt ;
|
||||||
|
* exécution de `check-postgresql.sh`.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
Mode JSON :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test --json-only
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Checkup/check-postgresql.sh`
|
||||||
|
|
||||||
|
Prépare PostgreSQL localement sur la cible :
|
||||||
|
|
||||||
|
* installation si absent ;
|
||||||
|
* démarrage du service ;
|
||||||
|
* test de disponibilité ;
|
||||||
|
* création du rôle PostgreSQL cible si nécessaire.
|
||||||
|
|
||||||
|
Ce script est prévu pour fonctionner en non interactif avec `sudo -n`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Checkup/check-target-readiness.sh`
|
||||||
|
|
||||||
|
Valide la préparation complète de la cible :
|
||||||
|
|
||||||
|
* lecture du `.env` cible ;
|
||||||
|
* vérification des chemins ;
|
||||||
|
* permissions locales ;
|
||||||
|
* permissions SSH ;
|
||||||
|
* `known_hosts` ;
|
||||||
|
* accès SSH au serveur de backup ;
|
||||||
|
* exécution de `check-postgresql.sh`.
|
||||||
|
|
||||||
|
Mode JSON disponible pour usage web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `rebuild-bdd-core.sh`
|
||||||
|
|
||||||
|
Script métier de reconstruction :
|
||||||
|
|
||||||
|
* validation des paramètres ;
|
||||||
|
* connexion au serveur de backup ;
|
||||||
|
* récupération du dernier dump ;
|
||||||
|
* récupération éventuelle du fichier des rôles ;
|
||||||
|
* suppression/recréation de la base si autorisé ;
|
||||||
|
* restauration des rôles ;
|
||||||
|
* restauration du dump PostgreSQL ;
|
||||||
|
* retour JSON final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `run-rebuild-bdd.sh`
|
||||||
|
|
||||||
|
Script orchestrateur principal.
|
||||||
|
|
||||||
|
Il peut :
|
||||||
|
|
||||||
|
* lancer le bootstrap si activé pour la cible ;
|
||||||
|
* synchroniser le dépôt distant ;
|
||||||
|
* lancer le précheck ;
|
||||||
|
* exécuter le rebuild.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--request-id web_001 \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Machine de lancement
|
||||||
|
|
||||||
|
Doit disposer de :
|
||||||
|
|
||||||
|
* `bash`
|
||||||
|
* `ssh`
|
||||||
|
* `scp`
|
||||||
|
* `git`
|
||||||
|
* `python3`
|
||||||
|
* `jq` si vous consommez les JSON côté tooling
|
||||||
|
|
||||||
|
### Machine cible
|
||||||
|
|
||||||
|
Le bootstrap suppose :
|
||||||
|
|
||||||
|
* accès SSH fonctionnel ;
|
||||||
|
* utilisateur bootstrap existant ;
|
||||||
|
* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial ;
|
||||||
|
* `known_hosts` correctement provisionné si le mode strict SSH est activé vers le serveur de backup.
|
||||||
|
|
||||||
|
### Serveur de backup
|
||||||
|
|
||||||
|
Doit :
|
||||||
|
|
||||||
|
* être joignable en SSH depuis la cible ;
|
||||||
|
* accepter la clé de lecture backup ;
|
||||||
|
* contenir les dumps dans l’arborescence attendue.
|
||||||
|
|
||||||
|
## Sécurité / déploiement
|
||||||
|
|
||||||
|
### Clés hôtes SSH
|
||||||
|
|
||||||
|
Si `GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes`, l’empreinte du serveur de backup doit déjà être présente dans le `known_hosts` de la cible. Le bootstrap et les checks ne font plus d’ajout aveugle en mode strict.
|
||||||
|
|
||||||
|
### Répertoires de clone
|
||||||
|
|
||||||
|
Les scripts refusent maintenant les chemins de clone manifestement dangereux comme `/`, `/root`, `/home` ou `/home/<user>` pour éviter un `rm -rf` destructeur dû à une mauvaise configuration.
|
||||||
|
|
||||||
|
### Ubuntu Server
|
||||||
|
|
||||||
|
Le flux de bootstrap est pensé pour des cibles Ubuntu Server avec `apt`, `systemctl` et `sudo -n`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure des backups attendue
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/malio-b/backups/
|
||||||
|
├── bdd-recette/
|
||||||
|
│ ├── sirh/
|
||||||
|
│ │ ├── sirh_2026-03-16_19-00-01.dump
|
||||||
|
│ ├── inventory/
|
||||||
|
│ ├── ferme/
|
||||||
|
│ └── user/
|
||||||
|
│ ├── user_2026-03-16_19-00-01.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script recherche :
|
||||||
|
|
||||||
|
* le dernier dump dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<BACKUP_REMOTE_DIR>/<db>/<db>_*.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
* le dernier fichier rôles dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<BACKUP_REMOTE_DIR>/<REMOTE_ROLES_DIR_NAME>/user_*.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Créer la configuration globale
|
||||||
|
|
||||||
|
Copier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/.env.exemple
|
||||||
|
```
|
||||||
|
|
||||||
|
vers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/global.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Renseigner ensuite :
|
||||||
|
|
||||||
|
* dépôt Git ;
|
||||||
|
* serveur de backup ;
|
||||||
|
* clé SSH backup ;
|
||||||
|
* defaults globaux.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Créer une cible
|
||||||
|
|
||||||
|
Deux possibilités.
|
||||||
|
|
||||||
|
#### A. À la main
|
||||||
|
|
||||||
|
Créer un fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/Targets/test.env
|
||||||
|
```
|
||||||
|
|
||||||
|
à partir de :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Config/Targets/test.env.exemple
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Via script
|
||||||
|
|
||||||
|
Utiliser :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exécution locale
|
||||||
|
|
||||||
|
### Bootstrap seul
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild complet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intégration web
|
||||||
|
|
||||||
|
L’interface web ne doit envoyer que les paramètres métier de l’exécution :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"target": "test",
|
||||||
|
"db": "sirh",
|
||||||
|
"overwrite": "yes",
|
||||||
|
"restore_roles": "yes",
|
||||||
|
"request_id": "web_20260317_001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le backend transforme cela en commande :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--request-id web_20260317_001 \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
Le web ne doit pas transmettre directement :
|
||||||
|
|
||||||
|
* les clés SSH ;
|
||||||
|
* les mots de passe PostgreSQL ;
|
||||||
|
* les paramètres bas niveau de la cible ;
|
||||||
|
* les chemins système sensibles.
|
||||||
|
|
||||||
|
Ces informations doivent être stockées dans la configuration serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ajouter une nouvelle machine depuis le web
|
||||||
|
|
||||||
|
Le flux recommandé est :
|
||||||
|
|
||||||
|
1. créer ou mettre à jour `Config/Targets/<cible>.env`
|
||||||
|
2. lancer `bootstrap-target-host.sh --target <cible>`
|
||||||
|
3. lancer ensuite `run-rebuild-bdd.sh --target <cible> ...`
|
||||||
|
|
||||||
|
Le bouton web **“Ajouter une machine”** doit donc :
|
||||||
|
|
||||||
|
* créer la configuration cible ;
|
||||||
|
* déclencher le bootstrap ;
|
||||||
|
* vérifier le retour ;
|
||||||
|
* rendre ensuite la cible disponible pour les rebuilds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sorties JSON
|
||||||
|
|
||||||
|
### Succès
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "restauration terminée avec succès",
|
||||||
|
"request_id": "web_001",
|
||||||
|
"environment": "RECETTE",
|
||||||
|
"database": "sirh",
|
||||||
|
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
|
||||||
|
"log_file": "/home/<RUNTIME_USER>/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "la base existe déjà et overwrite n'est pas autorisé",
|
||||||
|
"request_id": "web_001",
|
||||||
|
"environment": "RECETTE",
|
||||||
|
"database": "sirh",
|
||||||
|
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
|
||||||
|
"log_file": "/home/<RUNTIME_USER>/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Recommandations minimales
|
||||||
|
|
||||||
|
* utiliser des clés SSH dédiées ;
|
||||||
|
* limiter la clé backup à la lecture seule ;
|
||||||
|
* restreindre les permissions des fichiers de config ;
|
||||||
|
* exécuter les scripts avec un utilisateur dédié ;
|
||||||
|
* ne pas exposer les secrets dans l’interface web ;
|
||||||
|
* valider strictement toutes les entrées côté backend.
|
||||||
|
|
||||||
|
### `sudoers`
|
||||||
|
|
||||||
|
Le bootstrap peut installer un `sudoers.d` minimal pour l’utilisateur runtime :
|
||||||
|
|
||||||
|
```sudoers
|
||||||
|
<user> ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||||
|
<user> ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||||
|
```
|
||||||
|
|
||||||
|
Adapter si d’autres commandes doivent être autorisées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
Les logs de rebuild sont stockés dans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TARGET_BACKUP_LOG_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/<RUNTIME_USER>/logs/rebuild_bdd/
|
||||||
|
```
|
||||||
|
|
||||||
|
Le chemin du log est renvoyé dans le JSON final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limites connues
|
||||||
|
|
||||||
|
* le bootstrap initial nécessite un accès SSH bootstrap valide ;
|
||||||
|
* le bootstrap ne remplace pas une mauvaise architecture réseau ;
|
||||||
|
* les secrets doivent être gérés proprement par la couche web/backend ;
|
||||||
|
* des verrous d’exécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommandations de validation
|
||||||
|
|
||||||
|
Avant mise en production, tester au minimum :
|
||||||
|
|
||||||
|
1. bootstrap d’une machine neuve ;
|
||||||
|
2. rebuild complet d’une base ;
|
||||||
|
3. refus si la base existe et `overwrite=no` ;
|
||||||
|
4. relance complète une seconde fois sur la même cible ;
|
||||||
|
5. accès backup invalide ;
|
||||||
|
6. PostgreSQL absent au départ ;
|
||||||
|
7. `sudo -n` indisponible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
### Créer une cible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-target-config.sh \
|
||||||
|
--target test \
|
||||||
|
--host <TARGET_HOST> \
|
||||||
|
--port 22 \
|
||||||
|
--bootstrap-user <BOOTSTRAP_USER> \
|
||||||
|
--bootstrap-key /home/<LOCAL_USER>/.ssh/id_ed25519_target_test \
|
||||||
|
--runtime-user <RUNTIME_USER> \
|
||||||
|
--repo-dir /home/<RUNTIME_USER>/RebuildBdd \
|
||||||
|
--env-name RECETTE \
|
||||||
|
--pguser <PGUSER> \
|
||||||
|
--pgpassword secret \
|
||||||
|
--dbs "sirh inventory ferme" \
|
||||||
|
--backup-subdir bdd-recette
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bootstrap
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bootstrap-target-host.sh --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-rebuild-bdd.sh \
|
||||||
|
--target test \
|
||||||
|
--db sirh \
|
||||||
|
--overwrite yes \
|
||||||
|
--restore-roles yes \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État du projet
|
||||||
|
|
||||||
|
Le projet permet désormais une utilisation :
|
||||||
|
|
||||||
|
* locale ;
|
||||||
|
* automatisée ;
|
||||||
|
* intégrée au web ;
|
||||||
|
|
||||||
|
avec préparation des cibles, exécution non interactive et retour JSON.
|
||||||
597
RebuildBdd/bootstrap-target-host.sh
Executable file
597
RebuildBdd/bootstrap-target-host.sh
Executable file
@@ -0,0 +1,597 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||||
|
GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env"
|
||||||
|
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||||
|
GIT_TOPLEVEL="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
LOCAL_REPO_SUBDIR_DEFAULT=""
|
||||||
|
|
||||||
|
if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then
|
||||||
|
LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}"
|
||||||
|
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||||
|
|
||||||
|
TARGET_NAME="${TARGET_NAME:-}"
|
||||||
|
CLI_TARGET=""
|
||||||
|
JSON_ONLY="${JSON_ONLY:-no}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--global-env-file)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; }
|
||||||
|
GLOBAL_ENV_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--targets-dir)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; }
|
||||||
|
TARGETS_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--target)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
|
||||||
|
CLI_TARGET="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--json-only)
|
||||||
|
JSON_ONLY="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu : $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
python3 - <<'PY' "$1"
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps(sys.argv[1]))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
print_stdout() {
|
||||||
|
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
print_stdout "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local msg="$1"
|
||||||
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||||
|
printf '{"status":%s,"message":%s}\n' \
|
||||||
|
"$(json_escape "error")" \
|
||||||
|
"$(json_escape "$msg")"
|
||||||
|
else
|
||||||
|
echo "ERROR: $msg" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
local msg="$1"
|
||||||
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||||
|
printf '{"status":%s,"message":%s}\n' \
|
||||||
|
"$(json_escape "success")" \
|
||||||
|
"$(json_escape "$msg")"
|
||||||
|
else
|
||||||
|
log "$msg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_quote() {
|
||||||
|
printf "%q" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "${TMP_ENV_FILE:-}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
copy_file_to_remote_via_ssh() {
|
||||||
|
local local_file="$1"
|
||||||
|
local remote_final_path="$2"
|
||||||
|
local remote_mode="$3"
|
||||||
|
local remote_parent
|
||||||
|
local remote_tmp
|
||||||
|
|
||||||
|
[[ -f "$local_file" ]] || fail "fichier source introuvable : $local_file"
|
||||||
|
[[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file"
|
||||||
|
|
||||||
|
remote_parent="$(dirname "$remote_final_path")"
|
||||||
|
remote_tmp="/tmp/bootstrap_copy.$$.$RANDOM.tmp"
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p $(shell_quote "$remote_parent")
|
||||||
|
test -d $(shell_quote "$remote_parent")
|
||||||
|
test -w $(shell_quote "$remote_parent")
|
||||||
|
" >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent"
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||||
|
set -euo pipefail
|
||||||
|
cat > $(shell_quote "$remote_tmp")
|
||||||
|
" < "$local_file" >/dev/null 2>&1 || fail "échec d'écriture temporaire distante : $remote_tmp"
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "
|
||||||
|
set -euo pipefail
|
||||||
|
install -m $(shell_quote "$remote_mode") $(shell_quote "$remote_tmp") $(shell_quote "$remote_final_path")
|
||||||
|
rm -f $(shell_quote "$remote_tmp")
|
||||||
|
" >/dev/null 2>&1 || fail "échec d'installation distante : $remote_final_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}"
|
||||||
|
[[ -n "$TARGET_NAME" ]] || fail "target manquante"
|
||||||
|
|
||||||
|
TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET_NAME}.env"
|
||||||
|
|
||||||
|
[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE"
|
||||||
|
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$GLOBAL_ENV_FILE"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$TARGET_ENV_SOURCE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
BOOTSTRAP_HOST="${TARGET_HOST:-}"
|
||||||
|
BOOTSTRAP_PORT="${TARGET_PORT:-22}"
|
||||||
|
BOOTSTRAP_USER="${TARGET_BOOTSTRAP_USER:-}"
|
||||||
|
BOOTSTRAP_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}"
|
||||||
|
|
||||||
|
TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}"
|
||||||
|
TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-}}"
|
||||||
|
TARGET_REPO_DIR="${TARGET_REPO_DIR:-}"
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}"
|
||||||
|
TARGET_ENV_FILE_PATH="${TARGET_ENV_FILE:-}"
|
||||||
|
|
||||||
|
TARGET_ENV_NAME_VALUE="${TARGET_ENV_NAME:-}"
|
||||||
|
TARGET_PGHOST_VALUE="${TARGET_PGHOST:-${GLOBAL_PGHOST:-}}"
|
||||||
|
TARGET_PGPORT_VALUE="${TARGET_PGPORT:-${GLOBAL_PGPORT:-}}"
|
||||||
|
TARGET_PGUSER_VALUE="${TARGET_PGUSER:-}"
|
||||||
|
TARGET_PGPASSWORD_VALUE="${TARGET_PGPASSWORD:-}"
|
||||||
|
TARGET_DBS_VALUE="${TARGET_DBS:-}"
|
||||||
|
|
||||||
|
TARGET_BACKUP_REMOTE_USER_VALUE="${TARGET_BACKUP_REMOTE_USER:-${GLOBAL_BACKUP_REMOTE_USER:-}}"
|
||||||
|
TARGET_BACKUP_REMOTE_HOST_VALUE="${TARGET_BACKUP_REMOTE_HOST:-${GLOBAL_BACKUP_REMOTE_HOST:-}}"
|
||||||
|
TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="${TARGET_BACKUP_REMOTE_SSH_PORT:-${GLOBAL_BACKUP_REMOTE_PORT:-22}}"
|
||||||
|
GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR:-}"
|
||||||
|
TARGET_BACKUP_SUBDIR_VALUE="${TARGET_BACKUP_SUBDIR:-}"
|
||||||
|
TARGET_BACKUP_LOG_DIR_VALUE="${TARGET_BACKUP_LOG_DIR:-}"
|
||||||
|
|
||||||
|
TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY:-${GLOBAL_BACKUP_SSH_PRIVATE_KEY:-}}"
|
||||||
|
TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY:-${GLOBAL_BACKUP_SSH_PUBLIC_KEY:-}}"
|
||||||
|
TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE="${TARGET_BACKUP_KNOWN_HOSTS_STRICT:-${GLOBAL_BACKUP_KNOWN_HOSTS_STRICT:-yes}}"
|
||||||
|
|
||||||
|
TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_LOCAL_RESTORE_BASE_DIR:-${TARGET_REPO_DIR}/restore_tmp}"
|
||||||
|
TARGET_REMOTE_ROLES_DIR_NAME_VALUE="${TARGET_REMOTE_ROLES_DIR_NAME:-${GLOBAL_REMOTE_ROLES_DIR_NAME:-user}}"
|
||||||
|
TARGET_SSH_KEY_VALUE="${TARGET_SSH_KEY:-/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly}"
|
||||||
|
TARGET_AUTO_INSTALL_POSTGRES_VALUE="${TARGET_AUTO_INSTALL_POSTGRES:-${GLOBAL_AUTO_INSTALL_POSTGRES:-yes}}"
|
||||||
|
TARGET_AUTO_CREATE_PGUSER_VALUE="${TARGET_AUTO_CREATE_PGUSER:-${GLOBAL_AUTO_CREATE_PGUSER:-yes}}"
|
||||||
|
TARGET_PGUSER_SUPERUSER_VALUE="${TARGET_PGUSER_SUPERUSER:-${GLOBAL_PGUSER_SUPERUSER:-no}}"
|
||||||
|
TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="${TARGET_AUTO_CONFIGURE_SUDOERS:-${GLOBAL_AUTO_CONFIGURE_SUDOERS:-no}}"
|
||||||
|
TARGET_EXCLUDED_RESTORE_ROLES_VALUE="${TARGET_EXCLUDED_RESTORE_ROLES:-${GLOBAL_EXCLUDED_RESTORE_ROLES:-postgres}}"
|
||||||
|
|
||||||
|
TARGET_RUNTIME_USER_VALUE="${TARGET_RUNTIME_USER:-$BOOTSTRAP_USER}"
|
||||||
|
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE="${TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-${GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-yes}}"
|
||||||
|
|
||||||
|
[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST manquante"
|
||||||
|
[[ "$BOOTSTRAP_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide"
|
||||||
|
[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante"
|
||||||
|
[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante"
|
||||||
|
[[ -f "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap introuvable : $BOOTSTRAP_SSH_KEY"
|
||||||
|
[[ -r "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap non lisible : $BOOTSTRAP_SSH_KEY"
|
||||||
|
|
||||||
|
[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant"
|
||||||
|
[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant"
|
||||||
|
[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante"
|
||||||
|
[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE manquante"
|
||||||
|
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}"
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}"
|
||||||
|
|
||||||
|
TARGET_CLONE_DIR="$TARGET_REPO_DIR"
|
||||||
|
TARGET_SCRIPT_DIR="$TARGET_REPO_DIR"
|
||||||
|
if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||||
|
if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then
|
||||||
|
TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")"
|
||||||
|
else
|
||||||
|
TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
|
||||||
|
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
|
||||||
|
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME manquante"
|
||||||
|
[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST/GLOBAL_PGHOST manquant"
|
||||||
|
[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT/GLOBAL_PGPORT manquant"
|
||||||
|
[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER manquante"
|
||||||
|
[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD manquante"
|
||||||
|
[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS manquante"
|
||||||
|
|
||||||
|
[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_USER/TARGET_BACKUP_REMOTE_USER manquant"
|
||||||
|
[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_HOST/TARGET_BACKUP_REMOTE_HOST manquant"
|
||||||
|
[[ -n "$GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_BASE_DIR manquant"
|
||||||
|
[[ -n "$TARGET_BACKUP_SUBDIR_VALUE" ]] || fail "TARGET_BACKUP_SUBDIR manquante"
|
||||||
|
[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR manquante"
|
||||||
|
TARGET_BACKUP_REMOTE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE%/}/${TARGET_BACKUP_SUBDIR_VALUE}"
|
||||||
|
|
||||||
|
[[ -n "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "GLOBAL_BACKUP_SSH_PRIVATE_KEY/TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY manquant"
|
||||||
|
[[ -f "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE"
|
||||||
|
[[ -r "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE"
|
||||||
|
|
||||||
|
if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then
|
||||||
|
[[ -f "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE"
|
||||||
|
[[ -r "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" =~ ^[0-9]+$ ]] || fail "port backup invalide"
|
||||||
|
to_bool_yes_no "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE" >/dev/null || fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide"
|
||||||
|
to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || fail "TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO invalide"
|
||||||
|
|
||||||
|
ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")"
|
||||||
|
|
||||||
|
require_cmd ssh
|
||||||
|
require_cmd python3
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$BOOTSTRAP_SSH_KEY"
|
||||||
|
-p "$BOOTSTRAP_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
-o ConnectTimeout=8
|
||||||
|
)
|
||||||
|
|
||||||
|
REMOTE="${BOOTSTRAP_USER}@${BOOTSTRAP_HOST}"
|
||||||
|
|
||||||
|
log "Test de connexion SSH bootstrap vers ${REMOTE}:${BOOTSTRAP_PORT}"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "exit 0" >/dev/null 2>&1 \
|
||||||
|
|| fail "connexion SSH bootstrap impossible vers ${REMOTE}"
|
||||||
|
|
||||||
|
REMOTE_SETUP_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||||
|
\"\$@\"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
sudo \"\$@\" || {
|
||||||
|
echo 'sudo indisponible pour le bootstrap' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'ni root ni sudo disponible pour le bootstrap' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v apt-get >/dev/null 2>&1; then
|
||||||
|
echo 'apt-get absent sur la cible' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_root apt-get update
|
||||||
|
run_root apt-get install -y bash git python3 sudo curl openssh-client ca-certificates postgresql-client
|
||||||
|
|
||||||
|
mkdir -p $(shell_quote "$(dirname "$TARGET_CLONE_DIR")")
|
||||||
|
mkdir -p $(shell_quote "$(dirname "$TARGET_SCRIPT_DIR")")
|
||||||
|
mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")")
|
||||||
|
mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
|
||||||
|
mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
||||||
|
mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")")
|
||||||
|
|
||||||
|
chmod 700 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") || true
|
||||||
|
touch $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts")
|
||||||
|
chmod 644 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") || true
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Installation du socle minimal sur la cible"
|
||||||
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" >/dev/null \
|
||||||
|
|| fail "échec de préparation système distante"
|
||||||
|
else
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \
|
||||||
|
|| fail "échec de préparation système distante"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_ENV_FILE="$(mktemp)"
|
||||||
|
|
||||||
|
cat >"$TMP_ENV_FILE" <<EOF
|
||||||
|
ENV_NAME=$(shell_quote "$TARGET_ENV_NAME_VALUE")
|
||||||
|
PGHOST=$(shell_quote "$TARGET_PGHOST_VALUE")
|
||||||
|
PGPORT=$(shell_quote "$TARGET_PGPORT_VALUE")
|
||||||
|
PGUSER=$(shell_quote "$TARGET_PGUSER_VALUE")
|
||||||
|
PGPASSWORD=$(shell_quote "$TARGET_PGPASSWORD_VALUE")
|
||||||
|
DBS=$(shell_quote "$TARGET_DBS_VALUE")
|
||||||
|
|
||||||
|
BACKUP_REMOTE_USER=$(shell_quote "$TARGET_BACKUP_REMOTE_USER_VALUE")
|
||||||
|
BACKUP_REMOTE_HOST=$(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE")
|
||||||
|
BACKUP_REMOTE_DIR=$(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
||||||
|
BACKUP_REMOTE_SSH_PORT=$(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE")
|
||||||
|
|
||||||
|
BACKUP_LOG_DIR=$(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
|
||||||
|
LOCAL_RESTORE_BASE_DIR=$(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
||||||
|
REMOTE_ROLES_DIR_NAME=$(shell_quote "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE")
|
||||||
|
SSH_KEY=$(shell_quote "$TARGET_SSH_KEY_VALUE")
|
||||||
|
BACKUP_KNOWN_HOSTS_STRICT=$(shell_quote "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE")
|
||||||
|
|
||||||
|
AUTO_INSTALL_POSTGRES=$(shell_quote "$TARGET_AUTO_INSTALL_POSTGRES_VALUE")
|
||||||
|
AUTO_CREATE_PGUSER=$(shell_quote "$TARGET_AUTO_CREATE_PGUSER_VALUE")
|
||||||
|
PGUSER_SUPERUSER=$(shell_quote "$TARGET_PGUSER_SUPERUSER_VALUE")
|
||||||
|
AUTO_CONFIGURE_SUDOERS=$(shell_quote "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE")
|
||||||
|
EXCLUDED_RESTORE_ROLES=$(shell_quote "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "Copie du .env cible"
|
||||||
|
copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600"
|
||||||
|
|
||||||
|
REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")"
|
||||||
|
REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts"
|
||||||
|
|
||||||
|
log "Copie de la clé privée backup sur la cible"
|
||||||
|
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600"
|
||||||
|
|
||||||
|
if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then
|
||||||
|
log "Copie de la clé publique backup sur la cible"
|
||||||
|
copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_SSH_PERMS_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
chmod 700 $(shell_quote "$REMOTE_SSH_DIR")
|
||||||
|
chmod 600 $(shell_quote "$TARGET_SSH_KEY_VALUE")
|
||||||
|
if [[ -f $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") ]]; then
|
||||||
|
chmod 644 $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub")
|
||||||
|
fi
|
||||||
|
touch $(shell_quote "$REMOTE_KNOWN_HOSTS")
|
||||||
|
chmod 644 $(shell_quote "$REMOTE_KNOWN_HOSTS")
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Correction des permissions SSH côté cible"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SSH_PERMS_CMD" \
|
||||||
|
|| fail "échec de correction des permissions SSH sur la cible"
|
||||||
|
|
||||||
|
STRICT_OPTION="yes"
|
||||||
|
case "${TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE,,}" in
|
||||||
|
yes|y|oui|o|true|1) STRICT_OPTION="yes" ;;
|
||||||
|
no|n|non|false|0) STRICT_OPTION="no" ;;
|
||||||
|
*) fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
REMOTE_KNOWN_HOSTS_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v ssh-keyscan >/dev/null 2>&1; then
|
||||||
|
echo 'ssh-keyscan absent sur la cible' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ssh-keygen -F $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") -f $(shell_quote "$REMOTE_KNOWN_HOSTS") >/dev/null 2>&1; then
|
||||||
|
if [[ $(shell_quote "$STRICT_OPTION") == yes ]]; then
|
||||||
|
echo 'hôte backup absent de known_hosts en mode strict ; empreinte à provisionner manuellement' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ssh-keyscan -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") -H $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") >> $(shell_quote "$REMOTE_KNOWN_HOSTS") 2>/dev/null
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Ajout du serveur de backup dans known_hosts côté cible"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_KNOWN_HOSTS_CMD" \
|
||||||
|
|| fail "échec de préparation known_hosts sur la cible"
|
||||||
|
|
||||||
|
REMOTE_BACKUP_TEST_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ssh \
|
||||||
|
-i $(shell_quote "$TARGET_SSH_KEY_VALUE") \
|
||||||
|
-p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") \
|
||||||
|
-o IdentitiesOnly=yes \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o ConnectTimeout=8 \
|
||||||
|
-o StrictHostKeyChecking=$(shell_quote "$STRICT_OPTION") \
|
||||||
|
$(shell_quote "${TARGET_BACKUP_REMOTE_USER_VALUE}@${TARGET_BACKUP_REMOTE_HOST_VALUE}") \
|
||||||
|
test -d $(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Test de la connexion SSH cible -> backup"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_BACKUP_TEST_CMD" \
|
||||||
|
|| fail "la cible ne peut pas accéder au serveur de backup avec la clé fournie"
|
||||||
|
|
||||||
|
if [[ "$ALLOW_PASSWORDLESS_SUDO" == "yes" ]]; then
|
||||||
|
REMOTE_SUDOERS_PRECHECK_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v sudo >/dev/null 2>&1 || exit 1
|
||||||
|
sudo true </dev/null >/dev/null 2>&1
|
||||||
|
"
|
||||||
|
|
||||||
|
REMOTE_SUDOERS_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||||
|
\"\$@\"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
sudo \"\$@\" || {
|
||||||
|
echo 'sudo indisponible pour installer sudoers' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'ni root ni sudo disponible pour sudoers' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v visudo >/dev/null 2>&1; then
|
||||||
|
run_root apt-get update
|
||||||
|
run_root apt-get install -y sudo
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_SUDOERS_FILE=\$(mktemp)
|
||||||
|
cat >\"\$TMP_SUDOERS_FILE\" <<EOF
|
||||||
|
${TARGET_RUNTIME_USER_VALUE} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||||
|
${TARGET_RUNTIME_USER_VALUE} ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 440 \"\$TMP_SUDOERS_FILE\"
|
||||||
|
|
||||||
|
visudo -cf \"\$TMP_SUDOERS_FILE\" >/dev/null 2>&1 || {
|
||||||
|
rm -f \"\$TMP_SUDOERS_FILE\"
|
||||||
|
echo 'fichier sudoers généré invalide' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_root install -m 440 \"\$TMP_SUDOERS_FILE\" /etc/sudoers.d/rebuild-bdd-${TARGET_RUNTIME_USER_VALUE}
|
||||||
|
rm -f \"\$TMP_SUDOERS_FILE\"
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Installation du sudoers minimal"
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_PRECHECK_CMD" >/dev/null 2>&1; then
|
||||||
|
log "Installation du sudoers ignorée : élévation de privilèges indisponible sans interaction."
|
||||||
|
elif ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" >/dev/null 2>&1; then
|
||||||
|
log "Installation du sudoers ignorée : privilèges root/sudo insuffisants pour cette étape."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Installation du sudoers minimal désactivée."
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_REPO_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ ! -d $(shell_quote "${TARGET_CLONE_DIR}/.git") ]]; then
|
||||||
|
if [[ $(shell_quote "$TARGET_CLONE_DIR") == / || $(shell_quote "$TARGET_CLONE_DIR") == /root || $(shell_quote "$TARGET_CLONE_DIR") == /home || $(shell_quote "$TARGET_CLONE_DIR") =~ ^/home/[^/]+$ ]]; then
|
||||||
|
echo 'TARGET_CLONE_DIR dangereux refusé' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf $(shell_quote "$TARGET_CLONE_DIR")
|
||||||
|
git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_CLONE_DIR")
|
||||||
|
else
|
||||||
|
git -C $(shell_quote "$TARGET_CLONE_DIR") fetch --prune origin
|
||||||
|
git -C $(shell_quote "$TARGET_CLONE_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH")
|
||||||
|
git -C $(shell_quote "$TARGET_CLONE_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") 2>/dev/null || true
|
||||||
|
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") 2>/dev/null || true
|
||||||
|
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true
|
||||||
|
chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true
|
||||||
|
|
||||||
|
for required_file in \
|
||||||
|
$(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") \
|
||||||
|
$(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") \
|
||||||
|
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") \
|
||||||
|
$(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh"); do
|
||||||
|
if [[ ! -f \"\$required_file\" ]]; then
|
||||||
|
echo \"fichier requis absent après synchronisation du dépôt : \$required_file\" >&2
|
||||||
|
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Clone / mise à jour du dépôt distant"
|
||||||
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" >/dev/null \
|
||||||
|
|| fail "échec de synchronisation du dépôt sur la cible"
|
||||||
|
else
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \
|
||||||
|
|| fail "échec de synchronisation du dépôt sur la cible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_VALIDATE_SUDO_ROOT_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
command -v sudo >/dev/null 2>&1 || {
|
||||||
|
echo 'sudo absent sur la cible' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || {
|
||||||
|
echo 'sudo indisponible pour systemctl' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Validation initiale de sudo"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \
|
||||||
|
|| fail "sudo invalide sur la cible"
|
||||||
|
|
||||||
|
REMOTE_RUN_CHECK_PG_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-postgresql.sh")
|
||||||
|
ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH")
|
||||||
|
|
||||||
|
[[ -f \"\$CHECK_SCRIPT\" ]] || {
|
||||||
|
echo \"script PostgreSQL introuvable : \$CHECK_SCRIPT\" >&2
|
||||||
|
echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR") et TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR")\" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\"
|
||||||
|
|
||||||
|
\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Préparation PostgreSQL via check-postgresql.sh"
|
||||||
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" >/dev/null \
|
||||||
|
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
|
||||||
|
else
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \
|
||||||
|
|| fail "échec de préparation PostgreSQL pendant le bootstrap"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_VALIDATE_SUDO_POSTGRES_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
sudo -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || {
|
||||||
|
echo 'sudo -u postgres indisponible après préparation PostgreSQL' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Validation finale de sudo -u postgres"
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \
|
||||||
|
|| fail "sudo -u postgres invalide sur la cible"
|
||||||
|
|
||||||
|
success "bootstrap initial terminé pour ${TARGET_NAME}"
|
||||||
161
RebuildBdd/create-target-config.sh
Normal file
161
RebuildBdd/create-target-config.sh
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||||
|
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||||
|
|
||||||
|
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||||
|
|
||||||
|
TARGET=""
|
||||||
|
HOST=""
|
||||||
|
PORT="22"
|
||||||
|
BOOTSTRAP_USER=""
|
||||||
|
BOOTSTRAP_SSH_KEY=""
|
||||||
|
RUNTIME_USER=""
|
||||||
|
REPO_DIR=""
|
||||||
|
ENV_FILE=""
|
||||||
|
ENV_NAME=""
|
||||||
|
PGHOST=""
|
||||||
|
PGPORT=""
|
||||||
|
PGUSER=""
|
||||||
|
PGPASSWORD=""
|
||||||
|
DBS=""
|
||||||
|
BACKUP_SUBDIR=""
|
||||||
|
BACKUP_LOG_DIR=""
|
||||||
|
LOCAL_RESTORE_BASE_DIR=""
|
||||||
|
SSH_KEY_TARGET_PATH=""
|
||||||
|
ENABLE_BOOTSTRAP="yes"
|
||||||
|
ALLOW_PASSWORDLESS_SUDO="yes"
|
||||||
|
AUTO_INSTALL_POSTGRES="yes"
|
||||||
|
AUTO_CREATE_PGUSER="yes"
|
||||||
|
PGUSER_SUPERUSER="no"
|
||||||
|
AUTO_CONFIGURE_SUDOERS="no"
|
||||||
|
REMOTE_ROLES_DIR_NAME="user"
|
||||||
|
EXCLUDED_RESTORE_ROLES="postgres"
|
||||||
|
FORCE="no"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--targets-dir) TARGETS_DIR="$2"; shift 2 ;;
|
||||||
|
--target) TARGET="$2"; shift 2 ;;
|
||||||
|
--host) HOST="$2"; shift 2 ;;
|
||||||
|
--port) PORT="$2"; shift 2 ;;
|
||||||
|
--bootstrap-user) BOOTSTRAP_USER="$2"; shift 2 ;;
|
||||||
|
--bootstrap-key) BOOTSTRAP_SSH_KEY="$2"; shift 2 ;;
|
||||||
|
--runtime-user) RUNTIME_USER="$2"; shift 2 ;;
|
||||||
|
--repo-dir) REPO_DIR="$2"; shift 2 ;;
|
||||||
|
--env-file) ENV_FILE="$2"; shift 2 ;;
|
||||||
|
--env-name) ENV_NAME="$2"; shift 2 ;;
|
||||||
|
--pghost) PGHOST="$2"; shift 2 ;;
|
||||||
|
--pgport) PGPORT="$2"; shift 2 ;;
|
||||||
|
--pguser) PGUSER="$2"; shift 2 ;;
|
||||||
|
--pgpassword) PGPASSWORD="$2"; shift 2 ;;
|
||||||
|
--dbs) DBS="$2"; shift 2 ;;
|
||||||
|
--backup-subdir) BACKUP_SUBDIR="$2"; shift 2 ;;
|
||||||
|
--backup-log-dir) BACKUP_LOG_DIR="$2"; shift 2 ;;
|
||||||
|
--local-restore-base-dir) LOCAL_RESTORE_BASE_DIR="$2"; shift 2 ;;
|
||||||
|
--ssh-key-target-path) SSH_KEY_TARGET_PATH="$2"; shift 2 ;;
|
||||||
|
--enable-bootstrap) ENABLE_BOOTSTRAP="$2"; shift 2 ;;
|
||||||
|
--allow-passwordless-sudo) ALLOW_PASSWORDLESS_SUDO="$2"; shift 2 ;;
|
||||||
|
--auto-install-postgres) AUTO_INSTALL_POSTGRES="$2"; shift 2 ;;
|
||||||
|
--auto-create-pguser) AUTO_CREATE_PGUSER="$2"; shift 2 ;;
|
||||||
|
--pguser-superuser) PGUSER_SUPERUSER="$2"; shift 2 ;;
|
||||||
|
--auto-configure-sudoers) AUTO_CONFIGURE_SUDOERS="$2"; shift 2 ;;
|
||||||
|
--remote-roles-dir-name) REMOTE_ROLES_DIR_NAME="$2"; shift 2 ;;
|
||||||
|
--excluded-restore-roles) EXCLUDED_RESTORE_ROLES="$2"; shift 2 ;;
|
||||||
|
--force) FORCE="yes"; shift ;;
|
||||||
|
*) echo "Argument inconnu : $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n "$TARGET" ]] || fail "--target manquant"
|
||||||
|
[[ "$TARGET" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "target invalide"
|
||||||
|
|
||||||
|
[[ -n "$HOST" ]] || fail "--host manquant"
|
||||||
|
[[ -n "$BOOTSTRAP_USER" ]] || fail "--bootstrap-user manquant"
|
||||||
|
[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "--bootstrap-key manquant"
|
||||||
|
[[ -n "$REPO_DIR" ]] || fail "--repo-dir manquant"
|
||||||
|
[[ -n "$ENV_NAME" ]] || fail "--env-name manquant"
|
||||||
|
[[ -n "$PGUSER" ]] || fail "--pguser manquant"
|
||||||
|
[[ -n "$PGPASSWORD" ]] || fail "--pgpassword manquant"
|
||||||
|
[[ -n "$DBS" ]] || fail "--dbs manquant"
|
||||||
|
[[ -n "$BACKUP_SUBDIR" ]] || fail "--backup-subdir manquant"
|
||||||
|
[[ "$PORT" =~ ^[0-9]+$ ]] || fail "--port invalide"
|
||||||
|
|
||||||
|
[[ -n "$RUNTIME_USER" ]] || RUNTIME_USER="$BOOTSTRAP_USER"
|
||||||
|
[[ -n "$ENV_FILE" ]] || ENV_FILE="${REPO_DIR}/.env"
|
||||||
|
[[ -n "$PGHOST" ]] || PGHOST="127.0.0.1"
|
||||||
|
[[ -n "$PGPORT" ]] || PGPORT="5432"
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "--pgport invalide"
|
||||||
|
[[ -n "$BACKUP_LOG_DIR" ]] || BACKUP_LOG_DIR="/home/${RUNTIME_USER}/logs/rebuild_bdd"
|
||||||
|
[[ -n "$LOCAL_RESTORE_BASE_DIR" ]] || LOCAL_RESTORE_BASE_DIR="${REPO_DIR}/restore_tmp"
|
||||||
|
[[ -n "$SSH_KEY_TARGET_PATH" ]] || SSH_KEY_TARGET_PATH="/home/${RUNTIME_USER}/.ssh/id_ed25519_backup_readonly"
|
||||||
|
|
||||||
|
ENABLE_BOOTSTRAP="$(to_bool_yes_no "$ENABLE_BOOTSTRAP")" || fail "--enable-bootstrap invalide"
|
||||||
|
ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$ALLOW_PASSWORDLESS_SUDO")" || fail "--allow-passwordless-sudo invalide"
|
||||||
|
AUTO_INSTALL_POSTGRES="$(to_bool_yes_no "$AUTO_INSTALL_POSTGRES")" || fail "--auto-install-postgres invalide"
|
||||||
|
AUTO_CREATE_PGUSER="$(to_bool_yes_no "$AUTO_CREATE_PGUSER")" || fail "--auto-create-pguser invalide"
|
||||||
|
PGUSER_SUPERUSER="$(to_bool_yes_no "$PGUSER_SUPERUSER")" || fail "--pguser-superuser invalide"
|
||||||
|
AUTO_CONFIGURE_SUDOERS="$(to_bool_yes_no "$AUTO_CONFIGURE_SUDOERS")" || fail "--auto-configure-sudoers invalide"
|
||||||
|
|
||||||
|
mkdir -p "$TARGETS_DIR" || fail "impossible de créer $TARGETS_DIR"
|
||||||
|
|
||||||
|
TARGET_FILE="${TARGETS_DIR}/${TARGET}.env"
|
||||||
|
if [[ -f "$TARGET_FILE" && "$FORCE" != "yes" ]]; then
|
||||||
|
fail "fichier déjà existant : $TARGET_FILE (utiliser --force pour écraser)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >"$TARGET_FILE" <<EOF
|
||||||
|
TARGET_HOST=$(printf '%q' "$HOST")
|
||||||
|
TARGET_PORT=$(printf '%q' "$PORT")
|
||||||
|
TARGET_BOOTSTRAP_USER=$(printf '%q' "$BOOTSTRAP_USER")
|
||||||
|
TARGET_BOOTSTRAP_SSH_KEY=$(printf '%q' "$BOOTSTRAP_SSH_KEY")
|
||||||
|
TARGET_RUNTIME_USER=$(printf '%q' "$RUNTIME_USER")
|
||||||
|
|
||||||
|
TARGET_ENABLE_BOOTSTRAP=$(printf '%q' "$ENABLE_BOOTSTRAP")
|
||||||
|
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=$(printf '%q' "$ALLOW_PASSWORDLESS_SUDO")
|
||||||
|
|
||||||
|
TARGET_REPO_DIR=$(printf '%q' "$REPO_DIR")
|
||||||
|
TARGET_ENV_FILE=$(printf '%q' "$ENV_FILE")
|
||||||
|
|
||||||
|
TARGET_ENV_NAME=$(printf '%q' "$ENV_NAME")
|
||||||
|
TARGET_PGHOST=$(printf '%q' "$PGHOST")
|
||||||
|
TARGET_PGPORT=$(printf '%q' "$PGPORT")
|
||||||
|
TARGET_PGUSER=$(printf '%q' "$PGUSER")
|
||||||
|
TARGET_PGPASSWORD=$(printf '%q' "$PGPASSWORD")
|
||||||
|
TARGET_DBS=$(printf '%q' "$DBS")
|
||||||
|
|
||||||
|
TARGET_BACKUP_SUBDIR=$(printf '%q' "$BACKUP_SUBDIR")
|
||||||
|
|
||||||
|
TARGET_BACKUP_LOG_DIR=$(printf '%q' "$BACKUP_LOG_DIR")
|
||||||
|
TARGET_LOCAL_RESTORE_BASE_DIR=$(printf '%q' "$LOCAL_RESTORE_BASE_DIR")
|
||||||
|
TARGET_SSH_KEY=$(printf '%q' "$SSH_KEY_TARGET_PATH")
|
||||||
|
|
||||||
|
TARGET_REMOTE_ROLES_DIR_NAME=$(printf '%q' "$REMOTE_ROLES_DIR_NAME")
|
||||||
|
TARGET_EXCLUDED_RESTORE_ROLES=$(printf '%q' "$EXCLUDED_RESTORE_ROLES")
|
||||||
|
TARGET_AUTO_INSTALL_POSTGRES=$(printf '%q' "$AUTO_INSTALL_POSTGRES")
|
||||||
|
TARGET_AUTO_CREATE_PGUSER=$(printf '%q' "$AUTO_CREATE_PGUSER")
|
||||||
|
TARGET_PGUSER_SUPERUSER=$(printf '%q' "$PGUSER_SUPERUSER")
|
||||||
|
TARGET_AUTO_CONFIGURE_SUDOERS=$(printf '%q' "$AUTO_CONFIGURE_SUDOERS")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$TARGET_FILE" || fail "chmod impossible sur $TARGET_FILE"
|
||||||
|
|
||||||
|
echo "OK: ${TARGET_FILE}"
|
||||||
519
RebuildBdd/rebuild-bdd-core.sh
Executable file
519
RebuildBdd/rebuild-bdd-core.sh
Executable file
@@ -0,0 +1,519 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||||
|
|
||||||
|
CLI_DB=""
|
||||||
|
CLI_OVERWRITE=""
|
||||||
|
CLI_RESTORE_ROLES=""
|
||||||
|
CLI_REQUEST_ID=""
|
||||||
|
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||||
|
JSON_ONLY="${JSON_ONLY:-no}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--env-file)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
|
||||||
|
ENV_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
|
||||||
|
CLI_DB="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--overwrite)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
|
||||||
|
CLI_OVERWRITE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--restore-roles)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
|
||||||
|
CLI_RESTORE_ROLES="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--request-id)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||||
|
CLI_REQUEST_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
NON_INTERACTIVE="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--json-only)
|
||||||
|
JSON_ONLY="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu : $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
python3 - <<'PY' "$1"
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps(sys.argv[1]))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
print_json_and_exit() {
|
||||||
|
local status="$1"
|
||||||
|
local message="$2"
|
||||||
|
local exit_code="$3"
|
||||||
|
|
||||||
|
printf '{'
|
||||||
|
printf '"status":%s,' "$(json_escape "$status")"
|
||||||
|
printf '"message":%s,' "$(json_escape "$message")"
|
||||||
|
printf '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
|
||||||
|
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
|
||||||
|
printf '"database":%s,' "$(json_escape "${DB:-}")"
|
||||||
|
printf '"dump_file":%s,' "$(json_escape "${LAST_REMOTE_DB_DUMP:-}")"
|
||||||
|
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
|
||||||
|
printf '}\n'
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_stdout() {
|
||||||
|
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_FILE=/dev/stderr
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
echo "$msg" >>"$LOG_FILE"
|
||||||
|
print_stdout "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
print_json_and_exit "error" "$*" 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
download_remote_file() {
|
||||||
|
local remote_path="$1"
|
||||||
|
local local_path="$2"
|
||||||
|
local local_dir
|
||||||
|
|
||||||
|
local_dir="$(dirname "$local_path")"
|
||||||
|
mkdir -p "$local_dir" || fail "impossible de créer le dossier local de restauration : $local_dir"
|
||||||
|
|
||||||
|
if scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Téléchargement scp standard échoué, tentative avec scp -O"
|
||||||
|
scp -O "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_tty() {
|
||||||
|
[[ -t 0 && -t 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_escape_literal() {
|
||||||
|
local s="${1:-}"
|
||||||
|
s="${s//\'/\'\'}"
|
||||||
|
printf "%s" "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_db_name() {
|
||||||
|
local db_name="${1:-}"
|
||||||
|
|
||||||
|
[[ -n "$db_name" ]] || return 1
|
||||||
|
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
build_excluded_roles_regex() {
|
||||||
|
local roles_string="${1:-}"
|
||||||
|
local role
|
||||||
|
local -a escaped_roles=()
|
||||||
|
|
||||||
|
read -r -a roles_array <<< "$roles_string"
|
||||||
|
|
||||||
|
for role in "${roles_array[@]}"; do
|
||||||
|
[[ -n "$role" ]] || continue
|
||||||
|
[[ "$role" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || continue
|
||||||
|
escaped_roles+=("$role")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#escaped_roles[@]}" -eq 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local joined=""
|
||||||
|
local first="yes"
|
||||||
|
for role in "${escaped_roles[@]}"; do
|
||||||
|
if [[ "$first" == "yes" ]]; then
|
||||||
|
joined="$role"
|
||||||
|
first="no"
|
||||||
|
else
|
||||||
|
joined+="|$role"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s' "$joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
send_discord() {
|
||||||
|
local message="$1"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||||
|
has_cmd jq || return 0
|
||||||
|
has_cmd curl || return 0
|
||||||
|
|
||||||
|
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||||
|
|
||||||
|
curl -fsS "$DISCORD_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f \
|
||||||
|
"${LOCAL_DB_DUMP_FILE:-}" \
|
||||||
|
"${LOCAL_ROLES_FILE:-}" \
|
||||||
|
"${FILTERED_ROLES_FILE:-}" \
|
||||||
|
"${ROLES_CREATE_LIST:-}" \
|
||||||
|
"${ROLES_APPLY_FILE:-}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
[[ -f "$ENV_FILE" ]] || {
|
||||||
|
echo '{"status":"error","message":"fichier .env cible introuvable"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||||
|
: "${PGHOST:?Variable PGHOST manquante}"
|
||||||
|
: "${PGPORT:?Variable PGPORT manquante}"
|
||||||
|
: "${PGUSER:?Variable PGUSER manquante}"
|
||||||
|
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
|
||||||
|
: "${DBS:?Variable DBS manquante}"
|
||||||
|
: "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}"
|
||||||
|
: "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}"
|
||||||
|
: "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}"
|
||||||
|
: "${SSH_KEY:?Variable SSH_KEY manquante}"
|
||||||
|
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||||
|
|
||||||
|
LOCAL_RESTORE_BASE_DIR="${LOCAL_RESTORE_BASE_DIR:-${SCRIPT_DIR}/restore_tmp}"
|
||||||
|
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
|
||||||
|
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
|
||||||
|
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
|
||||||
|
|
||||||
|
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
|
||||||
|
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
|
||||||
|
ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}"
|
||||||
|
RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}"
|
||||||
|
|
||||||
|
ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || {
|
||||||
|
echo '{"status":"error","message":"ALLOW_OVERWRITE invalide"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || {
|
||||||
|
echo '{"status":"error","message":"RESTORE_ROLES invalide"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||||
|
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME invalide"
|
||||||
|
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_LOG_DIR" || {
|
||||||
|
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||||
|
SAFE_REQUEST_ID="${REQUEST_ID:-manual}"
|
||||||
|
SAFE_REQUEST_ID="${SAFE_REQUEST_ID//[^a-zA-Z0-9_.-]/_}"
|
||||||
|
|
||||||
|
LOG_FILE="${BACKUP_LOG_DIR}/restore_${ENV_NAME,,}_${SAFE_REQUEST_ID}_${TIMESTAMP}.log"
|
||||||
|
touch "$LOG_FILE" || {
|
||||||
|
echo '{"status":"error","message":"impossible de créer le log"}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_BASE_DIR}/${SAFE_REQUEST_ID}_${TIMESTAMP}"
|
||||||
|
mkdir -p "$LOCAL_RESTORE_DIR" || fail "impossible de créer le dossier temporaire local"
|
||||||
|
|
||||||
|
EXCLUDED_ROLES_REGEX=""
|
||||||
|
if EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex "$EXCLUDED_RESTORE_ROLES")"; then
|
||||||
|
log "Rôles exclus de la restauration : $EXCLUDED_RESTORE_ROLES"
|
||||||
|
else
|
||||||
|
log "Aucun rôle exclu de la restauration."
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename curl; do
|
||||||
|
require_cmd "$cmd"
|
||||||
|
done
|
||||||
|
|
||||||
|
CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh"
|
||||||
|
if [[ -x "$CHECK_SCRIPT" ]]; then
|
||||||
|
log "Précheck PostgreSQL déjà effectué par check-target-readiness.sh"
|
||||||
|
else
|
||||||
|
fail "script introuvable ou non exécutable : $CHECK_SCRIPT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$SSH_KEY" ]] || fail "clé SSH source backup introuvable : $SSH_KEY"
|
||||||
|
[[ -r "$SSH_KEY" ]] || fail "clé SSH source backup non lisible : $SSH_KEY"
|
||||||
|
[[ ! -L "$SSH_KEY" ]] || fail "clé SSH source backup ne doit pas être un lien symbolique : $SSH_KEY"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
)
|
||||||
|
|
||||||
|
SCP_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
)
|
||||||
|
|
||||||
|
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||||
|
|
||||||
|
read -r -a DBS_ARRAY <<< "$DBS"
|
||||||
|
[[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "aucune base définie dans DBS"
|
||||||
|
|
||||||
|
if [[ -z "$REQUESTED_DB" ]]; then
|
||||||
|
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||||
|
fail "REQUESTED_DB manquante en mode non interactif"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_tty; then
|
||||||
|
print_stdout "Bases disponibles :"
|
||||||
|
for i in "${!DBS_ARRAY[@]}"; do
|
||||||
|
print_stdout " $((i + 1))) ${DBS_ARRAY[$i]}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
read -r -p "Sélectionnez le numéro de la base à restaurer : " DB_INDEX
|
||||||
|
[[ "$DB_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de base invalide"
|
||||||
|
(( DB_INDEX >= 1 && DB_INDEX <= ${#DBS_ARRAY[@]} )) || fail "numéro hors plage"
|
||||||
|
REQUESTED_DB="${DBS_ARRAY[$((DB_INDEX - 1))]}"
|
||||||
|
else
|
||||||
|
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB=""
|
||||||
|
for candidate in "${DBS_ARRAY[@]}"; do
|
||||||
|
if [[ "$candidate" == "$REQUESTED_DB" ]]; then
|
||||||
|
DB="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$DB" ]] || fail "base refusée : non présente dans DBS"
|
||||||
|
validate_db_name "$DB" || fail "nom de base invalide"
|
||||||
|
|
||||||
|
log "Environnement : $ENV_NAME"
|
||||||
|
log "Base cible : $DB"
|
||||||
|
log "Request ID : ${REQUEST_ID:-N/A}"
|
||||||
|
log "Overwrite : $ALLOW_OVERWRITE"
|
||||||
|
log "Restore roles : $RESTORE_ROLES"
|
||||||
|
|
||||||
|
if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" \
|
||||||
|
>>"$LOG_FILE" 2>&1; then
|
||||||
|
fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Test SSH vers ${REMOTE_SSH}"
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" "exit 0" >>"$LOG_FILE" 2>&1; then
|
||||||
|
fail "connexion SSH impossible vers ${REMOTE_SSH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_DB_DIR="${BACKUP_REMOTE_DIR}/${DB}"
|
||||||
|
REMOTE_ROLES_DIR="${BACKUP_REMOTE_DIR}/${REMOTE_ROLES_DIR_NAME}"
|
||||||
|
|
||||||
|
LAST_REMOTE_DB_DUMP="$(
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
|
||||||
|
"find '${REMOTE_DB_DIR}' -maxdepth 1 -type f -name '${DB}_*.dump' | LC_ALL=C sort | tail -n 1"
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ -n "$LAST_REMOTE_DB_DUMP" ]] || fail "aucun dump trouvé pour ${DB} dans ${REMOTE_DB_DIR}"
|
||||||
|
log "Dernier dump sélectionné : ${LAST_REMOTE_DB_DUMP}"
|
||||||
|
|
||||||
|
LAST_REMOTE_ROLES_FILE=""
|
||||||
|
if [[ "$RESTORE_ROLES" == "yes" ]]; then
|
||||||
|
LAST_REMOTE_ROLES_FILE="$(
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
|
||||||
|
"find '${REMOTE_ROLES_DIR}' -maxdepth 1 -type f -name 'user_*.sql' | LC_ALL=C sort | tail -n 1"
|
||||||
|
)"
|
||||||
|
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||||
|
log "Dernier fichier rôles sélectionné : ${LAST_REMOTE_ROLES_FILE}"
|
||||||
|
else
|
||||||
|
log "Aucun fichier rôles trouvé ; la restauration des rôles sera ignorée."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Restauration des rôles désactivée."
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOCAL_DB_DUMP_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_DB_DUMP")"
|
||||||
|
LOCAL_ROLES_FILE=""
|
||||||
|
|
||||||
|
log "Téléchargement du dump principal"
|
||||||
|
download_remote_file "$LAST_REMOTE_DB_DUMP" "$LOCAL_DB_DUMP_FILE" \
|
||||||
|
|| fail "échec téléchargement du dump principal"
|
||||||
|
|
||||||
|
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||||
|
LOCAL_ROLES_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_ROLES_FILE")"
|
||||||
|
log "Téléchargement du fichier des rôles"
|
||||||
|
download_remote_file "$LAST_REMOTE_ROLES_FILE" "$LOCAL_ROLES_FILE" \
|
||||||
|
|| fail "échec téléchargement du fichier des rôles"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_EXISTS="$(
|
||||||
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname='$(sql_escape_literal "$DB")'" \
|
||||||
|
2>>"$LOG_FILE" || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$DB_EXISTS" == "1" ]]; then
|
||||||
|
if [[ "$ALLOW_OVERWRITE" != "yes" ]]; then
|
||||||
|
if [[ "$NON_INTERACTIVE" == "yes" || ! -t 0 ]]; then
|
||||||
|
fail "la base existe déjà et overwrite n'est pas autorisé"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -p "La base '${DB}' existe déjà. Voulez-vous l'écraser ? (oui/non) : " CONFIRM_OVERWRITE
|
||||||
|
CONFIRM_OVERWRITE="$(to_bool_yes_no "$CONFIRM_OVERWRITE")" || fail "réponse overwrite invalide"
|
||||||
|
[[ "$CONFIRM_OVERWRITE" == "yes" ]] || fail "restauration annulée par l'utilisateur"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Suppression de la base existante : ${DB}"
|
||||||
|
dropdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" --if-exists "$DB" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec suppression base ${DB}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||||
|
log "Restauration des rôles depuis : ${LOCAL_ROLES_FILE}"
|
||||||
|
|
||||||
|
FILTERED_ROLES_FILE="${LOCAL_RESTORE_DIR}/filtered_$(basename "$LOCAL_ROLES_FILE")"
|
||||||
|
ROLES_CREATE_LIST="${LOCAL_RESTORE_DIR}/roles_to_create_$(basename "$LOCAL_ROLES_FILE")"
|
||||||
|
ROLES_APPLY_FILE="${LOCAL_RESTORE_DIR}/roles_apply_$(basename "$LOCAL_ROLES_FILE")"
|
||||||
|
|
||||||
|
if [[ -n "$EXCLUDED_ROLES_REGEX" ]]; then
|
||||||
|
grep -viE "^(CREATE ROLE|ALTER ROLE) (${EXCLUDED_ROLES_REGEX})\\b" "$LOCAL_ROLES_FILE" \
|
||||||
|
> "$FILTERED_ROLES_FILE" || true
|
||||||
|
else
|
||||||
|
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Une exécution sous un rôle non superuser ne peut pas restaurer l'attribut
|
||||||
|
# SUPERUSER ; on ignore donc ces lignes pour laisser passer le reste.
|
||||||
|
sed -i -E '/^ALTER ROLE .* (NO)?SUPERUSER\b/d' "$FILTERED_ROLES_FILE"
|
||||||
|
|
||||||
|
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
|
||||||
|
|
||||||
|
sed -nE 's/^CREATE ROLE "?([^" ;]+)"?;$/\1/p' "$FILTERED_ROLES_FILE" \
|
||||||
|
> "$ROLES_CREATE_LIST" || true
|
||||||
|
|
||||||
|
if [[ -s "$ROLES_CREATE_LIST" ]]; then
|
||||||
|
while IFS= read -r role_name; do
|
||||||
|
[[ -n "$role_name" ]] || continue
|
||||||
|
[[ "$role_name" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
|
||||||
|
log "Rôle ignoré car non conforme : ${role_name}"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ROLE_EXISTS="$(
|
||||||
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||||
|
"SELECT 1 FROM pg_roles WHERE rolname='$(sql_escape_literal "$role_name")'" \
|
||||||
|
2>>"$LOG_FILE" || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||||
|
log "Création du rôle manquant : ${role_name}"
|
||||||
|
psql -v ON_ERROR_STOP=1 \
|
||||||
|
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres \
|
||||||
|
-c "CREATE ROLE \"${role_name}\";" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec création rôle ${role_name}"
|
||||||
|
else
|
||||||
|
log "Rôle déjà présent : ${role_name}"
|
||||||
|
fi
|
||||||
|
done < "$ROLES_CREATE_LIST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -viE '^CREATE ROLE ' "$FILTERED_ROLES_FILE" > "$ROLES_APPLY_FILE" || true
|
||||||
|
|
||||||
|
log "Application ALTER ROLE / privilèges / memberships"
|
||||||
|
psql -v ON_ERROR_STOP=1 \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
-d postgres \
|
||||||
|
-f "$ROLES_APPLY_FILE" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec restauration rôles"
|
||||||
|
else
|
||||||
|
log "Aucune restauration des rôles effectuée."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Création de la base : ${DB}"
|
||||||
|
createdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$DB" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec création base ${DB}"
|
||||||
|
|
||||||
|
log "Restauration de la base ${DB}"
|
||||||
|
pg_restore \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
-d "$DB" \
|
||||||
|
--clean \
|
||||||
|
--if-exists \
|
||||||
|
--no-owner \
|
||||||
|
--no-privileges \
|
||||||
|
"$LOCAL_DB_DUMP_FILE" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || fail "échec restauration base ${DB}"
|
||||||
|
|
||||||
|
SUCCESS_MESSAGE="✅ REBUILD BDD ${ENV_NAME}
|
||||||
|
Base restaurée : ${DB}
|
||||||
|
Hôte PostgreSQL : ${PGHOST}:${PGPORT}
|
||||||
|
Dump utilisé : $(basename "$LAST_REMOTE_DB_DUMP")
|
||||||
|
Log : ${LOG_FILE}"
|
||||||
|
|
||||||
|
send_discord "$SUCCESS_MESSAGE"
|
||||||
|
|
||||||
|
log "Restauration terminée avec succès pour ${DB}"
|
||||||
|
print_json_and_exit "success" "restauration terminée avec succès" 0
|
||||||
445
RebuildBdd/run-rebuild-bdd.sh
Executable file
445
RebuildBdd/run-rebuild-bdd.sh
Executable file
@@ -0,0 +1,445 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${SCRIPT_DIR}/Config"
|
||||||
|
GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env"
|
||||||
|
TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets"
|
||||||
|
GIT_TOPLEVEL="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
LOCAL_REPO_SUBDIR_DEFAULT=""
|
||||||
|
|
||||||
|
if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then
|
||||||
|
LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}"
|
||||||
|
TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}"
|
||||||
|
|
||||||
|
CLI_TARGET=""
|
||||||
|
CLI_DB=""
|
||||||
|
CLI_OVERWRITE=""
|
||||||
|
CLI_RESTORE_ROLES=""
|
||||||
|
CLI_REQUEST_ID=""
|
||||||
|
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--global-env-file)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; }
|
||||||
|
GLOBAL_ENV_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--targets-dir)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; }
|
||||||
|
TARGETS_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--target)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
|
||||||
|
CLI_TARGET="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
|
||||||
|
CLI_DB="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--overwrite)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
|
||||||
|
CLI_OVERWRITE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--restore-roles)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
|
||||||
|
CLI_RESTORE_ROLES="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--request-id)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
|
||||||
|
CLI_REQUEST_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
NON_INTERACTIVE="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu : $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
to_bool_yes_no() {
|
||||||
|
local v="${1:-}"
|
||||||
|
v="${v,,}"
|
||||||
|
case "$v" in
|
||||||
|
yes|y|oui|o|true|1) echo "yes" ;;
|
||||||
|
# Valeur vide traitée comme "no" pour conserver le comportement historique.
|
||||||
|
no|n|non|false|0|"") echo "no" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_tty() {
|
||||||
|
[[ -t 0 && -t 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_quote() {
|
||||||
|
printf "%q" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "${BOOTSTRAP_JSON:-}" "${REMOTE_RESULT_JSON:-}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE"
|
||||||
|
[[ -d "$TARGETS_DIR" ]] || fail "dossier targets introuvable : $TARGETS_DIR"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$GLOBAL_ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
require_cmd ssh
|
||||||
|
require_cmd git
|
||||||
|
require_cmd python3
|
||||||
|
|
||||||
|
TARGET="${CLI_TARGET:-${TARGET:-}}"
|
||||||
|
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
|
||||||
|
ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}"
|
||||||
|
RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}"
|
||||||
|
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-$(date '+%Y%m%d%H%M%S')_$RANDOM}}"
|
||||||
|
LOG_DIR="${RUN_REBUILD_BDD_LOG_DIR:-${SCRIPT_DIR}/logs}"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
LOG_FILE="${LOG_DIR}/run_rebuild_bdd_${REQUEST_ID}.log"
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || fail "ALLOW_OVERWRITE invalide"
|
||||||
|
RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || fail "RESTORE_ROLES invalide"
|
||||||
|
|
||||||
|
if [[ -z "$TARGET" ]]; then
|
||||||
|
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||||
|
fail "TARGET manquante en mode non interactif"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t TARGET_LIST < <(find "$TARGETS_DIR" -maxdepth 1 -type f -name '*.env' -printf '%f\n' | sed 's/\.env$//' | LC_ALL=C sort)
|
||||||
|
|
||||||
|
[[ "${#TARGET_LIST[@]}" -gt 0 ]] || fail "aucune cible définie dans ${TARGETS_DIR}"
|
||||||
|
|
||||||
|
if is_tty; then
|
||||||
|
echo "Cibles disponibles :"
|
||||||
|
for i in "${!TARGET_LIST[@]}"; do
|
||||||
|
echo " $((i + 1))) ${TARGET_LIST[$i]}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
read -r -p "Sélectionnez le numéro de la cible : " TARGET_INDEX
|
||||||
|
[[ "$TARGET_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de cible invalide"
|
||||||
|
(( TARGET_INDEX >= 1 && TARGET_INDEX <= ${#TARGET_LIST[@]} )) || fail "numéro hors plage"
|
||||||
|
TARGET="${TARGET_LIST[$((TARGET_INDEX - 1))]}"
|
||||||
|
else
|
||||||
|
fail "TARGET manquante et aucune interaction terminal disponible"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET}.env"
|
||||||
|
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$TARGET_ENV_SOURCE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
TARGET_HOST="${TARGET_HOST:-}"
|
||||||
|
TARGET_PORT="${TARGET_PORT:-22}"
|
||||||
|
TARGET_USER="${TARGET_BOOTSTRAP_USER:-}"
|
||||||
|
TARGET_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}"
|
||||||
|
TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}"
|
||||||
|
TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-main}}"
|
||||||
|
TARGET_REPO_DIR="${TARGET_REPO_DIR:-}"
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}"
|
||||||
|
TARGET_ENV_FILE="${TARGET_ENV_FILE:-}"
|
||||||
|
TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-yes}}"
|
||||||
|
|
||||||
|
[[ -n "$TARGET_HOST" ]] || fail "TARGET_HOST manquante"
|
||||||
|
[[ "$TARGET_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide"
|
||||||
|
[[ -n "$TARGET_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante"
|
||||||
|
[[ -n "$TARGET_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante"
|
||||||
|
[[ -f "$TARGET_SSH_KEY" ]] || fail "clé SSH cible introuvable : $TARGET_SSH_KEY"
|
||||||
|
[[ -r "$TARGET_SSH_KEY" ]] || fail "clé SSH cible non lisible : $TARGET_SSH_KEY"
|
||||||
|
|
||||||
|
[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant"
|
||||||
|
[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant"
|
||||||
|
[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante"
|
||||||
|
[[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE manquante"
|
||||||
|
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}"
|
||||||
|
TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}"
|
||||||
|
|
||||||
|
TARGET_CLONE_DIR="$TARGET_REPO_DIR"
|
||||||
|
TARGET_SCRIPT_DIR="$TARGET_REPO_DIR"
|
||||||
|
if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||||
|
if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then
|
||||||
|
TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")"
|
||||||
|
else
|
||||||
|
TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for critical_dir in "$TARGET_CLONE_DIR" "$TARGET_SCRIPT_DIR" "$TARGET_REPO_DIR"; do
|
||||||
|
[[ -n "$critical_dir" ]] || fail "répertoire critique vide"
|
||||||
|
[[ "$critical_dir" != "/" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ "$critical_dir" != "/root" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ "$critical_dir" != "/home" ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
[[ ! "$critical_dir" =~ ^/home/[^/]+$ ]] || fail "répertoire critique dangereux refusé : $critical_dir"
|
||||||
|
done
|
||||||
|
|
||||||
|
TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide"
|
||||||
|
|
||||||
|
BOOTSTRAP_SCRIPT_LOCAL="${SCRIPT_DIR}/bootstrap-target-host.sh"
|
||||||
|
[[ -f "$BOOTSTRAP_SCRIPT_LOCAL" ]] || fail "script bootstrap introuvable : $BOOTSTRAP_SCRIPT_LOCAL"
|
||||||
|
[[ -x "$BOOTSTRAP_SCRIPT_LOCAL" ]] || chmod 700 "$BOOTSTRAP_SCRIPT_LOCAL" || fail "chmod impossible sur $BOOTSTRAP_SCRIPT_LOCAL"
|
||||||
|
|
||||||
|
if [[ -z "$REQUESTED_DB" ]]; then
|
||||||
|
DBS_FOR_TARGET="${TARGET_DBS:-}"
|
||||||
|
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||||
|
fail "REQUESTED_DB manquante en mode non interactif"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -a DBS_ARRAY <<< "$DBS_FOR_TARGET"
|
||||||
|
[[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "TARGET_DBS vide"
|
||||||
|
|
||||||
|
if is_tty; then
|
||||||
|
echo "Bases disponibles :"
|
||||||
|
for i in "${!DBS_ARRAY[@]}"; do
|
||||||
|
echo " $((i + 1))) ${DBS_ARRAY[$i]}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
read -r -p "Nom exact de la base à restaurer : " REQUESTED_DB
|
||||||
|
else
|
||||||
|
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$REQUESTED_DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide"
|
||||||
|
|
||||||
|
if [[ "$TARGET_ENABLE_BOOTSTRAP" == "yes" ]]; then
|
||||||
|
log "Bootstrap initial activé pour la cible ${TARGET}"
|
||||||
|
BOOTSTRAP_JSON="/tmp/bootstrap_target_${REQUEST_ID}.json"
|
||||||
|
|
||||||
|
"$BOOTSTRAP_SCRIPT_LOCAL" \
|
||||||
|
--global-env-file "$GLOBAL_ENV_FILE" \
|
||||||
|
--targets-dir "$TARGETS_DIR" \
|
||||||
|
--target "$TARGET" \
|
||||||
|
--json-only >"$BOOTSTRAP_JSON" || {
|
||||||
|
cat "$BOOTSTRAP_JSON" 2>/dev/null || true
|
||||||
|
fail "échec du bootstrap initial de la cible ${TARGET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOTSTRAP_STATUS="$(
|
||||||
|
python3 - <<'PY' "$BOOTSTRAP_JSON"
|
||||||
|
import json, sys
|
||||||
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(data.get("status", "error"))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$BOOTSTRAP_STATUS" != "success" ]]; then
|
||||||
|
cat "$BOOTSTRAP_JSON"
|
||||||
|
fail "bootstrap initial échoué pour la cible ${TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Bootstrap initial terminé pour ${TARGET}"
|
||||||
|
else
|
||||||
|
log "Bootstrap initial désactivé pour ${TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$TARGET_SSH_KEY"
|
||||||
|
-p "$TARGET_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
-o ConnectTimeout=8
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \
|
||||||
|
|| fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}"
|
||||||
|
|
||||||
|
TARGET_CORE_SCRIPT="${TARGET_SCRIPT_DIR}/rebuild-bdd-core.sh"
|
||||||
|
REMOTE_RESULT_JSON="/tmp/run_rebuild_bdd_${REQUEST_ID}.json"
|
||||||
|
|
||||||
|
REMOTE_BOOTSTRAP_CMD="
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CLONE_DIR=$(shell_quote "$TARGET_CLONE_DIR")
|
||||||
|
REPO_DIR=$(shell_quote "$TARGET_SCRIPT_DIR")
|
||||||
|
REPO_URL=$(shell_quote "$TARGET_REPO_URL")
|
||||||
|
REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")
|
||||||
|
CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT")
|
||||||
|
PRECHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-target-readiness.sh")
|
||||||
|
TARGET_ENV_FILE=$(shell_quote "$TARGET_ENV_FILE")
|
||||||
|
REQUESTED_DB=$(shell_quote "$REQUESTED_DB")
|
||||||
|
ALLOW_OVERWRITE=$(shell_quote "$ALLOW_OVERWRITE")
|
||||||
|
RESTORE_ROLES=$(shell_quote "$RESTORE_ROLES")
|
||||||
|
REQUEST_ID=$(shell_quote "$REQUEST_ID")
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"git absent sur la cible\"}'; exit 1; }
|
||||||
|
command -v bash >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"bash absent sur la cible\"}'; exit 1; }
|
||||||
|
command -v python3 >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"python3 absent sur la cible\"}'; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p \"\$(dirname \"\$CLONE_DIR\")\"
|
||||||
|
mkdir -p \"\$(dirname \"\$REPO_DIR\")\"
|
||||||
|
|
||||||
|
if [[ ! -d \"\$CLONE_DIR/.git\" ]]; then
|
||||||
|
if [[ \"\$CLONE_DIR\" == / || \"\$CLONE_DIR\" == /root || \"\$CLONE_DIR\" == /home || \"\$CLONE_DIR\" =~ ^/home/[^/]+$ ]]; then
|
||||||
|
echo '{\"status\":\"error\",\"message\":\"TARGET_CLONE_DIR dangereux refusé\"}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf \"\$CLONE_DIR\"
|
||||||
|
git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_DIR\" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
git -C \"\$CLONE_DIR\" fetch --prune origin >/dev/null 2>&1
|
||||||
|
git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" >/dev/null 2>&1
|
||||||
|
git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; }
|
||||||
|
[[ -f \"\$PRECHECK_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script précheck introuvable sur la cible\"}'; exit 1; }
|
||||||
|
|
||||||
|
chmod 700 \"\$CORE_SCRIPT\"
|
||||||
|
chmod 700 \"\$PRECHECK_SCRIPT\"
|
||||||
|
|
||||||
|
PRECHECK_JSON=\"/tmp/check_target_\${REQUEST_ID}.json\"
|
||||||
|
PRECHECK_STDERR=\"/tmp/check_target_\${REQUEST_ID}.stderr\"
|
||||||
|
CORE_JSON=\"/tmp/rebuild_target_\${REQUEST_ID}.json\"
|
||||||
|
CORE_STDERR=\"/tmp/rebuild_target_\${REQUEST_ID}.stderr\"
|
||||||
|
|
||||||
|
\"\$PRECHECK_SCRIPT\" \
|
||||||
|
--env-file \"\$TARGET_ENV_FILE\" \
|
||||||
|
--request-id \"\$REQUEST_ID\" \
|
||||||
|
--non-interactive \
|
||||||
|
--json-only >\"\$PRECHECK_JSON\" 2>\"\$PRECHECK_STDERR\" || {
|
||||||
|
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
|
||||||
|
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
PRECHECK_STATUS=\"\$(python3 - <<'PY' \"\$PRECHECK_JSON\"
|
||||||
|
import json, sys
|
||||||
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(data.get('status', 'error'))
|
||||||
|
PY
|
||||||
|
)\" || {
|
||||||
|
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$PRECHECK_JSON\" 2>/dev/null || true
|
||||||
|
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then
|
||||||
|
cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$PRECHECK_JSON\"
|
||||||
|
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\"
|
||||||
|
|
||||||
|
\"\$CORE_SCRIPT\" \
|
||||||
|
--env-file \"\$TARGET_ENV_FILE\" \
|
||||||
|
--db \"\$REQUESTED_DB\" \
|
||||||
|
--overwrite \"\$ALLOW_OVERWRITE\" \
|
||||||
|
--restore-roles \"\$RESTORE_ROLES\" \
|
||||||
|
--request-id \"\$REQUEST_ID\" \
|
||||||
|
--non-interactive \
|
||||||
|
--json-only >\"\$CORE_JSON\" 2>\"\$CORE_STDERR\" || {
|
||||||
|
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$CORE_JSON\" 2>/dev/null || true
|
||||||
|
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
CORE_STATUS=\"\$(python3 - <<'PY' \"\$CORE_JSON\"
|
||||||
|
import json, sys
|
||||||
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(data.get('status', 'error'))
|
||||||
|
PY
|
||||||
|
)\" || {
|
||||||
|
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$CORE_JSON\" 2>/dev/null || true
|
||||||
|
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ \"\$CORE_STATUS\" != \"success\" ]]; then
|
||||||
|
cat \"\$CORE_STDERR\" >&2 2>/dev/null || true
|
||||||
|
cat \"\$CORE_JSON\"
|
||||||
|
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat \"\$CORE_JSON\"
|
||||||
|
rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||||
|
"
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" 2>&1 \
|
||||||
|
|| {
|
||||||
|
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
|
||||||
|
fail "échec d'exécution distante sur la cible ${TARGET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
REMOTE_STATUS="$(
|
||||||
|
python3 - <<'PY' "$REMOTE_RESULT_JSON"
|
||||||
|
import json, sys
|
||||||
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(data.get("status", "error"))
|
||||||
|
PY
|
||||||
|
)" || {
|
||||||
|
cat "$REMOTE_RESULT_JSON" 2>/dev/null || true
|
||||||
|
fail "réponse JSON invalide renvoyée par la cible ${TARGET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$REMOTE_STATUS" != "success" ]]; then
|
||||||
|
cat "$REMOTE_RESULT_JSON"
|
||||||
|
fail "restauration distante échouée pour la cible ${TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<'PY' "$REMOTE_RESULT_JSON"
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
message = data.get("message", "restauration terminée")
|
||||||
|
environment = data.get("environment") or "N/A"
|
||||||
|
database = data.get("database") or "N/A"
|
||||||
|
request_id = data.get("request_id") or "N/A"
|
||||||
|
dump_file = data.get("dump_file") or "N/A"
|
||||||
|
log_file = data.get("log_file") or "N/A"
|
||||||
|
|
||||||
|
print(f"[{request_id}] {message}")
|
||||||
|
print(f"Environnement : {environment}")
|
||||||
|
print(f"Base : {database}")
|
||||||
|
print(f"Dump : {dump_file}")
|
||||||
|
print(f"Log : {log_file}")
|
||||||
|
PY
|
||||||
418
RecetteScripts/README.md
Normal file
418
RecetteScripts/README.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# RecetteScripts
|
||||||
|
|
||||||
|
Scripts Bash permettant d’automatiser la gestion d’un environnement **PostgreSQL de recette**.
|
||||||
|
|
||||||
|
Ces scripts permettent :
|
||||||
|
|
||||||
|
* la **sauvegarde automatisée des bases**
|
||||||
|
* la **surveillance de la disponibilité des applications**
|
||||||
|
* la **reconstruction d’une base à partir d’un dump**
|
||||||
|
|
||||||
|
Chaque script possède son propre **fichier `.env` dédié** afin de séparer les configurations.(un global.env.exemple est disponible à la racine du projet)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0. Arborescence du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
RecetteScripts
|
||||||
|
│
|
||||||
|
├── backup-bdd-recette.sh # script de sauvegarde PostgreSQL
|
||||||
|
├── backup.env.exemple # exemple de configuration backup
|
||||||
|
│
|
||||||
|
├── check-statut-recette.sh # script de monitoring des applications
|
||||||
|
├── check-statut.env.exemple # exemple de configuration monitoring
|
||||||
|
│
|
||||||
|
├── rebuild-bdd-recette.sh # script de restauration PostgreSQL
|
||||||
|
├── rebuild.env.exemple # exemple de configuration restauration
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Principe général
|
||||||
|
|
||||||
|
Les scripts fonctionnent indépendamment mais utilisent le même principe :
|
||||||
|
|
||||||
|
1. chargement d’un fichier `.env`
|
||||||
|
2. vérification des variables obligatoires
|
||||||
|
3. exécution de la tâche principale
|
||||||
|
4. génération de logs
|
||||||
|
5. notification Discord (optionnelle)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Prérequis
|
||||||
|
|
||||||
|
Environnement Linux recommandé.
|
||||||
|
|
||||||
|
Packages nécessaires 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 d’exemple** :
|
||||||
|
|
||||||
|
```
|
||||||
|
backup.env.exemple
|
||||||
|
check-statut.env.exemple
|
||||||
|
rebuild.env.exemple
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour utiliser les scripts :
|
||||||
|
|
||||||
|
```
|
||||||
|
cp backup.env.exemple .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis modifier les variables.
|
||||||
|
|
||||||
|
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 l’exécution simultanée (lock)
|
||||||
|
4. exporte les rôles PostgreSQL
|
||||||
|
5. crée un dump de chaque base
|
||||||
|
6. transfère les dumps vers un serveur distant
|
||||||
|
7. applique une rotation des sauvegardes
|
||||||
|
8. envoie un résumé sur Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Format des fichiers
|
||||||
|
|
||||||
|
Dump base :
|
||||||
|
|
||||||
|
```
|
||||||
|
base_TIMESTAMP.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
Export utilisateurs :
|
||||||
|
|
||||||
|
```
|
||||||
|
user_TIMESTAMP.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation automatique
|
||||||
|
|
||||||
|
Suppression des sauvegardes plus anciennes que :
|
||||||
|
|
||||||
|
```
|
||||||
|
10 jours
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">
|
||||||
|
<strong>EggMaster</strong>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Question 5</summary>
|
||||||
|
|
||||||
|
Quelle option demande explicitement un decodage plutot qu'un encodage ?
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Indice commande 5</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
-d
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="list-style: none; cursor: pointer;">Fragment 5</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNR
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# 7. Script : rebuild-bdd-recette.sh
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Restaurer une base PostgreSQL à partir d’un dump distant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnement
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
|
||||||
|
1. charge la configuration `.env`
|
||||||
|
2. installe PostgreSQL si nécessaire
|
||||||
|
3. démarre le service PostgreSQL
|
||||||
|
4. demande la base à restaurer
|
||||||
|
5. récupère le dernier dump sur le serveur distant
|
||||||
|
6. récupère le dernier export des rôles
|
||||||
|
7. crée les rôles manquants
|
||||||
|
8. supprime la base existante si nécessaire
|
||||||
|
9. restaure la base via `pg_restore`
|
||||||
|
10. envoie une notification Discord
|
||||||
|
|
||||||
|
Le script supporte désormais le port SSH distant, un fichier `known_hosts` dédié et l’exclusion configurable des rôles via `EXCLUDED_RESTORE_ROLES`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sélection de la base
|
||||||
|
|
||||||
|
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
|
||||||
|
---
|
||||||
585
RecetteScripts/backup-bdd-recette.sh
Executable file
585
RecetteScripts/backup-bdd-recette.sh
Executable file
@@ -0,0 +1,585 @@
|
|||||||
|
#!/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 l’ensemble vers une machine distante de stockage.
|
||||||
|
#
|
||||||
|
# Fonctionnement global :
|
||||||
|
# 1. charge la configuration depuis le fichier .env ;
|
||||||
|
# 2. vérifie les dépendances nécessaires ;
|
||||||
|
# 3. prépare les chemins, logs et variables de connexion ;
|
||||||
|
# 4. empêche l’exécution simultanée grâce à un verrou ;
|
||||||
|
# 5. crée les dossiers de destination sur la machine distante ;
|
||||||
|
# 6. exporte les rôles PostgreSQL ;
|
||||||
|
# 7. dump chaque base au format personnalisé PostgreSQL ;
|
||||||
|
# 8. transfère chaque fichier vers le serveur distant ;
|
||||||
|
# 9. applique une rotation distante selon BACKUP_RETENTION_DAYS
|
||||||
|
# (10 jours par défaut) ;
|
||||||
|
# 10. envoie un bilan sur Discord :
|
||||||
|
# - 1 message global si tout est OK ;
|
||||||
|
# - en cas d’erreur partielle :
|
||||||
|
# * 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}"
|
||||||
|
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||||
|
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
: "${PGHOST:?Variable PGHOST manquante}"
|
||||||
|
: "${PGPORT:?Variable PGPORT manquante}"
|
||||||
|
: "${PGUSER:?Variable PGUSER manquante}"
|
||||||
|
: "${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}"
|
||||||
|
[[ "$BACKUP_REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||||
|
echo "Variable BACKUP_REMOTE_DIR invalide : $BACKUP_REMOTE_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
: "${SSH_KEY:?Variable SSH_KEY manquante}"
|
||||||
|
: "${SSH_TIMEOUT:?Variable SSH_TIMEOUT manquante}"
|
||||||
|
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# 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="${BACKUP_RETENTION_DAYS:-10}"
|
||||||
|
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||||
|
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
|
||||||
|
BACKUP_KNOWN_HOSTS_FILE="${BACKUP_KNOWN_HOSTS_FILE:-${HOME}/.ssh/known_hosts}"
|
||||||
|
|
||||||
|
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: BACKUP_REMOTE_SSH_PORT invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: PGPORT invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$SSH_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: SSH_TIMEOUT invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
|
||||||
|
echo "ERROR: PGUSER invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||||
|
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||||
|
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: BACKUP_KNOWN_HOSTS_STRICT invalide" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||||
|
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||||
|
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||||
|
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||||
|
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||||
|
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
SCP_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||||
|
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||||
|
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
LOG_DIR="${BACKUP_LOG_DIR}"
|
||||||
|
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"
|
||||||
|
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d /tmp/pg_dump_XXXXXX)" || {
|
||||||
|
echo "ERROR: impossible de créer le dossier temporaire" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande manquante : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_remove_dir() {
|
||||||
|
local dir="${1:-}"
|
||||||
|
[[ -n "$dir" ]] || return 0
|
||||||
|
[[ "$dir" == /tmp/pg_dump_* ]] || {
|
||||||
|
log "WARNING: suppression refusée pour le chemin inattendu : $dir"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rm -rf -- "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Vérification dépendances minimales
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
for cmd in ssh scp curl jq pg_dump pg_dumpall mktemp; do
|
||||||
|
require_cmd "$cmd"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -f "$SSH_KEY" ]] || {
|
||||||
|
echo "ERROR: clé SSH introuvable : $SSH_KEY" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -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}"
|
||||||
|
|
||||||
|
send_discord() {
|
||||||
|
local msg="$1"
|
||||||
|
local payload
|
||||||
|
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||||
|
|
||||||
|
payload="$(jq -n --arg content "$msg" '{content: $content}')" || {
|
||||||
|
log "ERROR: impossible de construire le payload JSON Discord"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)"
|
||||||
|
send_discord "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Messages USERS
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
discord_msg_users_ok_simple() {
|
||||||
|
local msg
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**BACKUP BDD ${ENV_NAME} 🟢**
|
||||||
|
Users backup validé
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
send_discord "$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
|
||||||
|
|
||||||
|
send_discord "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Messages DB
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
discord_msg_db_ok_simple() {
|
||||||
|
local db="$1"
|
||||||
|
local msg
|
||||||
|
msg="$(cat <<EOF
|
||||||
|
**BACKUP BDD ${ENV_NAME} 🟢**
|
||||||
|
Backup validé : ${db}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
send_discord "$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
|
||||||
|
|
||||||
|
send_discord "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Variables de statut globales
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
DUMPS_OK=true
|
||||||
|
USERS_OK=true
|
||||||
|
|
||||||
|
USERS_EXPORT_OK=true
|
||||||
|
USERS_TRANSFER_OK=true
|
||||||
|
USERS_DETAILS=""
|
||||||
|
|
||||||
|
declare -A DB_DUMP_OK
|
||||||
|
declare -A DB_TRANSFER_OK
|
||||||
|
declare -A DB_DETAILS
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Verrou d’exécution
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
||||||
|
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
||||||
|
|
||||||
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||||
|
stale_lock="no"
|
||||||
|
existing_pid=""
|
||||||
|
|
||||||
|
if [[ -f "$LOCK_PID_FILE" ]]; then
|
||||||
|
existing_pid="$(<"$LOCK_PID_FILE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
||||||
|
log "ERROR: Backup déjà en cours (PID ${existing_pid})"
|
||||||
|
discord_msg_users_error "" "" "Lock already exists (PID ${existing_pid})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stale_lock="yes"
|
||||||
|
log "WARNING: lock périmé détecté, nettoyage en cours"
|
||||||
|
rm -rf -- "$LOCK_DIR"
|
||||||
|
|
||||||
|
mkdir "$LOCK_DIR" 2>/dev/null || fail "impossible de recréer le lock après nettoyage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo $$ > "$LOCK_PID_FILE" || {
|
||||||
|
rm -rf -- "$LOCK_DIR"
|
||||||
|
fail "impossible d'écrire le PID du lock"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${stale_lock:-no}" == "yes" ]]; then
|
||||||
|
log "Lock périmé nettoyé."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf -- "$LOCK_DIR"
|
||||||
|
safe_remove_dir "$TMP_DIR" || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Préparation du dossier distant
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
log "Export des rôles PostgreSQL"
|
||||||
|
|
||||||
|
if pg_dumpall \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
--globals-only \
|
||||||
|
> "$ROLES_FILE"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
if scp "${SCP_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/user/"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
USERS_OK=
|
||||||
|
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
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Dump des bases
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
if pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
DUMPS_OK=
|
||||||
|
DB_DUMP_OK["$DB"]=
|
||||||
|
DB_TRANSFER_OK["$DB"]=
|
||||||
|
DB_DETAILS["$DB"]="dump failed"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/${DB}/"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
DUMPS_OK=
|
||||||
|
DB_TRANSFER_OK["$DB"]=
|
||||||
|
DB_DETAILS["$DB"]="transfer failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Rotation distante
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
log "Starting remote rotation: delete backups older than ${RETENTION_DAYS} days"
|
||||||
|
|
||||||
|
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
log "ERROR: remote rotation failed for users"
|
||||||
|
else
|
||||||
|
log "Remote rotation OK for users"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for DB in "${DBS_ARRAY[@]}"; do
|
||||||
|
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"; then
|
||||||
|
RET=0
|
||||||
|
else
|
||||||
|
RET=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $RET -ne 0 ]]; then
|
||||||
|
log "ERROR: remote rotation failed for ${DB}"
|
||||||
|
else
|
||||||
|
log "Remote rotation OK for ${DB}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
74
RecetteScripts/backup.env.exemple
Normal file
74
RecetteScripts/backup.env.exemple
Normal 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
|
||||||
253
RecetteScripts/check-statut-recette.sh
Executable file
253
RecetteScripts/check-statut-recette.sh
Executable file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/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}"
|
||||||
|
|
||||||
|
[[ "$CHECK_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: Variable CHECK_CONNECT_TIMEOUT invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$CHECK_MAX_TIME" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "ERROR: Variable CHECK_MAX_TIME invalide" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Sites à vérifier
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
read -r -a SITES <<< "$APP_URLS"
|
||||||
|
|
||||||
|
SCHEME="${APP_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
|
||||||
|
TMPFILES=()
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local tmpfile
|
||||||
|
for tmpfile in "${TMPFILES[@]}"; do
|
||||||
|
[[ -n "$tmpfile" ]] || continue
|
||||||
|
rm -f -- "$tmpfile"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# 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
|
||||||
|
#######################################
|
||||||
|
should_send_discord() {
|
||||||
|
if [[ "$FAILURES" -gt 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local current_hour current_minute
|
||||||
|
current_hour="$(date +'%H')"
|
||||||
|
current_minute="$(date +'%M')"
|
||||||
|
|
||||||
|
[[ "$current_hour" == "19" && "$current_minute" -ge 0 && "$current_minute" -le 4 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
send_discord() {
|
||||||
|
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||||
|
should_send_discord || return 0
|
||||||
|
|
||||||
|
local header_icon ping_prefix=""
|
||||||
|
if [[ "$FAILURES" -eq 0 ]]; then
|
||||||
|
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}')" || return 0
|
||||||
|
|
||||||
|
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)"
|
||||||
|
TMPFILES+=("$stderr")
|
||||||
|
|
||||||
|
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')"
|
||||||
|
|
||||||
|
log_line "DOWN" "$host" "curl exit=$curl_exit : ${err:-"(aucun)"}"
|
||||||
|
add_summary_line "$host" "DOWN" "DOWN - curl"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if [[ "$failures" -gt 0 ]]; then
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
42
RecetteScripts/check-statut.env.exemple
Normal file
42
RecetteScripts/check-statut.env.exemple
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Nom de l'environnement surveillé
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LOGS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Dossier où seront stockés les logs du script
|
||||||
|
APP_LOG_DIR=/var/log/script
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# PARAMÈTRES DE VÉRIFICATION HTTP
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Timeout de connexion à l'application (secondes)
|
||||||
|
# Si le serveur ne répond pas dans ce délai, la connexion échoue
|
||||||
|
CHECK_CONNECT_TIMEOUT=5
|
||||||
|
|
||||||
|
# Temps maximum total autorisé pour la requête HTTP (secondes)
|
||||||
|
CHECK_MAX_TIME=10
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# APPLICATIONS À SURVEILLER
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Liste des applications à vérifier (séparées par des espaces)
|
||||||
|
|
||||||
|
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# DISCORD
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Webhook Discord pour envoyer le résumé des vérifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Mention Discord en cas de problème
|
||||||
|
DISCORD_PING=@here
|
||||||
506
RecetteScripts/rebuild-bdd-recette.sh
Normal file
506
RecetteScripts/rebuild-bdd-recette.sh
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# 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}"
|
||||||
|
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||||
|
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
: "${PGHOST:?Variable PGHOST manquante}"
|
||||||
|
: "${PGPORT:?Variable PGPORT manquante}"
|
||||||
|
: "${PGUSER:?Variable PGUSER manquante}"
|
||||||
|
: "${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:-}"
|
||||||
|
rm -rf "${LOCAL_RESTORE_DIR:-}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_escape_literal() {
|
||||||
|
local s="${1:-}"
|
||||||
|
s="${s//\'/\'\'}"
|
||||||
|
printf "%s" "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_db_name() {
|
||||||
|
local db_name="${1:-}"
|
||||||
|
|
||||||
|
[[ -n "$db_name" ]] || return 1
|
||||||
|
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
build_excluded_roles_regex() {
|
||||||
|
local role regex=""
|
||||||
|
|
||||||
|
for role in $EXCLUDED_RESTORE_ROLES; do
|
||||||
|
[[ -z "$role" ]] && continue
|
||||||
|
[[ "$role" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || fail "rôle exclu invalide : ${role}"
|
||||||
|
if [[ -n "$regex" ]]; then
|
||||||
|
regex+="|"
|
||||||
|
fi
|
||||||
|
regex+="$role"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s' "$regex"
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Envoi Discord
|
||||||
|
#
|
||||||
|
# Envoi simple d'un message texte via webhook Discord.
|
||||||
|
# Si DISCORD_WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||||
|
###############################################################################
|
||||||
|
send_discord() {
|
||||||
|
local message="$1"
|
||||||
|
local payload=""
|
||||||
|
|
||||||
|
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||||
|
has_cmd jq || return 0
|
||||||
|
has_cmd curl || return 0
|
||||||
|
|
||||||
|
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||||
|
|
||||||
|
curl -fsS "$DISCORD_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Vérifications de base
|
||||||
|
###############################################################################
|
||||||
|
[[ -f "$SSH_KEY" ]] || fail "clé SSH introuvable : $SSH_KEY"
|
||||||
|
[[ -r "$SSH_KEY" ]] || fail "clé SSH non lisible : $SSH_KEY"
|
||||||
|
[[ ! -L "$SSH_KEY" ]] || fail "clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||||
|
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||||
|
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||||
|
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i "$SSH_KEY"
|
||||||
|
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||||
|
-o IdentitiesOnly=yes
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||||
|
-o StrictHostKeyChecking=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 ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb; then
|
||||||
|
log "PostgreSQL absent : installation en cours..."
|
||||||
|
|
||||||
|
sudo apt update >>"$LOG_FILE" 2>&1 || fail "échec de apt update"
|
||||||
|
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..."
|
||||||
|
PG_READY=false
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
PG_READY=true
|
||||||
|
log "PostgreSQL répond correctement."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$PG_READY" != true ]]; 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" || fail "nom de base invalide"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
[[ -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 "$SUCCESS_MESSAGE"
|
||||||
92
RecetteScripts/rebuild.env.exemple
Normal file
92
RecetteScripts/rebuild.env.exemple
Normal 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=
|
||||||
210
backup_pg.sh
210
backup_pg.sh
@@ -1,210 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Configuration
|
|
||||||
#######################################
|
|
||||||
DBS=("sirh" "inventory" "ferme")
|
|
||||||
|
|
||||||
PGHOST="localhost"
|
|
||||||
PGPORT="5432"
|
|
||||||
PGUSER="backup_liot"
|
|
||||||
PGPASSWORD="backup_liot"
|
|
||||||
|
|
||||||
IA_SSH="malio-b@192.168.0.179"
|
|
||||||
IA_BASE_DIR="/home/malio-b/backups"
|
|
||||||
|
|
||||||
SSH_KEY="/home/malio/.ssh/id_ed25519_backup"
|
|
||||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
|
||||||
|
|
||||||
LOG_DIR="/var/log/pg_backup"
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
|
|
||||||
TS="$(date +'%Y-%m-%d_%H-%M-%S')"
|
|
||||||
BACKUP_DIR_NAME="backup_${TS}"
|
|
||||||
LOG_FILE="${LOG_DIR}/${BACKUP_DIR_NAME}.log"
|
|
||||||
|
|
||||||
TMP_DIR="/tmp/pg_dump_${BACKUP_DIR_NAME}"
|
|
||||||
mkdir -p "$TMP_DIR"
|
|
||||||
|
|
||||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
||||||
log() { echo "---- $(date +'%Y-%m-%d %H:%M:%S') ---- $*"; }
|
|
||||||
|
|
||||||
export PGPASSWORD
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Discord (Webhook)
|
|
||||||
#######################################
|
|
||||||
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1478503102888935506/YCtJM09QZiKNMiCe5u7vCQb52VcLjHAd9wwEsKNltlJVcy7sKvoMTOJkvEKOOrk-Wpkh"
|
|
||||||
|
|
||||||
discord_ping() {
|
|
||||||
local details="${1:-}"
|
|
||||||
|
|
||||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
|
||||||
|
|
||||||
local color dumps_display users_display
|
|
||||||
if [[ -n "${DUMPS_OK:-}" && -n "${USERS_OK:-}" ]]; then
|
|
||||||
color="🟢"
|
|
||||||
else
|
|
||||||
color="🔴"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dumps_display=$([[ -n "${DUMPS_OK:-}" ]] && echo "✅" || echo "❌")
|
|
||||||
users_display=$([[ -n "${USERS_OK:-}" ]] && echo "✅" || echo "❌")
|
|
||||||
|
|
||||||
local msg="**@here BACKUP BDD RECETTE ${color}**\n"
|
|
||||||
msg+="Name: ${BACKUP_DIR_NAME}\n"
|
|
||||||
msg+="Dumps transfer: ${dumps_display}\n"
|
|
||||||
msg+="Users transfer: ${users_display}\n"
|
|
||||||
[[ -n "$details" ]] && msg+="Details: $details"
|
|
||||||
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Statuts init
|
|
||||||
#######################################
|
|
||||||
DUMPS_OK=true
|
|
||||||
USERS_OK=true
|
|
||||||
DUMP_ERRORS=""
|
|
||||||
USER_ERRORS=""
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Lock (évite 2 backups en même temps)
|
|
||||||
#######################################
|
|
||||||
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
|
||||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
||||||
log "ERROR: Backup déjà en cours (lock: $LOCK_DIR)"
|
|
||||||
DUMPS_OK=
|
|
||||||
USERS_OK=
|
|
||||||
discord_ping "Lock exists: $LOCK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Remote dir
|
|
||||||
#######################################
|
|
||||||
REMOTE_DIR="${IA_BASE_DIR}/${BACKUP_DIR_NAME}"
|
|
||||||
|
|
||||||
log "Starting backup process"
|
|
||||||
log "Remote directory: ${REMOTE_DIR}"
|
|
||||||
|
|
||||||
log "Creating remote directory"
|
|
||||||
if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "mkdir -p '${REMOTE_DIR}'"; then
|
|
||||||
log "ERROR: Création dossier distant impossible: ${REMOTE_DIR}"
|
|
||||||
DUMPS_OK=
|
|
||||||
USERS_OK=
|
|
||||||
discord_ping "Remote mkdir KO: ${REMOTE_DIR}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Export PostgreSQL roles (no passwords)
|
|
||||||
#######################################
|
|
||||||
ROLES_FILE="${TMP_DIR}/roles_${TS}.sql"
|
|
||||||
log "Exporting PostgreSQL roles"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -Atq <<'SQL' > "$ROLES_FILE"
|
|
||||||
SELECT
|
|
||||||
format(
|
|
||||||
'DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %L) THEN
|
|
||||||
CREATE ROLE %I;
|
|
||||||
END IF;
|
|
||||||
END $$;',
|
|
||||||
rolname, rolname
|
|
||||||
)
|
|
||||||
FROM pg_roles
|
|
||||||
WHERE rolname !~ '^pg_'
|
|
||||||
ORDER BY rolname;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
format(
|
|
||||||
'ALTER ROLE %I WITH %s%s%s%s%s%s%s%s;',
|
|
||||||
rolname,
|
|
||||||
CASE WHEN rolsuper THEN 'SUPERUSER ' ELSE 'NOSUPERUSER ' END,
|
|
||||||
CASE WHEN rolinherit THEN 'INHERIT ' ELSE 'NOINHERIT ' END,
|
|
||||||
CASE WHEN rolcreaterole THEN 'CREATEROLE ' ELSE 'NOCREATEROLE ' END,
|
|
||||||
CASE WHEN rolcreatedb THEN 'CREATEDB ' ELSE 'NOCREATEDB ' END,
|
|
||||||
CASE WHEN rolcanlogin THEN 'LOGIN ' ELSE 'NOLOGIN ' END,
|
|
||||||
CASE WHEN rolreplication THEN 'REPLICATION ' ELSE 'NOREPLICATION ' END,
|
|
||||||
CASE WHEN rolbypassrls THEN 'BYPASSRLS ' ELSE 'NOBYPASSRLS ' END,
|
|
||||||
'CONNECTION LIMIT ' || rolconnlimit
|
|
||||||
)
|
|
||||||
FROM pg_roles
|
|
||||||
WHERE rolname !~ '^pg_'
|
|
||||||
ORDER BY rolname;
|
|
||||||
SQL
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
USERS_OK=
|
|
||||||
USER_ERRORS+="roles_export "
|
|
||||||
log "ERROR: Users export failed"
|
|
||||||
else
|
|
||||||
log "Roles export completed: $ROLES_FILE"
|
|
||||||
fi
|
|
||||||
set -e
|
|
||||||
|
|
||||||
log "Sending roles file to IA server"
|
|
||||||
set +e
|
|
||||||
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
USERS_OK=
|
|
||||||
USER_ERRORS+="roles_scp "
|
|
||||||
log "ERROR: Users transfer failed (roles file)"
|
|
||||||
else
|
|
||||||
log "Roles transfer completed"
|
|
||||||
fi
|
|
||||||
set -e
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Dump des bases + transfert (continue même si KO)
|
|
||||||
#######################################
|
|
||||||
set +e
|
|
||||||
for DB in "${DBS[@]}"; do
|
|
||||||
FILE="${TMP_DIR}/${DB}_${TS}.dump"
|
|
||||||
|
|
||||||
log "Dumping database: $DB"
|
|
||||||
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc --no-owner --no-acl -d "$DB" -f "$FILE"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
DUMPS_OK=
|
|
||||||
DUMP_ERRORS+="${DB} "
|
|
||||||
log "ERROR: Dump failed for $DB"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
log "Dump completed: $FILE"
|
|
||||||
|
|
||||||
log "Sending dump to IA server"
|
|
||||||
scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/"
|
|
||||||
RET=$?
|
|
||||||
if [[ $RET -ne 0 ]]; then
|
|
||||||
DUMPS_OK=
|
|
||||||
DUMP_ERRORS+="${DB}(scp) "
|
|
||||||
log "ERROR: Transfer failed for $DB"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
log "Transfer completed for $DB"
|
|
||||||
done
|
|
||||||
set -e
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Nettoyage
|
|
||||||
#######################################
|
|
||||||
log "Cleaning temporary files"
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Envoi message Discord (final)
|
|
||||||
#######################################
|
|
||||||
DETAILS=""
|
|
||||||
[[ -z "${DUMPS_OK:-}" ]] && DETAILS+="Dumps KO: ${DUMP_ERRORS} "
|
|
||||||
[[ -z "${USERS_OK:-}" ]] && DETAILS+="Users KO: ${USER_ERRORS} "
|
|
||||||
discord_ping "$DETAILS"
|
|
||||||
|
|
||||||
log "Backup finished"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
DATA_DIR=
|
|
||||||
LOCAL_BACKUP=
|
|
||||||
REMOTE_USER=
|
|
||||||
REMOTE_HOST=
|
|
||||||
REMOTE_DIR=
|
|
||||||
SSH_KEY=
|
|
||||||
3
backup_vaultwarden/.gitignore
vendored
3
backup_vaultwarden/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
.env
|
|
||||||
|
|
||||||
backup.log
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# FONCTIONNEMENT DU SCRIPT VAULTWARDEN
|
|
||||||
Le script de backup de vaultwarden permet une sauvegard périodique des mots de passe et utilisateurs de celui-ci.
|
|
||||||
|
|
||||||
## INITIALISATION DES VARIABLES DE MANIÈRE SÉCURISÉ
|
|
||||||
|
|
||||||
1. Les informations sensibles ne sont pas stockées directement dans le script. Elles sont placées dans un fichier .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
WEBHOOK_URL=...
|
|
||||||
REMOTE_USER=...
|
|
||||||
REMOTE_HOST=...
|
|
||||||
SSH_KEY=...
|
|
||||||
DATA_DIR=...
|
|
||||||
```
|
|
||||||
|
|
||||||
2. on recupere les varibales dans le script
|
|
||||||
```bash
|
|
||||||
REMOTE_USER=$(grep -E '^REMOTE_USER=' .env | cut -d '=' -f2-)
|
|
||||||
```
|
|
||||||
|
|
||||||
Explication:
|
|
||||||
|
|
||||||
- grep recherche la variable dans le fichier .env
|
|
||||||
- cut récupère uniquement la valeur après =
|
|
||||||
- REMOTE_USER="user" Le script récupère >> "user"
|
|
||||||
|
|
||||||
Cela permet:
|
|
||||||
|
|
||||||
- d’améliorer la sécurité
|
|
||||||
- d’éviter de modifier le script si un paramètre change
|
|
||||||
|
|
||||||
## RÉCUPÉRATION DES DONNÉES
|
|
||||||
|
|
||||||
1. Le dossier data de Vaultwarden est dupliqué puis compressé afin de créer une archive :
|
|
||||||
```bash
|
|
||||||
tar -czf "$LOCAL_BACKUP" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Transfer vers le serveur de backup
|
|
||||||
```bash
|
|
||||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
|
||||||
```
|
|
||||||
La sauvegarde est envoyée vers une machine dédiée grâce à SCP. Pour éviter de saisir un mot de passe à chaque fois, une clé SSH est utilisée.
|
|
||||||
|
|
||||||
Cette clé SSH est générée sur la machine de backup et autorisée sur la machine Vaultwarden.
|
|
||||||
|
|
||||||
## NOTIFICATION DISCORD
|
|
||||||
|
|
||||||
Le script envoie une notification sur un salon Discord pour informer de l’état de la sauvegarde. Cela se fait grâce à un webhook Discord.
|
|
||||||
|
|
||||||
1. on défini le message
|
|
||||||
```bash
|
|
||||||
local msg="**@here Backup Vaultwarden $color**\n"
|
|
||||||
msg+="Backup: ${BACKUP_NAME}\n"
|
|
||||||
msg+="Data transfer: $dumps_display\n"
|
|
||||||
[[ -n "$details" ]] && msg+="Details: $details"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. on envoie le message sur discord avec le message et le webhook
|
|
||||||
```bash
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
```
|
|
||||||
Le message indique:
|
|
||||||
|
|
||||||
- si la sauvegarde a réussi 🟢
|
|
||||||
- si elle a échoué 🔴
|
|
||||||
- le nom du backup
|
|
||||||
- les détails de l’erreur si nécessaire
|
|
||||||
|
|
||||||
## PLANIFICATION AVEC CRON
|
|
||||||
|
|
||||||
Le script est exécuté automatiquement chaque jour grâce à cron.
|
|
||||||
|
|
||||||
1. Ouvrez le crontab pour l'édition :
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
```
|
|
||||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 19h :
|
|
||||||
```bash
|
|
||||||
0 19 * * * /chemin/vers/le/script/check_storage.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Signification:
|
|
||||||
|
|
||||||
- 0 minute 0
|
|
||||||
- 19 19h
|
|
||||||
- * tous les jours du mois
|
|
||||||
- * tous les mois
|
|
||||||
- * tous les jours de la semaine
|
|
||||||
|
|
||||||
Tous les jours à 19h, le script est exécuté et les logs sont enregistrés dans backup.log ce qui permet d’analyser les erreurs si un problème survient.
|
|
||||||
|
|
||||||
## NETTOYAGE
|
|
||||||
|
|
||||||
Une fois la sauvegarde envoyée sur la machine distante, le fichier temporaire est supprimé :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -f "$LOCAL_BACKUP"
|
|
||||||
```
|
|
||||||
|
|
||||||
Cela permet de garder le serveur propre et éviter de remplir le disque.
|
|
||||||
|
|
||||||
## RÉSUMÉ
|
|
||||||
|
|
||||||
Le script automatise complètement les sauvegardes Vaultwarden :
|
|
||||||
|
|
||||||
- sauvegarde du dossier data
|
|
||||||
- compression et datation
|
|
||||||
- transfert sécurisé via SSH
|
|
||||||
- notification Discord
|
|
||||||
- exécution automatique avec cron
|
|
||||||
- sécurisation des paramètres via .env
|
|
||||||
|
|
||||||
Cela permet d’avoir une sauvegarde quotidienne fiable et surveillée.
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Chemins fixes du script
|
|
||||||
#######################################
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ENV_FILE="/home/matt/vaultwarden/scripts/Scripts-Serveur/backup_vaultwarden/.env"
|
|
||||||
LOG_FILE="/var/log/vaultwarden_backup.log"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
|
||||||
touch "$LOG_FILE"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Vérification fichier .env
|
|
||||||
#######################################
|
|
||||||
[[ -f "$ENV_FILE" ]] || {
|
|
||||||
echo "ERROR: Fichier .env introuvable : $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Chargement du .env
|
|
||||||
#######################################
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Variables obligatoires
|
|
||||||
#######################################
|
|
||||||
: "${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}"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
|
||||||
|
|
||||||
mkdir -p "$LOCAL_BACKUP_DIR"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Notification Discord
|
|
||||||
#######################################
|
|
||||||
discord_ping() {
|
|
||||||
local success="$1"
|
|
||||||
local details="${2:-}"
|
|
||||||
|
|
||||||
[[ -z "$WEBHOOK_URL" ]] && return 0
|
|
||||||
|
|
||||||
local icon status_line
|
|
||||||
if [[ "$success" == "true" ]]; then
|
|
||||||
icon="🟢"
|
|
||||||
status_line="✅"
|
|
||||||
else
|
|
||||||
icon="🔴"
|
|
||||||
status_line="❌"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local msg
|
|
||||||
msg="**@here ${icon} Backup Vaultwarden**\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
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Fonction erreur
|
|
||||||
#######################################
|
|
||||||
fail() {
|
|
||||||
local detail="$1"
|
|
||||||
log "ERROR: $detail"
|
|
||||||
discord_ping "false" "$detail"
|
|
||||||
exit 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"
|
|
||||||
|
|
||||||
log "Début du backup Vaultwarden"
|
|
||||||
log "Source : $DATA_DIR"
|
|
||||||
log "Archive locale : $LOCAL_BACKUP_FILE"
|
|
||||||
log "Destination distante : ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Création du backup
|
|
||||||
#######################################
|
|
||||||
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" \
|
|
||||||
|| fail "Erreur lors de la compression du dossier $DATA_DIR"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Création dossier distant
|
|
||||||
#######################################
|
|
||||||
ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
|
|
||||||
|| fail "Impossible de créer le dossier distant $REMOTE_DIR"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Envoi du backup
|
|
||||||
#######################################
|
|
||||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
|
||||||
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Nettoyage local
|
|
||||||
#######################################
|
|
||||||
rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOCAL_BACKUP_FILE"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Fin
|
|
||||||
#######################################
|
|
||||||
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
|
||||||
discord_ping "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
|
||||||
echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -u
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Sites à vérifier
|
|
||||||
#######################################
|
|
||||||
SITES=(
|
|
||||||
"ferme.malio-dev.fr"
|
|
||||||
"sirh.malio-dev.fr"
|
|
||||||
"inventory.malio-dev.fr"
|
|
||||||
)
|
|
||||||
|
|
||||||
SCHEME="http"
|
|
||||||
CONNECT_TIMEOUT=3
|
|
||||||
MAX_TIME=8
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Logs
|
|
||||||
#######################################
|
|
||||||
LOG_DIR="/var/log/app_health"
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
LOG_FILE="${LOG_DIR}/app_health_$(date +'%Y-%m-%d').log"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Discord
|
|
||||||
#######################################
|
|
||||||
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1478379245842600007/tSxi3G6PbCn89pOdeqK34LR7c-GhXfT-lSCPolwBywJXcpa3ihL8rN4QRwsTjF6SS3w0"
|
|
||||||
|
|
||||||
discord_ping() {
|
|
||||||
local site="$1"
|
|
||||||
local status="$2"
|
|
||||||
local detail="$3"
|
|
||||||
|
|
||||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
|
||||||
|
|
||||||
local color icon
|
|
||||||
|
|
||||||
if [[ "$status" == "OK" ]]; then
|
|
||||||
color="🟢"
|
|
||||||
icon="✅"
|
|
||||||
else
|
|
||||||
color="🔴"
|
|
||||||
icon="❌"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local msg="**CHECK APP RECETTE $color**\n"
|
|
||||||
msg+="Application: ${site}\n"
|
|
||||||
msg+="Status: ${icon}\n"
|
|
||||||
msg+="Details: ${detail}"
|
|
||||||
|
|
||||||
curl -fsS -H "Content-Type: application/json" \
|
|
||||||
-d "{\"content\":\"$msg\"}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Logging
|
|
||||||
#######################################
|
|
||||||
log_line() {
|
|
||||||
# 2026-03-04 14:12:33 | LEVEL | site | message
|
|
||||||
printf "%s | %s | %s | %s\n" \
|
|
||||||
"$(date +'%Y-%m-%d %H:%M:%S')" "$1" "$2" "$3" | tee -a "$LOG_FILE"
|
|
||||||
|
|
||||||
# Envoi Discord par application
|
|
||||||
discord_ping "$2" "$1" "$3"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# DNS
|
|
||||||
#######################################
|
|
||||||
dns_ok() {
|
|
||||||
getent hosts "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Check application
|
|
||||||
#######################################
|
|
||||||
check_site() {
|
|
||||||
|
|
||||||
local host="$1"
|
|
||||||
local url="${SCHEME}://${host}/"
|
|
||||||
|
|
||||||
if ! dns_ok "$host"; then
|
|
||||||
log_line "DOWN" "$host" "Résolution impossible (getent hosts)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local http_code curl_exit stderr
|
|
||||||
|
|
||||||
stderr="$(mktemp)"
|
|
||||||
|
|
||||||
http_code="$(
|
|
||||||
curl -sS -o /dev/null \
|
|
||||||
-w '%{http_code}' \
|
|
||||||
--connect-timeout "$CONNECT_TIMEOUT" \
|
|
||||||
--max-time "$MAX_TIME" \
|
|
||||||
"$url" 2>"$stderr"
|
|
||||||
)"
|
|
||||||
|
|
||||||
curl_exit=$?
|
|
||||||
|
|
||||||
if [ $curl_exit -ne 0 ]; then
|
|
||||||
local err
|
|
||||||
err="$(head -n 1 "$stderr" | tr -d '\r')"
|
|
||||||
rm -f "$stderr"
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "curl exit=$curl_exit : ${err:-"(aucun)"}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$stderr"
|
|
||||||
|
|
||||||
if [[ "$http_code" =~ ^[0-9]{3}$ ]]; then
|
|
||||||
if [ "$http_code" -ge 200 ] && [ "$http_code" -le 399 ]; then
|
|
||||||
log_line "OK" "$host" "HTTP $http_code"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "HTTP $http_code (erreur appli)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_line "DOWN" "$host" "Code HTTP inattendu: $http_code"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Main
|
|
||||||
#######################################
|
|
||||||
main() {
|
|
||||||
|
|
||||||
local failures=0
|
|
||||||
|
|
||||||
for site in "${SITES[@]}"; do
|
|
||||||
if ! check_site "$site"; then
|
|
||||||
failures=$((failures + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$failures" -gt 0 ]; then
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
132
global.env.exemple
Normal file
132
global.env.exemple
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
###############################################################################
|
||||||
|
# FICHIER .env.example
|
||||||
|
#
|
||||||
|
# Ce fichier sert de modèle de configuration pour les scripts d'automatisation :
|
||||||
|
# - backup-bdd-recette.sh → sauvegarde PostgreSQL
|
||||||
|
# - rebuild-bdd-recette.sh → reconstruction d'une base PostgreSQL
|
||||||
|
# - check-statut-recette.sh → vérification disponibilité des applications
|
||||||
|
# - check-storage.sh → surveillance de l'espace disque
|
||||||
|
# - backup-vaultwarden.sh → sauvegarde du service Vaultwarden
|
||||||
|
#
|
||||||
|
# Copier ce fichier en .env puis remplir les valeurs.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# ENVIRONNEMENT
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Nom de l'environnement (ex : DEV / RECETTE / PROD)
|
||||||
|
ENV_NAME=RECETTE
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# DISCORD
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Webhook Discord utilisé pour envoyer les notifications
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# POSTGRESQL
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Adresse du serveur PostgreSQL
|
||||||
|
PGHOST=localhost
|
||||||
|
|
||||||
|
# Port PostgreSQL
|
||||||
|
PGPORT=5432
|
||||||
|
|
||||||
|
# Utilisateur utilisé pour les dumps
|
||||||
|
PGUSER=
|
||||||
|
|
||||||
|
# Mot de passe
|
||||||
|
PGPASSWORD=
|
||||||
|
|
||||||
|
# Bases de données à sauvegarder (séparées par espace)
|
||||||
|
# Utilisé par backup-bdd-recette.sh
|
||||||
|
DBS="sirh inventory ferme"
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# BACKUPS LOCAUX
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Dossier local où les dumps seront générés temporairement
|
||||||
|
BACKUP_LOCAL_DIR=/var/backups/postgresql
|
||||||
|
|
||||||
|
# Dossier des logs de sauvegarde
|
||||||
|
BACKUP_LOG_DIR=/var/log/script/...
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SERVEUR DISTANT DE STOCKAGE
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Utilisateur du serveur de backup distant
|
||||||
|
BACKUP_REMOTE_USER=
|
||||||
|
|
||||||
|
# Adresse IP ou hostname du serveur de stockage
|
||||||
|
BACKUP_REMOTE_HOST=
|
||||||
|
|
||||||
|
# Dossier distant où stocker les backups
|
||||||
|
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SSH
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Clé SSH utilisée pour se connecter au serveur distant
|
||||||
|
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||||
|
|
||||||
|
# Timeout SSH (secondes)
|
||||||
|
SSH_TIMEOUT=10
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# ROTATION DES BACKUPS
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Nombre de jours de conservation des sauvegardes
|
||||||
|
# Utilisé par backup-bdd-recette.sh et backup-vaultwarden.sh
|
||||||
|
BACKUP_RETENTION_DAYS=10
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# APPLICATIONS À SURVEILLER
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Liste des applications à vérifier (séparées par espace)
|
||||||
|
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||||
|
|
||||||
|
# Schéma utilisé pour les applications surveillées
|
||||||
|
APP_SCHEME="http"
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# VAULTWARDEN
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Dossier contenant les données Vaultwarden
|
||||||
|
VAULTWARDEN_DATA_DIR=/opt/vaultwarden/data
|
||||||
|
|
||||||
|
# Dossier local où stocker le backup
|
||||||
|
VAULTWARDEN_BACKUP_DIR=/var/backups/vaultwarden
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# SERVEUR IA / STOCKAGE CENTRAL
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Utilisateur SSH du serveur distant
|
||||||
|
IA_SSH_USER=
|
||||||
|
|
||||||
|
# Host du serveur distant
|
||||||
|
IA_SSH_HOST=
|
||||||
|
|
||||||
|
# Dossier racine contenant les dumps PostgreSQL
|
||||||
|
IA_BASE_DIR=/home/.../backups/bdd-recette
|
||||||
|
|
||||||
|
# Dossier contenant les rôles PostgreSQL exportés
|
||||||
|
REMOTE_ROLES_NAME=user
|
||||||
Reference in New Issue
Block a user