feat : update web available rebuild bdd (WIP)

This commit is contained in:
AkiNoKure
2026-03-16 11:10:10 +01:00
parent fb1aaac418
commit 858cad8269
3 changed files with 873 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEFAULT_ENV_FILE="${REPO_DIR}/.env"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
CLI_REQUEST_ID=""
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
while [[ $# -gt 0 ]]; do
case "$1" in
--env-file)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
ENV_FILE="$2"
shift 2
;;
--request-id)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
CLI_REQUEST_ID="$2"
shift 2
;;
--non-interactive)
NON_INTERACTIVE="yes"
shift
;;
*)
echo "Argument inconnu : $1" >&2
exit 1
;;
esac
done
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
fail() {
log "ERROR: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1
}
[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
: "${PGHOST:?Variable PGHOST manquante}"
: "${PGPORT:?Variable PGPORT manquante}"
: "${PGUSER:?Variable PGUSER manquante}"
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
AUTO_INSTALL_POSTGRES="${AUTO_INSTALL_POSTGRES:-yes}"
AUTO_CREATE_PGUSER="${AUTO_CREATE_PGUSER:-yes}"
POSTGRES_PACKAGE_LIST="${POSTGRES_PACKAGE_LIST:-postgresql postgresql-client postgresql-contrib}"
POSTGRES_SERVICE_NAME="${POSTGRES_SERVICE_NAME:-postgresql}"
SUDO_BIN="${SUDO_BIN:-sudo}"
export PGPASSWORD
if ! require_cmd "$SUDO_BIN"; then
fail "sudo absent sur la cible"
fi
POSTGRES_INSTALLED="no"
if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; 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"
POSTGRES_INSTALLED="yes"
log "Installation PostgreSQL terminée."
else
log "PostgreSQL déjà installé."
fi
if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then
log "Démarrage du service PostgreSQL..."
"$SUDO_BIN" systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1 || fail "impossible de démarrer PostgreSQL"
else
log "Service PostgreSQL déjà actif."
fi
log "Vérification de la disponibilité de PostgreSQL..."
for _ in {1..20}; do
if "$SUDO_BIN" -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
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
fail "PostgreSQL ne répond pas correctement"
fi
if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then
ROLE_EXISTS="$(
"$SUDO_BIN" -u postgres psql -d postgres -tAc \
"SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true
)"
if [[ "$ROLE_EXISTS" != "1" ]]; then
log "Création du rôle PostgreSQL ${PGUSER}..."
"$SUDO_BIN" -u postgres psql -d postgres -c \
"CREATE ROLE \"${PGUSER}\" WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${PGPASSWORD//\'/\'\'}';" \
>/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}"
log "Rôle PostgreSQL ${PGUSER} créé."
else
log "Rôle PostgreSQL ${PGUSER} déjà présent."
fi
fi
if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}"
fi
log "Check PostgreSQL terminé avec succès."

View File

