#!/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. ############################################################################### 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) # Flag accepté pour compatibilité avec les autres scripts du workflow. 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_FILE=/dev/stderr 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" ;; # Valeur vide traitée comme "no" pour conserver le comportement historique. 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" [[ "$ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] || fail "ENV_NAME 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" BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}" BACKUP_KNOWN_HOSTS_STRICT="${BACKUP_KNOWN_HOSTS_STRICT:-yes}" [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide" to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT" >/dev/null || fail "BACKUP_KNOWN_HOSTS_STRICT invalide" } 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" [[ -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" ]] || 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_dir="$(dirname "$SSH_KEY")" known_hosts="${ssh_dir}/known_hosts" 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 if [[ "$(to_bool_yes_no "$BACKUP_KNOWN_HOSTS_STRICT")" == "yes" ]]; then fail "hôte ${BACKUP_REMOTE_HOST} absent de known_hosts en mode strict ; provisionner l'empreinte manuellement" fi log "Ajout non strict 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." fi } test_backup_ssh() { local ssh_timeout ssh_timeout="${SSH_CONNECT_TIMEOUT:-8}" ssh \ -i "$SSH_KEY" \ -p "$BACKUP_REMOTE_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 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 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 /usr/bin/systemctl --version >/dev/null 2>&1 || \ fail "sudo indisponible pour systemctl" 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() { 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" 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." } [[ -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