7 Commits

26 changed files with 1125 additions and 428 deletions

View File

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

View File

@@ -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 dobtenir une sauvegarde portable et compressée.
@@ -200,7 +254,7 @@ Cela permet dobtenir une sauvegarde portable et compressée.
Une fois larchive 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 sexé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 dobtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
```

View File

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

View File

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

View File

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

View File

@@ -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 lespace disque sur Ubuntu Server, avec notification Discord optionnelle.
## Préambule
Ce script est conçu pour vérifier l'espace de stockage disponible sur un serveur et envoyer une alerte
La vérification de l'espace de stockage ce fait sur la partition racine.
La limite d'alerte est fixée à 70% d'utilisation, mais vous pouvez ajuster cette valeur dans le script selon vos besoins.
## Fonctionnement
## Installation du script
Le script :
1. Clonez le dépôt GitHub :
```bash
git clone https://gitea.malio.fr/MALIO-DEV/Scripts-Serveur.git
```
2. Accédez au répertoire du projet :
3. ```bash
cd Scripts-Serveur/CheckStorage
```
### Génération de la clé SSH
1. charge `.env`
2. lit lutilisation de la partition `/`
3. compare le taux doccupation au seuil configuré
4. envoie une alerte Discord si le seuil est dépassé
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 dalerte, optionnel, défaut `@here`
- `STORAGE_ALERT_LIMIT` : seuil dalerte 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
```

View File

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

View File

@@ -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 lensemble des **scripts dexploitation, de maintenance et dautomatisation** utilisés dans linfrastructure 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 quil soit au même niveau
* ajouter un schéma darchitecture (flux SSH / dump / restore)
* ou intégrer directement votre workflow réel (IA machine + backup server + cible) dans la doc

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -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 larborescence attendue.
## Sécurité / déploiement
### Clés hôtes SSH
Si `GLOBAL_BACKUP_KNOWN_HOSTS_STRICT=yes`, lempreinte du serveur de backup doit déjà être présente dans le `known_hosts` de la cible. Le bootstrap et les checks ne font plus dajout aveugle en mode strict.
### Répertoires de clone
Les scripts refusent maintenant les chemins de clone manifestement dangereux comme `/`, `/root`, `/home` ou `/home/<user>` pour éviter un `rm -rf` destructeur dû à une mauvaise configuration.
### Ubuntu Server
Le flux de bootstrap est pensé pour des cibles Ubuntu Server avec `apt`, `systemctl` et `sudo -n`.
---
## Structure des backups attendue
@@ -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.
```

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 dun 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 lexclusion configurable des rôles via `EXCLUDED_RESTORE_ROLES`.
---
## Sélection de la base

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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