@@ -0,0 +1,470 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
CLI_DB=""
CLI_OVERWRITE=""
CLI_RESTORE_ROLES=""
CLI_REQUEST_ID=""
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
JSON_ONLY="${JSON_ONLY:-no}"
while [[ $# -gt 0 ]]; do
case "$1" in
--env-file)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
ENV_FILE="$2"
shift 2
;;
--db)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
CLI_DB="$2"
shift 2
;;
--overwrite)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
CLI_OVERWRITE="$2"
shift 2
;;
--restore-roles)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
CLI_RESTORE_ROLES="$2"
shift 2
;;
--request-id)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
CLI_REQUEST_ID="$2"
shift 2
;;
--non-interactive)
NON_INTERACTIVE="yes"
shift
;;
--json-only)
JSON_ONLY="yes"
shift
;;
*)
echo "Argument inconnu : $1" >&2
exit 1
;;
esac
done
json_escape() {
python3 - <<'PY' "$1"
import json, sys
print(json.dumps(sys.argv[1]))
PY
}
print_json_and_exit() {
local status="$1"
local message="$2"
local exit_code="$3"
printf '{'
printf '"status":%s,' "$(json_escape "$status")"
printf '"message":%s,' "$(json_escape "$message")"
printf '"request_id":%s,' "$(json_escape "${REQUEST_ID:-}")"
printf '"environment":%s,' "$(json_escape "${ENV_NAME:-}")"
printf '"database":%s,' "$(json_escape "${DB:-}")"
printf '"dump_file":%s,' "$(json_escape "${LAST_REMOTE_DB_DUMP:-}")"
printf '"log_file":%s' "$(json_escape "${LOG_FILE:-}")"
printf '}\n'
exit "$exit_code"
}
print_stdout() {
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
}
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >>"$LOG_FILE"
print_stdout "$msg"
}
fail() {
log "ERROR: $*"
print_json_and_exit "error" "$*" 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1
}
to_bool_yes_no() {
local v="${1:-}"
v="${v,,}"
case "$v" in
yes|y|oui|o|true|1) echo "yes" ;;
no|n|non|false|0|"") echo "no" ;;
*) return 1 ;;
esac
}
is_tty() {
[[ -t 0 && -t 1 ]]
}
sql_escape_literal() {
local s="${1:-}"
s="${s//\'/\'\'}"
printf "%s" "$s"
}
build_excluded_roles_regex() {
local roles_string="${1:-}"
local role
local -a escaped_roles=()
read -r -a roles_array <<< "$roles_string"
for role in "${roles_array[@]}"; do
[[ -n "$role" ]] || continue
[[ "$role" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || continue
escaped_roles+=("$role")
done
if [[ "${#escaped_roles[@]}" -eq 0 ]]; then
return 1
fi
local joined=""
local first="yes"
for role in "${escaped_roles[@]}"; do
if [[ "$first" == "yes" ]]; then
joined="$role"
first="no"
else
joined+="|$role"
fi
done
printf '%s' "$joined"
}
cleanup() {
rm -f \
"${LOCAL_DB_DUMP_FILE:-}" \
"${LOCAL_ROLES_FILE:-}" \
"${FILTERED_ROLES_FILE:-}" \
"${ROLES_CREATE_LIST:-}" \
"${ROLES_APPLY_FILE:-}"
}
trap cleanup EXIT
[[ -f "$ENV_FILE" ]] || {
echo '{"status":"error","message":"fichier .env cible introuvable"}'
exit 1
}
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
: "${ENV_NAME:?Variable ENV_NAME manquante}"
: "${PGHOST:?Variable PGHOST manquante}"
: "${PGPORT:?Variable PGPORT manquante}"
: "${PGUSER:?Variable PGUSER manquante}"
: "${PGPASSWORD:?Variable PGPASSWORD manquante}"
: "${DBS:?Variable DBS manquante}"
: "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}"
: "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}"
: "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}"
: "${SSH_KEY:?Variable SSH_KEY manquante}"
: "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}"
LOCAL_RESTORE_BASE_DIR="${LOCAL_RESTORE_BASE_DIR:-${SCRIPT_DIR}/restore_tmp}"
REMOTE_ROLES_DIR_NAME="${REMOTE_ROLES_DIR_NAME:-user}"
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
EXCLUDED_RESTORE_ROLES="${EXCLUDED_RESTORE_ROLES:-postgres}"
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}"
RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}"
ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || {
echo '{"status":"error","message":"ALLOW_OVERWRITE invalide"}'
exit 1
}
RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || {
echo '{"status":"error","message":"RESTORE_ROLES invalide"}'
exit 1
}
mkdir -p "$BACKUP_LOG_DIR" || {
echo '{"status":"error","message":"impossible de créer le dossier de logs"}'
exit 1
}
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
SAFE_REQUEST_ID="${REQUEST_ID:-manual}"
SAFE_REQUEST_ID="${SAFE_REQUEST_ID//[^a-zA-Z0-9_.-]/_}"
LOG_FILE="${BACKUP_LOG_DIR}/restore_${ENV_NAME,,}_${SAFE_REQUEST_ID}_${TIMESTAMP}.log"
touch "$LOG_FILE" || {
echo '{"status":"error","message":"impossible de créer le log"}'
exit 1
}
LOCAL_RESTORE_DIR="${LOCAL_RESTORE_BASE_DIR}/${SAFE_REQUEST_ID}_${TIMESTAMP}"
mkdir -p "$LOCAL_RESTORE_DIR" || fail "impossible de créer le dossier temporaire local"
EXCLUDED_ROLES_REGEX=""
if EXCLUDED_ROLES_REGEX="$(build_excluded_roles_regex "$EXCLUDED_RESTORE_ROLES")"; then
log "Rôles exclus de la restauration : $EXCLUDED_RESTORE_ROLES"
else
log "Aucun rôle exclu de la restauration."
fi
for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basename; do
require_cmd "$cmd" || true
done
CHECK_SCRIPT="${SCRIPT_DIR}/checkup/check_postgresql.sh"
[[ -x "$CHECK_SCRIPT" ]] || fail "script introuvable ou non exécutable : $CHECK_SCRIPT"
"$CHECK_SCRIPT" \
--env-file "$ENV_FILE" \
--request-id "$SAFE_REQUEST_ID" \
--non-interactive \
>>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL locale"
[[ -f "$SSH_KEY" ]] || fail "clé SSH source backup introuvable : $SSH_KEY"
[[ -r "$SSH_KEY" ]] || fail "clé SSH source backup non lisible : $SSH_KEY"
export PGPASSWORD
SSH_OPTS=(
-i "$SSH_KEY"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
-o StrictHostKeyChecking=yes
)
REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}"
read -r -a DBS_ARRAY <<< "$DBS"
[[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "aucune base définie dans DBS"
if [[ -z "$REQUESTED_DB" ]]; then
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
fail "REQUESTED_DB manquante en mode non interactif"
fi
if is_tty; then
print_stdout "Bases disponibles :"
for i in "${!DBS_ARRAY[@]}"; do
print_stdout " $((i + 1))) ${DBS_ARRAY[$i]}"
done
echo
read -r -p "Sélectionnez le numéro de la base à restaurer : " DB_INDEX
[[ "$DB_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de base invalide"
(( DB_INDEX >= 1 && DB_INDEX <= ${#DBS_ARRAY[@]} )) || fail "numéro hors plage"
REQUESTED_DB="${DBS_ARRAY[$((DB_INDEX - 1))]}"
else
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
fi
fi
DB=""
for candidate in "${DBS_ARRAY[@]}"; do
if [[ "$candidate" == "$REQUESTED_DB" ]]; then
DB="$candidate"
break
fi
done
[[ -n "$DB" ]] || fail "base refusée : non présente dans DBS"
[[ "$DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide"
log "Environnement : $ENV_NAME"
log "Base cible : $DB"
log "Request ID : ${REQUEST_ID:-N/A}"
log "Overwrite : $ALLOW_OVERWRITE"
log "Restore roles : $RESTORE_ROLES"
if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" \
>>"$LOG_FILE" 2>&1; then
fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}"
fi
log "Test SSH vers ${REMOTE_SSH}"
if ! ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" "exit 0" >>"$LOG_FILE" 2>&1; then
fail "connexion SSH impossible vers ${REMOTE_SSH}"
fi
REMOTE_DB_DIR="${BACKUP_REMOTE_DIR}/${DB}"
REMOTE_ROLES_DIR="${BACKUP_REMOTE_DIR}/${REMOTE_ROLES_DIR_NAME}"
LAST_REMOTE_DB_DUMP="$(
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
"find '${REMOTE_DB_DIR}' -maxdepth 1 -type f -name '${DB}_*.dump' | LC_ALL=C sort | tail -n 1"
)"
[[ -n "$LAST_REMOTE_DB_DUMP" ]] || fail "aucun dump trouvé pour ${DB} dans ${REMOTE_DB_DIR}"
log "Dernier dump sélectionné : ${LAST_REMOTE_DB_DUMP}"
LAST_REMOTE_ROLES_FILE=""
if [[ "$RESTORE_ROLES" == "yes" ]]; then
LAST_REMOTE_ROLES_FILE="$(
ssh "${SSH_OPTS[@]}" "$REMOTE_SSH" \
"find '${REMOTE_ROLES_DIR}' -maxdepth 1 -type f -name 'user_*.sql' | LC_ALL=C sort | tail -n 1"
)"
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
log "Dernier fichier rôles sélectionné : ${LAST_REMOTE_ROLES_FILE}"
else
log "Aucun fichier rôles trouvé ; la restauration des rôles sera ignorée."
fi
else
log "Restauration des rôles désactivée."
fi
LOCAL_DB_DUMP_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_DB_DUMP")"
LOCAL_ROLES_FILE=""
log "Téléchargement du dump principal"
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_DB_DUMP}" "$LOCAL_DB_DUMP_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec téléchargement du dump principal"
if [[ -n "$LAST_REMOTE_ROLES_FILE" ]]; then
LOCAL_ROLES_FILE="${LOCAL_RESTORE_DIR}/$(basename "$LAST_REMOTE_ROLES_FILE")"
log "Téléchargement du fichier des rôles"
scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${LAST_REMOTE_ROLES_FILE}" "$LOCAL_ROLES_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec téléchargement du fichier des rôles"
fi
DB_EXISTS="$(
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname='$(sql_escape_literal "$DB")'" \
2>>"$LOG_FILE" || true
)"
if [[ "$DB_EXISTS" == "1" ]]; then
if [[ "$ALLOW_OVERWRITE" != "yes" ]]; then
if [[ "$NON_INTERACTIVE" == "yes" || ! -t 0 ]]; then
fail "la base existe déjà et overwrite n'est pas autorisé"
fi
read -r -p "La base '${DB}' existe déjà. Voulez-vous l'écraser ? (oui/non) : " CONFIRM_OVERWRITE
CONFIRM_OVERWRITE="$(to_bool_yes_no "$CONFIRM_OVERWRITE")" || fail "réponse overwrite invalide"
[[ "$CONFIRM_OVERWRITE" == "yes" ]] || fail "restauration annulée par l'utilisateur"
fi
log "Suppression de la base existante : ${DB}"
dropdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" --if-exists "$DB" \
>>"$LOG_FILE" 2>&1 || fail "échec suppression base ${DB}"
fi
if [[ -n "$LOCAL_ROLES_FILE" ]]; then
log "Restauration des rôles depuis : ${LOCAL_ROLES_FILE}"
FILTERED_ROLES_FILE="${LOCAL_RESTORE_DIR}/filtered_$(basename "$LOCAL_ROLES_FILE")"
ROLES_CREATE_LIST="${LOCAL_RESTORE_DIR}/roles_to_create_$(basename "$LOCAL_ROLES_FILE")"
ROLES_APPLY_FILE="${LOCAL_RESTORE_DIR}/roles_apply_$(basename "$LOCAL_ROLES_FILE")"
if [[ -n "$EXCLUDED_ROLES_REGEX" ]]; then
grep -viE "^(CREATE ROLE|ALTER ROLE) (${EXCLUDED_ROLES_REGEX})\\b" "$LOCAL_ROLES_FILE" \
> "$FILTERED_ROLES_FILE" || true
else
cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE"
fi
log "Fichier des rôles filtré généré : ${FILTERED_ROLES_FILE}"
sed -nE 's/^CREATE ROLE "?([^" ;]+)"?;$/\1/p' "$FILTERED_ROLES_FILE" \
> "$ROLES_CREATE_LIST" || true
if [[ -s "$ROLES_CREATE_LIST" ]]; then
while IFS= read -r role_name; do
[[ -n "$role_name" ]] || continue
[[ "$role_name" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || {
log "Rôle ignoré car non conforme : ${role_name}"
continue
}
ROLE_EXISTS="$(
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -tAc \
"SELECT 1 FROM pg_roles WHERE rolname='$(sql_escape_literal "$role_name")'" \
2>>"$LOG_FILE" || true
)"
if [[ "$ROLE_EXISTS" != "1" ]]; then
log "Création du rôle manquant : ${role_name}"
psql -v ON_ERROR_STOP=1 \
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres \
-c "CREATE ROLE \"${role_name}\";" \
>>"$LOG_FILE" 2>&1 || fail "échec création rôle ${role_name}"
else
log "Rôle déjà présent : ${role_name}"
fi
done < "$ROLES_CREATE_LIST"
fi
grep -viE '^CREATE ROLE ' "$FILTERED_ROLES_FILE" > "$ROLES_APPLY_FILE" || true
log "Application ALTER ROLE / privilèges / memberships"
psql -v ON_ERROR_STOP=1 \
-h "$PGHOST" \
-p "$PGPORT" \
-U "$PGUSER" \
-d postgres \
-f "$ROLES_APPLY_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec restauration rôles"
else
log "Aucune restauration des rôles effectuée."
fi
log "Création de la base : ${DB}"
createdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$DB" \
>>"$LOG_FILE" 2>&1 || fail "échec création base ${DB}"
log "Restauration de la base ${DB}"
pg_restore \
-h "$PGHOST" \
-p "$PGPORT" \
-U "$PGUSER" \
-d "$DB" \
--clean \
--if-exists \
--no-owner \
--no-privileges \
"$LOCAL_DB_DUMP_FILE" \
>>"$LOG_FILE" 2>&1 || fail "échec restauration base ${DB}"
send_discord_message() {
local message="$1"
local payload=""
[[ -n "$DISCORD_WEBHOOK_URL" ]] || return 0
require_cmd curl || 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"
log "Restauration terminée avec succès pour ${DB}"
print_json_and_exit "success" "restauration terminée avec succès" 0

View File

@@ -0,0 +1,276 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
CLI_TARGET=""
CLI_DB=""
CLI_OVERWRITE=""
CLI_RESTORE_ROLES=""
CLI_REQUEST_ID=""
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
JSON_ONLY="${JSON_ONLY:-no}"
while [[ $# -gt 0 ]]; do
case "$1" in
--env-file)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
ENV_FILE="$2"
shift 2
;;
--target)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
CLI_TARGET="$2"
shift 2
;;
--db)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --db" >&2; exit 1; }
CLI_DB="$2"
shift 2
;;
--overwrite)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --overwrite" >&2; exit 1; }
CLI_OVERWRITE="$2"
shift 2
;;
--restore-roles)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --restore-roles" >&2; exit 1; }
CLI_RESTORE_ROLES="$2"
shift 2
;;
--request-id)
[[ $# -ge 2 ]] || { echo "Argument manquant pour --request-id" >&2; exit 1; }
CLI_REQUEST_ID="$2"
shift 2
;;
--non-interactive)
NON_INTERACTIVE="yes"
shift
;;
--json-only)
JSON_ONLY="yes"
shift
;;
*)
echo "Argument inconnu : $1" >&2
exit 1
;;
esac
done
json_escape() {
python3 - <<'PY' "$1"
import json, sys
print(json.dumps(sys.argv[1]))
PY
}
print_json_and_exit() {
local status="$1"
local message="$2"
local exit_code="$3"
printf '{'
printf '"status":%s,' "$(json_escape "$status")"
printf '"message":%s,' "$(json_escape "$message")"
printf '"target":%s,' "$(json_escape "${TARGET:-}")"
printf '"request_id":%s' "$(json_escape "${REQUEST_ID:-}")"
printf '}\n'
exit "$exit_code"
}
fail() {
print_json_and_exit "error" "$*" 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1
}
to_bool_yes_no() {
local v="${1:-}"
v="${v,,}"
case "$v" in
yes|y|oui|o|true|1) echo "yes" ;;
no|n|non|false|0|"") echo "no" ;;
*) return 1 ;;
esac
}
is_tty() {
[[ -t 0 && -t 1 ]]
}
print_stdout() {
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
}
sanitize_key() {
local s="${1:-}"
s="${s//[^a-zA-Z0-9_]/_}"
printf "%s" "$s"
}
get_target_var() {
local target="$1"
local key="$2"
local safe_target
safe_target="$(sanitize_key "$target")"
local var_name="TARGET_${key}_${safe_target}"
printf "%s" "${!var_name:-}"
}
shell_quote() {
printf "%q" "$1"
}
[[ -f "$ENV_FILE" ]] || {
echo '{"status":"error","message":"fichier .env IA introuvable"}'
exit 1
}
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
for cmd in bash ssh python3; do
require_cmd "$cmd" || fail "commande requise absente sur IA : $cmd"
done
TARGET="${CLI_TARGET:-${TARGET:-}}"
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}"
ALLOW_OVERWRITE_RAW="${CLI_OVERWRITE:-${ALLOW_OVERWRITE:-no}}"
RESTORE_ROLES_RAW="${CLI_RESTORE_ROLES:-${RESTORE_ROLES:-yes}}"
ALLOW_OVERWRITE="$(to_bool_yes_no "$ALLOW_OVERWRITE_RAW")" || fail "ALLOW_OVERWRITE invalide"
RESTORE_ROLES="$(to_bool_yes_no "$RESTORE_ROLES_RAW")" || fail "RESTORE_ROLES invalide"
: "${TARGETS:?Variable TARGETS manquante dans le .env IA}"
read -r -a TARGETS_ARRAY <<< "$TARGETS"
[[ "${#TARGETS_ARRAY[@]}" -gt 0 ]] || fail "aucune cible définie dans TARGETS"
if [[ -z "$TARGET" ]]; then
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
fail "TARGET manquante en mode non interactif"
fi
if is_tty; then
print_stdout "Cibles disponibles :"
for i in "${!TARGETS_ARRAY[@]}"; do
print_stdout " $((i + 1))) ${TARGETS_ARRAY[$i]}"
done
echo
read -r -p "Sélectionnez le numéro de la cible : " TARGET_INDEX
[[ "$TARGET_INDEX" =~ ^[0-9]+$ ]] || fail "numéro de cible invalide"
(( TARGET_INDEX >= 1 && TARGET_INDEX <= ${#TARGETS_ARRAY[@]} )) || fail "numéro hors plage"
TARGET="${TARGETS_ARRAY[$((TARGET_INDEX - 1))]}"
else
fail "TARGET manquante et aucune interaction terminal disponible"
fi
fi
TARGET_ALLOWED="no"
for candidate in "${TARGETS_ARRAY[@]}"; do
if [[ "$candidate" == "$TARGET" ]]; then
TARGET_ALLOWED="yes"
break
fi
done
[[ "$TARGET_ALLOWED" == "yes" ]] || fail "cible refusée : non présente dans TARGETS"
TARGET_HOST="$(get_target_var "$TARGET" "HOST")"
TARGET_USER="$(get_target_var "$TARGET" "USER")"
TARGET_SSH_KEY="$(get_target_var "$TARGET" "SSH_KEY")"
TARGET_SSH_PORT="$(get_target_var "$TARGET" "SSH_PORT")"
TARGET_SSH_CONNECT_TIMEOUT="$(get_target_var "$TARGET" "SSH_CONNECT_TIMEOUT")"
TARGET_REPO_URL="$(get_target_var "$TARGET" "REPO_URL")"
TARGET_REPO_BRANCH="$(get_target_var "$TARGET" "REPO_BRANCH")"
TARGET_REPO_DIR="$(get_target_var "$TARGET" "REPO_DIR")"
TARGET_CORE_SCRIPT="$(get_target_var "$TARGET" "CORE_SCRIPT")"
TARGET_ENV_FILE="$(get_target_var "$TARGET" "ENV_FILE")"
TARGET_SSH_PORT="${TARGET_SSH_PORT:-22}"
TARGET_SSH_CONNECT_TIMEOUT="${TARGET_SSH_CONNECT_TIMEOUT:-8}"
TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-main}"
[[ -n "$TARGET_HOST" ]] || fail "TARGET_HOST_${TARGET} manquante"
[[ -n "$TARGET_USER" ]] || fail "TARGET_USER_${TARGET} manquante"
[[ -n "$TARGET_SSH_KEY" ]] || fail "TARGET_SSH_KEY_${TARGET} manquante"
[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${TARGET} manquante"
[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${TARGET} manquante"
[[ -n "$TARGET_CORE_SCRIPT" ]] || fail "TARGET_CORE_SCRIPT_${TARGET} manquante"
[[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE_${TARGET} manquante"
[[ -f "$TARGET_SSH_KEY" ]] || fail "clé SSH cible introuvable : $TARGET_SSH_KEY"
if [[ -z "$REQUEST_ID" ]]; then
REQUEST_ID="$(date '+%Y%m%d%H%M%S')_$$"
fi
if [[ -z "$REQUESTED_DB" ]]; then
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
fail "REQUESTED_DB manquante en mode non interactif"
fi
if is_tty; then
read -r -p "Nom exact de la base à restaurer : " REQUESTED_DB
[[ -n "$REQUESTED_DB" ]] || fail "nom de base vide"
else
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
fi
fi
[[ "$REQUESTED_DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide"
SSH_OPTS=(
-i "$TARGET_SSH_KEY"
-p "$TARGET_SSH_PORT"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o ConnectTimeout="$TARGET_SSH_CONNECT_TIMEOUT"
-o StrictHostKeyChecking=yes
)
REMOTE_BOOTSTRAP_CMD="
set -euo pipefail
REPO_DIR=$(shell_quote "$TARGET_REPO_DIR")
REPO_URL=$(shell_quote "$TARGET_REPO_URL")
REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")
CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT")
command -v git >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"git absent sur la cible\"}'; exit 1; }
command -v bash >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"bash absent sur la cible\"}'; exit 1; }
mkdir -p \"\$(dirname \"\$REPO_DIR\")\"
if [[ ! -d \"\$REPO_DIR/.git\" ]]; then
rm -rf \"\$REPO_DIR\"
git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$REPO_DIR\"
else
git -C \"\$REPO_DIR\" fetch --prune origin
git -C \"\$REPO_DIR\" checkout -f \"\$REPO_BRANCH\"
git -C \"\$REPO_DIR\" reset --hard \"origin/\$REPO_BRANCH\"
fi
[[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; }
chmod 700 \"\$CORE_SCRIPT\"
exec \"\$CORE_SCRIPT\" \
--env-file $(shell_quote "$TARGET_ENV_FILE") \
--db $(shell_quote "$REQUESTED_DB") \
--overwrite $(shell_quote "$ALLOW_OVERWRITE") \
--restore-roles $(shell_quote "$RESTORE_ROLES") \
--request-id $(shell_quote "$REQUEST_ID") \
--non-interactive \
--json-only
"
exec ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD"