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