#!/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"