From 858cad8269bdd93fbf306c1653eed8ca43618d60 Mon Sep 17 00:00:00 2001 From: AkiNoKure Date: Mon, 16 Mar 2026 11:10:10 +0100 Subject: [PATCH 01/33] feat : update web available rebuild bdd (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 127 +++++++ RebuildBdd/rebuild-bdd-core.sh | 470 +++++++++++++++++++++++++ RebuildBdd/run-rebuild-bdd.sh | 276 +++++++++++++++ 3 files changed, 873 insertions(+) create mode 100644 RebuildBdd/Checkup/check-postgresql.sh create mode 100644 RebuildBdd/rebuild-bdd-core.sh create mode 100644 RebuildBdd/run-rebuild-bdd.sh 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 From f0dfd6acb1beee0a9a1790e3fb789add3fdb11aa Mon Sep 17 00:00:00 2001 From: AkiNoKure Date: Tue, 17 Mar 2026 08:48:59 +0100 Subject: [PATCH 02/33] feat : update web available rebuild bdd (WIP) --- RebuildBdd/rebuild-bdd-core.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index d2840ff..20374b6 100644 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -231,7 +231,7 @@ for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basenam require_cmd "$cmd" || true done -CHECK_SCRIPT="${SCRIPT_DIR}/checkup/check_postgresql.sh" +CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh" [[ -x "$CHECK_SCRIPT" ]] || fail "script introuvable ou non exécutable : $CHECK_SCRIPT" "$CHECK_SCRIPT" \ From 2971ef0ff97fbe1498eb27b497092203c194a1a1 Mon Sep 17 00:00:00 2001 From: AkiNoKure Date: Tue, 17 Mar 2026 09:21:38 +0100 Subject: [PATCH 03/33] fix: add executable bit on rebuild scripts --- RebuildBdd/Checkup/check-postgresql.sh | 0 RebuildBdd/rebuild-bdd-core.sh | 0 RebuildBdd/run-rebuild-bdd.sh | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 RebuildBdd/Checkup/check-postgresql.sh mode change 100644 => 100755 RebuildBdd/rebuild-bdd-core.sh mode change 100644 => 100755 RebuildBdd/run-rebuild-bdd.sh diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh old mode 100644 new mode 100755 diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh old mode 100644 new mode 100755 diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh old mode 100644 new mode 100755 From 94537de551ab0440560178ec4f796936f67df278 Mon Sep 17 00:00:00 2001 From: AkiNoKure Date: Tue, 17 Mar 2026 10:11:00 +0100 Subject: [PATCH 04/33] feat : update web available rebuild bdd (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 38 ++- RebuildBdd/Checkup/check-target-readiness.sh | 338 +++++++++++++++++++ RebuildBdd/bootstrap-target-host.sh | 286 ++++++++++++++++ RebuildBdd/rebuild-bdd-core.sh | 12 +- RebuildBdd/run-rebuild-bdd.sh | 41 ++- 5 files changed, 694 insertions(+), 21 deletions(-) create mode 100644 RebuildBdd/Checkup/check-target-readiness.sh create mode 100644 RebuildBdd/bootstrap-target-host.sh diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 849a6bf..13ffc2b 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -59,6 +59,7 @@ set +a AUTO_INSTALL_POSTGRES="${AUTO_INSTALL_POSTGRES:-yes}" AUTO_CREATE_PGUSER="${AUTO_CREATE_PGUSER:-yes}" +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}" @@ -69,51 +70,70 @@ if ! require_cmd "$SUDO_BIN"; then fail "sudo absent sur la cible" fi +if ! "$SUDO_BIN" -n true >/dev/null 2>&1; then + fail "sudo non interactif indisponible pour l'utilisateur courant" +fi + +if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then + fail "sudo -n -u postgres indisponible" +fi + +if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then + fail "PGPORT invalide : $PGPORT" +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" + "$SUDO_BIN" -n apt update >/dev/null 2>&1 || fail "échec de apt update" + "$SUDO_BIN" -n 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 +if ! "$SUDO_BIN" -n 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" + "$SUDO_BIN" -n 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 + if "$SUDO_BIN" -n -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 +if ! "$SUDO_BIN" -n -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 \ + "$SUDO_BIN" -n -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//\'/\'\'}';" \ + + ROLE_ATTRIBUTES="LOGIN CREATEDB CREATEROLE" + if [[ "${PGUSER_SUPERUSER,,}" == "yes" ]]; then + ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE" + fi + + "$SUDO_BIN" -n -u postgres psql -d postgres -c \ + "CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} 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." diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh new file mode 100644 index 0000000..60d9f9a --- /dev/null +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -0,0 +1,338 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################################################### +# check-target-readiness.sh +# +# Prépare la machine cible pour permettre l'exécution non interactive du +# script de rebuild depuis une interface web. +# +# Ce script : +# - charge et valide le .env ; +# - crée/corrige les dossiers et permissions locales ; +# - génère la clé SSH si absente ; +# - alimente known_hosts pour le serveur de backup ; +# - teste la connexion SSH non interactive vers le serveur de backup ; +# - vérifie/installe la config sudoers si autorisée ; +# - lance ensuite check-postgresql.sh. +############################################################################### + +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}" +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 + ;; + --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 '"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 || fail "commande requise absente : $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 +} + +require_env_vars() { + local missing=() + local var + + for var in \ + ENV_NAME PGHOST PGPORT PGUSER PGPASSWORD DBS \ + BACKUP_REMOTE_USER BACKUP_REMOTE_HOST BACKUP_REMOTE_DIR \ + SSH_KEY BACKUP_LOG_DIR + do + [[ -n "${!var:-}" ]] || missing+=("$var") + done + + if (( ${#missing[@]} > 0 )); then + fail "variables .env manquantes : ${missing[*]}" + fi +} + +validate_env_values() { + [[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide" + [[ -n "$DBS" ]] || fail "DBS vide" + [[ "$PGUSER" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || fail "PGUSER invalide" + [[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide" + [[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide" + [[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide" +} + +prepare_log_file() { + mkdir -p "$BACKUP_LOG_DIR" || { + echo '{"status":"error","message":"impossible de créer le dossier de logs"}' + exit 1 + } + + local ts safe_request_id + ts="$(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}/check_target_${ENV_NAME,,}_${safe_request_id}_${ts}.log" + touch "$LOG_FILE" || { + echo '{"status":"error","message":"impossible de créer le fichier de log"}' + exit 1 + } +} + +prepare_local_paths() { + local restore_base + restore_base="${LOCAL_RESTORE_BASE_DIR:-${REPO_DIR}/restore_tmp}" + + mkdir -p "$BACKUP_LOG_DIR" || fail "création BACKUP_LOG_DIR impossible" + [[ -w "$BACKUP_LOG_DIR" ]] || fail "BACKUP_LOG_DIR non inscriptible" + + mkdir -p "$restore_base" || fail "création LOCAL_RESTORE_BASE_DIR impossible" + [[ -w "$restore_base" ]] || fail "LOCAL_RESTORE_BASE_DIR non inscriptible" + + log "Dossiers locaux prêts." +} + +prepare_scripts_permissions() { + local core_script check_pg_script + core_script="${REPO_DIR}/rebuild-bdd-core.sh" + check_pg_script="${REPO_DIR}/Checkup/check-postgresql.sh" + + [[ -f "$core_script" ]] || fail "script core introuvable : $core_script" + [[ -f "$check_pg_script" ]] || fail "script PostgreSQL introuvable : $check_pg_script" + + chmod 700 "$core_script" || fail "chmod impossible sur $core_script" + chmod 700 "$check_pg_script" || fail "chmod impossible sur $check_pg_script" + + log "Permissions scripts corrigées." +} + +prepare_ssh_key() { + local key_dir + key_dir="$(dirname "$SSH_KEY")" + + mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir" + chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir" + + if [[ ! -f "$SSH_KEY" ]]; then + log "Clé SSH absente : génération de ${SSH_KEY}" + ssh-keygen -t ed25519 -N "" -f "$SSH_KEY" >/dev/null 2>&1 || \ + fail "impossible de générer la clé SSH" + fi + + chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée" + + [[ -f "${SSH_KEY}.pub" ]] || fail "clé publique absente : ${SSH_KEY}.pub" + chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" + + log "Clé SSH prête." +} + +prepare_known_hosts() { + local ssh_dir known_hosts ssh_port + ssh_dir="$(dirname "$SSH_KEY")" + known_hosts="${ssh_dir}/known_hosts" + ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" + + touch "$known_hosts" || fail "impossible de créer known_hosts" + chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts" + + if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then + log "Ajout de ${BACKUP_REMOTE_HOST}:${ssh_port} à known_hosts" + ssh-keyscan -p "$ssh_port" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ + fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}" + else + log "Host déjà présent dans known_hosts." + fi +} + +test_backup_ssh() { + local ssh_port ssh_timeout + ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" + ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" + + ssh \ + -i "$SSH_KEY" \ + -p "$ssh_port" \ + -o IdentitiesOnly=yes \ + -o BatchMode=yes \ + -o ConnectTimeout="$ssh_timeout" \ + -o StrictHostKeyChecking=yes \ + "${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" \ + "test -d '$BACKUP_REMOTE_DIR'" \ + >>"$LOG_FILE" 2>&1 || \ + fail "connexion SSH backup impossible, clé non autorisée, ou dossier distant absent" + + log "Connexion SSH backup validée." +} + +install_sudoers_if_allowed() { + local auto_configure sudoers_file tmp_file + auto_configure="$(to_bool_yes_no "${AUTO_CONFIGURE_SUDOERS:-no}")" || fail "AUTO_CONFIGURE_SUDOERS invalide" + + if [[ "$auto_configure" != "yes" ]]; then + log "Installation sudoers automatique désactivée." + return 0 + fi + + if ! sudo -n true >/dev/null 2>&1; then + fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo -n n'est pas disponible ; configuration initiale manuelle requise" + fi + + require_cmd visudo + + sudoers_file="/etc/sudoers.d/rebuild-bdd-${USER}" + tmp_file="$(mktemp)" + + cat >"$tmp_file" </dev/null 2>&1 || { + rm -f "$tmp_file" + fail "fichier sudoers généré invalide" + } + + sudo install -m 440 "$tmp_file" "$sudoers_file" || { + rm -f "$tmp_file" + fail "impossible d'installer $sudoers_file" + } + + rm -f "$tmp_file" + log "Fichier sudoers installé : $sudoers_file" +} + +check_sudo_non_interactive() { + sudo -n true >/dev/null 2>&1 || \ + fail "sudo non interactif indisponible pour ${USER}" + + sudo -n -u postgres true >/dev/null 2>&1 || \ + fail "sudo -n -u postgres indisponible pour ${USER}" + + log "sudo non interactif validé." +} + +run_postgresql_check() { + local check_script + check_script="${REPO_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 "$REQUEST_ID" \ + --non-interactive \ + >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" + + log "Préparation PostgreSQL validée." +} + +############################################################################### +# Main +############################################################################### + +[[ -f "$ENV_FILE" ]] || { + echo '{"status":"error","message":"fichier .env introuvable"}' + exit 1 +} + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-}}" + +require_env_vars +validate_env_values +prepare_log_file + +require_cmd bash +require_cmd python3 +require_cmd ssh +require_cmd ssh-keygen +require_cmd ssh-keyscan +require_cmd sudo + +prepare_local_paths +prepare_scripts_permissions +prepare_ssh_key +prepare_known_hosts +test_backup_ssh +install_sudoers_if_allowed +check_sudo_non_interactive +run_postgresql_check + +log "Machine cible prête pour le rebuild." +print_json_and_exit "success" "machine cible prête" 0 \ No newline at end of file diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh new file mode 100644 index 0000000..c23ad21 --- /dev/null +++ b/RebuildBdd/bootstrap-target-host.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################################################### +# bootstrap-target-host.sh +# +# Bootstrap initial d'une machine cible neuve ou quasi neuve. +# +# Ce script est lancé depuis la machine de pilotage et : +# 1. charge le .env local ; +# 2. récupère la configuration bootstrap de la cible ; +# 3. teste la connexion SSH de bootstrap ; +# 4. installe le socle minimal sur la cible ; +# 5. crée les dossiers de travail ; +# 6. génère le .env cible ; +# 7. clone ou met à jour le dépôt distant. +# +# À ce stade, il ne gère pas encore : +# - la clé SSH backup ; +# - known_hosts backup ; +# - sudoers PostgreSQL ; +# - le lancement du rebuild. +############################################################################### + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env" + +ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}" +TARGET_NAME="${TARGET_NAME:-}" +CLI_TARGET="" +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 + ;; + --json-only) + JSON_ONLY="yes" + shift + ;; + *) + echo "Argument inconnu : $1" >&2 + exit 1 + ;; + esac +done + +print_stdout() { + [[ "$JSON_ONLY" == "yes" ]] || echo "$*" +} + +log() { + print_stdout "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +fail() { + local msg="$1" + if [[ "$JSON_ONLY" == "yes" ]]; then + printf '{"status":"error","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + else + echo "ERROR: $msg" >&2 + fi + exit 1 +} + +success() { + local msg="$1" + if [[ "$JSON_ONLY" == "yes" ]]; then + printf '{"status":"success","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + else + log "$msg" + fi +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" +} + +sanitize_key() { + local value="${1:-}" + value="${value//[^a-zA-Z0-9_]/_}" + printf '%s' "$value" +} + +get_env_var() { + local var_name="$1" + printf '%s' "${!var_name:-}" +} + +shell_quote() { + printf "%q" "$1" +} + +[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE" + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" +[[ -n "$TARGET_NAME" ]] || fail "target manquante" + +SAFE_TARGET="$(sanitize_key "$TARGET_NAME")" + +BOOTSTRAP_HOST="$(get_env_var "TARGET_HOST_${SAFE_TARGET}")" +BOOTSTRAP_PORT="$(get_env_var "TARGET_PORT_${SAFE_TARGET}")" +BOOTSTRAP_USER="$(get_env_var "TARGET_BOOTSTRAP_USER_${SAFE_TARGET}")" +BOOTSTRAP_SSH_KEY="$(get_env_var "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET}")" + +TARGET_REPO_URL="$(get_env_var "TARGET_REPO_URL_${SAFE_TARGET}")" +TARGET_REPO_BRANCH="$(get_env_var "TARGET_REPO_BRANCH_${SAFE_TARGET}")" +TARGET_REPO_DIR="$(get_env_var "TARGET_REPO_DIR_${SAFE_TARGET}")" +TARGET_ENV_FILE_PATH="$(get_env_var "TARGET_ENV_FILE_${SAFE_TARGET}")" + +TARGET_ENV_NAME_VALUE="$(get_env_var "TARGET_ENV_NAME_${SAFE_TARGET}")" +TARGET_PGHOST_VALUE="$(get_env_var "TARGET_PGHOST_${SAFE_TARGET}")" +TARGET_PGPORT_VALUE="$(get_env_var "TARGET_PGPORT_${SAFE_TARGET}")" +TARGET_PGUSER_VALUE="$(get_env_var "TARGET_PGUSER_${SAFE_TARGET}")" +TARGET_PGPASSWORD_VALUE="$(get_env_var "TARGET_PGPASSWORD_${SAFE_TARGET}")" +TARGET_DBS_VALUE="$(get_env_var "TARGET_DBS_${SAFE_TARGET}")" + +TARGET_BACKUP_REMOTE_USER_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET}")" +TARGET_BACKUP_REMOTE_HOST_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET}")" +TARGET_BACKUP_REMOTE_DIR_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET}")" +TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_SSH_PORT_${SAFE_TARGET}")" +TARGET_BACKUP_LOG_DIR_VALUE="$(get_env_var "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET}")" + +TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="$(get_env_var "TARGET_LOCAL_RESTORE_BASE_DIR_${SAFE_TARGET}")" +TARGET_REMOTE_ROLES_DIR_NAME_VALUE="$(get_env_var "TARGET_REMOTE_ROLES_DIR_NAME_${SAFE_TARGET}")" +TARGET_SSH_KEY_VALUE="$(get_env_var "TARGET_SSH_KEY_${SAFE_TARGET}")" +TARGET_AUTO_INSTALL_POSTGRES_VALUE="$(get_env_var "TARGET_AUTO_INSTALL_POSTGRES_${SAFE_TARGET}")" +TARGET_AUTO_CREATE_PGUSER_VALUE="$(get_env_var "TARGET_AUTO_CREATE_PGUSER_${SAFE_TARGET}")" +TARGET_PGUSER_SUPERUSER_VALUE="$(get_env_var "TARGET_PGUSER_SUPERUSER_${SAFE_TARGET}")" +TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="$(get_env_var "TARGET_AUTO_CONFIGURE_SUDOERS_${SAFE_TARGET}")" +TARGET_EXCLUDED_RESTORE_ROLES_VALUE="$(get_env_var "TARGET_EXCLUDED_RESTORE_ROLES_${SAFE_TARGET}")" + +[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST_${SAFE_TARGET} manquante" +[[ -n "$BOOTSTRAP_PORT" ]] || BOOTSTRAP_PORT="22" +[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER_${SAFE_TARGET} manquante" +[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET} manquante" + +[[ -f "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap introuvable : $BOOTSTRAP_SSH_KEY" +[[ -r "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap non lisible : $BOOTSTRAP_SSH_KEY" + +[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${SAFE_TARGET} manquante" +[[ -n "$TARGET_REPO_BRANCH" ]] || fail "TARGET_REPO_BRANCH_${SAFE_TARGET} manquante" +[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${SAFE_TARGET} manquante" +[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE_${SAFE_TARGET} manquante" + +[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME_${SAFE_TARGET} manquante" +[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST_${SAFE_TARGET} manquante" +[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT_${SAFE_TARGET} manquante" +[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER_${SAFE_TARGET} manquante" +[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD_${SAFE_TARGET} manquante" +[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS_${SAFE_TARGET} manquante" + +[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET} manquante" +[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET} manquante" +[[ -n "$TARGET_BACKUP_REMOTE_DIR_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET} manquante" +[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET} manquante" + +[[ -n "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" ]] || TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="22" +[[ -n "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE" ]] || TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_REPO_DIR}/restore_tmp" +[[ -n "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE" ]] || TARGET_REMOTE_ROLES_DIR_NAME_VALUE="user" +[[ -n "$TARGET_SSH_KEY_VALUE" ]] || TARGET_SSH_KEY_VALUE="/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly" +[[ -n "$TARGET_AUTO_INSTALL_POSTGRES_VALUE" ]] || TARGET_AUTO_INSTALL_POSTGRES_VALUE="yes" +[[ -n "$TARGET_AUTO_CREATE_PGUSER_VALUE" ]] || TARGET_AUTO_CREATE_PGUSER_VALUE="yes" +[[ -n "$TARGET_PGUSER_SUPERUSER_VALUE" ]] || TARGET_PGUSER_SUPERUSER_VALUE="no" +[[ -n "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE" ]] || TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="no" +[[ -n "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE" ]] || TARGET_EXCLUDED_RESTORE_ROLES_VALUE="postgres" + +require_cmd ssh +require_cmd scp +require_cmd python3 + +SSH_OPTS=( + -i "$BOOTSTRAP_SSH_KEY" + -p "$BOOTSTRAP_PORT" + -o IdentitiesOnly=yes + -o BatchMode=yes + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=8 +) + +REMOTE="${BOOTSTRAP_USER}@${BOOTSTRAP_HOST}" + +log "Test de connexion SSH bootstrap vers ${REMOTE}:${BOOTSTRAP_PORT}" +ssh "${SSH_OPTS[@]}" "$REMOTE" "exit 0" >/dev/null 2>&1 \ + || fail "connexion SSH bootstrap impossible vers ${REMOTE}" + +REMOTE_SETUP_CMD=" +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive + +if command -v apt-get >/dev/null 2>&1; then + if command -v sudo >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y bash git python3 sudo curl openssh-client ca-certificates + else + apt-get update + apt-get install -y bash git python3 sudo curl openssh-client ca-certificates + fi +else + echo 'apt-get absent sur la cible' >&2 + exit 1 +fi + +mkdir -p $(shell_quote "$(dirname "$TARGET_REPO_DIR")") +mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")") +mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE") +mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE") +mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") +" + +log "Installation du socle minimal sur la cible" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ + || fail "échec de préparation système distante" + +TMP_ENV_FILE="$(mktemp)" +cleanup() { + rm -f "$TMP_ENV_FILE" +} +trap cleanup EXIT + +cat >"$TMP_ENV_FILE" </dev/null 2>&1 \ + || fail "échec de copie du .env cible" + +REMOTE_REPO_CMD=" +set -euo pipefail + +if [[ ! -d $(shell_quote "${TARGET_REPO_DIR}/.git") ]]; then + rm -rf $(shell_quote "$TARGET_REPO_DIR") + git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_REPO_DIR") +else + git -C $(shell_quote "$TARGET_REPO_DIR") fetch --prune origin + git -C $(shell_quote "$TARGET_REPO_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH") + git -C $(shell_quote "$TARGET_REPO_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH") +fi + +chmod 700 $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true +" + +log "Clone / mise à jour du dépôt distant" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \ + || fail "échec de synchronisation du dépôt sur la cible" + +success "bootstrap initial terminé pour ${TARGET_NAME}" \ No newline at end of file diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index 20374b6..4de89e8 100755 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -232,13 +232,11 @@ for cmd in ssh scp psql pg_restore createdb dropdb python3 grep sed find basenam 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" +if [[ -x "$CHECK_SCRIPT" ]]; then + log "Précheck PostgreSQL déjà effectué par check-target-readiness.sh" +else + fail "script introuvable ou non exécutable : $CHECK_SCRIPT" +fi [[ -f "$SSH_KEY" ]] || fail "clé SSH source backup introuvable : $SSH_KEY" [[ -r "$SSH_KEY" ]] || fail "clé SSH source backup non lisible : $SSH_KEY" diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index e4f1b59..03fcc6e 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -245,9 +245,16 @@ 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") +PRECHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-target-readiness.sh") +TARGET_ENV_FILE=$(shell_quote "$TARGET_ENV_FILE") +REQUESTED_DB=$(shell_quote "$REQUESTED_DB") +ALLOW_OVERWRITE=$(shell_quote "$ALLOW_OVERWRITE") +RESTORE_ROLES=$(shell_quote "$RESTORE_ROLES") +REQUEST_ID=$(shell_quote "$REQUEST_ID") 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; } +command -v python3 >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"python3 absent sur la cible\"}'; exit 1; } mkdir -p \"\$(dirname \"\$REPO_DIR\")\" @@ -261,14 +268,38 @@ else fi [[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; } +[[ -f \"\$PRECHECK_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script précheck introuvable sur la cible\"}'; exit 1; } + chmod 700 \"\$CORE_SCRIPT\" +chmod 700 \"\$PRECHECK_SCRIPT\" + +PRECHECK_JSON=\"/tmp/check_target_\${REQUEST_ID}.json\" + +\"\$PRECHECK_SCRIPT\" \ + --env-file \"\$TARGET_ENV_FILE\" \ + --request-id \"\$REQUEST_ID\" \ + --non-interactive \ + --json-only >\"\$PRECHECK_JSON\" + +PRECHECK_STATUS=\"\$(python3 - <<'PY' \"\$PRECHECK_JSON\" +import json, sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) +print(data.get('status', 'error')) +PY +)\" + +if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then + cat \"\$PRECHECK_JSON\" + exit 1 +fi 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") \ + --env-file \"\$TARGET_ENV_FILE\" \ + --db \"\$REQUESTED_DB\" \ + --overwrite \"\$ALLOW_OVERWRITE\" \ + --restore-roles \"\$RESTORE_ROLES\" \ + --request-id \"\$REQUEST_ID\" \ --non-interactive \ --json-only " From 0d4ffd939159ae52c4eab04fa638b913c85d1a28 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 10:19:20 +0100 Subject: [PATCH 05/33] feat : web interface valide (WIP) --- RecetteScripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RecetteScripts/README.md b/RecetteScripts/README.md index 5d8c69d..a211530 100644 --- a/RecetteScripts/README.md +++ b/RecetteScripts/README.md @@ -1,4 +1,4 @@ -# RecetteScripts +# RecetteScripts Scripts Bash permettant d’automatiser la gestion d’un environnement **PostgreSQL de recette**. From a1fb6f550426d2c2197b43f64e8be786ec8ed746 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 11:40:35 +0100 Subject: [PATCH 06/33] feat : Utilisation web disponible et simplification du deployement des scripts (WIP) --- BackupVaultWarden/backup-vaultwarden.sh | 0 CheckStorage/check-storage.sh | 0 RebuildBdd/Checkup/check-postgresql.sh | 11 +- RebuildBdd/Checkup/check-target-readiness.sh | 42 +- RebuildBdd/Config/.env.exemple | 38 ++ RebuildBdd/Config/Targets/prod.env.example | 30 + RebuildBdd/Config/Targets/test.env.example | 38 ++ RebuildBdd/README.md | 566 +++++++++++++++++++ RebuildBdd/bootstrap-target-host.sh | 422 ++++++++++---- RebuildBdd/create-target-config.sh | 160 ++++++ RebuildBdd/rebuild-bdd-core.sh | 10 +- RebuildBdd/run-rebuild-bdd.sh | 227 ++++---- RecetteScripts/backup-bdd-recette.sh | 0 RecetteScripts/check-statut-recette.sh | 0 14 files changed, 1287 insertions(+), 257 deletions(-) mode change 100644 => 100755 BackupVaultWarden/backup-vaultwarden.sh mode change 100644 => 100755 CheckStorage/check-storage.sh mode change 100644 => 100755 RebuildBdd/Checkup/check-target-readiness.sh create mode 100644 RebuildBdd/Config/.env.exemple create mode 100644 RebuildBdd/Config/Targets/prod.env.example create mode 100644 RebuildBdd/Config/Targets/test.env.example create mode 100644 RebuildBdd/README.md mode change 100644 => 100755 RebuildBdd/bootstrap-target-host.sh create mode 100644 RebuildBdd/create-target-config.sh mode change 100644 => 100755 RecetteScripts/backup-bdd-recette.sh mode change 100644 => 100755 RecetteScripts/check-statut-recette.sh diff --git a/BackupVaultWarden/backup-vaultwarden.sh b/BackupVaultWarden/backup-vaultwarden.sh old mode 100644 new mode 100755 diff --git a/CheckStorage/check-storage.sh b/CheckStorage/check-storage.sh old mode 100644 new mode 100755 diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 13ffc2b..5c8562f 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -74,23 +74,16 @@ if ! "$SUDO_BIN" -n true >/dev/null 2>&1; then fail "sudo non interactif indisponible pour l'utilisateur courant" fi -if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then - fail "sudo -n -u postgres indisponible" -fi - if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then fail "PGPORT invalide : $PGPORT" 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" -n apt update >/dev/null 2>&1 || fail "échec de apt update" "$SUDO_BIN" -n 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é." @@ -140,6 +133,10 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then fi fi +if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then + fail "sudo -n -u postgres indisponible" +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 diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh old mode 100644 new mode 100755 index 60d9f9a..0bc61e8 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -6,15 +6,6 @@ set -euo pipefail # # Prépare la machine cible pour permettre l'exécution non interactive du # script de rebuild depuis une interface web. -# -# Ce script : -# - charge et valide le .env ; -# - crée/corrige les dossiers et permissions locales ; -# - génère la clé SSH si absente ; -# - alimente known_hosts pour le serveur de backup ; -# - teste la connexion SSH non interactive vers le serveur de backup ; -# - vérifie/installe la config sudoers si autorisée ; -# - lance ensuite check-postgresql.sh. ############################################################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -128,6 +119,8 @@ validate_env_values() { [[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide" [[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide" [[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide" + BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}" + [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide" } prepare_log_file() { @@ -182,32 +175,26 @@ prepare_ssh_key() { mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir" chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir" - if [[ ! -f "$SSH_KEY" ]]; then - log "Clé SSH absente : génération de ${SSH_KEY}" - ssh-keygen -t ed25519 -N "" -f "$SSH_KEY" >/dev/null 2>&1 || \ - fail "impossible de générer la clé SSH" - fi - + [[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY" chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée" - [[ -f "${SSH_KEY}.pub" ]] || fail "clé publique absente : ${SSH_KEY}.pub" - chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" + [[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub" + [[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" log "Clé SSH prête." } prepare_known_hosts() { - local ssh_dir known_hosts ssh_port + local ssh_dir known_hosts ssh_dir="$(dirname "$SSH_KEY")" known_hosts="${ssh_dir}/known_hosts" - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" touch "$known_hosts" || fail "impossible de créer known_hosts" chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts" if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then - log "Ajout de ${BACKUP_REMOTE_HOST}:${ssh_port} à known_hosts" - ssh-keyscan -p "$ssh_port" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ + log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts" + ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}" else log "Host déjà présent dans known_hosts." @@ -215,13 +202,12 @@ prepare_known_hosts() { } test_backup_ssh() { - local ssh_port ssh_timeout - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" + local ssh_timeout ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" ssh \ -i "$SSH_KEY" \ - -p "$ssh_port" \ + -p "$BACKUP_REMOTE_SSH_PORT" \ -o IdentitiesOnly=yes \ -o BatchMode=yes \ -o ConnectTimeout="$ssh_timeout" \ @@ -277,9 +263,6 @@ check_sudo_non_interactive() { sudo -n true >/dev/null 2>&1 || \ fail "sudo non interactif indisponible pour ${USER}" - sudo -n -u postgres true >/dev/null 2>&1 || \ - fail "sudo -n -u postgres indisponible pour ${USER}" - log "sudo non interactif validé." } @@ -295,13 +278,10 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" + sudo -n -u postgres true >/dev/null 2>&1 || fail "sudo -n -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } -############################################################################### -# Main -############################################################################### - [[ -f "$ENV_FILE" ]] || { echo '{"status":"error","message":"fichier .env introuvable"}' exit 1 diff --git a/RebuildBdd/Config/.env.exemple b/RebuildBdd/Config/.env.exemple new file mode 100644 index 0000000..ff66962 --- /dev/null +++ b/RebuildBdd/Config/.env.exemple @@ -0,0 +1,38 @@ +############################################################################### +# config/global.env.example +############################################################################### + +# Defaults d'exécution +ALLOW_OVERWRITE=no +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 + +# Defaults PostgreSQL +GLOBAL_PGHOST=127.0.0.1 +GLOBAL_PGPORT=5432 + +# Defaults scripts +GLOBAL_REMOTE_ROLES_DIR_NAME=user +GLOBAL_EXCLUDED_RESTORE_ROLES="postgres" + +# Defaults bootstrap / cible +GLOBAL_ENABLE_BOOTSTRAP=yes +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 \ No newline at end of file diff --git a/RebuildBdd/Config/Targets/prod.env.example b/RebuildBdd/Config/Targets/prod.env.example new file mode 100644 index 0000000..7070ae3 --- /dev/null +++ b/RebuildBdd/Config/Targets/prod.env.example @@ -0,0 +1,30 @@ + +############################################################################### +# CIBLE : prod +############################################################################### + +# TARGET_HOST_prod=10.0.0.20 +# TARGET_PORT_prod=22 +# TARGET_BOOTSTRAP_USER_prod=backup_liot +# TARGET_BOOTSTRAP_SSH_KEY_prod=/home/matteo/.ssh/id_ed25519_target_prod +# TARGET_RUNTIME_USER_prod=backup_liot +# TARGET_ENABLE_BOOTSTRAP_prod=yes +# TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_prod=yes +# TARGET_REPO_DIR_prod=/home/backup_liot/RebuildBdd +# TARGET_ENV_FILE_prod=/home/backup_liot/RebuildBdd/.env +# TARGET_ENV_NAME_prod=PROD +# TARGET_PGHOST_prod=127.0.0.1 +# TARGET_PGPORT_prod=5432 +# TARGET_PGUSER_prod=backup_liot +# TARGET_PGPASSWORD_prod=change_me_prod_password +# TARGET_DBS_prod="sirh inventory ferme" +# TARGET_BACKUP_SUBDIR_prod=bdd-prod +# TARGET_BACKUP_LOG_DIR_prod=/home/backup_liot/logs/rebuild_bdd +# TARGET_LOCAL_RESTORE_BASE_DIR_prod=/home/backup_liot/RebuildBdd/restore_tmp +# TARGET_SSH_KEY_prod=/home/backup_liot/.ssh/id_ed25519_backup_readonly +# TARGET_REMOTE_ROLES_DIR_NAME_prod=user +# TARGET_EXCLUDED_RESTORE_ROLES_prod="postgres" +# TARGET_AUTO_INSTALL_POSTGRES_prod=yes +# TARGET_AUTO_CREATE_PGUSER_prod=yes +# TARGET_PGUSER_SUPERUSER_prod=no +# TARGET_AUTO_CONFIGURE_SUDOERS_prod=no \ No newline at end of file diff --git a/RebuildBdd/Config/Targets/test.env.example b/RebuildBdd/Config/Targets/test.env.example new file mode 100644 index 0000000..ff66962 --- /dev/null +++ b/RebuildBdd/Config/Targets/test.env.example @@ -0,0 +1,38 @@ +############################################################################### +# config/global.env.example +############################################################################### + +# Defaults d'exécution +ALLOW_OVERWRITE=no +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 + +# Defaults PostgreSQL +GLOBAL_PGHOST=127.0.0.1 +GLOBAL_PGPORT=5432 + +# Defaults scripts +GLOBAL_REMOTE_ROLES_DIR_NAME=user +GLOBAL_EXCLUDED_RESTORE_ROLES="postgres" + +# Defaults bootstrap / cible +GLOBAL_ENABLE_BOOTSTRAP=yes +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 \ No newline at end of file diff --git a/RebuildBdd/README.md b/RebuildBdd/README.md new file mode 100644 index 0000000..ed2ff86 --- /dev/null +++ b/RebuildBdd/README.md @@ -0,0 +1,566 @@ +# RebuildBdd + +Orchestration de reconstruction de bases PostgreSQL à partir de dumps distants, avec préparation automatique des machines cibles, exécution non interactive et intégration web. + +--- + +## Objectif + +Ce projet permet de : + +- préparer automatiquement une machine cible neuve ou partiellement configurée ; +- déployer et mettre à jour les scripts sur la cible ; +- préparer PostgreSQL localement sur la cible ; +- récupérer le dernier dump disponible depuis un serveur de backup ; +- restaurer une base PostgreSQL de manière non interactive ; +- exposer un flux exploitable depuis une interface web via des retours JSON. + +--- + +## Fonctionnement global + +Le flux standard est le suivant : + +1. **Création ou mise à jour de la configuration d’une cible** +2. **Bootstrap initial de la cible** +3. **Précheck de préparation** +4. **Rebuild de la base** + +En pratique : + +- `create-target-config.sh` crée un fichier de configuration cible ; +- `bootstrap-target-host.sh` prépare la machine cible ; +- `Checkup/check-target-readiness.sh` valide l’environnement ; +- `rebuild-bdd-core.sh` exécute la restauration ; +- `run-rebuild-bdd.sh` orchestre l’ensemble. + +--- + +## Architecture + +### Configuration + +Le projet utilise deux niveaux de configuration : + +#### 1. Configuration globale +Fichier : + +```bash +config/global.env +```` + +Contient les paramètres stables, par exemple : + +* dépôt Git des scripts ; +* serveur de backup ; +* clé SSH de lecture backup ; +* valeurs par défaut PostgreSQL ; +* options globales de bootstrap. + +#### 2. Configuration par cible + +Fichiers : + +```bash +config/targets/.env +``` + +Chaque fichier cible contient : + +* accès SSH bootstrap ; +* répertoires locaux de la cible ; +* paramètres PostgreSQL ; +* sous-répertoire backup associé ; +* options spécifiques à la cible. + +--- + +## Arborescence recommandée + +```bash +RebuildBdd/ +├── bootstrap-target-host.sh +├── create-target-config.sh +├── run-rebuild-bdd.sh +├── rebuild-bdd-core.sh +├── config/ +│ ├── global.env +│ └── targets/ +│ ├── test.env +│ └── prod.env +└── Checkup/ + ├── check-postgresql.sh + └── check-target-readiness.sh +``` + +--- + +## Scripts + +### `create-target-config.sh` + +Crée ou met à jour un fichier cible dans : + +```bash +config/targets/.env +``` + +Usage : + +```bash +./create-target-config.sh \ + --target test \ + --host 192.168.1.50 \ + --port 22 \ + --bootstrap-user backup_liot \ + --bootstrap-key /home/user/.ssh/id_ed25519_target_test \ + --runtime-user backup_liot \ + --repo-dir /home/backup_liot/RebuildBdd \ + --env-name RECETTE \ + --pguser backup_liot \ + --pgpassword secret \ + --dbs "sirh inventory ferme" \ + --backup-subdir bdd-recette +``` + +--- + +### `bootstrap-target-host.sh` + +Prépare une machine cible neuve ou quasi neuve : + +* connexion SSH bootstrap ; +* installation des paquets minimum ; +* création des dossiers ; +* génération du `.env` cible ; +* copie de la clé SSH backup ; +* préparation de `known_hosts` ; +* installation éventuelle d’un `sudoers.d` minimal ; +* synchronisation du dépôt ; +* exécution de `check-postgresql.sh`. + +Usage : + +```bash +./bootstrap-target-host.sh --target test +``` + +Mode JSON : + +```bash +./bootstrap-target-host.sh --target test --json-only +``` + +--- + +### `Checkup/check-postgresql.sh` + +Prépare PostgreSQL localement sur la cible : + +* installation si absent ; +* démarrage du service ; +* test de disponibilité ; +* création du rôle PostgreSQL cible si nécessaire. + +Ce script est prévu pour fonctionner en non interactif avec `sudo -n`. + +--- + +### `Checkup/check-target-readiness.sh` + +Valide la préparation complète de la cible : + +* lecture du `.env` cible ; +* vérification des chemins ; +* permissions locales ; +* permissions SSH ; +* `known_hosts` ; +* accès SSH au serveur de backup ; +* exécution de `check-postgresql.sh`. + +Mode JSON disponible pour usage web. + +--- + +### `rebuild-bdd-core.sh` + +Script métier de reconstruction : + +* validation des paramètres ; +* connexion au serveur de backup ; +* récupération du dernier dump ; +* récupération éventuelle du fichier des rôles ; +* suppression/recréation de la base si autorisé ; +* restauration des rôles ; +* restauration du dump PostgreSQL ; +* retour JSON final. + +--- + +### `run-rebuild-bdd.sh` + +Script orchestrateur principal. + +Il peut : + +* lancer le bootstrap si activé pour la cible ; +* synchroniser le dépôt distant ; +* lancer le précheck ; +* exécuter le rebuild. + +Usage : + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --request-id web_001 \ + --non-interactive +``` + +--- + +## Prérequis + +### Machine de lancement + +Doit disposer de : + +* `bash` +* `ssh` +* `scp` +* `git` +* `python3` + +### Machine cible + +Le bootstrap suppose : + +* accès SSH fonctionnel ; +* utilisateur bootstrap existant ; +* soit `root`, soit `sudo -n` déjà disponible pour le bootstrap initial. + +### Serveur de backup + +Doit : + +* être joignable en SSH depuis la cible ; +* accepter la clé de lecture backup ; +* contenir les dumps dans l’arborescence attendue. + +--- + +## Structure des backups attendue + +Exemple : + +```bash +/home/malio-b/backups/ +├── bdd-recette/ +│ ├── sirh/ +│ │ ├── sirh_2026-03-16_19-00-01.dump +│ ├── inventory/ +│ ├── ferme/ +│ └── user/ +│ ├── user_2026-03-16_19-00-01.sql +``` + +Le script recherche : + +* le dernier dump dans : + +```bash +//_*.dump +``` + +* le dernier fichier rôles dans : + +```bash +//user_*.sql +``` + +--- + +## Configuration + +### 1. Créer la configuration globale + +Copier : + +```bash +config/global.env.example +``` + +vers : + +```bash +config/global.env +``` + +Renseigner ensuite : + +* dépôt Git ; +* serveur de backup ; +* clé SSH backup ; +* defaults globaux. + +--- + +### 2. Créer une cible + +Deux possibilités. + +#### A. À la main + +Créer un fichier : + +```bash +config/targets/test.env +``` + +à partir de : + +```bash +config/targets/test.env.example +``` + +#### B. Via script + +Utiliser : + +```bash +./create-target-config.sh ... +``` + +--- + +## Exécution locale + +### Bootstrap seul + +```bash +./bootstrap-target-host.sh --target test +``` + +### Rebuild complet + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --non-interactive +``` + +--- + +## Intégration web + +L’interface web ne doit envoyer que les paramètres métier de l’exécution : + +```json +{ + "target": "test", + "db": "sirh", + "overwrite": "yes", + "restore_roles": "yes", + "request_id": "web_20260317_001" +} +``` + +Le backend transforme cela en commande : + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --request-id web_20260317_001 \ + --non-interactive +``` + +### Important + +Le web ne doit pas transmettre directement : + +* les clés SSH ; +* les mots de passe PostgreSQL ; +* les paramètres bas niveau de la cible ; +* les chemins système sensibles. + +Ces informations doivent être stockées dans la configuration serveur. + +--- + +## Ajouter une nouvelle machine depuis le web + +Le flux recommandé est : + +1. créer ou mettre à jour `config/targets/.env` +2. lancer `bootstrap-target-host.sh --target ` +3. lancer ensuite `run-rebuild-bdd.sh --target ...` + +Le bouton web **“Ajouter une machine”** doit donc : + +* créer la configuration cible ; +* déclencher le bootstrap ; +* vérifier le retour ; +* rendre ensuite la cible disponible pour les rebuilds. + +--- + +## Sorties JSON + +### Succès + +Exemple : + +```json +{ + "status": "success", + "message": "restauration terminée avec succès", + "request_id": "web_001", + "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" +} +``` + +### Erreur + +Exemple : + +```json +{ + "status": "error", + "message": "la base existe déjà et overwrite n'est pas autorisé", + "request_id": "web_001", + "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" +} +``` + +--- + +## Sécurité + +### Recommandations minimales + +* utiliser des clés SSH dédiées ; +* limiter la clé backup à la lecture seule ; +* restreindre les permissions des fichiers de config ; +* exécuter les scripts avec un utilisateur dédié ; +* ne pas exposer les secrets dans l’interface web ; +* valider strictement toutes les entrées côté backend. + +### `sudoers` + +Le bootstrap peut installer un `sudoers.d` minimal pour l’utilisateur runtime : + +```sudoers + ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl + ALL=(postgres) NOPASSWD: /usr/bin/psql +``` + +Adapter si d’autres commandes doivent être autorisées. + +--- + +## Logs + +Les logs de rebuild sont stockés dans : + +```bash +TARGET_BACKUP_LOG_DIR +``` + +Exemple : + +```bash +/home/backup_liot/logs/rebuild_bdd/ +``` + +Le chemin du log est renvoyé dans le JSON final. + +--- + +## Limites connues + +* le bootstrap initial nécessite un accès SSH bootstrap valide ; +* le bootstrap ne remplace pas une mauvaise architecture réseau ; +* les secrets doivent être gérés proprement par la couche web/backend ; +* des verrous d’exécution peuvent être ajoutés si plusieurs rebuilds concurrents sont prévus. + +--- + +## Recommandations de validation + +Avant mise en production, tester au minimum : + +1. bootstrap d’une machine neuve ; +2. rebuild complet d’une base ; +3. refus si la base existe et `overwrite=no` ; +4. relance complète une seconde fois sur la même cible ; +5. accès backup invalide ; +6. PostgreSQL absent au départ ; +7. `sudo -n` indisponible. + +--- + +## Commandes utiles + +### Créer une cible + +```bash +./create-target-config.sh \ + --target test \ + --host 192.168.1.50 \ + --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 \ + --env-name RECETTE \ + --pguser backup_liot \ + --pgpassword secret \ + --dbs "sirh inventory ferme" \ + --backup-subdir bdd-recette +``` + +### Bootstrap + +```bash +./bootstrap-target-host.sh --target test +``` + +### Rebuild + +```bash +./run-rebuild-bdd.sh \ + --target test \ + --db sirh \ + --overwrite yes \ + --restore-roles yes \ + --non-interactive +``` + +--- + +## État du projet + +Le projet permet désormais une utilisation : + +* locale ; +* automatisée ; +* intégrée au web ; + +avec préparation des cibles, exécution non interactive et retour JSON. + +``` diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh old mode 100644 new mode 100755 index c23ad21..9181ad3 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -1,40 +1,28 @@ #!/usr/bin/env bash set -euo pipefail -############################################################################### -# bootstrap-target-host.sh -# -# Bootstrap initial d'une machine cible neuve ou quasi neuve. -# -# Ce script est lancé depuis la machine de pilotage et : -# 1. charge le .env local ; -# 2. récupère la configuration bootstrap de la cible ; -# 3. teste la connexion SSH de bootstrap ; -# 4. installe le socle minimal sur la cible ; -# 5. crée les dossiers de travail ; -# 6. génère le .env cible ; -# 7. clone ou met à jour le dépôt distant. -# -# À ce stade, il ne gère pas encore : -# - la clé SSH backup ; -# - known_hosts backup ; -# - sudoers PostgreSQL ; -# - le lancement du rebuild. -############################################################################### - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env" +CONFIG_DIR="${SCRIPT_DIR}/config" +GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" + +GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" +TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" -ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}" TARGET_NAME="${TARGET_NAME:-}" CLI_TARGET="" 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" + --global-env-file) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; } + GLOBAL_ENV_FILE="$2" + shift 2 + ;; + --targets-dir) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; } + TARGETS_DIR="$2" shift 2 ;; --target) @@ -53,6 +41,13 @@ while [[ $# -gt 0 ]]; do esac done +json_escape() { + python3 - <<'PY' "$1" +import json, sys +print(json.dumps(sys.argv[1])) +PY +} + print_stdout() { [[ "$JSON_ONLY" == "yes" ]] || echo "$*" } @@ -64,7 +59,9 @@ log() { fail() { local msg="$1" if [[ "$JSON_ONLY" == "yes" ]]; then - printf '{"status":"error","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + printf '{"status":%s,"message":%s}\n' \ + "$(json_escape "error")" \ + "$(json_escape "$msg")" else echo "ERROR: $msg" >&2 fi @@ -74,7 +71,9 @@ fail() { success() { local msg="$1" if [[ "$JSON_ONLY" == "yes" ]]; then - printf '{"status":"success","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')" + printf '{"status":%s,"message":%s}\n' \ + "$(json_escape "success")" \ + "$(json_escape "$msg")" else log "$msg" fi @@ -84,99 +83,120 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" } -sanitize_key() { - local value="${1:-}" - value="${value//[^a-zA-Z0-9_]/_}" - printf '%s' "$value" -} - -get_env_var() { - local var_name="$1" - printf '%s' "${!var_name:-}" +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 } shell_quote() { printf "%q" "$1" } -[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE" - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a +cleanup() { + rm -f "${TMP_ENV_FILE:-}" +} +trap cleanup EXIT TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" [[ -n "$TARGET_NAME" ]] || fail "target manquante" -SAFE_TARGET="$(sanitize_key "$TARGET_NAME")" +TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET_NAME}.env" -BOOTSTRAP_HOST="$(get_env_var "TARGET_HOST_${SAFE_TARGET}")" -BOOTSTRAP_PORT="$(get_env_var "TARGET_PORT_${SAFE_TARGET}")" -BOOTSTRAP_USER="$(get_env_var "TARGET_BOOTSTRAP_USER_${SAFE_TARGET}")" -BOOTSTRAP_SSH_KEY="$(get_env_var "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET}")" +[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE" +[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE" -TARGET_REPO_URL="$(get_env_var "TARGET_REPO_URL_${SAFE_TARGET}")" -TARGET_REPO_BRANCH="$(get_env_var "TARGET_REPO_BRANCH_${SAFE_TARGET}")" -TARGET_REPO_DIR="$(get_env_var "TARGET_REPO_DIR_${SAFE_TARGET}")" -TARGET_ENV_FILE_PATH="$(get_env_var "TARGET_ENV_FILE_${SAFE_TARGET}")" +set -a +# shellcheck disable=SC1090 +source "$GLOBAL_ENV_FILE" +# shellcheck disable=SC1090 +source "$TARGET_ENV_SOURCE" +set +a -TARGET_ENV_NAME_VALUE="$(get_env_var "TARGET_ENV_NAME_${SAFE_TARGET}")" -TARGET_PGHOST_VALUE="$(get_env_var "TARGET_PGHOST_${SAFE_TARGET}")" -TARGET_PGPORT_VALUE="$(get_env_var "TARGET_PGPORT_${SAFE_TARGET}")" -TARGET_PGUSER_VALUE="$(get_env_var "TARGET_PGUSER_${SAFE_TARGET}")" -TARGET_PGPASSWORD_VALUE="$(get_env_var "TARGET_PGPASSWORD_${SAFE_TARGET}")" -TARGET_DBS_VALUE="$(get_env_var "TARGET_DBS_${SAFE_TARGET}")" +BOOTSTRAP_HOST="${TARGET_HOST:-}" +BOOTSTRAP_PORT="${TARGET_PORT:-22}" +BOOTSTRAP_USER="${TARGET_BOOTSTRAP_USER:-}" +BOOTSTRAP_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" -TARGET_BACKUP_REMOTE_USER_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_HOST_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_DIR_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET}")" -TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_SSH_PORT_${SAFE_TARGET}")" -TARGET_BACKUP_LOG_DIR_VALUE="$(get_env_var "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET}")" +TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" +TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-}}" +TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_ENV_FILE_PATH="${TARGET_ENV_FILE:-}" -TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="$(get_env_var "TARGET_LOCAL_RESTORE_BASE_DIR_${SAFE_TARGET}")" -TARGET_REMOTE_ROLES_DIR_NAME_VALUE="$(get_env_var "TARGET_REMOTE_ROLES_DIR_NAME_${SAFE_TARGET}")" -TARGET_SSH_KEY_VALUE="$(get_env_var "TARGET_SSH_KEY_${SAFE_TARGET}")" -TARGET_AUTO_INSTALL_POSTGRES_VALUE="$(get_env_var "TARGET_AUTO_INSTALL_POSTGRES_${SAFE_TARGET}")" -TARGET_AUTO_CREATE_PGUSER_VALUE="$(get_env_var "TARGET_AUTO_CREATE_PGUSER_${SAFE_TARGET}")" -TARGET_PGUSER_SUPERUSER_VALUE="$(get_env_var "TARGET_PGUSER_SUPERUSER_${SAFE_TARGET}")" -TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="$(get_env_var "TARGET_AUTO_CONFIGURE_SUDOERS_${SAFE_TARGET}")" -TARGET_EXCLUDED_RESTORE_ROLES_VALUE="$(get_env_var "TARGET_EXCLUDED_RESTORE_ROLES_${SAFE_TARGET}")" +TARGET_ENV_NAME_VALUE="${TARGET_ENV_NAME:-}" +TARGET_PGHOST_VALUE="${TARGET_PGHOST:-${GLOBAL_PGHOST:-}}" +TARGET_PGPORT_VALUE="${TARGET_PGPORT:-${GLOBAL_PGPORT:-}}" +TARGET_PGUSER_VALUE="${TARGET_PGUSER:-}" +TARGET_PGPASSWORD_VALUE="${TARGET_PGPASSWORD:-}" +TARGET_DBS_VALUE="${TARGET_DBS:-}" -[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST_${SAFE_TARGET} manquante" -[[ -n "$BOOTSTRAP_PORT" ]] || BOOTSTRAP_PORT="22" -[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER_${SAFE_TARGET} manquante" -[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET} manquante" +TARGET_BACKUP_REMOTE_USER_VALUE="${TARGET_BACKUP_REMOTE_USER:-${GLOBAL_BACKUP_REMOTE_USER:-}}" +TARGET_BACKUP_REMOTE_HOST_VALUE="${TARGET_BACKUP_REMOTE_HOST:-${GLOBAL_BACKUP_REMOTE_HOST:-}}" +TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="${TARGET_BACKUP_REMOTE_SSH_PORT:-${GLOBAL_BACKUP_REMOTE_PORT:-22}}" +GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR:-}" +TARGET_BACKUP_SUBDIR_VALUE="${TARGET_BACKUP_SUBDIR:-}" +TARGET_BACKUP_LOG_DIR_VALUE="${TARGET_BACKUP_LOG_DIR:-}" +TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY:-${GLOBAL_BACKUP_SSH_PRIVATE_KEY:-}}" +TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE="${TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY:-${GLOBAL_BACKUP_SSH_PUBLIC_KEY:-}}" +TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE="${TARGET_BACKUP_KNOWN_HOSTS_STRICT:-${GLOBAL_BACKUP_KNOWN_HOSTS_STRICT:-yes}}" + +TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_LOCAL_RESTORE_BASE_DIR:-${TARGET_REPO_DIR}/restore_tmp}" +TARGET_REMOTE_ROLES_DIR_NAME_VALUE="${TARGET_REMOTE_ROLES_DIR_NAME:-${GLOBAL_REMOTE_ROLES_DIR_NAME:-user}}" +TARGET_SSH_KEY_VALUE="${TARGET_SSH_KEY:-/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly}" +TARGET_AUTO_INSTALL_POSTGRES_VALUE="${TARGET_AUTO_INSTALL_POSTGRES:-${GLOBAL_AUTO_INSTALL_POSTGRES:-yes}}" +TARGET_AUTO_CREATE_PGUSER_VALUE="${TARGET_AUTO_CREATE_PGUSER:-${GLOBAL_AUTO_CREATE_PGUSER:-yes}}" +TARGET_PGUSER_SUPERUSER_VALUE="${TARGET_PGUSER_SUPERUSER:-${GLOBAL_PGUSER_SUPERUSER:-no}}" +TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="${TARGET_AUTO_CONFIGURE_SUDOERS:-${GLOBAL_AUTO_CONFIGURE_SUDOERS:-no}}" +TARGET_EXCLUDED_RESTORE_ROLES_VALUE="${TARGET_EXCLUDED_RESTORE_ROLES:-${GLOBAL_EXCLUDED_RESTORE_ROLES:-postgres}}" + +TARGET_RUNTIME_USER_VALUE="${TARGET_RUNTIME_USER:-$BOOTSTRAP_USER}" +TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE="${TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-${GLOBAL_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO:-yes}}" + +[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST manquante" +[[ "$BOOTSTRAP_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide" +[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante" +[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante" [[ -f "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap introuvable : $BOOTSTRAP_SSH_KEY" [[ -r "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap non lisible : $BOOTSTRAP_SSH_KEY" -[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${SAFE_TARGET} manquante" -[[ -n "$TARGET_REPO_BRANCH" ]] || fail "TARGET_REPO_BRANCH_${SAFE_TARGET} manquante" -[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${SAFE_TARGET} manquante" -[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE_${SAFE_TARGET} manquante" +[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant" +[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant" +[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" +[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE manquante" -[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER_${SAFE_TARGET} manquante" -[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD_${SAFE_TARGET} manquante" -[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS_${SAFE_TARGET} manquante" +[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME manquante" +[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST/GLOBAL_PGHOST manquant" +[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT/GLOBAL_PGPORT manquant" +[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER manquante" +[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD manquante" +[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS manquante" -[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_REMOTE_DIR_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET} manquante" -[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET} manquante" +[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_USER/TARGET_BACKUP_REMOTE_USER manquant" +[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_HOST/TARGET_BACKUP_REMOTE_HOST manquant" +[[ -n "$GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE" ]] || fail "GLOBAL_BACKUP_REMOTE_BASE_DIR manquant" +[[ -n "$TARGET_BACKUP_SUBDIR_VALUE" ]] || fail "TARGET_BACKUP_SUBDIR manquante" +[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR manquante" +TARGET_BACKUP_REMOTE_DIR_VALUE="${GLOBAL_BACKUP_REMOTE_BASE_DIR_VALUE%/}/${TARGET_BACKUP_SUBDIR_VALUE}" -[[ -n "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" ]] || TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="22" -[[ -n "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE" ]] || TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_REPO_DIR}/restore_tmp" -[[ -n "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE" ]] || TARGET_REMOTE_ROLES_DIR_NAME_VALUE="user" -[[ -n "$TARGET_SSH_KEY_VALUE" ]] || TARGET_SSH_KEY_VALUE="/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly" -[[ -n "$TARGET_AUTO_INSTALL_POSTGRES_VALUE" ]] || TARGET_AUTO_INSTALL_POSTGRES_VALUE="yes" -[[ -n "$TARGET_AUTO_CREATE_PGUSER_VALUE" ]] || TARGET_AUTO_CREATE_PGUSER_VALUE="yes" -[[ -n "$TARGET_PGUSER_SUPERUSER_VALUE" ]] || TARGET_PGUSER_SUPERUSER_VALUE="no" -[[ -n "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE" ]] || TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="no" -[[ -n "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE" ]] || TARGET_EXCLUDED_RESTORE_ROLES_VALUE="postgres" +[[ -n "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "GLOBAL_BACKUP_SSH_PRIVATE_KEY/TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY manquant" +[[ -f "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" +[[ -r "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" ]] || fail "clé privée backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" + +if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then + [[ -f "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup introuvable : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" + [[ -r "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]] || fail "clé publique backup non lisible : $TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" +fi + +[[ "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" =~ ^[0-9]+$ ]] || fail "port backup invalide" +to_bool_yes_no "$TARGET_BACKUP_KNOWN_HOSTS_STRICT_VALUE" >/dev/null || fail "TARGET_BACKUP_KNOWN_HOSTS_STRICT invalide" +to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || fail "TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO invalide" + +ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")" require_cmd ssh require_cmd scp @@ -202,24 +222,41 @@ set -euo pipefail export DEBIAN_FRONTEND=noninteractive -if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y bash git python3 sudo curl openssh-client ca-certificates - else - apt-get update - apt-get install -y bash git python3 sudo curl openssh-client ca-certificates +run_root() { + if [ \"\$(id -u)\" -eq 0 ]; then + \"\$@\" + return 0 fi -else + + if command -v sudo >/dev/null 2>&1; then + sudo -n \"\$@\" || { + echo 'sudo -n indisponible pour le bootstrap' >&2 + exit 1 + } + return 0 + fi + + echo 'ni root ni sudo disponible pour le bootstrap' >&2 + exit 1 +} + +if ! command -v apt-get >/dev/null 2>&1; then echo 'apt-get absent sur la cible' >&2 exit 1 fi +run_root apt-get update +run_root apt-get install -y bash git python3 sudo curl openssh-client ca-certificates postgresql-client + mkdir -p $(shell_quote "$(dirname "$TARGET_REPO_DIR")") mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")") mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE") mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE") mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") + +chmod 700 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")") || true +touch $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") +chmod 644 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") || true " log "Installation du socle minimal sur la cible" @@ -227,10 +264,6 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ || fail "échec de préparation système distante" TMP_ENV_FILE="$(mktemp)" -cleanup() { - rm -f "$TMP_ENV_FILE" -} -trap cleanup EXIT cat >"$TMP_ENV_FILE" </dev/null 2>&1 \ || fail "échec de copie du .env cible" +REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" +REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" + +log "Copie de la clé privée backup sur la cible" +scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "$TARGET_SSH_KEY_VALUE")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé privée backup" + +if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then + log "Copie de la clé publique backup sur la cible" + scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "${TARGET_SSH_KEY_VALUE}.pub")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé publique backup" +fi + +REMOTE_SSH_PERMS_CMD=" +set -euo pipefail +chmod 700 $(shell_quote "$REMOTE_SSH_DIR") +chmod 600 $(shell_quote "$TARGET_SSH_KEY_VALUE") +if [[ -f $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") ]]; then + chmod 644 $(shell_quote "${TARGET_SSH_KEY_VALUE}.pub") +fi +touch $(shell_quote "$REMOTE_KNOWN_HOSTS") +chmod 644 $(shell_quote "$REMOTE_KNOWN_HOSTS") +" + +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" + +REMOTE_KNOWN_HOSTS_CMD=" +set -euo pipefail + +if ! command -v ssh-keyscan >/dev/null 2>&1; then + echo 'ssh-keyscan absent sur la cible' >&2 + exit 1 +fi + +if ! ssh-keygen -F $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") -f $(shell_quote "$REMOTE_KNOWN_HOSTS") >/dev/null 2>&1; then + ssh-keyscan -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") -H $(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE") >> $(shell_quote "$REMOTE_KNOWN_HOSTS") 2>/dev/null +fi +" + +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 + +ssh \ + -i $(shell_quote "$TARGET_SSH_KEY_VALUE") \ + -p $(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE") \ + -o IdentitiesOnly=yes \ + -o BatchMode=yes \ + -o ConnectTimeout=8 \ + -o StrictHostKeyChecking=$(shell_quote "$STRICT_OPTION") \ + $(shell_quote "${TARGET_BACKUP_REMOTE_USER_VALUE}@${TARGET_BACKUP_REMOTE_HOST_VALUE}") \ + test -d $(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE") +" + +log "Test de la connexion SSH cible -> backup" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_BACKUP_TEST_CMD" \ + || fail "la cible ne peut pas accéder au serveur de backup avec la clé fournie" + +if [[ "$ALLOW_PASSWORDLESS_SUDO" == "yes" ]]; then + REMOTE_SUDOERS_CMD=" +set -euo pipefail + +run_root() { + if [ \"\$(id -u)\" -eq 0 ]; then + \"\$@\" + return 0 + fi + + if command -v sudo >/dev/null 2>&1; then + sudo -n \"\$@\" || { + echo 'sudo -n indisponible pour installer sudoers' >&2 + exit 1 + } + return 0 + fi + + echo 'ni root ni sudo disponible pour sudoers' >&2 + exit 1 +} + +if ! command -v visudo >/dev/null 2>&1; then + run_root apt-get update + run_root apt-get install -y sudo +fi + +TMP_SUDOERS_FILE=\$(mktemp) +cat >\"\$TMP_SUDOERS_FILE\" </dev/null 2>&1 || { + rm -f \"\$TMP_SUDOERS_FILE\" + echo 'fichier sudoers généré invalide' >&2 + exit 1 +} + +run_root install -m 440 \"\$TMP_SUDOERS_FILE\" /etc/sudoers.d/rebuild-bdd-${TARGET_RUNTIME_USER_VALUE} +rm -f \"\$TMP_SUDOERS_FILE\" +" + + log "Installation du sudoers non interactif minimal" + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" \ + || fail "échec d'installation du sudoers non interactif" +else + log "Installation du sudoers non interactif désactivée." +fi + REMOTE_REPO_CMD=" set -euo pipefail @@ -283,4 +442,47 @@ log "Clone / mise à jour du dépôt distant" ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \ || fail "échec de synchronisation du dépôt sur la cible" +REMOTE_VALIDATE_SUDO_ROOT_CMD=" +set -euo pipefail +command -v sudo >/dev/null 2>&1 || { + echo 'sudo absent sur la cible' >&2 + exit 1 +} +sudo -n true >/dev/null 2>&1 || { + echo 'sudo -n indisponible' >&2 + exit 1 +} +" + +log "Validation initiale de sudo -n" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \ + || fail "sudo -n invalide sur la cible" + +REMOTE_RUN_CHECK_PG_CMD=" +set -euo pipefail + +CHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-postgresql.sh") +ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH") + +[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\" + +\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive +" + +log "Préparation PostgreSQL via check-postgresql.sh" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ + || fail "échec de préparation PostgreSQL pendant le bootstrap" + +REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" +set -euo pipefail +sudo -n -u postgres true >/dev/null 2>&1 || { + echo 'sudo -n -u postgres indisponible après préparation PostgreSQL' >&2 + exit 1 +} +" + +log "Validation finale de sudo -n -u postgres" +ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \ + || fail "sudo -n -u postgres invalide sur la cible" + success "bootstrap initial terminé pour ${TARGET_NAME}" \ No newline at end of file diff --git a/RebuildBdd/create-target-config.sh b/RebuildBdd/create-target-config.sh new file mode 100644 index 0000000..1d17ae6 --- /dev/null +++ b/RebuildBdd/create-target-config.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="${SCRIPT_DIR}/config" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" + +TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" + +TARGET="" +HOST="" +PORT="22" +BOOTSTRAP_USER="" +BOOTSTRAP_SSH_KEY="" +RUNTIME_USER="" +REPO_DIR="" +ENV_FILE="" +ENV_NAME="" +PGHOST="" +PGPORT="" +PGUSER="" +PGPASSWORD="" +DBS="" +BACKUP_SUBDIR="" +BACKUP_LOG_DIR="" +LOCAL_RESTORE_BASE_DIR="" +SSH_KEY_TARGET_PATH="" +ENABLE_BOOTSTRAP="yes" +ALLOW_PASSWORDLESS_SUDO="yes" +AUTO_INSTALL_POSTGRES="yes" +AUTO_CREATE_PGUSER="yes" +PGUSER_SUPERUSER="no" +AUTO_CONFIGURE_SUDOERS="no" +REMOTE_ROLES_DIR_NAME="user" +EXCLUDED_RESTORE_ROLES="postgres" +FORCE="no" + +while [[ $# -gt 0 ]]; do + case "$1" in + --targets-dir) TARGETS_DIR="$2"; shift 2 ;; + --target) TARGET="$2"; shift 2 ;; + --host) HOST="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --bootstrap-user) BOOTSTRAP_USER="$2"; shift 2 ;; + --bootstrap-key) BOOTSTRAP_SSH_KEY="$2"; shift 2 ;; + --runtime-user) RUNTIME_USER="$2"; shift 2 ;; + --repo-dir) REPO_DIR="$2"; shift 2 ;; + --env-file) ENV_FILE="$2"; shift 2 ;; + --env-name) ENV_NAME="$2"; shift 2 ;; + --pghost) PGHOST="$2"; shift 2 ;; + --pgport) PGPORT="$2"; shift 2 ;; + --pguser) PGUSER="$2"; shift 2 ;; + --pgpassword) PGPASSWORD="$2"; shift 2 ;; + --dbs) DBS="$2"; shift 2 ;; + --backup-subdir) BACKUP_SUBDIR="$2"; shift 2 ;; + --backup-log-dir) BACKUP_LOG_DIR="$2"; shift 2 ;; + --local-restore-base-dir) LOCAL_RESTORE_BASE_DIR="$2"; shift 2 ;; + --ssh-key-target-path) SSH_KEY_TARGET_PATH="$2"; shift 2 ;; + --enable-bootstrap) ENABLE_BOOTSTRAP="$2"; shift 2 ;; + --allow-passwordless-sudo) ALLOW_PASSWORDLESS_SUDO="$2"; shift 2 ;; + --auto-install-postgres) AUTO_INSTALL_POSTGRES="$2"; shift 2 ;; + --auto-create-pguser) AUTO_CREATE_PGUSER="$2"; shift 2 ;; + --pguser-superuser) PGUSER_SUPERUSER="$2"; shift 2 ;; + --auto-configure-sudoers) AUTO_CONFIGURE_SUDOERS="$2"; shift 2 ;; + --remote-roles-dir-name) REMOTE_ROLES_DIR_NAME="$2"; shift 2 ;; + --excluded-restore-roles) EXCLUDED_RESTORE_ROLES="$2"; shift 2 ;; + --force) FORCE="yes"; shift ;; + *) echo "Argument inconnu : $1" >&2; exit 1 ;; + esac +done + +fail() { + echo "ERROR: $*" >&2 + exit 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 +} + +[[ -n "$TARGET" ]] || fail "--target manquant" +[[ "$TARGET" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "target invalide" + +[[ -n "$HOST" ]] || fail "--host manquant" +[[ -n "$BOOTSTRAP_USER" ]] || fail "--bootstrap-user manquant" +[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "--bootstrap-key manquant" +[[ -n "$REPO_DIR" ]] || fail "--repo-dir manquant" +[[ -n "$ENV_NAME" ]] || fail "--env-name manquant" +[[ -n "$PGUSER" ]] || fail "--pguser manquant" +[[ -n "$PGPASSWORD" ]] || fail "--pgpassword manquant" +[[ -n "$DBS" ]] || fail "--dbs manquant" +[[ -n "$BACKUP_SUBDIR" ]] || fail "--backup-subdir manquant" +[[ "$PORT" =~ ^[0-9]+$ ]] || fail "--port invalide" + +[[ -n "$RUNTIME_USER" ]] || RUNTIME_USER="$BOOTSTRAP_USER" +[[ -n "$ENV_FILE" ]] || ENV_FILE="${REPO_DIR}/.env" +[[ -n "$PGHOST" ]] || PGHOST="127.0.0.1" +[[ -n "$PGPORT" ]] || PGPORT="5432" +[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "--pgport invalide" +[[ -n "$BACKUP_LOG_DIR" ]] || BACKUP_LOG_DIR="/home/${RUNTIME_USER}/logs/rebuild_bdd" +[[ -n "$LOCAL_RESTORE_BASE_DIR" ]] || LOCAL_RESTORE_BASE_DIR="${REPO_DIR}/restore_tmp" +[[ -n "$SSH_KEY_TARGET_PATH" ]] || SSH_KEY_TARGET_PATH="/home/${RUNTIME_USER}/.ssh/id_ed25519_backup_readonly" + +ENABLE_BOOTSTRAP="$(to_bool_yes_no "$ENABLE_BOOTSTRAP")" || fail "--enable-bootstrap invalide" +ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$ALLOW_PASSWORDLESS_SUDO")" || fail "--allow-passwordless-sudo invalide" +AUTO_INSTALL_POSTGRES="$(to_bool_yes_no "$AUTO_INSTALL_POSTGRES")" || fail "--auto-install-postgres invalide" +AUTO_CREATE_PGUSER="$(to_bool_yes_no "$AUTO_CREATE_PGUSER")" || fail "--auto-create-pguser invalide" +PGUSER_SUPERUSER="$(to_bool_yes_no "$PGUSER_SUPERUSER")" || fail "--pguser-superuser invalide" +AUTO_CONFIGURE_SUDOERS="$(to_bool_yes_no "$AUTO_CONFIGURE_SUDOERS")" || fail "--auto-configure-sudoers invalide" + +mkdir -p "$TARGETS_DIR" || fail "impossible de créer $TARGETS_DIR" + +TARGET_FILE="${TARGETS_DIR}/${TARGET}.env" +if [[ -f "$TARGET_FILE" && "$FORCE" != "yes" ]]; then + fail "fichier déjà existant : $TARGET_FILE (utiliser --force pour écraser)" +fi + +cat >"$TARGET_FILE" <&2; exit 1; } - ENV_FILE="$2" + --global-env-file) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --global-env-file" >&2; exit 1; } + GLOBAL_ENV_FILE="$2" + shift 2 + ;; + --targets-dir) + [[ $# -ge 2 ]] || { echo "Argument manquant pour --targets-dir" >&2; exit 1; } + TARGETS_DIR="$2" shift 2 ;; --target) @@ -50,10 +57,6 @@ while [[ $# -gt 0 ]]; do NON_INTERACTIVE="yes" shift ;; - --json-only) - JSON_ONLY="yes" - shift - ;; *) echo "Argument inconnu : $1" >&2 exit 1 @@ -61,33 +64,17 @@ while [[ $# -gt 0 ]]; do 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" +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } fail() { - print_json_and_exit "error" "$*" 1 + log "ERROR: $*" >&2 + exit 1 } require_cmd() { - command -v "$1" >/dev/null 2>&1 + command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" } to_bool_yes_no() { @@ -104,124 +91,112 @@ 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 +cleanup() { + rm -f "${BOOTSTRAP_JSON:-}" } +trap cleanup EXIT + +[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE" +[[ -d "$TARGETS_DIR" ]] || fail "dossier targets introuvable : $TARGETS_DIR" set -a # shellcheck disable=SC1090 -source "$ENV_FILE" +source "$GLOBAL_ENV_FILE" set +a -for cmd in bash ssh python3; do - require_cmd "$cmd" || fail "commande requise absente sur IA : $cmd" -done +require_cmd ssh +require_cmd git +require_cmd python3 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}}" +REQUEST_ID="${CLI_REQUEST_ID:-${REQUEST_ID:-$(date '+%Y%m%d%H%M%S')_$RANDOM}}" 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 + mapfile -t TARGET_LIST < <(find "$TARGETS_DIR" -maxdepth 1 -type f -name '*.env' -printf '%f\n' | sed 's/\.env$//' | LC_ALL=C sort) + + [[ "${#TARGET_LIST[@]}" -gt 0 ]] || fail "aucune cible définie dans ${TARGETS_DIR}" + if is_tty; then - print_stdout "Cibles disponibles :" - for i in "${!TARGETS_ARRAY[@]}"; do - print_stdout " $((i + 1))) ${TARGETS_ARRAY[$i]}" + echo "Cibles disponibles :" + for i in "${!TARGET_LIST[@]}"; do + echo " $((i + 1))) ${TARGET_LIST[$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))]}" + (( TARGET_INDEX >= 1 && TARGET_INDEX <= ${#TARGET_LIST[@]} )) || fail "numéro hors plage" + TARGET="${TARGET_LIST[$((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_ENV_SOURCE="${TARGETS_DIR}/${TARGET}.env" +[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE" -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")" +set -a +# shellcheck disable=SC1090 +source "$TARGET_ENV_SOURCE" +set +a -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" +TARGET_HOST="${TARGET_HOST:-}" +TARGET_PORT="${TARGET_PORT:-22}" +TARGET_USER="${TARGET_BOOTSTRAP_USER:-}" +TARGET_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" +TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" +TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-main}}" +TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_ENV_FILE="${TARGET_ENV_FILE:-}" +TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-yes}}" +[[ -n "$TARGET_HOST" ]] || fail "TARGET_HOST manquante" +[[ "$TARGET_PORT" =~ ^[0-9]+$ ]] || fail "TARGET_PORT invalide" +[[ -n "$TARGET_USER" ]] || fail "TARGET_BOOTSTRAP_USER manquante" +[[ -n "$TARGET_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY manquante" [[ -f "$TARGET_SSH_KEY" ]] || fail "clé SSH cible introuvable : $TARGET_SSH_KEY" +[[ -r "$TARGET_SSH_KEY" ]] || fail "clé SSH cible non lisible : $TARGET_SSH_KEY" -if [[ -z "$REQUEST_ID" ]]; then - REQUEST_ID="$(date '+%Y%m%d%H%M%S')_$$" -fi +[[ -n "$TARGET_REPO_URL" ]] || fail "GLOBAL_REPO_URL/TARGET_REPO_URL manquant" +[[ -n "$TARGET_REPO_BRANCH" ]] || fail "GLOBAL_REPO_BRANCH/TARGET_REPO_BRANCH manquant" +[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" +[[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE manquante" + +TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide" + +BOOTSTRAP_SCRIPT_LOCAL="${SCRIPT_DIR}/bootstrap-target-host.sh" +[[ -f "$BOOTSTRAP_SCRIPT_LOCAL" ]] || fail "script bootstrap introuvable : $BOOTSTRAP_SCRIPT_LOCAL" +[[ -x "$BOOTSTRAP_SCRIPT_LOCAL" ]] || chmod 700 "$BOOTSTRAP_SCRIPT_LOCAL" || fail "chmod impossible sur $BOOTSTRAP_SCRIPT_LOCAL" if [[ -z "$REQUESTED_DB" ]]; then + DBS_FOR_TARGET="${TARGET_DBS:-}" if [[ "$NON_INTERACTIVE" == "yes" ]]; then fail "REQUESTED_DB manquante en mode non interactif" fi + read -r -a DBS_ARRAY <<< "$DBS_FOR_TARGET" + [[ "${#DBS_ARRAY[@]}" -gt 0 ]] || fail "TARGET_DBS vide" + if is_tty; then + echo "Bases disponibles :" + for i in "${!DBS_ARRAY[@]}"; do + echo " $((i + 1))) ${DBS_ARRAY[$i]}" + done + echo 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 @@ -229,15 +204,52 @@ fi [[ "$REQUESTED_DB" =~ ^[a-zA-Z0-9_]+$ ]] || fail "nom de base invalide" +if [[ "$TARGET_ENABLE_BOOTSTRAP" == "yes" ]]; then + log "Bootstrap initial activé pour la cible ${TARGET}" + BOOTSTRAP_JSON="/tmp/bootstrap_target_${REQUEST_ID}.json" + + "$BOOTSTRAP_SCRIPT_LOCAL" \ + --global-env-file "$GLOBAL_ENV_FILE" \ + --targets-dir "$TARGETS_DIR" \ + --target "$TARGET" \ + --json-only >"$BOOTSTRAP_JSON" || { + cat "$BOOTSTRAP_JSON" 2>/dev/null || true + fail "échec du bootstrap initial de la cible ${TARGET}" + } + + BOOTSTRAP_STATUS="$( + python3 - <<'PY' "$BOOTSTRAP_JSON" +import json, sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) +print(data.get("status", "error")) +PY + )" + + if [[ "$BOOTSTRAP_STATUS" != "success" ]]; then + cat "$BOOTSTRAP_JSON" + fail "bootstrap initial échoué pour la cible ${TARGET}" + fi + + log "Bootstrap initial terminé pour ${TARGET}" +else + log "Bootstrap initial désactivé pour ${TARGET}" +fi + SSH_OPTS=( -i "$TARGET_SSH_KEY" - -p "$TARGET_SSH_PORT" + -p "$TARGET_PORT" -o IdentitiesOnly=yes -o BatchMode=yes - -o ConnectTimeout="$TARGET_SSH_CONNECT_TIMEOUT" - -o StrictHostKeyChecking=yes + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=8 ) +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \ + || fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}" + +TARGET_CORE_SCRIPT="${TARGET_REPO_DIR}/rebuild-bdd-core.sh" + REMOTE_BOOTSTRAP_CMD=" set -euo pipefail @@ -291,9 +303,12 @@ PY if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then cat \"\$PRECHECK_JSON\" + rm -f \"\$PRECHECK_JSON\" exit 1 fi +rm -f \"\$PRECHECK_JSON\" + exec \"\$CORE_SCRIPT\" \ --env-file \"\$TARGET_ENV_FILE\" \ --db \"\$REQUESTED_DB\" \ @@ -304,4 +319,4 @@ exec \"\$CORE_SCRIPT\" \ --json-only " -exec ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" \ No newline at end of file +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" \ No newline at end of file diff --git a/RecetteScripts/backup-bdd-recette.sh b/RecetteScripts/backup-bdd-recette.sh old mode 100644 new mode 100755 diff --git a/RecetteScripts/check-statut-recette.sh b/RecetteScripts/check-statut-recette.sh old mode 100644 new mode 100755 From 8ef81add148f2c6671dd2394c842c618a9e69312 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 13:43:34 +0100 Subject: [PATCH 07/33] feat : utilisation web disponible et simplification du deployement des scripts (WIP) --- RebuildBdd/Config/Targets/test.env.example | 62 ++++++++++++---------- RebuildBdd/bootstrap-target-host.sh | 47 +++++++++++----- RebuildBdd/create-target-config.sh | 4 +- RebuildBdd/run-rebuild-bdd.sh | 4 +- 4 files changed, 72 insertions(+), 45 deletions(-) diff --git a/RebuildBdd/Config/Targets/test.env.example b/RebuildBdd/Config/Targets/test.env.example index ff66962..8244082 100644 --- a/RebuildBdd/Config/Targets/test.env.example +++ b/RebuildBdd/Config/Targets/test.env.example @@ -1,38 +1,42 @@ ############################################################################### -# config/global.env.example +# config/targets/test.env.example ############################################################################### -# Defaults d'exécution -ALLOW_OVERWRITE=no -RESTORE_ROLES=yes +# SSH bootstrap cible +TARGET_HOST=192.168.1.50 +TARGET_PORT=22 +TARGET_BOOTSTRAP_USER=backup_liot +TARGET_BOOTSTRAP_SSH_KEY=/home/matteo/.ssh/id_ed25519_target_test +TARGET_RUNTIME_USER=backup_liot -# Dépôt scripts -GLOBAL_REPO_URL=git@gitea.example.tld:team/RebuildBdd.git -GLOBAL_REPO_BRANCH=main +# Bootstrap +TARGET_ENABLE_BOOTSTRAP=yes +TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes -# 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 +# Repo local cible +TARGET_REPO_DIR=/home/backup_liot/RebuildBdd +TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env -# 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 +# PostgreSQL cible +TARGET_ENV_NAME=RECETTE +TARGET_PGHOST=127.0.0.1 +TARGET_PGPORT=5432 +TARGET_PGUSER=backup_liot +TARGET_PGPASSWORD=change_me_pg_password +TARGET_DBS="sirh inventory ferme" -# Defaults PostgreSQL -GLOBAL_PGHOST=127.0.0.1 -GLOBAL_PGPORT=5432 +# Backup cible +TARGET_BACKUP_SUBDIR=bdd-recette -# Defaults scripts -GLOBAL_REMOTE_ROLES_DIR_NAME=user -GLOBAL_EXCLUDED_RESTORE_ROLES="postgres" +# 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 -# Defaults bootstrap / cible -GLOBAL_ENABLE_BOOTSTRAP=yes -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 \ No newline at end of file +# Options cible +TARGET_REMOTE_ROLES_DIR_NAME=user +TARGET_EXCLUDED_RESTORE_ROLES="postgres" +TARGET_AUTO_INSTALL_POSTGRES=yes +TARGET_AUTO_CREATE_PGUSER=yes +TARGET_PGUSER_SUPERUSER=no +TARGET_AUTO_CONFIGURE_SUDOERS=no \ No newline at end of file diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 9181ad3..e030f8b 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -2,9 +2,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_DIR="${SCRIPT_DIR}/config" +CONFIG_DIR="${SCRIPT_DIR}/Config" GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env" -TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets" GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" @@ -102,6 +102,36 @@ cleanup() { } trap cleanup EXIT +copy_file_to_remote() { + local local_file="$1" + local remote_final_path="$2" + local remote_mode="$3" + local remote_parent + local remote_tmp + + [[ -f "$local_file" ]] || fail "fichier source introuvable : $local_file" + [[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file" + + remote_parent="$(dirname "$remote_final_path")" + remote_tmp="/tmp/$(basename "$remote_final_path").$$.$RANDOM.tmp" + + ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + mkdir -p $(shell_quote "$remote_parent") + test -d $(shell_quote "$remote_parent") + test -w $(shell_quote "$remote_parent") + " >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent" + + scp "${SSH_OPTS[@]}" "$local_file" "${REMOTE}:${remote_tmp}" >/dev/null 2>&1 \ + || fail "échec de copie temporaire vers ${remote_tmp}" + + ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + install -m $(shell_quote "$remote_mode") $(shell_quote "$remote_tmp") $(shell_quote "$remote_final_path") + rm -f $(shell_quote "$remote_tmp") + " >/dev/null 2>&1 || fail "échec d'installation distante : $remote_final_path" +} + TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" [[ -n "$TARGET_NAME" ]] || fail "target manquante" @@ -291,24 +321,17 @@ EXCLUDED_RESTORE_ROLES=$(printf '%s\n' "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE") EOF log "Copie du .env cible" -scp "${SSH_OPTS[@]}" "$TMP_ENV_FILE" "${REMOTE}:$(printf '%q' "$TARGET_ENV_FILE_PATH")" >/dev/null 2>&1 \ - || fail "échec de copie du .env cible" +copy_file_to_remote "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600" REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" log "Copie de la clé privée backup sur la cible" -scp "${SSH_OPTS[@]}" \ - "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" \ - "${REMOTE}:$(printf '%q' "$TARGET_SSH_KEY_VALUE")" >/dev/null 2>&1 \ - || fail "échec de copie de la clé privée backup" +copy_file_to_remote "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600" if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then log "Copie de la clé publique backup sur la cible" - scp "${SSH_OPTS[@]}" \ - "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" \ - "${REMOTE}:$(printf '%q' "${TARGET_SSH_KEY_VALUE}.pub")" >/dev/null 2>&1 \ - || fail "échec de copie de la clé publique backup" + copy_file_to_remote "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644" fi REMOTE_SSH_PERMS_CMD=" diff --git a/RebuildBdd/create-target-config.sh b/RebuildBdd/create-target-config.sh index 1d17ae6..3878ba9 100644 --- a/RebuildBdd/create-target-config.sh +++ b/RebuildBdd/create-target-config.sh @@ -2,8 +2,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_DIR="${SCRIPT_DIR}/config" -TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" +CONFIG_DIR="${SCRIPT_DIR}/Config" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets" TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index c4779a5..cf3bc01 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -2,9 +2,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_DIR="${SCRIPT_DIR}/config" +CONFIG_DIR="${SCRIPT_DIR}/Config" GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env" -TARGETS_DIR_DEFAULT="${CONFIG_DIR}/targets" +TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets" GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" From 741fef225b8dfa58cf757b0b16d1fbb30bcc6181 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 13:49:14 +0100 Subject: [PATCH 08/33] feat : correction bug scp (WIP) --- RebuildBdd/bootstrap-target-host.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index e030f8b..b534114 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -102,7 +102,7 @@ cleanup() { } trap cleanup EXIT -copy_file_to_remote() { +copy_file_to_remote_via_ssh() { local local_file="$1" local remote_final_path="$2" local remote_mode="$3" @@ -113,7 +113,7 @@ copy_file_to_remote() { [[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file" remote_parent="$(dirname "$remote_final_path")" - remote_tmp="/tmp/$(basename "$remote_final_path").$$.$RANDOM.tmp" + remote_tmp="/tmp/bootstrap_copy.$$.$RANDOM.tmp" ssh "${SSH_OPTS[@]}" "$REMOTE" " set -euo pipefail @@ -122,8 +122,10 @@ copy_file_to_remote() { test -w $(shell_quote "$remote_parent") " >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent" - scp "${SSH_OPTS[@]}" "$local_file" "${REMOTE}:${remote_tmp}" >/dev/null 2>&1 \ - || fail "échec de copie temporaire vers ${remote_tmp}" + cat "$local_file" | ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + cat > $(shell_quote "$remote_tmp") + " >/dev/null 2>&1 || fail "échec de copie distante via SSH vers ${remote_tmp}" ssh "${SSH_OPTS[@]}" "$REMOTE" " set -euo pipefail @@ -229,7 +231,6 @@ to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || f ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")" require_cmd ssh -require_cmd scp require_cmd python3 SSH_OPTS=( @@ -321,17 +322,17 @@ EXCLUDED_RESTORE_ROLES=$(printf '%s\n' "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE") EOF log "Copie du .env cible" -copy_file_to_remote "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600" +copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600" REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" log "Copie de la clé privée backup sur la cible" -copy_file_to_remote "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600" +copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600" if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then log "Copie de la clé publique backup sur la cible" - copy_file_to_remote "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644" + copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644" fi REMOTE_SSH_PERMS_CMD=" From 122f53f80402cca5336dc64cbd1401d60f5cb308 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 13:55:25 +0100 Subject: [PATCH 09/33] feat : correction bug scp (WIP) --- RebuildBdd/bootstrap-target-host.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index b534114..f6b9243 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -122,10 +122,10 @@ copy_file_to_remote_via_ssh() { test -w $(shell_quote "$remote_parent") " >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent" - cat "$local_file" | ssh "${SSH_OPTS[@]}" "$REMOTE" " + ssh "${SSH_OPTS[@]}" "$REMOTE" " set -euo pipefail cat > $(shell_quote "$remote_tmp") - " >/dev/null 2>&1 || fail "échec de copie distante via SSH vers ${remote_tmp}" + " < "$local_file" >/dev/null 2>&1 || fail "échec d'écriture temporaire distante : $remote_tmp" ssh "${SSH_OPTS[@]}" "$REMOTE" " set -euo pipefail From b76b6613bf8b8a529189d84d574fafc1364cee59 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:14:52 +0100 Subject: [PATCH 10/33] feat : sudoers bug (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 19 +++---- RebuildBdd/Checkup/check-target-readiness.sh | 42 +++++++------- RebuildBdd/bootstrap-target-host.sh | 58 ++++++-------------- 3 files changed, 49 insertions(+), 70 deletions(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 5c8562f..2d9feee 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -70,7 +70,7 @@ if ! require_cmd "$SUDO_BIN"; then fail "sudo absent sur la cible" fi -if ! "$SUDO_BIN" -n true >/dev/null 2>&1; then +if ! "$SUDO_BIN" -n /usr/bin/true >/dev/null 2>&1; then fail "sudo non interactif indisponible pour l'utilisateur courant" fi @@ -78,12 +78,15 @@ if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then fail "PGPORT invalide : $PGPORT" 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" -n apt update >/dev/null 2>&1 || fail "échec de apt update" "$SUDO_BIN" -n 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é." @@ -98,20 +101,20 @@ fi log "Vérification de la disponibilité de PostgreSQL..." for _ in {1..20}; do - if "$SUDO_BIN" -n -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + if "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then log "PostgreSQL répond correctement." break fi sleep 1 done -if ! "$SUDO_BIN" -n -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then - fail "PostgreSQL ne répond pas correctement" +if ! "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + fail "PostgreSQL ne répond pas correctement ou sudo -u postgres n'autorise pas psql" fi if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then ROLE_EXISTS="$( - "$SUDO_BIN" -n -u postgres psql -d postgres -tAc \ + "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -tAc \ "SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true )" @@ -123,7 +126,7 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE" fi - "$SUDO_BIN" -n -u postgres psql -d postgres -c \ + "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c \ "CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} PASSWORD '${PGPASSWORD//\'/\'\'}';" \ >/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}" @@ -133,10 +136,6 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then fi fi -if ! "$SUDO_BIN" -n -u postgres true >/dev/null 2>&1; then - fail "sudo -n -u postgres indisponible" -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 diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh index 0bc61e8..e9f0d70 100755 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -3,9 +3,6 @@ set -euo pipefail ############################################################################### # check-target-readiness.sh -# -# Prépare la machine cible pour permettre l'exécution non interactive du -# script de rebuild depuis une interface web. ############################################################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -119,8 +116,6 @@ validate_env_values() { [[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide" [[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide" [[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide" - BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}" - [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide" } prepare_log_file() { @@ -175,26 +170,32 @@ prepare_ssh_key() { mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir" chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir" - [[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY" + if [[ ! -f "$SSH_KEY" ]]; then + log "Clé SSH absente : génération de ${SSH_KEY}" + ssh-keygen -t ed25519 -N "" -f "$SSH_KEY" >/dev/null 2>&1 || \ + fail "impossible de générer la clé SSH" + fi + chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée" - [[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub" - [[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" + [[ -f "${SSH_KEY}.pub" ]] || fail "clé publique absente : ${SSH_KEY}.pub" + chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" log "Clé SSH prête." } prepare_known_hosts() { - local ssh_dir known_hosts + local ssh_dir known_hosts ssh_port ssh_dir="$(dirname "$SSH_KEY")" known_hosts="${ssh_dir}/known_hosts" + ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" touch "$known_hosts" || fail "impossible de créer known_hosts" chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts" if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then - log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts" - ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ + log "Ajout de ${BACKUP_REMOTE_HOST}:${ssh_port} à known_hosts" + ssh-keyscan -p "$ssh_port" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}" else log "Host déjà présent dans known_hosts." @@ -202,12 +203,13 @@ prepare_known_hosts() { } test_backup_ssh() { - local ssh_timeout + local ssh_port ssh_timeout + ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" ssh \ -i "$SSH_KEY" \ - -p "$BACKUP_REMOTE_SSH_PORT" \ + -p "$ssh_port" \ -o IdentitiesOnly=yes \ -o BatchMode=yes \ -o ConnectTimeout="$ssh_timeout" \ @@ -229,7 +231,7 @@ install_sudoers_if_allowed() { return 0 fi - if ! sudo -n true >/dev/null 2>&1; then + if ! sudo -n /usr/bin/true >/dev/null 2>&1; then fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo -n n'est pas disponible ; configuration initiale manuelle requise" fi @@ -239,8 +241,8 @@ install_sudoers_if_allowed() { tmp_file="$(mktemp)" cat >"$tmp_file" </dev/null 2>&1 || \ - fail "sudo non interactif indisponible pour ${USER}" + sudo -n /usr/bin/true >/dev/null 2>&1 || \ + fail "sudo non interactif root indisponible pour ${USER}" + + sudo -n -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || \ + fail "sudo -n -u postgres indisponible pour psql pour ${USER}" log "sudo non interactif validé." } @@ -278,7 +283,6 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" - sudo -n -u postgres true >/dev/null 2>&1 || fail "sudo -n -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index f6b9243..851d587 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -102,38 +102,6 @@ cleanup() { } trap cleanup EXIT -copy_file_to_remote_via_ssh() { - local local_file="$1" - local remote_final_path="$2" - local remote_mode="$3" - local remote_parent - local remote_tmp - - [[ -f "$local_file" ]] || fail "fichier source introuvable : $local_file" - [[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file" - - remote_parent="$(dirname "$remote_final_path")" - remote_tmp="/tmp/bootstrap_copy.$$.$RANDOM.tmp" - - ssh "${SSH_OPTS[@]}" "$REMOTE" " - set -euo pipefail - mkdir -p $(shell_quote "$remote_parent") - test -d $(shell_quote "$remote_parent") - test -w $(shell_quote "$remote_parent") - " >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent" - - ssh "${SSH_OPTS[@]}" "$REMOTE" " - set -euo pipefail - cat > $(shell_quote "$remote_tmp") - " < "$local_file" >/dev/null 2>&1 || fail "échec d'écriture temporaire distante : $remote_tmp" - - ssh "${SSH_OPTS[@]}" "$REMOTE" " - set -euo pipefail - install -m $(shell_quote "$remote_mode") $(shell_quote "$remote_tmp") $(shell_quote "$remote_final_path") - rm -f $(shell_quote "$remote_tmp") - " >/dev/null 2>&1 || fail "échec d'installation distante : $remote_final_path" -} - TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" [[ -n "$TARGET_NAME" ]] || fail "target manquante" @@ -231,6 +199,7 @@ to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || f ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")" require_cmd ssh +require_cmd scp require_cmd python3 SSH_OPTS=( @@ -322,17 +291,24 @@ EXCLUDED_RESTORE_ROLES=$(printf '%s\n' "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE") EOF log "Copie du .env cible" -copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600" +scp "${SSH_OPTS[@]}" "$TMP_ENV_FILE" "${REMOTE}:$(printf '%q' "$TARGET_ENV_FILE_PATH")" >/dev/null 2>&1 \ + || fail "échec de copie du .env cible" REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" log "Copie de la clé privée backup sur la cible" -copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600" +scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "$TARGET_SSH_KEY_VALUE")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé privée backup" if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then log "Copie de la clé publique backup sur la cible" - copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644" + scp "${SSH_OPTS[@]}" \ + "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" \ + "${REMOTE}:$(printf '%q' "${TARGET_SSH_KEY_VALUE}.pub")" >/dev/null 2>&1 \ + || fail "échec de copie de la clé publique backup" fi REMOTE_SSH_PERMS_CMD=" @@ -421,8 +397,8 @@ fi TMP_SUDOERS_FILE=\$(mktemp) cat >\"\$TMP_SUDOERS_FILE\" </dev/null 2>&1 || { echo 'sudo absent sur la cible' >&2 exit 1 } -sudo -n true >/dev/null 2>&1 || { - echo 'sudo -n indisponible' >&2 +sudo -n /usr/bin/true >/dev/null 2>&1 || { + echo 'sudo -n root indisponible pour /usr/bin/true' >&2 exit 1 } " @@ -499,8 +475,8 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" set -euo pipefail -sudo -n -u postgres true >/dev/null 2>&1 || { - echo 'sudo -n -u postgres indisponible après préparation PostgreSQL' >&2 +sudo -n -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || { + echo 'sudo -n -u postgres indisponible pour psql' >&2 exit 1 } " From 7974491e93d1744fcdc277555051b0405fb65617 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:18:35 +0100 Subject: [PATCH 11/33] feat : sudoers bug (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 18 ++++--- RebuildBdd/Checkup/check-target-readiness.sh | 42 +++++++-------- RebuildBdd/bootstrap-target-host.sh | 56 ++++++++++++++------ 3 files changed, 70 insertions(+), 46 deletions(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 2d9feee..faac0a0 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -70,8 +70,12 @@ if ! require_cmd "$SUDO_BIN"; then fail "sudo absent sur la cible" fi -if ! "$SUDO_BIN" -n /usr/bin/true >/dev/null 2>&1; then - fail "sudo non interactif indisponible pour l'utilisateur courant" +if ! "$SUDO_BIN" -n /usr/bin/systemctl --version >/dev/null 2>&1; then + fail "sudo non interactif indisponible pour systemctl" +fi + +if ! "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + fail "sudo -n -u postgres indisponible" fi if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then @@ -101,20 +105,20 @@ fi log "Vérification de la disponibilité de PostgreSQL..." for _ in {1..20}; do - if "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + if "$SUDO_BIN" -n -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" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then - fail "PostgreSQL ne répond pas correctement ou sudo -u postgres n'autorise pas psql" +if ! "$SUDO_BIN" -n -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" -n -u postgres /usr/bin/psql -d postgres -tAc \ + "$SUDO_BIN" -n -u postgres psql -d postgres -tAc \ "SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true )" @@ -126,7 +130,7 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE" fi - "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c \ + "$SUDO_BIN" -n -u postgres psql -d postgres -c \ "CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} PASSWORD '${PGPASSWORD//\'/\'\'}';" \ >/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}" diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh index e9f0d70..0bc61e8 100755 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -3,6 +3,9 @@ set -euo pipefail ############################################################################### # check-target-readiness.sh +# +# Prépare la machine cible pour permettre l'exécution non interactive du +# script de rebuild depuis une interface web. ############################################################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -116,6 +119,8 @@ validate_env_values() { [[ -n "$BACKUP_REMOTE_HOST" ]] || fail "BACKUP_REMOTE_HOST vide" [[ -n "$BACKUP_REMOTE_USER" ]] || fail "BACKUP_REMOTE_USER vide" [[ -n "$BACKUP_REMOTE_DIR" ]] || fail "BACKUP_REMOTE_DIR vide" + BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}" + [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide" } prepare_log_file() { @@ -170,32 +175,26 @@ prepare_ssh_key() { mkdir -p "$key_dir" || fail "impossible de créer le dossier SSH : $key_dir" chmod 700 "$key_dir" || fail "impossible de chmod 700 sur $key_dir" - if [[ ! -f "$SSH_KEY" ]]; then - log "Clé SSH absente : génération de ${SSH_KEY}" - ssh-keygen -t ed25519 -N "" -f "$SSH_KEY" >/dev/null 2>&1 || \ - fail "impossible de générer la clé SSH" - fi - + [[ -f "$SSH_KEY" ]] || fail "clé SSH absente : $SSH_KEY" chmod 600 "$SSH_KEY" || fail "impossible de chmod 600 sur la clé privée" - [[ -f "${SSH_KEY}.pub" ]] || fail "clé publique absente : ${SSH_KEY}.pub" - chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" + [[ -f "${SSH_KEY}.pub" ]] || log "clé publique absente : ${SSH_KEY}.pub" + [[ ! -f "${SSH_KEY}.pub" ]] || chmod 644 "${SSH_KEY}.pub" || fail "impossible de chmod 644 sur la clé publique" log "Clé SSH prête." } prepare_known_hosts() { - local ssh_dir known_hosts ssh_port + local ssh_dir known_hosts ssh_dir="$(dirname "$SSH_KEY")" known_hosts="${ssh_dir}/known_hosts" - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" touch "$known_hosts" || fail "impossible de créer known_hosts" chmod 644 "$known_hosts" || fail "impossible de chmod 644 sur known_hosts" if ! ssh-keygen -F "$BACKUP_REMOTE_HOST" -f "$known_hosts" >/dev/null 2>&1; then - log "Ajout de ${BACKUP_REMOTE_HOST}:${ssh_port} à known_hosts" - ssh-keyscan -p "$ssh_port" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ + log "Ajout de ${BACKUP_REMOTE_HOST}:${BACKUP_REMOTE_SSH_PORT} à known_hosts" + ssh-keyscan -p "$BACKUP_REMOTE_SSH_PORT" -H "$BACKUP_REMOTE_HOST" >>"$known_hosts" 2>/dev/null || \ fail "échec de récupération de la clé hôte pour ${BACKUP_REMOTE_HOST}" else log "Host déjà présent dans known_hosts." @@ -203,13 +202,12 @@ prepare_known_hosts() { } test_backup_ssh() { - local ssh_port ssh_timeout - ssh_port="${BACKUP_REMOTE_SSH_PORT:-22}" + local ssh_timeout ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" ssh \ -i "$SSH_KEY" \ - -p "$ssh_port" \ + -p "$BACKUP_REMOTE_SSH_PORT" \ -o IdentitiesOnly=yes \ -o BatchMode=yes \ -o ConnectTimeout="$ssh_timeout" \ @@ -231,7 +229,7 @@ install_sudoers_if_allowed() { return 0 fi - if ! sudo -n /usr/bin/true >/dev/null 2>&1; then + if ! sudo -n true >/dev/null 2>&1; then fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo -n n'est pas disponible ; configuration initiale manuelle requise" fi @@ -241,8 +239,8 @@ install_sudoers_if_allowed() { tmp_file="$(mktemp)" cat >"$tmp_file" </dev/null 2>&1 || \ - fail "sudo non interactif root indisponible pour ${USER}" - - sudo -n -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || \ - fail "sudo -n -u postgres indisponible pour psql pour ${USER}" + sudo -n true >/dev/null 2>&1 || \ + fail "sudo non interactif indisponible pour ${USER}" log "sudo non interactif validé." } @@ -283,6 +278,7 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" + sudo -n -u postgres true >/dev/null 2>&1 || fail "sudo -n -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 851d587..4abae51 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -102,6 +102,38 @@ cleanup() { } trap cleanup EXIT +copy_file_to_remote_via_ssh() { + local local_file="$1" + local remote_final_path="$2" + local remote_mode="$3" + local remote_parent + local remote_tmp + + [[ -f "$local_file" ]] || fail "fichier source introuvable : $local_file" + [[ -r "$local_file" ]] || fail "fichier source non lisible : $local_file" + + remote_parent="$(dirname "$remote_final_path")" + remote_tmp="/tmp/bootstrap_copy.$$.$RANDOM.tmp" + + ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + mkdir -p $(shell_quote "$remote_parent") + test -d $(shell_quote "$remote_parent") + test -w $(shell_quote "$remote_parent") + " >/dev/null 2>&1 || fail "dossier distant absent ou non inscriptible : $remote_parent" + + ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + cat > $(shell_quote "$remote_tmp") + " < "$local_file" >/dev/null 2>&1 || fail "échec d'écriture temporaire distante : $remote_tmp" + + ssh "${SSH_OPTS[@]}" "$REMOTE" " + set -euo pipefail + install -m $(shell_quote "$remote_mode") $(shell_quote "$remote_tmp") $(shell_quote "$remote_final_path") + rm -f $(shell_quote "$remote_tmp") + " >/dev/null 2>&1 || fail "échec d'installation distante : $remote_final_path" +} + TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}" [[ -n "$TARGET_NAME" ]] || fail "target manquante" @@ -199,7 +231,6 @@ to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE" >/dev/null || f ALLOW_PASSWORDLESS_SUDO="$(to_bool_yes_no "$TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE")" require_cmd ssh -require_cmd scp require_cmd python3 SSH_OPTS=( @@ -291,24 +322,17 @@ EXCLUDED_RESTORE_ROLES=$(printf '%s\n' "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE") EOF log "Copie du .env cible" -scp "${SSH_OPTS[@]}" "$TMP_ENV_FILE" "${REMOTE}:$(printf '%q' "$TARGET_ENV_FILE_PATH")" >/dev/null 2>&1 \ - || fail "échec de copie du .env cible" +copy_file_to_remote_via_ssh "$TMP_ENV_FILE" "$TARGET_ENV_FILE_PATH" "600" REMOTE_SSH_DIR="$(dirname "$TARGET_SSH_KEY_VALUE")" REMOTE_KNOWN_HOSTS="${REMOTE_SSH_DIR}/known_hosts" log "Copie de la clé privée backup sur la cible" -scp "${SSH_OPTS[@]}" \ - "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" \ - "${REMOTE}:$(printf '%q' "$TARGET_SSH_KEY_VALUE")" >/dev/null 2>&1 \ - || fail "échec de copie de la clé privée backup" +copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PRIVATE_KEY_VALUE" "$TARGET_SSH_KEY_VALUE" "600" if [[ -n "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" ]]; then log "Copie de la clé publique backup sur la cible" - scp "${SSH_OPTS[@]}" \ - "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" \ - "${REMOTE}:$(printf '%q' "${TARGET_SSH_KEY_VALUE}.pub")" >/dev/null 2>&1 \ - || fail "échec de copie de la clé publique backup" + copy_file_to_remote_via_ssh "$TARGET_BACKUP_SOURCE_SSH_PUBLIC_KEY_VALUE" "${TARGET_SSH_KEY_VALUE}.pub" "644" fi REMOTE_SSH_PERMS_CMD=" @@ -397,8 +421,8 @@ fi TMP_SUDOERS_FILE=\$(mktemp) cat >\"\$TMP_SUDOERS_FILE\" </dev/null 2>&1 || { echo 'sudo absent sur la cible' >&2 exit 1 } -sudo -n /usr/bin/true >/dev/null 2>&1 || { - echo 'sudo -n root indisponible pour /usr/bin/true' >&2 +sudo -n /usr/bin/systemctl --version >/dev/null 2>&1 || { + echo 'sudo -n indisponible pour systemctl' >&2 exit 1 } " @@ -476,7 +500,7 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" set -euo pipefail sudo -n -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || { - echo 'sudo -n -u postgres indisponible pour psql' >&2 + echo 'sudo -n -u postgres indisponible après préparation PostgreSQL' >&2 exit 1 } " From e5b15426a1c718ce445c1694710277aacbb1a231 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:35:40 +0100 Subject: [PATCH 12/33] feat : repertorie bug(WIP) --- RebuildBdd/bootstrap-target-host.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 4abae51..339a701 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -460,6 +460,18 @@ chmod 700 $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") 2>/dev/null || tr chmod 700 $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") 2>/dev/null || true chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true + +for required_file in \ + $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") \ + $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") \ + $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") \ + $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh"); do + if [[ ! -f \"\$required_file\" ]]; then + echo \"fichier requis absent après synchronisation du dépôt : \$required_file\" >&2 + echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2 + exit 1 + fi +done " log "Clone / mise à jour du dépôt distant" @@ -488,6 +500,11 @@ set -euo pipefail CHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-postgresql.sh") ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH") +[[ -f \"\$CHECK_SCRIPT\" ]] || { + echo \"script PostgreSQL introuvable : \$CHECK_SCRIPT\" >&2 + echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR")\" >&2 + exit 1 +} [[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\" \"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive @@ -509,4 +526,4 @@ log "Validation finale de sudo -n -u postgres" ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \ || fail "sudo -n -u postgres invalide sur la cible" -success "bootstrap initial terminé pour ${TARGET_NAME}" \ No newline at end of file +success "bootstrap initial terminé pour ${TARGET_NAME}" From 01ac392fa90c22630afe1c958a80115589bb20f3 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:38:17 +0100 Subject: [PATCH 13/33] feat : repertorie bug(WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 26 ++++++++++---------- RebuildBdd/Checkup/check-target-readiness.sh | 14 +++++------ RebuildBdd/bootstrap-target-host.sh | 24 +++++++++--------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index faac0a0..ab9b0d1 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -70,12 +70,12 @@ if ! require_cmd "$SUDO_BIN"; then fail "sudo absent sur la cible" fi -if ! "$SUDO_BIN" -n /usr/bin/systemctl --version >/dev/null 2>&1; then - fail "sudo non interactif indisponible pour systemctl" +if ! "$SUDO_BIN" /usr/bin/systemctl --version >/dev/null 2>&1; then + fail "sudo indisponible pour systemctl" fi -if ! "$SUDO_BIN" -n -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then - fail "sudo -n -u postgres indisponible" +if ! "$SUDO_BIN" -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + fail "sudo -u postgres indisponible" fi if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then @@ -88,37 +88,37 @@ if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! [[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no" log "PostgreSQL absent : installation en cours..." - "$SUDO_BIN" -n apt update >/dev/null 2>&1 || fail "échec de apt update" - "$SUDO_BIN" -n apt install -y $POSTGRES_PACKAGE_LIST >/dev/null 2>&1 || fail "échec de l'installation PostgreSQL" + "$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" -n systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then +if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then log "Démarrage du service PostgreSQL..." - "$SUDO_BIN" -n systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1 || fail "impossible de démarrer 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" -n -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + 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" -n -u postgres psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then +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" -n -u postgres psql -d postgres -tAc \ + "$SUDO_BIN" -u postgres psql -d postgres -tAc \ "SELECT 1 FROM pg_roles WHERE rolname='${PGUSER//\'/\'\'}'" 2>/dev/null || true )" @@ -130,7 +130,7 @@ if [[ "${AUTO_CREATE_PGUSER,,}" == "yes" ]]; then ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE" fi - "$SUDO_BIN" -n -u postgres psql -d postgres -c \ + "$SUDO_BIN" -u postgres psql -d postgres -c \ "CREATE ROLE \"${PGUSER}\" WITH ${ROLE_ATTRIBUTES} PASSWORD '${PGPASSWORD//\'/\'\'}';" \ >/dev/null 2>&1 || fail "échec de création du rôle ${PGUSER}" @@ -144,4 +144,4 @@ if ! psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -c "SELECT 1;" >/de fail "connexion PostgreSQL locale impossible avec PGUSER=${PGUSER}" fi -log "Check PostgreSQL terminé avec succès." \ No newline at end of file +log "Check PostgreSQL terminé avec succès." diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh index 0bc61e8..3e9ebc6 100755 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -229,8 +229,8 @@ install_sudoers_if_allowed() { return 0 fi - if ! sudo -n true >/dev/null 2>&1; then - fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo -n n'est pas disponible ; configuration initiale manuelle requise" + if ! sudo true >/dev/null 2>&1; then + fail "AUTO_CONFIGURE_SUDOERS=yes mais sudo n'est pas disponible ; configuration initiale manuelle requise" fi require_cmd visudo @@ -260,10 +260,10 @@ EOF } check_sudo_non_interactive() { - sudo -n true >/dev/null 2>&1 || \ - fail "sudo non interactif indisponible pour ${USER}" + sudo true >/dev/null 2>&1 || \ + fail "sudo indisponible pour ${USER}" - log "sudo non interactif validé." + log "sudo validé." } run_postgresql_check() { @@ -278,7 +278,7 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" - sudo -n -u postgres true >/dev/null 2>&1 || fail "sudo -n -u postgres indisponible après préparation PostgreSQL" + sudo -u postgres true >/dev/null 2>&1 || fail "sudo -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } @@ -315,4 +315,4 @@ check_sudo_non_interactive run_postgresql_check log "Machine cible prête pour le rebuild." -print_json_and_exit "success" "machine cible prête" 0 \ No newline at end of file +print_json_and_exit "success" "machine cible prête" 0 diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 339a701..82b429a 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -260,8 +260,8 @@ run_root() { fi if command -v sudo >/dev/null 2>&1; then - sudo -n \"\$@\" || { - echo 'sudo -n indisponible pour le bootstrap' >&2 + sudo \"\$@\" || { + echo 'sudo indisponible pour le bootstrap' >&2 exit 1 } return 0 @@ -403,8 +403,8 @@ run_root() { fi if command -v sudo >/dev/null 2>&1; then - sudo -n \"\$@\" || { - echo 'sudo -n indisponible pour installer sudoers' >&2 + sudo \"\$@\" || { + echo 'sudo indisponible pour installer sudoers' >&2 exit 1 } return 0 @@ -484,15 +484,15 @@ command -v sudo >/dev/null 2>&1 || { echo 'sudo absent sur la cible' >&2 exit 1 } -sudo -n /usr/bin/systemctl --version >/dev/null 2>&1 || { - echo 'sudo -n indisponible pour systemctl' >&2 +sudo /usr/bin/systemctl --version >/dev/null 2>&1 || { + echo 'sudo indisponible pour systemctl' >&2 exit 1 } " -log "Validation initiale de sudo -n" +log "Validation initiale de sudo" ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \ - || fail "sudo -n invalide sur la cible" + || fail "sudo invalide sur la cible" REMOTE_RUN_CHECK_PG_CMD=" set -euo pipefail @@ -516,14 +516,14 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" set -euo pipefail -sudo -n -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || { - echo 'sudo -n -u postgres indisponible après préparation PostgreSQL' >&2 +sudo -u postgres /usr/bin/psql -d postgres -c 'SELECT 1;' >/dev/null 2>&1 || { + echo 'sudo -u postgres indisponible après préparation PostgreSQL' >&2 exit 1 } " -log "Validation finale de sudo -n -u postgres" +log "Validation finale de sudo -u postgres" ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \ - || fail "sudo -n -u postgres invalide sur la cible" + || fail "sudo -u postgres invalide sur la cible" success "bootstrap initial terminé pour ${TARGET_NAME}" From 38b29796d38bdd020b515ccb29109d9b5853b088 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:41:29 +0100 Subject: [PATCH 14/33] feat : repertorie bug(WIP) --- RebuildBdd/bootstrap-target-host.sh | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 82b429a..94938bb 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -393,6 +393,17 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_BACKUP_TEST_CMD" \ || fail "la cible ne peut pas accéder au serveur de backup avec la clé fournie" if [[ "$ALLOW_PASSWORDLESS_SUDO" == "yes" ]]; then + REMOTE_SUDOERS_PRECHECK_CMD=" +set -euo pipefail + +if [ \"\$(id -u)\" -eq 0 ]; then + exit 0 +fi + +command -v sudo >/dev/null 2>&1 || exit 1 +sudo true /dev/null 2>&1 +" + REMOTE_SUDOERS_CMD=" set -euo pipefail @@ -435,13 +446,16 @@ visudo -cf \"\$TMP_SUDOERS_FILE\" >/dev/null 2>&1 || { run_root install -m 440 \"\$TMP_SUDOERS_FILE\" /etc/sudoers.d/rebuild-bdd-${TARGET_RUNTIME_USER_VALUE} rm -f \"\$TMP_SUDOERS_FILE\" -" + " - log "Installation du sudoers non interactif minimal" - ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" \ - || fail "échec d'installation du sudoers non interactif" + log "Installation du sudoers minimal" + if ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_PRECHECK_CMD" >/dev/null 2>&1; then + log "Installation du sudoers ignorée : élévation de privilèges indisponible sans interaction." + elif ! ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SUDOERS_CMD" >/dev/null 2>&1; then + log "Installation du sudoers ignorée : privilèges root/sudo insuffisants pour cette étape." + fi else - log "Installation du sudoers non interactif désactivée." + log "Installation du sudoers minimal désactivée." fi REMOTE_REPO_CMD=" From fbefe3fb03d58225c8c58793db2b39de289eca9f Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:45:25 +0100 Subject: [PATCH 15/33] feat : repertorie bug (WIP) --- RebuildBdd/bootstrap-target-host.sh | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 94938bb..64379f3 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -5,6 +5,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_DIR="${SCRIPT_DIR}/Config" GLOBAL_ENV_FILE_DEFAULT="${CONFIG_DIR}/global.env" TARGETS_DIR_DEFAULT="${CONFIG_DIR}/Targets" +GIT_TOPLEVEL="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)" +LOCAL_REPO_SUBDIR_DEFAULT="" + +if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then + LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}" +fi GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" @@ -157,6 +163,7 @@ BOOTSTRAP_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-}}" TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}" TARGET_ENV_FILE_PATH="${TARGET_ENV_FILE:-}" TARGET_ENV_NAME_VALUE="${TARGET_ENV_NAME:-}" @@ -201,6 +208,19 @@ TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO_VALUE="${TARGET_BOOTSTRAP_ALLOW_PASSWOR [[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" [[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE manquante" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}" + +TARGET_CLONE_DIR="$TARGET_REPO_DIR" +TARGET_SCRIPT_DIR="$TARGET_REPO_DIR" +if [[ -n "$TARGET_REPO_SUBDIR" ]]; then + if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then + TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")" + else + TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}" + fi +fi + [[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME manquante" [[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST/GLOBAL_PGHOST manquant" [[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT/GLOBAL_PGPORT manquant" @@ -279,7 +299,8 @@ fi run_root apt-get update run_root apt-get install -y bash git python3 sudo curl openssh-client ca-certificates postgresql-client -mkdir -p $(shell_quote "$(dirname "$TARGET_REPO_DIR")") +mkdir -p $(shell_quote "$(dirname "$TARGET_CLONE_DIR")") +mkdir -p $(shell_quote "$(dirname "$TARGET_SCRIPT_DIR")") mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")") mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE") mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE") @@ -461,28 +482,28 @@ fi REMOTE_REPO_CMD=" set -euo pipefail -if [[ ! -d $(shell_quote "${TARGET_REPO_DIR}/.git") ]]; then - rm -rf $(shell_quote "$TARGET_REPO_DIR") - git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_REPO_DIR") +if [[ ! -d $(shell_quote "${TARGET_CLONE_DIR}/.git") ]]; then + rm -rf $(shell_quote "$TARGET_CLONE_DIR") + git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_CLONE_DIR") else - git -C $(shell_quote "$TARGET_REPO_DIR") fetch --prune origin - git -C $(shell_quote "$TARGET_REPO_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH") - git -C $(shell_quote "$TARGET_REPO_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH") + git -C $(shell_quote "$TARGET_CLONE_DIR") fetch --prune origin + git -C $(shell_quote "$TARGET_CLONE_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH") + git -C $(shell_quote "$TARGET_CLONE_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH") fi -chmod 700 $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") 2>/dev/null || true -chmod 700 $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") 2>/dev/null || true -chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true -chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true +chmod 700 $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true for required_file in \ - $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") \ - $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") \ - $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") \ - $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh"); do + $(shell_quote "$TARGET_SCRIPT_DIR/run-rebuild-bdd.sh") \ + $(shell_quote "$TARGET_SCRIPT_DIR/rebuild-bdd-core.sh") \ + $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-postgresql.sh") \ + $(shell_quote "$TARGET_SCRIPT_DIR/Checkup/check-target-readiness.sh"); do if [[ ! -f \"\$required_file\" ]]; then echo \"fichier requis absent après synchronisation du dépôt : \$required_file\" >&2 - echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2 + echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR"), TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR"), TARGET_REPO_URL=$(shell_quote "$TARGET_REPO_URL"), TARGET_REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH")\" >&2 exit 1 fi done @@ -511,12 +532,12 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \ REMOTE_RUN_CHECK_PG_CMD=" set -euo pipefail -CHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-postgresql.sh") +CHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-postgresql.sh") ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH") [[ -f \"\$CHECK_SCRIPT\" ]] || { echo \"script PostgreSQL introuvable : \$CHECK_SCRIPT\" >&2 - echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR")\" >&2 + echo \"vérifier TARGET_REPO_DIR=$(shell_quote "$TARGET_REPO_DIR") et TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR")\" >&2 exit 1 } [[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\" From 12bbe6b1d91b2a23107720207ed05e7f59339ff4 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:49:44 +0100 Subject: [PATCH 16/33] feat : repertorie bug (WIP) --- RebuildBdd/bootstrap-target-host.sh | 38 ++++++++++----------- RebuildBdd/create-target-config.sh | 52 ++++++++++++++--------------- RebuildBdd/run-rebuild-bdd.sh | 23 ++++++++++--- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 64379f3..72e69e5 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -318,28 +318,28 @@ ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ TMP_ENV_FILE="$(mktemp)" cat >"$TMP_ENV_FILE" <"$TARGET_FILE" </dev/null || true)" +LOCAL_REPO_SUBDIR_DEFAULT="" + +if [[ -n "$GIT_TOPLEVEL" && "$SCRIPT_DIR" == "$GIT_TOPLEVEL"/* ]]; then + LOCAL_REPO_SUBDIR_DEFAULT="${SCRIPT_DIR#"$GIT_TOPLEVEL"/}" +fi GLOBAL_ENV_FILE="${GLOBAL_ENV_FILE:-$GLOBAL_ENV_FILE_DEFAULT}" TARGETS_DIR="${TARGETS_DIR:-$TARGETS_DIR_DEFAULT}" @@ -160,6 +166,7 @@ TARGET_SSH_KEY="${TARGET_BOOTSTRAP_SSH_KEY:-}" TARGET_REPO_URL="${TARGET_REPO_URL:-${GLOBAL_REPO_URL:-}}" TARGET_REPO_BRANCH="${TARGET_REPO_BRANCH:-${GLOBAL_REPO_BRANCH:-main}}" TARGET_REPO_DIR="${TARGET_REPO_DIR:-}" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR:-$LOCAL_REPO_SUBDIR_DEFAULT}" TARGET_ENV_FILE="${TARGET_ENV_FILE:-}" TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-yes}}" @@ -175,6 +182,14 @@ TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-y [[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR manquante" [[ -n "$TARGET_ENV_FILE" ]] || fail "TARGET_ENV_FILE manquante" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}" +TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}" + +TARGET_SCRIPT_DIR="$TARGET_REPO_DIR" +if [[ -n "$TARGET_REPO_SUBDIR" && "$TARGET_REPO_DIR" != */"$TARGET_REPO_SUBDIR" ]]; then + TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}" +fi + TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide" BOOTSTRAP_SCRIPT_LOCAL="${SCRIPT_DIR}/bootstrap-target-host.sh" @@ -248,16 +263,16 @@ SSH_OPTS=( ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \ || fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}" -TARGET_CORE_SCRIPT="${TARGET_REPO_DIR}/rebuild-bdd-core.sh" +TARGET_CORE_SCRIPT="${TARGET_SCRIPT_DIR}/rebuild-bdd-core.sh" REMOTE_BOOTSTRAP_CMD=" set -euo pipefail -REPO_DIR=$(shell_quote "$TARGET_REPO_DIR") +REPO_DIR=$(shell_quote "$TARGET_SCRIPT_DIR") REPO_URL=$(shell_quote "$TARGET_REPO_URL") REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH") CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT") -PRECHECK_SCRIPT=$(shell_quote "${TARGET_REPO_DIR}/Checkup/check-target-readiness.sh") +PRECHECK_SCRIPT=$(shell_quote "${TARGET_SCRIPT_DIR}/Checkup/check-target-readiness.sh") TARGET_ENV_FILE=$(shell_quote "$TARGET_ENV_FILE") REQUESTED_DB=$(shell_quote "$REQUESTED_DB") ALLOW_OVERWRITE=$(shell_quote "$ALLOW_OVERWRITE") @@ -319,4 +334,4 @@ exec \"\$CORE_SCRIPT\" \ --json-only " -ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" \ No newline at end of file +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" From 0ff3244c1d79bf0ae578da2c3f79e1b58e349099 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:51:45 +0100 Subject: [PATCH 17/33] feat : repertorie bug (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index ab9b0d1..b60da49 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -74,10 +74,6 @@ if ! "$SUDO_BIN" /usr/bin/systemctl --version >/dev/null 2>&1; then fail "sudo indisponible pour systemctl" fi -if ! "$SUDO_BIN" -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1; then - fail "sudo -u postgres indisponible" -fi - if [[ ! "$PGPORT" =~ ^[0-9]+$ ]]; then fail "PGPORT invalide : $PGPORT" fi From b3de87a452b087633867ad49fa49e2d451131642 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:53:43 +0100 Subject: [PATCH 18/33] feat : preparation de postgresql (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index b60da49..a6e4428 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -45,6 +45,28 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 } +start_postgres_service() { + if "$SUDO_BIN" systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1; then + return 0 + fi + + if require_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 + local version cluster + while read -r version cluster _; do + [[ -n "$version" && -n "$cluster" ]] || continue + if "$SUDO_BIN" pg_ctlcluster "$version" "$cluster" start >/dev/null 2>&1; then + return 0 + fi + done < <(pg_lsclusters --no-header 2>/dev/null || true) + fi + + return 1 +} + [[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE" set -a @@ -94,7 +116,7 @@ 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" + start_postgres_service || fail "impossible de démarrer PostgreSQL" else log "Service PostgreSQL déjà actif." fi From 29371b65295d1017277e63136ca3629e5e036c0e Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:55:33 +0100 Subject: [PATCH 19/33] feat : preparation de postgresql (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index a6e4428..3473922 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -45,6 +45,26 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 } +collect_postgres_diagnostics() { + local 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 + 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 + diagnostics+="pg_lsclusters: $(pg_lsclusters --no-header 2>/dev/null | tr '\n' ' '); " + fi + + if require_cmd journalctl; then + diagnostics+="journalctl: $( "$SUDO_BIN" journalctl -u "$POSTGRES_SERVICE_NAME" -n 10 --no-pager 2>/dev/null | tr '\n' ' ' ); " + fi + + printf '%s' "${diagnostics% }" +} + start_postgres_service() { if "$SUDO_BIN" systemctl start "$POSTGRES_SERVICE_NAME" >/dev/null 2>&1; then return 0 @@ -116,7 +136,9 @@ fi if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then log "Démarrage du service PostgreSQL..." - start_postgres_service || fail "impossible de démarrer PostgreSQL" + if ! start_postgres_service; then + fail "impossible de démarrer PostgreSQL. $(collect_postgres_diagnostics)" + fi else log "Service PostgreSQL déjà actif." fi From 447b04ce2011ef89aab5b481969a694644b89d98 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:57:18 +0100 Subject: [PATCH 20/33] feat : preparation de postgresql (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 3473922..33ae3e6 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -45,6 +45,31 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 } +ensure_postgres_cluster() { + if ! require_cmd pg_lsclusters || ! require_cmd pg_createcluster; then + return 0 + fi + + if pg_lsclusters --no-header 2>/dev/null | grep -q .; then + return 0 + fi + + local version="" + if [[ -d /etc/postgresql ]]; then + 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 + version="$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)" + fi + + [[ -n "$version" ]] || return 1 + + log "Aucun cluster PostgreSQL détecté, création de ${version}/main..." + "$SUDO_BIN" pg_createcluster "$version" main --start >/dev/null 2>&1 || return 1 + return 0 +} + collect_postgres_diagnostics() { local diagnostics="" @@ -134,6 +159,8 @@ else log "PostgreSQL déjà installé." fi +ensure_postgres_cluster || fail "aucun cluster PostgreSQL disponible et création automatique impossible" + if ! "$SUDO_BIN" systemctl is-active --quiet "$POSTGRES_SERVICE_NAME"; then log "Démarrage du service PostgreSQL..." if ! start_postgres_service; then From 6c61f6e543f0ddb9bbe715b20714eea27a0b79b7 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 15:59:32 +0100 Subject: [PATCH 21/33] feat : preparation de postgresql (WIP) --- RebuildBdd/Checkup/check-postgresql.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/RebuildBdd/Checkup/check-postgresql.sh b/RebuildBdd/Checkup/check-postgresql.sh index 33ae3e6..e28de4b 100755 --- a/RebuildBdd/Checkup/check-postgresql.sh +++ b/RebuildBdd/Checkup/check-postgresql.sh @@ -45,6 +45,13 @@ require_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 + return 0 +} + ensure_postgres_cluster() { if ! require_cmd pg_lsclusters || ! require_cmd pg_createcluster; then return 0 @@ -147,7 +154,7 @@ fi POSTGRES_INSTALLED="no" -if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb; then +if ! require_cmd psql || ! require_cmd pg_restore || ! require_cmd createdb || ! require_cmd dropdb || ! postgres_server_ready; then [[ "${AUTO_INSTALL_POSTGRES,,}" == "yes" ]] || fail "PostgreSQL absent et AUTO_INSTALL_POSTGRES=no" log "PostgreSQL absent : installation en cours..." From 5b128bc81a1d66b5df82ee5ae468266c163080a3 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:03:19 +0100 Subject: [PATCH 22/33] feat : preparation de postgresql (WIP) --- RebuildBdd/bootstrap-target-host.sh | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/RebuildBdd/bootstrap-target-host.sh b/RebuildBdd/bootstrap-target-host.sh index 72e69e5..0504b18 100755 --- a/RebuildBdd/bootstrap-target-host.sh +++ b/RebuildBdd/bootstrap-target-host.sh @@ -312,8 +312,13 @@ chmod 644 $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")/known_hosts") || tru " log "Installation du socle minimal sur la cible" -ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ - || fail "échec de préparation système distante" +if [[ "$JSON_ONLY" == "yes" ]]; then + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" >/dev/null \ + || fail "échec de préparation système distante" +else + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \ + || fail "échec de préparation système distante" +fi TMP_ENV_FILE="$(mktemp)" @@ -510,8 +515,13 @@ done " log "Clone / mise à jour du dépôt distant" -ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \ - || fail "échec de synchronisation du dépôt sur la cible" +if [[ "$JSON_ONLY" == "yes" ]]; then + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" >/dev/null \ + || fail "échec de synchronisation du dépôt sur la cible" +else + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \ + || fail "échec de synchronisation du dépôt sur la cible" +fi REMOTE_VALIDATE_SUDO_ROOT_CMD=" set -euo pipefail @@ -546,8 +556,13 @@ ENV_FILE=$(shell_quote "$TARGET_ENV_FILE_PATH") " log "Préparation PostgreSQL via check-postgresql.sh" -ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ - || fail "échec de préparation PostgreSQL pendant le bootstrap" +if [[ "$JSON_ONLY" == "yes" ]]; then + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" >/dev/null \ + || fail "échec de préparation PostgreSQL pendant le bootstrap" +else + ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_RUN_CHECK_PG_CMD" \ + || fail "échec de préparation PostgreSQL pendant le bootstrap" +fi REMOTE_VALIDATE_SUDO_POSTGRES_CMD=" set -euo pipefail From cb94e744141975f8b6916ff6ee30324a20dcf35d Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:05:26 +0100 Subject: [PATCH 23/33] feat : preparation de postgresql (WIP) --- RebuildBdd/run-rebuild-bdd.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index d06a0bd..e050fb1 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -185,9 +185,14 @@ TARGET_ENABLE_BOOTSTRAP="${TARGET_ENABLE_BOOTSTRAP:-${GLOBAL_ENABLE_BOOTSTRAP:-y TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR#/}" TARGET_REPO_SUBDIR="${TARGET_REPO_SUBDIR%/}" +TARGET_CLONE_DIR="$TARGET_REPO_DIR" TARGET_SCRIPT_DIR="$TARGET_REPO_DIR" -if [[ -n "$TARGET_REPO_SUBDIR" && "$TARGET_REPO_DIR" != */"$TARGET_REPO_SUBDIR" ]]; then - TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}" +if [[ -n "$TARGET_REPO_SUBDIR" ]]; then + if [[ "$TARGET_REPO_DIR" == */"$TARGET_REPO_SUBDIR" ]]; then + TARGET_CLONE_DIR="$(dirname "$TARGET_REPO_DIR")" + else + TARGET_SCRIPT_DIR="${TARGET_REPO_DIR}/${TARGET_REPO_SUBDIR}" + fi fi TARGET_ENABLE_BOOTSTRAP="$(to_bool_yes_no "$TARGET_ENABLE_BOOTSTRAP")" || fail "TARGET_ENABLE_BOOTSTRAP invalide" @@ -268,6 +273,7 @@ TARGET_CORE_SCRIPT="${TARGET_SCRIPT_DIR}/rebuild-bdd-core.sh" REMOTE_BOOTSTRAP_CMD=" set -euo pipefail +CLONE_DIR=$(shell_quote "$TARGET_CLONE_DIR") REPO_DIR=$(shell_quote "$TARGET_SCRIPT_DIR") REPO_URL=$(shell_quote "$TARGET_REPO_URL") REPO_BRANCH=$(shell_quote "$TARGET_REPO_BRANCH") @@ -283,15 +289,16 @@ command -v git >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"gi command -v bash >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"bash absent sur la cible\"}'; exit 1; } command -v python3 >/dev/null 2>&1 || { echo '{\"status\":\"error\",\"message\":\"python3 absent sur la cible\"}'; exit 1; } +mkdir -p \"\$(dirname \"\$CLONE_DIR\")\" 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\" +if [[ ! -d \"\$CLONE_DIR/.git\" ]]; then + rm -rf \"\$CLONE_DIR\" + git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_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\" + git -C \"\$CLONE_DIR\" fetch --prune origin + git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" + git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" fi [[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; } From f12c937b394d8e2b9d97c160bbd68931fbff3756 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:09:00 +0100 Subject: [PATCH 24/33] feat : correctif mineur --- RebuildBdd/run-rebuild-bdd.sh | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index e050fb1..1ba8481 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -102,7 +102,7 @@ shell_quote() { } cleanup() { - rm -f "${BOOTSTRAP_JSON:-}" + rm -f "${BOOTSTRAP_JSON:-}" "${REMOTE_RESULT_JSON:-}" } trap cleanup EXIT @@ -269,6 +269,7 @@ ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "exit 0" >/dev/null 2>&1 \ || fail "connexion SSH impossible vers la cible ${TARGET_USER}@${TARGET_HOST}" TARGET_CORE_SCRIPT="${TARGET_SCRIPT_DIR}/rebuild-bdd-core.sh" +REMOTE_RESULT_JSON="/tmp/run_rebuild_bdd_${REQUEST_ID}.json" REMOTE_BOOTSTRAP_CMD=" set -euo pipefail @@ -341,4 +342,42 @@ exec \"\$CORE_SCRIPT\" \ --json-only " -ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" \ + || { + cat "$REMOTE_RESULT_JSON" 2>/dev/null || true + fail "échec d'exécution distante sur la cible ${TARGET}" + } + +REMOTE_STATUS="$( + python3 - <<'PY' "$REMOTE_RESULT_JSON" +import json, sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) +print(data.get("status", "error")) +PY +)" + +if [[ "$REMOTE_STATUS" != "success" ]]; then + cat "$REMOTE_RESULT_JSON" + fail "restauration distante échouée pour la cible ${TARGET}" +fi + +python3 - <<'PY' "$REMOTE_RESULT_JSON" +import json, sys + +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) + +message = data.get("message", "restauration terminée") +environment = data.get("environment") or "N/A" +database = data.get("database") or "N/A" +request_id = data.get("request_id") or "N/A" +dump_file = data.get("dump_file") or "N/A" +log_file = data.get("log_file") or "N/A" + +print(f"[{request_id}] {message}") +print(f"Environnement : {environment}") +print(f"Base : {database}") +print(f"Dump : {dump_file}") +print(f"Log : {log_file}") +PY From 6a99a8115f8f52fb3b135b8117211d9e3183793a Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:11:00 +0100 Subject: [PATCH 25/33] feat : correctif mineur --- RebuildBdd/run-rebuild-bdd.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index 1ba8481..cf7242f 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -295,11 +295,11 @@ mkdir -p \"\$(dirname \"\$REPO_DIR\")\" if [[ ! -d \"\$CLONE_DIR/.git\" ]]; then rm -rf \"\$CLONE_DIR\" - git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_DIR\" + git clone --branch \"\$REPO_BRANCH\" --single-branch \"\$REPO_URL\" \"\$CLONE_DIR\" >/dev/null 2>&1 else - git -C \"\$CLONE_DIR\" fetch --prune origin - git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" - git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" + git -C \"\$CLONE_DIR\" fetch --prune origin >/dev/null 2>&1 + git -C \"\$CLONE_DIR\" checkout -f \"\$REPO_BRANCH\" >/dev/null 2>&1 + git -C \"\$CLONE_DIR\" reset --hard \"origin/\$REPO_BRANCH\" >/dev/null 2>&1 fi [[ -f \"\$CORE_SCRIPT\" ]] || { echo '{\"status\":\"error\",\"message\":\"script core introuvable sur la cible\"}'; exit 1; } @@ -355,7 +355,10 @@ with open(sys.argv[1], 'r', encoding='utf-8') as f: data = json.load(f) print(data.get("status", "error")) PY -)" +)" || { + cat "$REMOTE_RESULT_JSON" 2>/dev/null || true + fail "réponse JSON invalide renvoyée par la cible ${TARGET}" +} if [[ "$REMOTE_STATUS" != "success" ]]; then cat "$REMOTE_RESULT_JSON" From 685a65e2d194aea4ea6af6c16f0b1721d0dacf74 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:12:46 +0100 Subject: [PATCH 26/33] feat : correctif mineur --- RebuildBdd/run-rebuild-bdd.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index cf7242f..e8a11c8 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -342,7 +342,7 @@ exec \"\$CORE_SCRIPT\" \ --json-only " -ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" \ +ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" 2>&1 \ || { cat "$REMOTE_RESULT_JSON" 2>/dev/null || true fail "échec d'exécution distante sur la cible ${TARGET}" From 3faf8ab71d24dd2a272b9fe2a1e5c519606e9223 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:19:20 +0100 Subject: [PATCH 27/33] feat : debbug more easy --- RebuildBdd/run-rebuild-bdd.sh | 54 +++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/RebuildBdd/run-rebuild-bdd.sh b/RebuildBdd/run-rebuild-bdd.sh index e8a11c8..b22687b 100755 --- a/RebuildBdd/run-rebuild-bdd.sh +++ b/RebuildBdd/run-rebuild-bdd.sh @@ -309,12 +309,20 @@ chmod 700 \"\$CORE_SCRIPT\" chmod 700 \"\$PRECHECK_SCRIPT\" PRECHECK_JSON=\"/tmp/check_target_\${REQUEST_ID}.json\" +PRECHECK_STDERR=\"/tmp/check_target_\${REQUEST_ID}.stderr\" +CORE_JSON=\"/tmp/rebuild_target_\${REQUEST_ID}.json\" +CORE_STDERR=\"/tmp/rebuild_target_\${REQUEST_ID}.stderr\" \"\$PRECHECK_SCRIPT\" \ --env-file \"\$TARGET_ENV_FILE\" \ --request-id \"\$REQUEST_ID\" \ --non-interactive \ - --json-only >\"\$PRECHECK_JSON\" + --json-only >\"\$PRECHECK_JSON\" 2>\"\$PRECHECK_STDERR\" || { + cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true + cat \"\$PRECHECK_JSON\" 2>/dev/null || true + rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\" + exit 1 + } PRECHECK_STATUS=\"\$(python3 - <<'PY' \"\$PRECHECK_JSON\" import json, sys @@ -322,24 +330,58 @@ with open(sys.argv[1], 'r', encoding='utf-8') as f: data = json.load(f) print(data.get('status', 'error')) PY -)\" +)\" || { + cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true + cat \"\$PRECHECK_JSON\" 2>/dev/null || true + rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\" + exit 1 +} if [[ \"\$PRECHECK_STATUS\" != \"success\" ]]; then + cat \"\$PRECHECK_STDERR\" >&2 2>/dev/null || true cat \"\$PRECHECK_JSON\" - rm -f \"\$PRECHECK_JSON\" + rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\" exit 1 fi -rm -f \"\$PRECHECK_JSON\" +rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\" -exec \"\$CORE_SCRIPT\" \ +\"\$CORE_SCRIPT\" \ --env-file \"\$TARGET_ENV_FILE\" \ --db \"\$REQUESTED_DB\" \ --overwrite \"\$ALLOW_OVERWRITE\" \ --restore-roles \"\$RESTORE_ROLES\" \ --request-id \"\$REQUEST_ID\" \ --non-interactive \ - --json-only + --json-only >\"\$CORE_JSON\" 2>\"\$CORE_STDERR\" || { + cat \"\$CORE_STDERR\" >&2 2>/dev/null || true + cat \"\$CORE_JSON\" 2>/dev/null || true + rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\" + exit 1 + } + +CORE_STATUS=\"\$(python3 - <<'PY' \"\$CORE_JSON\" +import json, sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + data = json.load(f) +print(data.get('status', 'error')) +PY +)\" || { + cat \"\$CORE_STDERR\" >&2 2>/dev/null || true + cat \"\$CORE_JSON\" 2>/dev/null || true + rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\" + exit 1 +} + +if [[ \"\$CORE_STATUS\" != \"success\" ]]; then + cat \"\$CORE_STDERR\" >&2 2>/dev/null || true + cat \"\$CORE_JSON\" + rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\" + exit 1 +fi + +cat \"\$CORE_JSON\" +rm -f \"\$CORE_JSON\" \"\$CORE_STDERR\" " ssh "${SSH_OPTS[@]}" "${TARGET_USER}@${TARGET_HOST}" "$REMOTE_BOOTSTRAP_CMD" >"$REMOTE_RESULT_JSON" 2>&1 \ From 41df83fe329b2a10e9800cc5c865a3cc6677b8ce Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:21:46 +0100 Subject: [PATCH 28/33] feat : debbug more easy --- RebuildBdd/Checkup/check-target-readiness.sh | 26 +++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/RebuildBdd/Checkup/check-target-readiness.sh b/RebuildBdd/Checkup/check-target-readiness.sh index 3e9ebc6..e44b378 100755 --- a/RebuildBdd/Checkup/check-target-readiness.sh +++ b/RebuildBdd/Checkup/check-target-readiness.sh @@ -260,10 +260,27 @@ EOF } check_sudo_non_interactive() { - sudo true >/dev/null 2>&1 || \ - fail "sudo indisponible pour ${USER}" + sudo /usr/bin/systemctl --version >/dev/null 2>&1 || \ + fail "sudo indisponible pour systemctl" - log "sudo validé." + log "sudo pour systemctl validé." + + if command -v apt >/dev/null 2>&1; then + sudo /usr/bin/apt --version >/dev/null 2>&1 || \ + fail "sudo indisponible pour apt" + log "sudo pour apt validé." + elif command -v apt-get >/dev/null 2>&1; then + sudo /usr/bin/apt-get --version >/dev/null 2>&1 || \ + fail "sudo indisponible pour apt-get" + log "sudo pour apt-get validé." + else + fail "ni apt ni apt-get disponibles sur la cible" + fi + + sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \ + fail "sudo -u postgres indisponible pour psql" + + log "sudo -u postgres pour psql validé." } run_postgresql_check() { @@ -278,7 +295,8 @@ run_postgresql_check() { --non-interactive \ >>"$LOG_FILE" 2>&1 || fail "échec de préparation PostgreSQL" - sudo -u postgres true >/dev/null 2>&1 || fail "sudo -u postgres indisponible après préparation PostgreSQL" + sudo -u postgres /usr/bin/psql -d postgres -c "SELECT 1;" >/dev/null 2>&1 || \ + fail "sudo -u postgres indisponible après préparation PostgreSQL" log "Préparation PostgreSQL validée." } From f6e66e7bff1de200d394138ac0e40e79fb17ac67 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:23:43 +0100 Subject: [PATCH 29/33] feat : debbug more easy --- RebuildBdd/rebuild-bdd-core.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index 964bfc9..9140fae 100755 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -98,6 +98,18 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 } +download_remote_file() { + local remote_path="$1" + local local_path="$2" + + if scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then + return 0 + fi + + log "Téléchargement scp standard échoué, tentative avec scp -O" + scp -O "${SSH_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1 +} + to_bool_yes_no() { local v="${1:-}" v="${v,,}" @@ -338,14 +350,14 @@ 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" +download_remote_file "$LAST_REMOTE_DB_DUMP" "$LOCAL_DB_DUMP_FILE" \ + || 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" + download_remote_file "$LAST_REMOTE_ROLES_FILE" "$LOCAL_ROLES_FILE" \ + || fail "échec téléchargement du fichier des rôles" fi DB_EXISTS="$( @@ -469,4 +481,4 @@ 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 +print_json_and_exit "success" "restauration terminée avec succès" 0 From 0ee0c1328afe380bb5d652da5272c65c4f6b7763 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:28:54 +0100 Subject: [PATCH 30/33] feat : bug telechargement --- RebuildBdd/rebuild-bdd-core.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index 9140fae..735e7be 100755 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -101,6 +101,10 @@ require_cmd() { download_remote_file() { local remote_path="$1" local local_path="$2" + local local_dir + + local_dir="$(dirname "$local_path")" + mkdir -p "$local_dir" || fail "impossible de créer le dossier local de restauration : $local_dir" if scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then return 0 From 26101f211214b3994140d6019961bdbfdb0befbc Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:31:56 +0100 Subject: [PATCH 31/33] feat : bug telechargement --- RebuildBdd/rebuild-bdd-core.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index 735e7be..0b49c7c 100755 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -106,12 +106,12 @@ download_remote_file() { local_dir="$(dirname "$local_path")" mkdir -p "$local_dir" || fail "impossible de créer le dossier local de restauration : $local_dir" - if scp "${SSH_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then + if scp "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1; then return 0 fi log "Téléchargement scp standard échoué, tentative avec scp -O" - scp -O "${SSH_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1 + scp -O "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 2>&1 } to_bool_yes_no() { @@ -272,6 +272,15 @@ SSH_OPTS=( -o StrictHostKeyChecking=yes ) +SCP_OPTS=( + -i "$SSH_KEY" + -P "$BACKUP_REMOTE_SSH_PORT" + -o IdentitiesOnly=yes + -o BatchMode=yes + -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" + -o StrictHostKeyChecking=yes +) + REMOTE_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" read -r -a DBS_ARRAY <<< "$DBS" From 66abdfca538d4e9216a8335985d90300bea4358e Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 17 Mar 2026 16:34:40 +0100 Subject: [PATCH 32/33] feat : correctif --- RebuildBdd/rebuild-bdd-core.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RebuildBdd/rebuild-bdd-core.sh b/RebuildBdd/rebuild-bdd-core.sh index 0b49c7c..fc80f98 100755 --- a/RebuildBdd/rebuild-bdd-core.sh +++ b/RebuildBdd/rebuild-bdd-core.sh @@ -409,6 +409,10 @@ if [[ -n "$LOCAL_ROLES_FILE" ]]; then cp "$LOCAL_ROLES_FILE" "$FILTERED_ROLES_FILE" fi + # Une exécution sous un rôle non superuser ne peut pas restaurer l'attribut + # SUPERUSER ; on ignore donc ces lignes pour laisser passer le reste. + 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" \ From 863fee91a9b17028b1c9673076c2c99c5b9f4a42 Mon Sep 17 00:00:00 2001 From: Matteo Date: Wed, 18 Mar 2026 11:37:53 +0100 Subject: [PATCH 33/33] fix : fichier executable --- RebuildBdd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RebuildBdd/README.md b/RebuildBdd/README.md index ed2ff86..102cf3c 100644 --- a/RebuildBdd/README.md +++ b/RebuildBdd/README.md @@ -549,7 +549,7 @@ Avant mise en production, tester au minimum : --overwrite yes \ --restore-roles yes \ --non-interactive -``` +``` ---