Compare commits

5 Commits

21 changed files with 512 additions and 190 deletions

View File

@@ -101,6 +101,38 @@ REMOTE_DIR=/home/backup/backups/vaultwarden
# 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é :
@@ -291,7 +323,7 @@ crontab -e
Ajouter :
```bash
0 19 * * * /home/<USER>/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 :
@@ -353,4 +385,3 @@ Le script automatise :
Ce système permet dobtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
```

View File

@@ -41,6 +41,10 @@ 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}"
@@ -109,11 +113,14 @@ 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
@@ -132,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
}
@@ -143,7 +149,7 @@ discord_ping() {
fail() {
local detail="$1"
log "ERROR: $detail"
discord_ping "false" "$detail"
send_discord "false" "$detail"
exit 1
}
@@ -151,6 +157,21 @@ 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
#######################################
@@ -209,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"
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

@@ -5,6 +5,20 @@ et applique le versionnement semantique.
## [Unreleased]
### Changed
- Harmonisation des fonctions d'envoi Discord dans les scripts de sauvegarde, de supervision et de reconstruction.
- Ajout d'un fichier de log dedie a l'orchestrateur `RebuildBdd/run-rebuild-bdd.sh`.
- Renforcement de la journalisation et de la gestion des erreurs dans `RecetteScripts/backup-bdd-recette.sh`.
### Fixed
- Nettoyage du backup local temporaire dans `BackupVaultWarden/backup-vaultwarden.sh` en sortie de script.
- Gestion plus robuste des dependances shell et des verifications de disponibilite PostgreSQL dans les scripts `RebuildBdd`.
- Acceptation explicite du flag `--non-interactive` dans `RebuildBdd/Checkup/check-target-readiness.sh` pour compatibilite de workflow.
- Validation des noms de base et refus des cles SSH symboliques dans les scripts de reconstruction.
- Suppression des notifications Discord fragiles quand `jq` ou `curl` sont absents, avec envoi silencieux en secours.
- Detection et nettoyage des verrous perimes dans `RecetteScripts/backup-bdd-recette.sh`.
- Filtrage des privileges `SUPERUSER` lors de la restauration des roles dans `RecetteScripts/rebuild-bdd-recette.sh`.
## [1.0.0] - 2026-03-18
### Added

View File

@@ -11,6 +11,37 @@ Le script :
3. compare le taux doccupation au seuil configuré
4. envoie une alerte Discord si le seuil est dépassé
<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 :

View File

@@ -52,6 +52,20 @@ if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
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
###############################################################################
@@ -77,15 +91,7 @@ if [ "$usage" -ge "$limit" ]; then
msgLimit="${DISCORD_PING}\n**CHECK STOCKAGE :red_circle:**\nLimite autorisée : ${limit}%\nUtilisation actuelle : ${usage}%\nEspace restant : ${free}%\nUtilisé / total : ${used_gb} GB / ${total_gb} GB\nDisponible : ${avail_gb} GB\nHeure : $(date)"
if [[ -n "${DISCORD_WEBHOOK_URL:-}" ]]; then
payload="$(jq -n --arg content "$msgLimit" '{content: $content}')"
curl -fsS \
-H "Accept: application/json" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$payload" \
"$DISCORD_WEBHOOK_URL" >/dev/null || true
fi
send_discord "$msgLimit"
fi

View File

@@ -120,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`
@@ -185,3 +193,50 @@ Le dépôt est maintenant pensé prioritairement pour des cibles **Ubuntu Server
* `RecetteScripts` = **legacy en cours de migration**
* Objectif : convergence vers une **chaîne unique, robuste et automatisable (web/API)**
---
<details>
<summary style="list-style: none; cursor: pointer;">
<strong>EggMaster</strong>
</summary>
Un message est disperse dans les `README` du depot.
Ordre de reconstruction :
1. `README.md`
2. `BackupVaultWarden/README.md`
3. `CheckStorage/README.md`
4. `RebuildBdd/README.md`
5. `RecetteScripts/README.md`
La commande de dechiffrement n'est pas donnee directement.
Elle se reconstruit aussi via des questions cachees dans les `README`.
<details>
<summary style="list-style: none; cursor: pointer;">Question 1</summary>
Quelle commande shell permet d'afficher exactement une chaine, sans interpretation particuliere, avant de la transmettre a une autre commande ?
</details>
<details>
<summary style="list-style: none; cursor: pointer;">Indice commande 1</summary>
```text
printf
```
</details>
<details>
<summary style="list-style: none; cursor: pointer;">Fragment 1</summary>
```text
YmllbiB2dSB0dSBtJ2FzIHRyb3V2ZXIgbW9pIG
```
</details>
</details>

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
;;
@@ -92,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

View File

@@ -12,13 +12,13 @@ GLOBAL_REPO_BRANCH=main
# Backup central
GLOBAL_BACKUP_REMOTE_USER=backup
GLOBAL_BACKUP_REMOTE_HOST=192.168.1.60
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/matteo/.ssh/id_ed25519_backup_readonly
GLOBAL_BACKUP_SSH_PUBLIC_KEY=/home/matteo/.ssh/id_ed25519_backup_readonly.pub
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

View File

@@ -3,25 +3,25 @@
###############################################################################
# SSH bootstrap cible
TARGET_HOST=192.168.1.60
TARGET_HOST=<TARGET_HOST>
TARGET_PORT=22
TARGET_BOOTSTRAP_USER=backup_liot
TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_prod
TARGET_RUNTIME_USER=backup_liot
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/backup_liot/RebuildBdd
TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env
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=backup_liot
TARGET_PGUSER=<PGUSER>
TARGET_PGPASSWORD=change_me_pg_password
TARGET_DBS="sirh inventory ferme"
@@ -29,9 +29,9 @@ TARGET_DBS="sirh inventory ferme"
TARGET_BACKUP_SUBDIR=bdd-prod
# Logs / tmp / ssh cible
TARGET_BACKUP_LOG_DIR=/home/backup_liot/logs/rebuild_bdd
TARGET_LOCAL_RESTORE_BASE_DIR=/home/backup_liot/RebuildBdd/restore_tmp
TARGET_SSH_KEY=/home/backup_liot/.ssh/id_ed25519_backup_readonly
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

View File

@@ -3,25 +3,25 @@
###############################################################################
# SSH bootstrap cible
TARGET_HOST=192.168.1.50
TARGET_HOST=<TARGET_HOST>
TARGET_PORT=22
TARGET_BOOTSTRAP_USER=backup_liot
TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_test
TARGET_RUNTIME_USER=backup_liot
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/backup_liot/RebuildBdd
TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env
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=backup_liot
TARGET_PGUSER=<PGUSER>
TARGET_PGPASSWORD=change_me_pg_password
TARGET_DBS="sirh inventory ferme"
@@ -29,9 +29,9 @@ TARGET_DBS="sirh inventory ferme"
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
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

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
@@ -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
@@ -443,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"
}
```
@@ -459,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"
}
```
@@ -500,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.
@@ -537,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
@@ -578,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
@@ -385,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
@@ -406,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

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

@@ -97,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
}
@@ -121,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
@@ -136,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
@@ -167,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:-}" \
@@ -251,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"
@@ -263,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
@@ -318,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"
@@ -478,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"

View File

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

View File

@@ -18,7 +18,8 @@ umask 077
# 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 :
@@ -50,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}"
@@ -58,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}"
@@ -87,7 +96,7 @@ for DB in "${DBS_ARRAY[@]}"; do
done
IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
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}"
@@ -153,17 +162,22 @@ TS="$(date +'%Y-%m-%d_%H-%M-%S')"
BACKUP_DIR_NAME="backup_${TS}"
LOG_FILE="${LOG_DIR}/${BACKUP_DIR_NAME}.log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR="$(mktemp -d /tmp/pg_dump_XXXXXX)" || {
echo "ERROR: impossible de créer le dossier temporaire" >&2
exit 1
}
exec > >(tee -a "$LOG_FILE") 2>&1
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
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() {
@@ -183,10 +197,7 @@ export PGPASSWORD
#######################################
for cmd in ssh scp curl jq pg_dump pg_dumpall mktemp; do
require_cmd "$cmd" || {
echo "ERROR: commande manquante : $cmd" >&2
exit 1
}
require_cmd "$cmd"
done
[[ -f "$SSH_KEY" ]] || {
@@ -213,14 +224,14 @@ chmod 600 "$SSH_KEY" || true
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 \
@@ -242,7 +253,7 @@ Dumps transfer: ✅
Users transfer: ✅
EOF
)"
discord_send "$msg"
send_discord "$msg"
}
#######################################
@@ -256,7 +267,7 @@ discord_msg_users_ok_simple() {
Users backup validé
EOF
)"
discord_send "$msg"
send_discord "$msg"
}
discord_msg_users_error() {
@@ -288,7 +299,7 @@ EOF
)"
fi
discord_send "$msg"
send_discord "$msg"
}
#######################################
@@ -303,7 +314,7 @@ discord_msg_db_ok_simple() {
Backup validé : ${db}
EOF
)"
discord_send "$msg"
send_discord "$msg"
}
discord_msg_db_error() {
@@ -338,7 +349,7 @@ EOF
)"
fi
discord_send "$msg"
send_discord "$msg"
}
#######################################
@@ -361,13 +372,38 @@ 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"
stale_lock="no"
existing_pid=""
if [[ -f "$LOCK_PID_FILE" ]]; then
existing_pid="$(<"$LOCK_PID_FILE")"
fi
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
log "ERROR: Backup déjà en cours (PID ${existing_pid})"
discord_msg_users_error "" "" "Lock already exists (PID ${existing_pid})"
exit 1
fi
stale_lock="yes"
log "WARNING: lock périmé détecté, nettoyage en cours"
rm -rf -- "$LOCK_DIR"
mkdir "$LOCK_DIR" 2>/dev/null || fail "impossible de recréer le lock après nettoyage"
fi
echo $$ > "$LOCK_PID_FILE" || {
rm -rf -- "$LOCK_DIR"
fail "impossible d'écrire le PID du lock"
}
if [[ "${stale_lock:-no}" == "yes" ]]; then
log "Lock périmé nettoyé."
fi
cleanup() {
rm -rf -- "$LOCK_DIR"
safe_remove_dir "$TMP_DIR" || true
@@ -397,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"
> "$ROLES_FILE"; then
RET=0
else
RET=$?
fi
if [[ $RET -ne 0 ]]; then
USERS_OK=
@@ -419,8 +455,11 @@ else
fi
if [[ -n "${USERS_EXPORT_OK:-}" ]]; then
scp "${SCP_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/user/"
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=
@@ -435,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"
@@ -452,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"
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=
@@ -463,8 +501,11 @@ for DB in "${DBS_ARRAY[@]}"; do
continue
fi
scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/${DB}/"
if scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_REMOTE_DIR}/${DB}/"; then
RET=0
else
RET=$?
fi
if [[ $RET -ne 0 ]]; then
DUMPS_OK=
@@ -473,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 '${BACKUP_REMOTE_DIR}/user' -type f -name 'user_*.sql' -mtime +${RETENTION_DAYS} -delete"
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"
@@ -493,8 +533,11 @@ else
fi
for DB in "${DBS_ARRAY[@]}"; do
ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${BACKUP_REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete"
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}"
@@ -503,8 +546,6 @@ for DB in "${DBS_ARRAY[@]}"; do
fi
done
set -e
log "Remote rotation finished"
#######################################

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"
@@ -210,7 +241,7 @@ main() {
done
FAILURES="$failures"
send_discord_summary
send_discord
if [[ "$failures" -gt 0 ]]; then
exit 2

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}"
@@ -110,10 +115,15 @@ 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
}
@@ -124,11 +134,10 @@ sql_escape_literal() {
}
validate_db_name() {
local db_name="$1"
local db_name="${1:-}"
[[ -n "$db_name" ]] || fail "nom de base vide"
[[ "$db_name" =~ ^[A-Za-z0-9_]+$ ]] || \
fail "nom de base invalide : seuls les lettres, chiffres et underscores sont autorisés"
[[ -n "$db_name" ]] || return 1
[[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
}
build_excluded_roles_regex() {
@@ -150,31 +159,22 @@ build_excluded_roles_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
}
###############################################################################
@@ -182,6 +182,7 @@ 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"
@@ -215,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"
@@ -242,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
@@ -297,7 +300,7 @@ else
read -r -p "Nom exact de la base à restaurer : " DB
fi
validate_db_name "$DB"
validate_db_name "$DB" || fail "nom de base invalide"
log "Environnement : $ENV_NAME"
log "Base cible sélectionnée : $DB"
@@ -418,6 +421,8 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
fi
sed -i -E '/^ALTER ROLE .* (NO)?SUPERUSER\b/d' "$FILTERED_ROLES_FILE"
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
sed -nE 's/^CREATE ROLE "?([^" ;]+)"?;$/\1/p' "$FILTERED_ROLES_FILE" \
@@ -498,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

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