Compare commits
7 Commits
7261823806
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b223f514 | |||
| a9e492962c | |||
| 3bad5bad82 | |||
| 9af65f7739 | |||
| 11f69a9eda | |||
| e68c99a8b3 | |||
| 7b91691ef8 |
@@ -33,4 +33,23 @@ REMOTE_DIR=
|
||||
#############################################
|
||||
|
||||
# Chemin vers la clé privée SSH utilisée pour la connexion
|
||||
SSH_KEY=
|
||||
SSH_KEY=
|
||||
|
||||
# Port SSH du serveur distant
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
# Timeout SSH en secondes
|
||||
SSH_CONNECT_TIMEOUT=10
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
|
||||
|
||||
#############################################
|
||||
# ROTATION DES BACKUPS
|
||||
#############################################
|
||||
|
||||
# Nombre de jours de conservation des sauvegardes
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
|
||||
@@ -28,12 +28,13 @@ Avant de mettre en place le script, vérifier que les éléments suivants sont d
|
||||
- `ssh`
|
||||
- `curl`
|
||||
- `cron`
|
||||
- `jq`
|
||||
|
||||
Installation sur Debian / Ubuntu :
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y tar openssh-client curl cron
|
||||
sudo apt install -y tar openssh-client curl cron jq
|
||||
````
|
||||
|
||||
---
|
||||
@@ -43,13 +44,13 @@ sudo apt install -y tar openssh-client curl cron
|
||||
Le script est situé dans :
|
||||
|
||||
```bash
|
||||
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||
/home/<USER>/Malio-ops/BackupVaultWarden/
|
||||
```
|
||||
|
||||
Structure recommandée :
|
||||
|
||||
```bash
|
||||
/home/matt/vaultwarden/Malio-ops/BackupVaultWarden/
|
||||
/home/<USER>/Malio-ops/BackupVaultWarden/
|
||||
├── backup-vaultwarden.sh
|
||||
├── .env
|
||||
└── README.md
|
||||
@@ -65,29 +66,73 @@ Elles doivent être placées dans un fichier `.env`.
|
||||
## Exemple de fichier `.env`
|
||||
|
||||
```bash
|
||||
WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||
REMOTE_USER=<USER>
|
||||
REMOTE_HOST=<IP_SERVEUR>
|
||||
SSH_KEY=/home/matt/.ssh/id_ed25519_vaultwarden_backup
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
DATA_DIR=/opt/vaultwarden/data
|
||||
LOCAL_BACKUP=/var/backups/vaultwarden
|
||||
REMOTE_DIR=/home/backup/backups/vaultwarden
|
||||
# BACKUP_REMOTE_SSH_PORT=22
|
||||
# SSH_CONNECT_TIMEOUT=10
|
||||
# BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
# BACKUP_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
```
|
||||
|
||||
## Description des variables
|
||||
|
||||
| Variable | Description |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| WEBHOOK_URL | Webhook Discord pour les notifications |
|
||||
| REMOTE_USER | Utilisateur du serveur distant |
|
||||
| REMOTE_HOST | Adresse IP ou DNS du serveur de sauvegarde |
|
||||
| SSH_KEY | Chemin vers la clé SSH utilisée pour le transfert |
|
||||
| DATA_DIR | Dossier `data` de Vaultwarden |
|
||||
| REMOTE_DIR | Dossier de stockage des backups sur le serveur distant |
|
||||
| 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é :
|
||||
@@ -121,13 +166,13 @@ Le transfert des sauvegardes utilise une **clé SSH** afin de permettre une conn
|
||||
Sur la machine exécutant les scripts :
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_bitwarden
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_backup
|
||||
```
|
||||
|
||||
#### 2. Copie de la clé vers le serveur distant
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519_bitwarden.pub <USER>@<IP_SERVEUR>
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519_backup.pub <USER>@<IP_SERVEUR>
|
||||
```
|
||||
|
||||
Cette commande ajoute la clé dans :
|
||||
@@ -141,20 +186,20 @@ sur la machine IA.
|
||||
#### 3. Vérification de la connexion
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_ed25519_bitwarden backup@192.168.0.179
|
||||
ssh -i ~/.ssh/id_ed25519_backup -o StrictHostKeyChecking=yes <USER>@<IP_SERVEUR>
|
||||
```
|
||||
|
||||
#### 4. Vérification des fichiers de clé
|
||||
|
||||
```bash
|
||||
ls ~/.ssh/id_ed25519_bitwarden*
|
||||
ls ~/.ssh/id_ed25519_backup*
|
||||
```
|
||||
|
||||
Fichiers attendus :
|
||||
|
||||
```
|
||||
~/.ssh/id_ed25519_bitwarden
|
||||
~/.ssh/id_ed25519_bitwarden.pub
|
||||
~/.ssh/id_ed25519_backup
|
||||
~/.ssh/id_ed25519_backup.pub
|
||||
```
|
||||
|
||||
#### 5. Permissions SSH
|
||||
@@ -163,8 +208,8 @@ Machine locale :
|
||||
|
||||
```bash
|
||||
chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/id_ed25519_bitwarden
|
||||
chmod 644 ~/.ssh/id_ed25519_bitwarden.pub
|
||||
chmod 600 ~/.ssh/id_ed25519_backup
|
||||
chmod 644 ~/.ssh/id_ed25519_backup.pub
|
||||
```
|
||||
|
||||
Machine distante :
|
||||
@@ -174,10 +219,19 @@ chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
#### 6. Déclaration dans `.env`
|
||||
#### 6. Provisionnement de `known_hosts`
|
||||
|
||||
Le script est prévu pour fonctionner avec validation stricte des hôtes SSH.
|
||||
|
||||
```bash
|
||||
SSH_KEY=/home/matt/.ssh/id_ed25519_bitwarden
|
||||
ssh-keyscan -H <IP_SERVEUR> >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
```
|
||||
|
||||
#### 7. Déclaration dans `.env`
|
||||
|
||||
```bash
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
```
|
||||
|
||||
Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour transférer les sauvegardes.
|
||||
@@ -188,7 +242,7 @@ Cette clé sera utilisée automatiquement par les scripts (`scp` / `ssh`) pour t
|
||||
Le script crée une archive compressée du dossier `data` :
|
||||
|
||||
```bash
|
||||
tar -czf "$LOCAL_BACKUP" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
```
|
||||
|
||||
Cela permet d’obtenir une sauvegarde portable et compressée.
|
||||
@@ -200,7 +254,7 @@ Cela permet d’obtenir une sauvegarde portable et compressée.
|
||||
Une fois l’archive créée :
|
||||
|
||||
```bash
|
||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
||||
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
|
||||
```
|
||||
|
||||
Le fichier est envoyé vers le serveur de sauvegarde via SCP.
|
||||
@@ -209,23 +263,22 @@ Le fichier est envoyé vers le serveur de sauvegarde via SCP.
|
||||
|
||||
# 9. Notification Discord
|
||||
|
||||
Le script envoie une notification Discord pour informer de l’état de la sauvegarde.
|
||||
Le script envoie une notification Discord pour informer de l'etat de la sauvegarde.
|
||||
|
||||
Construction du message :
|
||||
|
||||
```bash
|
||||
local msg="**@here Backup Vaultwarden $color**\n"
|
||||
msg="**${ping}Backup Vaultwarden ${icon}**\n"
|
||||
msg+="Backup: ${BACKUP_NAME}\n"
|
||||
msg+="Data transfer: $dumps_display\n"
|
||||
[[ -n "$details" ]] && msg+="Details: $details"
|
||||
msg+="Data transfer: ${status_line}\n"
|
||||
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||
```
|
||||
|
||||
Envoi du message :
|
||||
|
||||
```bash
|
||||
curl -fsS -H "Content-Type: application/json" \
|
||||
-d "{\"content\":\"$msg\"}" \
|
||||
"$WEBHOOK_URL"
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
Le message indique :
|
||||
@@ -237,7 +290,27 @@ Le message indique :
|
||||
|
||||
---
|
||||
|
||||
# 10. Planification avec cron
|
||||
# 10. Rotation distante des sauvegardes
|
||||
|
||||
Le script supprime les archives distantes plus anciennes que la durée de retention configurée.
|
||||
|
||||
Configuration dans `.env` :
|
||||
|
||||
```bash
|
||||
# BACKUP_RETENTION_DAYS=10
|
||||
```
|
||||
|
||||
Commande utilisée :
|
||||
|
||||
```bash
|
||||
find "$REMOTE_DIR" -type f -name 'vaultwarden-backup-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
```
|
||||
|
||||
Si la variable n'est pas définie, le script utilise `10` jours par défaut.
|
||||
|
||||
---
|
||||
|
||||
# 11. Planification avec cron
|
||||
|
||||
Le script est exécuté automatiquement tous les jours à 19h.
|
||||
|
||||
@@ -250,7 +323,7 @@ crontab -e
|
||||
Ajouter :
|
||||
|
||||
```bash
|
||||
0 19 * * * /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh >> /var/log/vaultwarden_backup.log 2>&1
|
||||
0 19 * * * /home/<USER>/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh 2>&1
|
||||
```
|
||||
|
||||
Signification :
|
||||
@@ -267,29 +340,29 @@ Le script s’exécute donc **tous les jours à 19h00**.
|
||||
|
||||
---
|
||||
|
||||
# 11. Nettoyage
|
||||
# 12. Nettoyage
|
||||
|
||||
Une fois la sauvegarde transférée :
|
||||
|
||||
```bash
|
||||
rm -f "$LOCAL_BACKUP"
|
||||
rm -f "$LOCAL_BACKUP_FILE"
|
||||
```
|
||||
|
||||
Cela évite de remplir le disque de la machine Vaultwarden.
|
||||
|
||||
---
|
||||
|
||||
# 12. Test manuel
|
||||
# 13. Test manuel
|
||||
|
||||
Avant de mettre le script en cron, tester :
|
||||
|
||||
```bash
|
||||
bash /home/matt/vaultwarden/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
|
||||
bash /home/<USER>/Malio-ops/BackupVaultWarden/backup-vaultwarden.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 13. Vérification des logs
|
||||
# 14. Vérification des logs
|
||||
|
||||
Logs :
|
||||
|
||||
@@ -299,7 +372,7 @@ cat /var/log/vaultwarden_backup.log
|
||||
|
||||
---
|
||||
|
||||
# 14. Résumé
|
||||
# 15. Résumé
|
||||
|
||||
Le script automatise :
|
||||
|
||||
@@ -312,4 +385,3 @@ Le script automatise :
|
||||
|
||||
Ce système permet d’obtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
#######################################
|
||||
# Chemins fixes du script
|
||||
@@ -27,6 +28,7 @@ log() {
|
||||
# Chargement du .env
|
||||
#######################################
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
@@ -39,7 +41,15 @@ set +a
|
||||
: "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}"
|
||||
: "${REMOTE_HOST:?Variable REMOTE_HOST manquante dans .env}"
|
||||
: "${REMOTE_DIR:?Variable REMOTE_DIR manquante dans .env}"
|
||||
[[ "$REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||
echo "ERROR: Variable REMOTE_DIR invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${SSH_KEY:?Variable SSH_KEY manquante dans .env}"
|
||||
: "${BACKUP_REMOTE_SSH_PORT:=22}"
|
||||
: "${SSH_CONNECT_TIMEOUT:=10}"
|
||||
: "${BACKUP_KNOWN_HOSTS_STRICT:=yes}"
|
||||
: "${BACKUP_KNOWN_HOSTS_FILE:=${HOME}/.ssh/known_hosts}"
|
||||
|
||||
#######################################
|
||||
# Variables backup
|
||||
@@ -50,18 +60,67 @@ BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz"
|
||||
LOCAL_BACKUP_FILE="${LOCAL_BACKUP}/${BACKUP_NAME}"
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
|
||||
|
||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10)
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable BACKUP_REMOTE_SSH_PORT invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$SSH_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable SSH_CONNECT_TIMEOUT invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable BACKUP_RETENTION_DAYS invalide dans .env" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||
*)
|
||||
echo "ERROR: Variable BACKUP_KNOWN_HOSTS_STRICT invalide dans .env" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||
)
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT"
|
||||
-o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE"
|
||||
)
|
||||
|
||||
mkdir -p "$LOCAL_BACKUP"
|
||||
|
||||
#######################################
|
||||
# Notification Discord
|
||||
#######################################
|
||||
discord_ping() {
|
||||
send_discord() {
|
||||
local success="$1"
|
||||
local details="${2:-}"
|
||||
local payload=""
|
||||
|
||||
[[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0
|
||||
require_cmd jq || return 0
|
||||
require_cmd curl || return 0
|
||||
|
||||
local icon status_line
|
||||
if [[ "$success" == "true" ]]; then
|
||||
@@ -80,7 +139,6 @@ discord_ping() {
|
||||
msg+="Data transfer: ${status_line}\n"
|
||||
[[ -n "$details" ]] && msg+="Détails: ${details}"
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||
}
|
||||
@@ -91,15 +149,41 @@ discord_ping() {
|
||||
fail() {
|
||||
local detail="$1"
|
||||
log "ERROR: $detail"
|
||||
discord_ping "false" "$detail"
|
||||
send_discord "false" "$detail"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Verrou d'execution
|
||||
#######################################
|
||||
LOCK_DIR="/tmp/vaultwarden_backup.lock.d"
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
fail "Backup deja en cours"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -f "${LOCAL_BACKUP_FILE:-}"
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Vérifications préalables
|
||||
#######################################
|
||||
[[ -d "$DATA_DIR" ]] || fail "Le dossier source n'existe pas : $DATA_DIR"
|
||||
[[ -f "$SSH_KEY" ]] || fail "La clé SSH est introuvable : $SSH_KEY"
|
||||
[[ -r "$SSH_KEY" ]] || fail "La clé SSH est non lisible : $SSH_KEY"
|
||||
[[ ! -L "$SSH_KEY" ]] || fail "La clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||
chmod 600 "$SSH_KEY" || true
|
||||
|
||||
for cmd in tar ssh scp jq curl find; do
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
|
||||
log "Début du backup Vaultwarden"
|
||||
log "Source : $DATA_DIR"
|
||||
@@ -123,7 +207,7 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \
|
||||
#######################################
|
||||
# Envoi du backup
|
||||
#######################################
|
||||
scp "${SSH_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
||||
scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
|
||||
|| fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST"
|
||||
|
||||
log "Backup envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||
@@ -146,5 +230,5 @@ rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOC
|
||||
# Fin
|
||||
#######################################
|
||||
log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"
|
||||
discord_ping "true" "Backup envoyé avec succès vers $REMOTE_HOST"
|
||||
echo "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"
|
||||
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,45 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
Liste des évolutions du projet Scripts Serveur
|
||||
Ce projet suit le format [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
|
||||
et applique le versionnement semantique.
|
||||
|
||||
## [1.0.0]
|
||||
### Parameters
|
||||
Ajouter dans le fichier /RecetteScripts/.env
|
||||
SSH_TIMEOUT
|
||||
BACKUP_LOG_DIR
|
||||
APP_LOG_DIR
|
||||
DISCORD_WEBHOOK_URL
|
||||
DISCORD_PING
|
||||
CHECK_CONNECT_TIMEOUT
|
||||
CHECK_MAX_TIME
|
||||
APP_URLS
|
||||
## [Unreleased]
|
||||
|
||||
Ajouter dans le fichier /CheckStorage/.env
|
||||
WEBHOOK_URL
|
||||
|
||||
Ajouter dans le fichier /BackupVaultWarden/.env
|
||||
DATA_DIR
|
||||
LOCAL_BACKUP
|
||||
REMOTE_USER
|
||||
REMOTE_HOST
|
||||
REMOTE_DIR
|
||||
SSH_KEY
|
||||
|
||||
### Added
|
||||
* [#361] Script dump BDD
|
||||
* [#367] Avoir une notification discord quand les backup sont faites
|
||||
* [#368] Script pour check que les applis ne sont pas hors-ligne
|
||||
* first push
|
||||
* Reorganisation des fichiers et dossiers
|
||||
* [#372] Script de check si la machine a le stockage plein
|
||||
* [#378] Script Backup BDD Vaultwarden
|
||||
* [#381] Variabiliser tous les scripts
|
||||
* [#384] Fix Correctif
|
||||
* [#390] Rotation des scripts
|
||||
* [#392] Scripts de reconstruction des bases de données
|
||||
* [#427] Correctifs
|
||||
* [#433] Ajout du dossier RebuildBdd et creation de la theorique du script de rebuild de base de données
|
||||
* [#002-19] correctif generaliser sur l'application suite aux nombreuses modifications et ajout de script
|
||||
### Changed
|
||||
- Harmonisation des fonctions d'envoi Discord dans les scripts de sauvegarde, de supervision et de reconstruction.
|
||||
- Ajout d'un fichier de log dedie a l'orchestrateur `RebuildBdd/run-rebuild-bdd.sh`.
|
||||
- Renforcement de la journalisation et de la gestion des erreurs dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||
|
||||
### Fixed
|
||||
- Nettoyage du backup local temporaire dans `BackupVaultWarden/backup-vaultwarden.sh` en sortie de script.
|
||||
- Gestion plus robuste des dependances shell et des verifications de disponibilite PostgreSQL dans les scripts `RebuildBdd`.
|
||||
- Acceptation explicite du flag `--non-interactive` dans `RebuildBdd/Checkup/check-target-readiness.sh` pour compatibilite de workflow.
|
||||
- Validation des noms de base et refus des cles SSH symboliques dans les scripts de reconstruction.
|
||||
- Suppression des notifications Discord fragiles quand `jq` ou `curl` sont absents, avec envoi silencieux en secours.
|
||||
- Detection et nettoyage des verrous perimes dans `RecetteScripts/backup-bdd-recette.sh`.
|
||||
- Filtrage des privileges `SUPERUSER` lors de la restauration des roles dans `RecetteScripts/rebuild-bdd-recette.sh`.
|
||||
|
||||
## [1.0.0] - 2026-03-18
|
||||
|
||||
### Added
|
||||
- Ajout des scripts legacy de sauvegarde PostgreSQL, de verification de statut applicatif et de supervision du stockage.
|
||||
- Ajout du script de sauvegarde Vaultwarden.
|
||||
- Ajout des scripts de reconstruction de bases PostgreSQL dans `RebuildBdd/`.
|
||||
- Ajout du bootstrap cible, du precheck et de l'orchestration de reconstruction.
|
||||
- Ajout du support d'utilisation via une interface web avec sorties JSON.
|
||||
- Ajout des fichiers d'exemple de configuration pour les scripts et les cibles.
|
||||
|
||||
### Changed
|
||||
- Reorganisation des fichiers et des dossiers du projet.
|
||||
- Variabilisation des scripts via des fichiers `.env`.
|
||||
- Ajout de la rotation des sauvegardes.
|
||||
- Harmonisation progressive de la documentation et des exemples de configuration.
|
||||
- Ajout du bit executable sur les scripts qui doivent etre lancables directement.
|
||||
|
||||
### Fixed
|
||||
- Correctifs multiples sur les scripts legacy et sur le flux `RebuildBdd`.
|
||||
- Corrections de chemins de configuration et de telechargement des dumps.
|
||||
- Corrections de messages Discord, de revue de code et d'orthographe dans la documentation.
|
||||
- Correctifs sur la preparation PostgreSQL, `scp`, `sudoers` et le reperage des environnements cibles.
|
||||
|
||||
@@ -4,3 +4,9 @@
|
||||
|
||||
# Webhook Discord pour notifications
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
# Mention Discord en cas d'alerte
|
||||
DISCORD_PING=@here
|
||||
|
||||
# Seuil d'alerte en pourcentage d'utilisation
|
||||
STORAGE_ALERT_LIMIT=70
|
||||
|
||||
@@ -1,65 +1,82 @@
|
||||
# Scripts de vérification de l'espace de stockage
|
||||
# CheckStorage
|
||||
|
||||
Ce projet contient des scripts pour vérifier l'espace de stockage
|
||||
Script de vérification de l’espace disque sur Ubuntu Server, avec notification Discord optionnelle.
|
||||
|
||||
## Préambule
|
||||
Ce script est conçu pour vérifier l'espace de stockage disponible sur un serveur et envoyer une alerte
|
||||
La vérification de l'espace de stockage ce fait sur la partition racine.
|
||||
La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cette valeur dans le script selon vos besoins.
|
||||
## Fonctionnement
|
||||
|
||||
## Installation du script
|
||||
Le script :
|
||||
|
||||
1. Clonez le dépôt GitHub :
|
||||
```bash
|
||||
git clone https://gitea.malio.fr/MALIO-DEV/Scripts-Serveur.git
|
||||
```
|
||||
|
||||
2. Accédez au répertoire du projet :
|
||||
3. ```bash
|
||||
cd Scripts-Serveur/CheckStorage
|
||||
```
|
||||
### Génération de la clé SSH
|
||||
1. charge `.env`
|
||||
2. lit l’utilisation de la partition `/`
|
||||
3. compare le taux d’occupation au seuil configuré
|
||||
4. envoie une alerte Discord si le seuil est dépassé
|
||||
|
||||
Sur la machine exécutant les scripts :
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 3</summary>
|
||||
|
||||
Quel operateur shell permet d'envoyer la sortie d'une commande vers la suivante ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 3</summary>
|
||||
|
||||
```text
|
||||
|
|
||||
```
|
||||
|
||||
</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
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/check_storage_key
|
||||
sudo apt update
|
||||
sudo apt install -y coreutils gawk jq curl
|
||||
```
|
||||
Copier la clé sur le serveur distant :
|
||||
|
||||
`jq` et `curl` ne sont nécessaires que si `DISCORD_WEBHOOK_URL` est renseigné.
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/check_storage_key.pub user@serveur
|
||||
cp .env.exemple .env
|
||||
chmod 600 .env
|
||||
```
|
||||
Tester la connexion sans mot de passe :
|
||||
|
||||
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
|
||||
ssh -i ~/.ssh/check_storage_key <USER>@<HOST>
|
||||
chmod 700 check-storage.sh
|
||||
./check-storage.sh
|
||||
```
|
||||
## Utilisation du script
|
||||
0. Copiez le fichier d'environnement exemple et modifiez les variables selon votre configuration :
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
1. Donnez les permissions d'exécution au script :
|
||||
```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
|
||||
1. Ouvrez le crontab pour l'édition :
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
2. Ajoutez la ligne suivante pour exécuter le script tous les jours à 7h50 du matin :
|
||||
```bash
|
||||
50 7 * * * /chemin/vers/le/script/check-storage.sh
|
||||
```
|
||||
|
||||
## Avertissement
|
||||
Assurez-vous de remplacer `/chemin/vers/le/script/check-storage.sh` par le chemin réel où se trouve le script sur votre système.
|
||||
## Cron
|
||||
|
||||
Exemple quotidien à `07:50` :
|
||||
|
||||
```bash
|
||||
50 7 * * * /chemin/vers/CheckStorage/check-storage.sh
|
||||
```
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# CHARGEMENT DU .env
|
||||
@@ -22,8 +23,48 @@ set +a
|
||||
# CONFIGURATION
|
||||
###############################################################################
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "ERROR: commande requise absente : $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Limite maximale d'utilisation du disque en pourcentage
|
||||
limit=70
|
||||
limit="${STORAGE_ALERT_LIMIT:-70}"
|
||||
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
[[ "$limit" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: STORAGE_ALERT_LIMIT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
(( limit >= 1 && limit <= 99 )) || {
|
||||
echo "ERROR: STORAGE_ALERT_LIMIT doit être compris entre 1 et 99" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd df
|
||||
require_cmd awk
|
||||
|
||||
if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
|
||||
require_cmd jq
|
||||
require_cmd curl
|
||||
fi
|
||||
|
||||
send_discord() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "${DISCORD_WEBHOOK_URL:-}" ]] || return 0
|
||||
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||
|
||||
curl -fsS \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL" >/dev/null || true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# RÉCUPÉRATION DES INFORMATIONS DISQUE
|
||||
@@ -48,15 +89,9 @@ avail_gb=$(awk -v b="$avail_bytes" 'BEGIN {printf "%.2f", b/1024/1024/1024}')
|
||||
|
||||
if [ "$usage" -ge "$limit" ]; then
|
||||
|
||||
msgLimit="@here\n**CHECK STOCKAGE :red_circle:**\nLimite autorisé : ${limit}%\nUtilisation actuelle: ${usage}%\nEspace restant: ${free}%\nUtilise / total: ${used_gb} GB / ${total_gb} GB\nDisponible: ${avail_gb} GB\nHeure: $(date)"
|
||||
msgLimit="${DISCORD_PING}\n**CHECK STOCKAGE :red_circle:**\nLimite autorisée : ${limit}%\nUtilisation actuelle : ${usage}%\nEspace restant : ${free}%\nUtilisé / total : ${used_gb} GB / ${total_gb} GB\nDisponible : ${avail_gb} GB\nHeure : $(date)"
|
||||
|
||||
payload="$(jq -n --arg content "$msgLimit" '{content: $content}')"
|
||||
|
||||
curl -X POST \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json; charset=utf-8" \
|
||||
-d "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
send_discord "$msgLimit"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
71
README.md
71
README.md
@@ -1,8 +1,3 @@
|
||||
Voici une version **corrigée, structurée et professionnalisée**, adaptée à votre contexte actuel (scripts rebuild + infra + exploitation).
|
||||
Format directement exploitable pour README.md / BookStack.
|
||||
|
||||
---
|
||||
|
||||
# MALIO-OPS
|
||||
|
||||
Ce dépôt centralise l’ensemble des **scripts d’exploitation, de maintenance et d’automatisation** utilisés dans l’infrastructure MALIO.
|
||||
@@ -125,12 +120,20 @@ Un modèle est fourni :
|
||||
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`
|
||||
@@ -150,6 +153,16 @@ cp global.env.exemple global.env
|
||||
* 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
|
||||
|
||||
---
|
||||
|
||||
@@ -182,8 +195,48 @@ cp global.env.exemple global.env
|
||||
|
||||
---
|
||||
|
||||
Si vous le souhaitez, je peux :
|
||||
<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>
|
||||
|
||||
* restructurer votre README `RebuildBdd` pour qu’il soit au même niveau
|
||||
* ajouter un schéma d’architecture (flux SSH / dump / restore)
|
||||
* ou intégrer directement votre workflow réel (IA machine + backup server + cible) dans la doc
|
||||
|
||||
@@ -41,19 +41,19 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
postgres_server_ready() {
|
||||
require_cmd postgres || return 1
|
||||
require_cmd pg_ctlcluster || return 1
|
||||
require_cmd pg_lsclusters || return 1
|
||||
has_cmd postgres || return 1
|
||||
has_cmd pg_ctlcluster || return 1
|
||||
has_cmd pg_lsclusters || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_postgres_cluster() {
|
||||
if ! require_cmd pg_lsclusters || ! require_cmd pg_createcluster; then
|
||||
if ! has_cmd pg_lsclusters || ! has_cmd pg_createcluster; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -66,7 +66,7 @@ ensure_postgres_cluster() {
|
||||
version="$(find /etc/postgresql -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | LC_ALL=C sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -z "$version" ]] && require_cmd psql; then
|
||||
if [[ -z "$version" ]] && has_cmd psql; then
|
||||
version="$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)"
|
||||
fi
|
||||
|
||||
@@ -82,15 +82,15 @@ collect_postgres_diagnostics() {
|
||||
|
||||
if "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager >/dev/null 2>&1; then
|
||||
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: OK; "
|
||||
elif require_cmd systemctl; then
|
||||
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 require_cmd pg_lsclusters; then
|
||||
if has_cmd pg_lsclusters; then
|
||||
diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); "
|
||||
fi
|
||||
|
||||
if require_cmd journalctl; then
|
||||
if has_cmd journalctl; then
|
||||
diagnostics+="journalctl: $( "$SUDO_BIN" journalctl -u "$POSTGRES_SERVICE_NAME" -n 10 --no-pager 2>/dev/null | tr '\n' ' ' ); "
|
||||
fi
|
||||
|
||||
@@ -102,11 +102,11 @@ start_postgres_service() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if require_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||
if has_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if require_cmd pg_lsclusters && require_cmd pg_ctlcluster; then
|
||||
if has_cmd pg_lsclusters && has_cmd pg_ctlcluster; then
|
||||
local version cluster
|
||||
while read -r version cluster _; do
|
||||
[[ -n "$version" && -n "$cluster" ]] || continue
|
||||
@@ -137,10 +137,13 @@ 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 ! require_cmd "$SUDO_BIN"; then
|
||||
if ! has_cmd "$SUDO_BIN"; then
|
||||
fail "sudo absent sur la cible"
|
||||
fi
|
||||
|
||||
@@ -154,12 +157,12 @@ fi
|
||||
|
||||
POSTGRES_INSTALLED="no"
|
||||
|
||||
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb || ! postgres_server_ready; then
|
||||
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_PACKAGE_LIST >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||
"$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
|
||||
@@ -178,15 +181,17 @@ else
|
||||
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 ! "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
if [[ "$PG_READY" != true ]]; then
|
||||
fail "PostgreSQL ne répond pas correctement"
|
||||
fi
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ while [[ $# -gt 0 ]]; do
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
# Flag accepté pour compatibilité avec les autres scripts du workflow.
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
@@ -70,6 +71,8 @@ print_stdout() {
|
||||
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||
}
|
||||
|
||||
LOG_FILE=/dev/stderr
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
echo "$msg" >>"$LOG_FILE"
|
||||
@@ -90,6 +93,7 @@ to_bool_yes_no() {
|
||||
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
|
||||
@@ -114,13 +118,16 @@ require_env_vars() {
|
||||
|
||||
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() {
|
||||
@@ -193,7 +200,11 @@ prepare_known_hosts() {
|
||||
chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts"
|
||||
|
||||
if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then
|
||||
log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts"
|
||||
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
|
||||
|
||||
@@ -8,18 +8,18 @@ RESTORE_ROLES=yes
|
||||
|
||||
# Dépôt scripts
|
||||
GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git
|
||||
GLOBAL_REPO_BRANCH=main
|
||||
|
||||
# Backup central
|
||||
GLOBAL_BACKUP_REMOTE_USER=backup
|
||||
GLOBAL_BACKUP_REMOTE_HOST=192.168.1.60
|
||||
GLOBAL_BACKUP_REMOTE_PORT=22
|
||||
GLOBAL_BACKUP_REMOTE_BASE_DIR=/home/backup/backups
|
||||
|
||||
# Clé SSH de lecture backup copiée sur les cibles
|
||||
GLOBAL_BACKUP_SSH_PRIVATE_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly
|
||||
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub
|
||||
GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
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
|
||||
@@ -35,4 +35,4 @@ 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
|
||||
GLOBAL_AUTO_CONFIGURE_SUDOERS=no
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
###############################################################################
|
||||
# CIBLE : prod
|
||||
###############################################################################
|
||||
|
||||
# TARGET_HOST_prod=10.0.0.20
|
||||
# TARGET_PORT_prod=22
|
||||
# TARGET_BOOTSTRAP_USER_prod=backup_liot
|
||||
# TARGET_BOOTSTRAP_SSH_KEY_prod=/home/matteo/.ssh/id_ed25519_target_prod
|
||||
# TARGET_RUNTIME_USER_prod=backup_liot
|
||||
# TARGET_ENABLE_BOOTSTRAP_prod=yes
|
||||
# TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_prod=yes
|
||||
# TARGET_REPO_DIR_prod=/home/backup_liot/RebuildBdd
|
||||
# TARGET_ENV_FILE_prod=/home/backup_liot/RebuildBdd/.env
|
||||
# TARGET_ENV_NAME_prod=PROD
|
||||
# TARGET_PGHOST_prod=127.0.0.1
|
||||
# TARGET_PGPORT_prod=5432
|
||||
# TARGET_PGUSER_prod=backup_liot
|
||||
# TARGET_PGPASSWORD_prod=change_me_prod_password
|
||||
# TARGET_DBS_prod="sirh inventory ferme"
|
||||
# TARGET_BACKUP_SUBDIR_prod=bdd-prod
|
||||
# TARGET_BACKUP_LOG_DIR_prod=/home/backup_liot/logs/rebuild_bdd
|
||||
# TARGET_LOCAL_RESTORE_BASE_DIR_prod=/home/backup_liot/RebuildBdd/restore_tmp
|
||||
# TARGET_SSH_KEY_prod=/home/backup_liot/.ssh/id_ed25519_backup_readonly
|
||||
# TARGET_REMOTE_ROLES_DIR_NAME_prod=user
|
||||
# TARGET_EXCLUDED_RESTORE_ROLES_prod="postgres"
|
||||
# TARGET_AUTO_INSTALL_POSTGRES_prod=yes
|
||||
# TARGET_AUTO_CREATE_PGUSER_prod=yes
|
||||
# TARGET_PGUSER_SUPERUSER_prod=no
|
||||
# TARGET_AUTO_CONFIGURE_SUDOERS_prod=no
|
||||
42
RebuildBdd/Config/Targets/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
|
||||
@@ -1,42 +1,42 @@
|
||||
###############################################################################
|
||||
# config/targets/test.env.example
|
||||
###############################################################################
|
||||
|
||||
# SSH bootstrap cible
|
||||
TARGET_HOST=192.168.1.50
|
||||
TARGET_PORT=22
|
||||
TARGET_BOOTSTRAP_USER=backup_liot
|
||||
TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_test
|
||||
TARGET_RUNTIME_USER=backup_liot
|
||||
|
||||
# Bootstrap
|
||||
TARGET_ENABLE_BOOTSTRAP=yes
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||
|
||||
# Repo local cible
|
||||
TARGET_REPO_DIR=/home/backup_liot/RebuildBdd
|
||||
TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env
|
||||
|
||||
# PostgreSQL cible
|
||||
TARGET_ENV_NAME=RECETTE
|
||||
TARGET_PGHOST=127.0.0.1
|
||||
TARGET_PGPORT=5432
|
||||
TARGET_PGUSER=backup_liot
|
||||
TARGET_PGPASSWORD=change_me_pg_password
|
||||
TARGET_DBS="sirh inventory ferme"
|
||||
|
||||
# Backup cible
|
||||
TARGET_BACKUP_SUBDIR=bdd-recette
|
||||
|
||||
# Logs / tmp / ssh cible
|
||||
TARGET_BACKUP_LOG_DIR=/home/backup_liot/logs/rebuild_bdd
|
||||
TARGET_LOCAL_RESTORE_BASE_DIR=/home/backup_liot/RebuildBdd/restore_tmp
|
||||
TARGET_SSH_KEY=/home/backup_liot/.ssh/id_ed25519_backup_readonly
|
||||
|
||||
# Options cible
|
||||
TARGET_REMOTE_ROLES_DIR_NAME=user
|
||||
TARGET_EXCLUDED_RESTORE_ROLES="postgres"
|
||||
TARGET_AUTO_INSTALL_POSTGRES=yes
|
||||
TARGET_AUTO_CREATE_PGUSER=yes
|
||||
TARGET_PGUSER_SUPERUSER=no
|
||||
TARGET_AUTO_CONFIGURE_SUDOERS=no
|
||||
###############################################################################
|
||||
# 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
|
||||
@@ -36,6 +36,38 @@ En pratique :
|
||||
|
||||
---
|
||||
|
||||
<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
|
||||
@@ -46,7 +78,7 @@ Le projet utilise deux niveaux de configuration :
|
||||
Fichier :
|
||||
|
||||
```bash
|
||||
config/global.env
|
||||
Config/global.env
|
||||
````
|
||||
|
||||
Contient les paramètres stables, par exemple :
|
||||
@@ -62,7 +94,7 @@ Contient les paramètres stables, par exemple :
|
||||
Fichiers :
|
||||
|
||||
```bash
|
||||
config/targets/<nom_cible>.env
|
||||
Config/Targets/<nom_cible>.env
|
||||
```
|
||||
|
||||
Chaque fichier cible contient :
|
||||
@@ -83,9 +115,9 @@ RebuildBdd/
|
||||
├── create-target-config.sh
|
||||
├── run-rebuild-bdd.sh
|
||||
├── rebuild-bdd-core.sh
|
||||
├── config/
|
||||
├── Config/
|
||||
│ ├── global.env
|
||||
│ └── targets/
|
||||
│ └── Targets/
|
||||
│ ├── test.env
|
||||
│ └── prod.env
|
||||
└── Checkup/
|
||||
@@ -102,7 +134,7 @@ RebuildBdd/
|
||||
Crée ou met à jour un fichier cible dans :
|
||||
|
||||
```bash
|
||||
config/targets/<cible>.env
|
||||
Config/Targets/<cible>.env
|
||||
```
|
||||
|
||||
Usage :
|
||||
@@ -110,14 +142,14 @@ Usage :
|
||||
```bash
|
||||
./create-target-config.sh \
|
||||
--target test \
|
||||
--host 192.168.1.50 \
|
||||
--host <TARGET_HOST> \
|
||||
--port 22 \
|
||||
--bootstrap-user backup_liot \
|
||||
--bootstrap-user <BOOTSTRAP_USER> \
|
||||
--bootstrap-key /home/user/.ssh/id_ed25519_target_test \
|
||||
--runtime-user backup_liot \
|
||||
--repo-dir /home/backup_liot/RebuildBdd \
|
||||
--runtime-user <RUNTIME_USER> \
|
||||
--repo-dir /home/<RUNTIME_USER>/RebuildBdd \
|
||||
--env-name RECETTE \
|
||||
--pguser backup_liot \
|
||||
--pguser <PGUSER> \
|
||||
--pgpassword secret \
|
||||
--dbs "sirh inventory ferme" \
|
||||
--backup-subdir bdd-recette
|
||||
@@ -233,6 +265,7 @@ Doit disposer de :
|
||||
* `scp`
|
||||
* `git`
|
||||
* `python3`
|
||||
* `jq` si vous consommez les JSON côté tooling
|
||||
|
||||
### Machine cible
|
||||
|
||||
@@ -240,7 +273,8 @@ Le bootstrap suppose :
|
||||
|
||||
* accès SSH fonctionnel ;
|
||||
* utilisateur bootstrap existant ;
|
||||
* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial.
|
||||
* 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
|
||||
|
||||
@@ -250,6 +284,20 @@ Doit :
|
||||
* 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
|
||||
@@ -290,13 +338,13 @@ Le script recherche :
|
||||
Copier :
|
||||
|
||||
```bash
|
||||
config/global.env.example
|
||||
Config/.env.exemple
|
||||
```
|
||||
|
||||
vers :
|
||||
|
||||
```bash
|
||||
config/global.env
|
||||
Config/global.env
|
||||
```
|
||||
|
||||
Renseigner ensuite :
|
||||
@@ -317,13 +365,13 @@ Deux possibilités.
|
||||
Créer un fichier :
|
||||
|
||||
```bash
|
||||
config/targets/test.env
|
||||
Config/Targets/test.env
|
||||
```
|
||||
|
||||
à partir de :
|
||||
|
||||
```bash
|
||||
config/targets/test.env.example
|
||||
Config/Targets/test.env.exemple
|
||||
```
|
||||
|
||||
#### B. Via script
|
||||
@@ -400,7 +448,7 @@ Ces informations doivent être stockées dans la configuration serveur.
|
||||
|
||||
Le flux recommandé est :
|
||||
|
||||
1. créer ou mettre à jour `config/targets/<cible>.env`
|
||||
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> ...`
|
||||
|
||||
@@ -427,7 +475,7 @@ Exemple :
|
||||
"environment": "RECETTE",
|
||||
"database": "sirh",
|
||||
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
|
||||
"log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||
"log_file": "/home/<RUNTIME_USER>/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -443,7 +491,7 @@ Exemple :
|
||||
"environment": "RECETTE",
|
||||
"database": "sirh",
|
||||
"dump_file": "/home/backup/backups/bdd-recette/sirh/sirh_2026-03-16_19-00-01.dump",
|
||||
"log_file": "/home/backup_liot/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||
"log_file": "/home/<RUNTIME_USER>/logs/rebuild_bdd/restore_recette_web_001_2026-03-17_09-10-00.log"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -484,7 +532,7 @@ TARGET_BACKUP_LOG_DIR
|
||||
Exemple :
|
||||
|
||||
```bash
|
||||
/home/backup_liot/logs/rebuild_bdd/
|
||||
/home/<RUNTIME_USER>/logs/rebuild_bdd/
|
||||
```
|
||||
|
||||
Le chemin du log est renvoyé dans le JSON final.
|
||||
@@ -521,14 +569,14 @@ Avant mise en production, tester au minimum :
|
||||
```bash
|
||||
./create-target-config.sh \
|
||||
--target test \
|
||||
--host 192.168.1.50 \
|
||||
--host <TARGET_HOST> \
|
||||
--port 22 \
|
||||
--bootstrap-user backup_liot \
|
||||
--bootstrap-key /home/matteo/.ssh/id_ed25519_target_test \
|
||||
--runtime-user backup_liot \
|
||||
--repo-dir /home/backup_liot/RebuildBdd \
|
||||
--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 backup_liot \
|
||||
--pguser <PGUSER> \
|
||||
--pgpassword secret \
|
||||
--dbs "sirh inventory ferme" \
|
||||
--backup-subdir bdd-recette
|
||||
@@ -562,5 +610,3 @@ Le projet permet désormais une utilisation :
|
||||
* intégrée au web ;
|
||||
|
||||
avec préparation des cibles, exécution non interactive et retour JSON.
|
||||
|
||||
```
|
||||
|
||||
@@ -94,6 +94,7 @@ to_bool_yes_no() {
|
||||
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
|
||||
@@ -221,6 +222,14 @@ if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||
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"
|
||||
@@ -258,7 +267,7 @@ SSH_OPTS=(
|
||||
-p "$BOOTSTRAP_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o ConnectTimeout=8
|
||||
)
|
||||
|
||||
@@ -339,6 +348,7 @@ 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")
|
||||
@@ -376,6 +386,13 @@ 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
|
||||
|
||||
@@ -385,6 +402,10 @@ if ! command -v ssh-keyscan >/dev/null 2>&1; then
|
||||
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
|
||||
"
|
||||
@@ -393,13 +414,6 @@ log "Ajout du serveur de backup dans known_hosts côté cible"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_KNOWN_HOSTS_CMD" \
|
||||
|| fail "échec de préparation known_hosts sur la cible"
|
||||
|
||||
STRICT_OPTION="yes"
|
||||
case "${TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE,,}" in
|
||||
yes|y|oui|o|true|1) STRICT_OPTION="yes" ;;
|
||||
no|n|non|false|0) STRICT_OPTION="no" ;;
|
||||
*) fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" ;;
|
||||
esac
|
||||
|
||||
REMOTE_BACKUP_TEST_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
@@ -488,6 +502,10 @@ 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
|
||||
|
||||
@@ -79,6 +79,7 @@ to_bool_yes_no() {
|
||||
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
|
||||
|
||||
@@ -83,6 +83,8 @@ print_stdout() {
|
||||
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
||||
}
|
||||
|
||||
LOG_FILE=/dev/stderr
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
echo "$msg" >>"$LOG_FILE"
|
||||
@@ -95,6 +97,10 @@ fail() {
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
@@ -119,6 +125,7 @@ to_bool_yes_no() {
|
||||
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
|
||||
@@ -134,6 +141,13 @@ sql_escape_literal() {
|
||||
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
|
||||
@@ -165,6 +179,22 @@ build_excluded_roles_regex() {
|
||||
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:-}" \
|
||||
@@ -220,6 +250,7 @@ RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || {
|
||||
}
|
||||
|
||||
[[ "$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" || {
|
||||
@@ -248,7 +279,7 @@ else
|
||||
fi
|
||||
|
||||
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename curl; do
|
||||
require_cmd "$cmd" || fail "commande requise absente : $cmd"
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
|
||||
CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh"
|
||||
@@ -260,6 +291,7 @@ 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
|
||||
|
||||
@@ -315,7 +347,7 @@ for candidate in "${DBS_ARRAY[@]}"; do
|
||||
done
|
||||
|
||||
[[ -n "$DB" ]] || fail "base refusée : non présente dans DBS"
|
||||
[[ "$DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide"
|
||||
validate_db_name "$DB" || fail "nom de base invalide"
|
||||
|
||||
log "Environnement : $ENV_NAME"
|
||||
log "Base cible : $DB"
|
||||
@@ -475,27 +507,13 @@ pg_restore \
|
||||
"$LOCAL_DB_DUMP_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec restauration base ${DB}"
|
||||
|
||||
send_discord_message() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||
|
||||
payload="$(python3 -c 'import json,sys; print(json.dumps({"content": sys.argv[1]}))' "$message")" || return 0
|
||||
|
||||
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null || true
|
||||
}
|
||||
|
||||
SUCCESS_MESSAGE="✅ REBUILD BDD ${ENV_NAME}
|
||||
Base restaurée : ${DB}
|
||||
Hôte PostgreSQL : ${PGHOST}:${PGPORT}
|
||||
Dump utilisé : $(basename "$LAST_REMOTE_DB_DUMP")
|
||||
Log : ${LOG_FILE}"
|
||||
|
||||
send_discord_message "$SUCCESS_MESSAGE"
|
||||
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
|
||||
|
||||
@@ -88,6 +88,7 @@ to_bool_yes_no() {
|
||||
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
|
||||
@@ -123,6 +124,10 @@ 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"
|
||||
@@ -195,6 +200,14 @@ if [[ -n "$TARGET_REPO_SUBDIR" ]]; then
|
||||
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"
|
||||
@@ -261,7 +274,7 @@ SSH_OPTS=(
|
||||
-p "$TARGET_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o ConnectTimeout=8
|
||||
)
|
||||
|
||||
@@ -294,6 +307,10 @@ 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
|
||||
|
||||
@@ -47,14 +47,13 @@ Les scripts fonctionnent indépendamment mais utilisent le même principe :
|
||||
|
||||
Environnement Linux recommandé.
|
||||
|
||||
Packages nécessaires :
|
||||
Packages nécessaires sur Ubuntu Server :
|
||||
|
||||
```
|
||||
postgresql-client
|
||||
curl
|
||||
jq
|
||||
ssh
|
||||
scp
|
||||
openssh-client
|
||||
```
|
||||
|
||||
Commandes PostgreSQL requises :
|
||||
@@ -116,11 +115,18 @@ ssh-copy-id -i ~/.ssh/id_backup_postgres.pub backup@192.168.1.50
|
||||
Tester la connexion sans mot de passe :
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_backup_postgres backup@192.168.1.50
|
||||
ssh -i ~/.ssh/id_backup_postgres -o StrictHostKeyChecking=yes backup@192.168.1.50
|
||||
```
|
||||
|
||||
La connexion doit fonctionner **sans demander de mot de passe**.
|
||||
|
||||
Provisionner aussi `known_hosts` avant le premier run :
|
||||
|
||||
```bash
|
||||
ssh-keyscan -H 192.168.1.50 >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sécuriser les permissions
|
||||
@@ -153,13 +159,18 @@ cp backup.env.exemple .env
|
||||
|
||||
Puis modifier les variables.
|
||||
|
||||
Variables SSH supplémentaires désormais supportées :
|
||||
|
||||
```
|
||||
BACKUP_REMOTE_SSH_PORT
|
||||
BACKUP_KNOWN_HOSTS_STRICT
|
||||
BACKUP_KNOWN_HOSTS_FILE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. Script : backup-bdd-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Sauvegarder plusieurs bases PostgreSQL et transférer les dumps vers un serveur distant.
|
||||
@@ -205,6 +216,8 @@ Suppression des sauvegardes plus anciennes que :
|
||||
10 jours
|
||||
```
|
||||
|
||||
Le script utilise maintenant des options SSH strictes et refuse les clés privées symboliques.
|
||||
|
||||
---
|
||||
|
||||
## Exécution
|
||||
@@ -217,9 +230,6 @@ Suppression des sauvegardes plus anciennes que :
|
||||
|
||||
# 6. Script : check-statut-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Vérifier la disponibilité des applications web.
|
||||
@@ -279,12 +289,40 @@ CHECK APP RECETTE 🟢
|
||||
```
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">
|
||||
<strong>EggMaster</strong>
|
||||
</summary>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Question 5</summary>
|
||||
|
||||
Quelle option demande explicitement un decodage plutot qu'un encodage ?
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Indice commande 5</summary>
|
||||
|
||||
```text
|
||||
-d
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="list-style: none; cursor: pointer;">Fragment 5</summary>
|
||||
|
||||
```text
|
||||
dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNR
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
# 7. Script : rebuild-bdd-recette.sh
|
||||
|
||||
Script :
|
||||
|
||||
|
||||
## Objectif
|
||||
|
||||
Restaurer une base PostgreSQL à partir d’un dump distant.
|
||||
@@ -306,6 +344,8 @@ Le script :
|
||||
9. restaure la base via `pg_restore`
|
||||
10. envoie une notification Discord
|
||||
|
||||
Le script supporte désormais le port SSH distant, un fichier `known_hosts` dédié et l’exclusion configurable des rôles via `EXCLUDED_RESTORE_ROLES`.
|
||||
|
||||
---
|
||||
|
||||
## Sélection de la base
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# backup-bdd-recette.sh
|
||||
@@ -17,7 +18,8 @@ set -euo pipefail
|
||||
# 6. exporte les rôles PostgreSQL ;
|
||||
# 7. dump chaque base au format personnalisé PostgreSQL ;
|
||||
# 8. transfère chaque fichier vers le serveur distant ;
|
||||
# 9. applique une rotation distante sur 10 jours ;
|
||||
# 9. applique une rotation distante selon BACKUP_RETENTION_DAYS
|
||||
# (10 jours par défaut) ;
|
||||
# 10. envoie un bilan sur Discord :
|
||||
# - 1 message global si tout est OK ;
|
||||
# - en cas d’erreur partielle :
|
||||
@@ -49,6 +51,10 @@ set +a
|
||||
#######################################
|
||||
|
||||
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
@@ -57,6 +63,10 @@ set +a
|
||||
: "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}"
|
||||
: "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}"
|
||||
: "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}"
|
||||
[[ "$BACKUP_REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || {
|
||||
echo "Variable BACKUP_REMOTE_DIR invalide : $BACKUP_REMOTE_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${SSH_KEY:?Variable SSH_KEY manquante}"
|
||||
: "${SSH_TIMEOUT:?Variable SSH_TIMEOUT manquante}"
|
||||
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
|
||||
@@ -67,15 +77,82 @@ set +a
|
||||
|
||||
read -r -a DBS_ARRAY <<< "$DBS"
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="$1"
|
||||
|
||||
[[ -n "$db_name" ]] || {
|
||||
echo "ERROR: nom de base vide dans DBS" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || {
|
||||
echo "ERROR: nom de base invalide dans DBS : $db_name" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
validate_db_name "$DB"
|
||||
done
|
||||
|
||||
IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
IA_BASE_DIR="${BACKUP_REMOTE_DIR}"
|
||||
RETENTION_DAYS=10
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}"
|
||||
BACKUP_KNOWN_HOSTS_FILE="${BACKUP_KNOWN_HOSTS_FILE:-${HOME}/.ssh/known_hosts}"
|
||||
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: BACKUP_REMOTE_SSH_PORT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: PGPORT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$SSH_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: SSH_TIMEOUT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
|
||||
echo "ERROR: PGUSER invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in
|
||||
yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;;
|
||||
no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;;
|
||||
*)
|
||||
echo "ERROR: BACKUP_KNOWN_HOSTS_STRICT invalide" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")"
|
||||
chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true
|
||||
touch "$BACKUP_KNOWN_HOSTS_FILE"
|
||||
chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_TIMEOUT}"
|
||||
-o StrictHostKeyChecking="${BACKUP_KNOWN_HOSTS_STRICT}"
|
||||
-o UserKnownHostsFile="${BACKUP_KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
LOG_DIR="${BACKUP_LOG_DIR}"
|
||||
@@ -85,15 +162,32 @@ TS="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||
BACKUP_DIR_NAME="backup_${TS}"
|
||||
LOG_FILE="${LOG_DIR}/${BACKUP_DIR_NAME}.log"
|
||||
|
||||
TMP_DIR="/tmp/pg_dump_${BACKUP_DIR_NAME}"
|
||||
mkdir -p "$TMP_DIR"
|
||||
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
log() { echo "---- $(date +'%Y-%m-%d %H:%M:%S') ---- $*"; }
|
||||
TMP_DIR="$(mktemp -d /tmp/pg_dump_XXXXXX)" || {
|
||||
echo "ERROR: impossible de créer le dossier temporaire" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande manquante : $1"
|
||||
}
|
||||
|
||||
safe_remove_dir() {
|
||||
local dir="${1:-}"
|
||||
[[ -n "$dir" ]] || return 0
|
||||
[[ "$dir" == /tmp/pg_dump_* ]] || {
|
||||
log "WARNING: suppression refusée pour le chemin inattendu : $dir"
|
||||
return 1
|
||||
}
|
||||
rm -rf -- "$dir"
|
||||
}
|
||||
|
||||
export PGPASSWORD
|
||||
@@ -102,13 +196,27 @@ export PGPASSWORD
|
||||
# Vérification dépendances minimales
|
||||
#######################################
|
||||
|
||||
for cmd in ssh scp curl jq pg_dump pg_dumpall; do
|
||||
require_cmd "$cmd" || {
|
||||
echo "ERROR: commande manquante : $cmd" >&2
|
||||
exit 1
|
||||
}
|
||||
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
|
||||
#######################################
|
||||
@@ -116,14 +224,14 @@ done
|
||||
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||
DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
discord_send() {
|
||||
send_discord() {
|
||||
local msg="$1"
|
||||
local payload
|
||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')" || {
|
||||
log "ERROR: impossible de construire le payload JSON Discord"
|
||||
return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
curl -fsS \
|
||||
@@ -145,7 +253,7 @@ Dumps transfer: ✅
|
||||
Users transfer: ✅
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -159,7 +267,7 @@ discord_msg_users_ok_simple() {
|
||||
Users backup validé
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
discord_msg_users_error() {
|
||||
@@ -191,7 +299,7 @@ EOF
|
||||
)"
|
||||
fi
|
||||
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -206,7 +314,7 @@ discord_msg_db_ok_simple() {
|
||||
Backup validé : ${db}
|
||||
EOF
|
||||
)"
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
discord_msg_db_error() {
|
||||
@@ -241,7 +349,7 @@ EOF
|
||||
)"
|
||||
fi
|
||||
|
||||
discord_send "$msg"
|
||||
send_discord "$msg"
|
||||
}
|
||||
|
||||
#######################################
|
||||
@@ -264,26 +372,53 @@ declare -A DB_DETAILS
|
||||
#######################################
|
||||
|
||||
LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d"
|
||||
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
log "ERROR: Backup déjà en cours"
|
||||
discord_msg_users_error "" "" "Lock already exists"
|
||||
exit 1
|
||||
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
|
||||
|
||||
trap 'rm -rf "$LOCK_DIR" "$TMP_DIR"' EXIT
|
||||
echo $$ > "$LOCK_PID_FILE" || {
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
fail "impossible d'écrire le PID du lock"
|
||||
}
|
||||
|
||||
if [[ "${stale_lock:-no}" == "yes" ]]; then
|
||||
log "Lock périmé nettoyé."
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -rf -- "$LOCK_DIR"
|
||||
safe_remove_dir "$TMP_DIR" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Préparation du dossier distant
|
||||
#######################################
|
||||
|
||||
REMOTE_DIR="${IA_BASE_DIR}"
|
||||
|
||||
log "Creating remote directories"
|
||||
|
||||
MKDIR_CMD="mkdir -p '${REMOTE_DIR}/user'"
|
||||
MKDIR_CMD="mkdir -p '${BACKUP_REMOTE_DIR}/user'"
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
MKDIR_CMD+=" '${REMOTE_DIR}/${DB}'"
|
||||
MKDIR_CMD+=" '${BACKUP_REMOTE_DIR}/${DB}'"
|
||||
done
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "$MKDIR_CMD"; then
|
||||
@@ -298,18 +433,18 @@ fi
|
||||
|
||||
ROLES_FILE="${TMP_DIR}/user_${TS}.sql"
|
||||
|
||||
set +e
|
||||
|
||||
log "Export des rôles PostgreSQL"
|
||||
|
||||
pg_dumpall \
|
||||
if pg_dumpall \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
--globals-only \
|
||||
> "$ROLES_FILE"
|
||||
|
||||
RET=$?
|
||||
> "$ROLES_FILE"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
USERS_OK=
|
||||
@@ -320,8 +455,11 @@ else
|
||||
fi
|
||||
|
||||
if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
|
||||
scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/user/"
|
||||
RET=$?
|
||||
if scp "${SCP_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/user/"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
USERS_OK=
|
||||
@@ -336,14 +474,10 @@ if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
#######################################
|
||||
# Dump des bases
|
||||
#######################################
|
||||
|
||||
set +e
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
FILE="${TMP_DIR}/${DB}_${TS}.dump"
|
||||
|
||||
@@ -353,8 +487,11 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
|
||||
log "Dump $DB"
|
||||
|
||||
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"
|
||||
RET=$?
|
||||
if pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
DUMPS_OK=
|
||||
@@ -364,8 +501,11 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/${DB}/"
|
||||
RET=$?
|
||||
if scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/${DB}/"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
DUMPS_OK=
|
||||
@@ -374,18 +514,17 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
#######################################
|
||||
# Rotation distante
|
||||
#######################################
|
||||
|
||||
log "Starting remote rotation: delete backups older than ${RETENTION_DAYS} days"
|
||||
|
||||
set +e
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"
|
||||
RET=$?
|
||||
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
log "ERROR: remote rotation failed for users"
|
||||
@@ -394,8 +533,11 @@ else
|
||||
fi
|
||||
|
||||
for DB in "${DBS_ARRAY[@]}"; do
|
||||
ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"
|
||||
RET=$?
|
||||
if ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"; then
|
||||
RET=0
|
||||
else
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
log "ERROR: remote rotation failed for ${DB}"
|
||||
@@ -404,15 +546,13 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
log "Remote rotation finished"
|
||||
|
||||
#######################################
|
||||
# Nettoyage local
|
||||
#######################################
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
safe_remove_dir "$TMP_DIR" || true
|
||||
|
||||
#######################################
|
||||
# Bilan final Discord
|
||||
@@ -442,4 +582,4 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
exit 2
|
||||
exit 2
|
||||
|
||||
@@ -44,9 +44,18 @@ BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
# Clé SSH utilisée pour se connecter au serveur distant
|
||||
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
|
||||
# Port SSH du serveur de backup
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
# Timeout de connexion SSH (secondes)
|
||||
SSH_TIMEOUT=10
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
|
||||
|
||||
###############################################################################
|
||||
# LOGS
|
||||
###############################################################################
|
||||
@@ -62,4 +71,4 @@ BACKUP_LOG_DIR=/var/log/script/
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
# Mention envoyée en cas d'erreur
|
||||
DISCORD_PING=@here
|
||||
DISCORD_PING=@here
|
||||
|
||||
@@ -44,13 +44,23 @@ set +a
|
||||
: "${CHECK_MAX_TIME:?Variable CHECK_MAX_TIME manquante}"
|
||||
: "${APP_URLS:?Variable APP_URLS manquante}"
|
||||
|
||||
[[ "$CHECK_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable CHECK_CONNECT_TIMEOUT invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$CHECK_MAX_TIME" =~ ^[0-9]+$ ]] || {
|
||||
echo "ERROR: Variable CHECK_MAX_TIME invalide" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Sites à vérifier
|
||||
#######################################
|
||||
|
||||
read -r -a SITES <<< "$APP_URLS"
|
||||
|
||||
SCHEME="http"
|
||||
SCHEME="${APP_SCHEME:-http}"
|
||||
CONNECT_TIMEOUT="${CHECK_CONNECT_TIMEOUT}"
|
||||
MAX_TIME="${CHECK_MAX_TIME}"
|
||||
|
||||
@@ -75,6 +85,16 @@ DISCORD_PING="${DISCORD_PING:-@here}"
|
||||
|
||||
SUMMARY_LINES=()
|
||||
FAILURES=0
|
||||
TMPFILES=()
|
||||
|
||||
cleanup() {
|
||||
local tmpfile
|
||||
for tmpfile in "${TMPFILES[@]}"; do
|
||||
[[ -n "$tmpfile" ]] || continue
|
||||
rm -f -- "$tmpfile"
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Logging
|
||||
@@ -115,8 +135,21 @@ add_summary_line() {
|
||||
#######################################
|
||||
# Envoi du message Discord récapitulatif
|
||||
#######################################
|
||||
send_discord_summary() {
|
||||
should_send_discord() {
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local current_hour current_minute
|
||||
current_hour="$(date +'%H')"
|
||||
current_minute="$(date +'%M')"
|
||||
|
||||
[[ "$current_hour" == "19" && "$current_minute" -ge 0 && "$current_minute" -le 4 ]]
|
||||
}
|
||||
|
||||
send_discord() {
|
||||
[[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return 0
|
||||
should_send_discord || return 0
|
||||
|
||||
local header_icon ping_prefix=""
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
@@ -134,7 +167,7 @@ send_discord_summary() {
|
||||
done
|
||||
|
||||
local payload
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')"
|
||||
payload="$(jq -n --arg content "$msg" '{content: $content}')" || return 0
|
||||
|
||||
curl -fsS -H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
@@ -158,6 +191,7 @@ check_site() {
|
||||
local http_code curl_exit err
|
||||
local stderr
|
||||
stderr="$(mktemp)"
|
||||
TMPFILES+=("$stderr")
|
||||
|
||||
http_code="$(
|
||||
curl -sS -o /dev/null \
|
||||
@@ -170,15 +204,12 @@ check_site() {
|
||||
|
||||
if [[ "$curl_exit" -ne 0 ]]; then
|
||||
err="$(head -n 1 "$stderr" | tr -d '\r')"
|
||||
rm -f "$stderr"
|
||||
|
||||
log_line "DOWN" "$host" "curl exit=$curl_exit : ${err:-"(aucun)"}"
|
||||
add_summary_line "$host" "DOWN" "DOWN - curl"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$stderr"
|
||||
|
||||
if [[ "$http_code" =~ ^[0-9]{3}$ ]]; then
|
||||
if [[ "$http_code" -ge 200 && "$http_code" -le 399 ]]; then
|
||||
log_line "OK" "$host" "HTTP $http_code"
|
||||
@@ -201,8 +232,6 @@ check_site() {
|
||||
#######################################
|
||||
|
||||
main() {
|
||||
trap '[[ -n "$STDERR_TMP" ]] && rm -f "$STDERR_TMP"' EXIT
|
||||
|
||||
local failures=0
|
||||
|
||||
for site in "${SITES[@]}"; do
|
||||
@@ -212,7 +241,7 @@ main() {
|
||||
done
|
||||
|
||||
FAILURES="$failures"
|
||||
send_discord_summary
|
||||
send_discord
|
||||
|
||||
if [[ "$failures" -gt 0 ]]; then
|
||||
exit 2
|
||||
@@ -221,4 +250,4 @@ main() {
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
main "$@"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
###############################################################################
|
||||
# rebuild-bdd-recette.sh
|
||||
@@ -49,6 +50,10 @@ set +a
|
||||
# Variables obligatoires
|
||||
###############################################################################
|
||||
: "${ENV_NAME:?Variable ENV_NAME manquante}"
|
||||
[[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||
echo "Variable ENV_NAME invalide : $ENV_NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${PGHOST:?Variable PGHOST manquante}"
|
||||
: "${PGPORT:?Variable PGPORT manquante}"
|
||||
: "${PGUSER:?Variable PGUSER manquante}"
|
||||
@@ -65,8 +70,10 @@ set +a
|
||||
###############################################################################
|
||||
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_DIR:-${SCRIPT_DIR}/restore_tmp}"
|
||||
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
|
||||
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
|
||||
|
||||
###############################################################################
|
||||
# Préparation des dossiers locaux
|
||||
@@ -108,42 +115,66 @@ cleanup() {
|
||||
"${FILTERED_ROLES_FILE:-}" \
|
||||
"${ROLES_CREATE_LIST:-}" \
|
||||
"${ROLES_APPLY_FILE:-}"
|
||||
rm -rf "${LOCAL_RESTORE_DIR:-}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
sql_escape_literal() {
|
||||
local s="${1:-}"
|
||||
s="${s//\'/\'\'}"
|
||||
printf "%s" "$s"
|
||||
}
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="${1:-}"
|
||||
|
||||
[[ -n "$db_name" ]] || return 1
|
||||
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
|
||||
}
|
||||
|
||||
build_excluded_roles_regex() {
|
||||
local role regex=""
|
||||
|
||||
for role in $EXCLUDED_RESTORE_ROLES; do
|
||||
[[ -z "$role" ]] && continue
|
||||
[[ "$role" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || fail "rôle exclu invalide : ${role}"
|
||||
if [[ -n "$regex" ]]; then
|
||||
regex+="|"
|
||||
fi
|
||||
regex+="$role"
|
||||
done
|
||||
|
||||
printf '%s' "$regex"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Envoi Discord
|
||||
#
|
||||
# Envoi simple d'un message texte via webhook Discord.
|
||||
# Si WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||
# Si DISCORD_WEBHOOK_URL n'est pas défini, on ignore silencieusement l'envoi.
|
||||
###############################################################################
|
||||
send_discord_message() {
|
||||
send_discord() {
|
||||
local message="$1"
|
||||
local payload=""
|
||||
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || {
|
||||
log "WEBHOOK_URL non défini : notification Discord ignorée."
|
||||
return 0
|
||||
}
|
||||
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
|
||||
has_cmd jq || return 0
|
||||
has_cmd curl || return 0
|
||||
|
||||
if ! require_cmd curl; then
|
||||
log "curl absent : notification Discord ignorée."
|
||||
return 0
|
||||
fi
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || return 0
|
||||
|
||||
payload="$(jq -n --arg content "$message" '{content: $content}')" || {
|
||||
log "Impossible de construire le payload JSON Discord."
|
||||
return 0
|
||||
}
|
||||
|
||||
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
|
||||
curl -fsS "$DISCORD_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null || log "Échec d'envoi de la notification Discord."
|
||||
>/dev/null || true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
@@ -151,15 +182,29 @@ send_discord_message() {
|
||||
###############################################################################
|
||||
[[ -f "$SSH_KEY" ]] || fail "clé SSH introuvable : $SSH_KEY"
|
||||
[[ -r "$SSH_KEY" ]] || fail "clé SSH non lisible : $SSH_KEY"
|
||||
[[ ! -L "$SSH_KEY" ]] || fail "clé SSH ne doit pas être un lien symbolique : $SSH_KEY"
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||
[[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
SCP_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-P "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=yes
|
||||
)
|
||||
|
||||
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
@@ -171,7 +216,7 @@ REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
|
||||
###############################################################################
|
||||
POSTGRES_INSTALLED=false
|
||||
|
||||
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; then
|
||||
if ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb; then
|
||||
log "PostgreSQL absent : installation en cours..."
|
||||
|
||||
sudo apt update >>"$LOG_FILE" 2>&1 || fail "échec de apt update"
|
||||
@@ -198,15 +243,17 @@ fi
|
||||
# Attente disponibilité PostgreSQL
|
||||
###############################################################################
|
||||
log "Vérification de la disponibilité de PostgreSQL..."
|
||||
PG_READY=false
|
||||
for _ in {1..20}; do
|
||||
if sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
PG_READY=true
|
||||
log "PostgreSQL répond correctement."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! sudo -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
if [[ "$PG_READY" != true ]]; then
|
||||
fail "PostgreSQL ne répond pas correctement"
|
||||
fi
|
||||
|
||||
@@ -217,7 +264,7 @@ if [[ "$POSTGRES_INSTALLED" == "true" ]]; then
|
||||
log "Création du rôle PostgreSQL ${PGUSER} suite à une installation neuve..."
|
||||
|
||||
sudo -u postgres psql -d postgres -c \
|
||||
"CREATE ROLE \"${PGUSER}\" WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${PGPASSWORD}';" \
|
||||
"CREATE ROLE \"${PGUSER}\" WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '$(sql_escape_literal "$PGPASSWORD")';" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec de création du rôle ${PGUSER}"
|
||||
|
||||
log "Rôle PostgreSQL ${PGUSER} créé."
|
||||
@@ -251,9 +298,10 @@ if [[ "${USE_LIST,,}" == "oui" || "${USE_LIST,,}" == "o" ]]; then
|
||||
DB="${DBS_ARRAY[$((DB_INDEX - 1))]}"
|
||||
else
|
||||
read -r -p "Nom exact de la base à restaurer : " DB
|
||||
[[ -n "$DB" ]] || fail "nom de base vide"
|
||||
fi
|
||||
|
||||
validate_db_name "$DB" || fail "nom de base invalide"
|
||||
|
||||
log "Environnement : $ENV_NAME"
|
||||
log "Base cible sélectionnée : $DB"
|
||||
|
||||
@@ -312,7 +360,7 @@ LOCAL_DB_DUMP_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_DB_DUMP")"
|
||||
LOCAL_ROLES_FILE=""
|
||||
|
||||
log "Téléchargement du dump..."
|
||||
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
|
||||
scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du dump principal"
|
||||
|
||||
###############################################################################
|
||||
@@ -322,7 +370,7 @@ if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
|
||||
LOCAL_ROLES_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_ROLES_FILE")"
|
||||
|
||||
log "Téléchargement du fichier des rôles..."
|
||||
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
|
||||
scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
|
||||
>>"$LOG_FILE" 2>&1 || fail "échec du téléchargement du fichier des rôles"
|
||||
else
|
||||
log "La restauration des rôles sera ignorée."
|
||||
@@ -341,7 +389,7 @@ fi
|
||||
###############################################################################
|
||||
DB_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='${DB}'" 2>>"$LOG_FILE" || true
|
||||
"SELECT 1 FROM pg_database WHERE datname='$(sql_escape_literal "$DB")'" 2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$DB_EXISTS" == "1" ]]; then
|
||||
@@ -364,9 +412,16 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||
FILTERED_ROLES_FILE="${LOCAL_RESTORE_DIR}/filtered_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_CREATE_LIST="${LOCAL_RESTORE_DIR}/roles_to_create_$(basename "$LOCAL_ROLES_FILE")"
|
||||
ROLES_APPLY_FILE="${LOCAL_RESTORE_DIR}/roles_apply_$(basename "$LOCAL_ROLES_FILE")"
|
||||
EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex)"
|
||||
|
||||
grep -viE '^(CREATE ROLE|ALTER ROLE) (backup_liot|postgres)\b' "$LOCAL_ROLES_FILE" \
|
||||
> "$FILTERED_ROLES_FILE" || true
|
||||
if [[ -n "$EXCLUDED_ROLES_REGEX" ]]; then
|
||||
grep -viE "^(CREATE ROLE|ALTER ROLE) (${EXCLUDED_ROLES_REGEX})\\b" "$LOCAL_ROLES_FILE" \
|
||||
> "$FILTERED_ROLES_FILE" || true
|
||||
else
|
||||
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
|
||||
fi
|
||||
|
||||
sed -i -E '/^ALTER ROLE .* (NO)?SUPERUSER\b/d' "$FILTERED_ROLES_FILE"
|
||||
|
||||
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
|
||||
|
||||
@@ -383,7 +438,7 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then
|
||||
|
||||
ROLE_EXISTS="$(
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='${role_name}'" 2>>"$LOG_FILE" || true
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='$(sql_escape_literal "$role_name")'" 2>>"$LOG_FILE" || true
|
||||
)"
|
||||
|
||||
if [[ "$ROLE_EXISTS" != "1" ]]; then
|
||||
@@ -448,4 +503,4 @@ Hôte PostgreSQL : ${PGHOST}:${PGPORT}
|
||||
Dump utilisé : $(basename "$LAST_REMOTE_DB_DUMP")
|
||||
Log : ${LOG_FILE}"
|
||||
|
||||
send_discord_message "$SUCCESS_MESSAGE"
|
||||
send_discord "$SUCCESS_MESSAGE"
|
||||
|
||||
@@ -39,6 +39,9 @@ BACKUP_REMOTE_HOST=
|
||||
# Répertoire racine distant :
|
||||
BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
|
||||
# Port SSH du serveur de backup
|
||||
BACKUP_REMOTE_SSH_PORT=22
|
||||
|
||||
###############################################################################
|
||||
# SSH
|
||||
###############################################################################
|
||||
@@ -50,6 +53,12 @@ SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
# Variable optionnelle dans le script, mais utile ici comme valeur par défaut
|
||||
SSH_CONNECT_TIMEOUT=8
|
||||
|
||||
# Validation stricte des clés hôtes SSH (yes/no)
|
||||
BACKUP_KNOWN_HOSTS_STRICT=yes
|
||||
|
||||
# Fichier known_hosts utilisé par ssh/scp
|
||||
BACKUP_KNOWN_HOSTS_FILE=/home/.../.ssh/known_hosts
|
||||
|
||||
###############################################################################
|
||||
# LOGS
|
||||
###############################################################################
|
||||
@@ -72,9 +81,12 @@ LOCAL_RESTORE_DIR=/tmp/rebuild-bdd-recette
|
||||
# Nom du dossier distant contenant les exports SQL des rôles
|
||||
REMOTE_ROLES_DIR_NAME=user
|
||||
|
||||
# Rôles PostgreSQL à exclure lors de la restauration
|
||||
EXCLUDED_RESTORE_ROLES="postgres"
|
||||
|
||||
###############################################################################
|
||||
# DISCORD
|
||||
###############################################################################
|
||||
|
||||
# Webhook Discord pour notifier le succès de la restauration
|
||||
DISCORD_WEBHOOK_URL=
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
@@ -78,7 +78,7 @@ BACKUP_REMOTE_DIR=/home/.../backups/bdd-recette
|
||||
#############################################
|
||||
|
||||
# Clé SSH utilisée pour se connecter au serveur distant
|
||||
SSH_KEY=/home/.../.ssh/id_ed25519_backup
|
||||
SSH_KEY=/home/<USER>/.ssh/id_ed25519_backup
|
||||
|
||||
# Timeout SSH (secondes)
|
||||
SSH_TIMEOUT=10
|
||||
@@ -89,6 +89,7 @@ SSH_TIMEOUT=10
|
||||
#############################################
|
||||
|
||||
# Nombre de jours de conservation des sauvegardes
|
||||
# Utilisé par backup-bdd-recette.sh et backup-vaultwarden.sh
|
||||
BACKUP_RETENTION_DAYS=10
|
||||
|
||||
|
||||
@@ -96,12 +97,11 @@ BACKUP_RETENTION_DAYS=10
|
||||
# APPLICATIONS À SURVEILLER
|
||||
#############################################
|
||||
|
||||
# Liste des applications à vérifier
|
||||
APPS="
|
||||
ferme.malio-dev.fr
|
||||
inventory.malio-dev.fr
|
||||
sirh.malio-dev.fr
|
||||
"
|
||||
# Liste des applications à vérifier (séparées par espace)
|
||||
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||
|
||||
# Schéma utilisé pour les applications surveillées
|
||||
APP_SCHEME="http"
|
||||
|
||||
|
||||
#############################################
|
||||
|
||||
Reference in New Issue
Block a user