diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh new file mode 100644 index 0000000..849a6bf --- /dev/null +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -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." \ No newline at end of file diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh new file mode 100644 index 0000000..d2840ff --- /dev/null +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -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 \ No newline at end of file diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh new file mode 100644 index 0000000..e4f1b59 --- /dev/null +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -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" \ No newline at end of file