fix/correctif #17
@@ -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 d’obtenir **une sauvegarde fiable, centralisée et surveillée de Vaultwarden**.
|
||||
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -11,6 +11,37 @@ Le script :
|
||||
3. compare le taux d’occupation 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 :
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
55
README.md
55
README.md
@@ -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>
|
||||
|
||||
|
||||
@@ -41,19 +41,19 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
postgres_server_ready() {
|
||||
require_cmd postgres || return 1
|
||||
require_cmd pg_ctlcluster || return 1
|
||||
require_cmd pg_lsclusters || return 1
|
||||
has_cmd postgres || return 1
|
||||
has_cmd pg_ctlcluster || return 1
|
||||
has_cmd pg_lsclusters || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_postgres_cluster() {
|
||||
if ! require_cmd pg_lsclusters || ! require_cmd pg_createcluster; then
|
||||
if ! has_cmd pg_lsclusters || ! has_cmd pg_createcluster; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -66,7 +66,7 @@ ensure_postgres_cluster() {
|
||||
version="$(find /etc/postgresql -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | LC_ALL=C sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -z "$version" ]] && require_cmd psql; then
|
||||
if [[ -z "$version" ]] && has_cmd psql; then
|
||||
version="$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)"
|
||||
fi
|
||||
|
||||
@@ -82,15 +82,15 @@ collect_postgres_diagnostics() {
|
||||
|
||||
if "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager >/dev/null 2>&1; then
|
||||
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: OK; "
|
||||
elif require_cmd systemctl; then
|
||||
elif has_cmd systemctl; then
|
||||
diagnostics+="systemctl status ${POSTGRES_SERVICE_NAME}: $( "$SUDO_BIN" systemctl status "$POSTGRES_SERVICE_NAME" --no-pager 2>/dev/null | tail -n 5 | tr '\n' ' ' ); "
|
||||
fi
|
||||
|
||||
if require_cmd pg_lsclusters; then
|
||||
if has_cmd pg_lsclusters; then
|
||||
diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); "
|
||||
fi
|
||||
|
||||
if require_cmd journalctl; then
|
||||
if has_cmd journalctl; then
|
||||
diagnostics+="journalctl: $( "$SUDO_BIN" journalctl -u "$POSTGRES_SERVICE_NAME" -n 10 --no-pager 2>/dev/null | tr '\n' ' ' ); "
|
||||
fi
|
||||
|
||||
@@ -102,11 +102,11 @@ start_postgres_service() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if require_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||
if has_cmd service && "$SUDO_BIN" service "$POSTGRES_SERVICE_NAME" start >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if require_cmd pg_lsclusters && require_cmd pg_ctlcluster; then
|
||||
if has_cmd pg_lsclusters && has_cmd pg_ctlcluster; then
|
||||
local version cluster
|
||||
while read -r version cluster _; do
|
||||
[[ -n "$version" && -n "$cluster" ]] || continue
|
||||
@@ -137,10 +137,13 @@ PGUSER_SUPERUSER="${PGUSER_SUPERUSER:-no}"
|
||||
POSTGRES_PACKAGE_LIST="${POSTGRES_PACKAGE_LIST:-postgresql postgresql-client postgresql-contrib}"
|
||||
POSTGRES_SERVICE_NAME="${POSTGRES_SERVICE_NAME:-postgresql}"
|
||||
SUDO_BIN="${SUDO_BIN:-sudo}"
|
||||
read -r -a POSTGRES_PACKAGES <<< "$POSTGRES_PACKAGE_LIST"
|
||||
|
||||
[[ "${#POSTGRES_PACKAGES[@]}" -gt 0 ]] || fail "POSTGRES_PACKAGE_LIST vide"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
if ! require_cmd "$SUDO_BIN"; then
|
||||
if ! has_cmd "$SUDO_BIN"; then
|
||||
fail "sudo absent sur la cible"
|
||||
fi
|
||||
|
||||
@@ -154,12 +157,12 @@ fi
|
||||
|
||||
POSTGRES_INSTALLED="no"
|
||||
|
||||
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb || ! postgres_server_ready; then
|
||||
if ! has_cmd psql || ! has_cmd pg_restore || ! has_cmd createdb || ! has_cmd dropdb || ! postgres_server_ready; then
|
||||
[[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no"
|
||||
|
||||
log "PostgreSQL absent : installation en cours..."
|
||||
"$SUDO_BIN" apt update >/dev/null 2>&1 || fail "échec de apt update"
|
||||
"$SUDO_BIN" apt install -y $POSTGRES_PACKAGE_LIST >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||
"$SUDO_BIN" apt install -y "${POSTGRES_PACKAGES[@]}" >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL"
|
||||
POSTGRES_INSTALLED="yes"
|
||||
log "Installation PostgreSQL terminée."
|
||||
else
|
||||
@@ -178,15 +181,17 @@ else
|
||||
fi
|
||||
|
||||
log "Vérification de la disponibilité de PostgreSQL..."
|
||||
PG_READY=false
|
||||
for _ in {1..20}; do
|
||||
if "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
PG_READY=true
|
||||
log "PostgreSQL répond correctement."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
|
||||
if [[ "$PG_READY" != true ]]; then
|
||||
fail "PostgreSQL ne répond pas correctement"
|
||||
fi
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ while [[ $# -gt 0 ]]; do
|
||||
shift 2
|
||||
;;
|
||||
--non-interactive)
|
||||
# Flag accepté pour compatibilité avec les autres scripts du workflow.
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 d’un dump distant.
|
||||
|
||||
@@ -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 d’erreur 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,11 +372,36 @@ 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
|
||||
|
||||
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() {
|
||||
@@ -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"
|
||||
|
||||
RET=$?
|
||||
> "$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/"
|
||||
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=
|
||||
@@ -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"
|
||||
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=
|
||||
@@ -463,8 +501,11 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
scp "${SCP_OPTS[@]}" "$FILE" "$IA_SSH:${BACKUP_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=
|
||||
@@ -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"
|
||||
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"
|
||||
@@ -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"
|
||||
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}"
|
||||
@@ -503,8 +546,6 @@ for DB in "${DBS_ARRAY[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
log "Remote rotation finished"
|
||||
|
||||
#######################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -89,6 +89,7 @@ SSH_TIMEOUT=10
|
||||
#############################################
|
||||
|
||||
# Nombre de jours de conservation des sauvegardes
|
||||
# Utilisé par backup-bdd-recette.sh et backup-vaultwarden.sh
|
||||
BACKUP_RETENTION_DAYS=10
|
||||
|
||||
|
||||
@@ -96,12 +97,11 @@ BACKUP_RETENTION_DAYS=10
|
||||
# APPLICATIONS À SURVEILLER
|
||||
#############################################
|
||||
|
||||
# Liste des applications à vérifier
|
||||
APPS="
|
||||
ferme.malio-dev.fr
|
||||
inventory.malio-dev.fr
|
||||
sirh.malio-dev.fr
|
||||
"
|
||||
# Liste des applications à vérifier (séparées par espace)
|
||||
APP_URLS="ferme.malio-dev.fr inventory.malio-dev.fr sirh.malio-dev.fr"
|
||||
|
||||
# Schéma utilisé pour les applications surveillées
|
||||
APP_SCHEME="http"
|
||||
|
||||
|
||||
#############################################
|
||||
|
||||
Reference in New Issue
Block a user