Merge branch 'develop' into fix/correctif
This commit is contained in:
0
BackupVaultWarden/backup-vaultwarden.sh
Normal file → Executable file
0
BackupVaultWarden/backup-vaultwarden.sh
Normal file → Executable file
0
CheckStorage/check-storage.sh
Normal file → Executable file
0
CheckStorage/check-storage.sh
Normal file → Executable file
221
RebuildBdd/Checkup/check-postgresql.sh
Executable file
221
RebuildBdd/Checkup/check-postgresql.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
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
|
||||
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=""
|
||||
|
||||
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
|
||||
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
|
||||
# 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}"
|
||||
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}"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
if ! require_cmd "$SUDO_BIN"; then
|
||||
fail "sudo absent sur la cible"
|
||||
fi
|
||||
|
||||
if ! "$SUDO_BIN" /usr/bin/systemctl --version >/dev/null 2>&1; then
|
||||
fail "sudo indisponible pour systemctl"
|
||||
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 || ! postgres_server_ready; 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
|
||||
|
||||
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
|
||||
fail "impossible de démarrer PostgreSQL. $(collect_postgres_diagnostics)"
|
||||
fi
|
||||
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}..."
|
||||
|
||||
ROLE_ATTRIBUTES="LOGIN CREATEDB CREATEROLE"
|
||||
if [[ "${PGUSER_SUPERUSER,,}" == "yes" ]]; then
|
||||
ROLE_ATTRIBUTES="LOGIN SUPERUSER CREATEDB CREATEROLE"
|
||||
fi
|
||||
|
||||
"$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}"
|
||||
|
||||
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."
|
||||
336
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
336
RebuildBdd/Checkup/check-target-readiness.sh
Executable file
@@ -0,0 +1,336 @@
|
||||
#!/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)
|
||||
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"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT 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
|
||||
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."
|
||||
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" <<EOF
|
||||
${USER} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||
${USER} ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||
EOF
|
||||
|
||||
chmod 440 "$tmp_file"
|
||||
|
||||
visudo -cf "$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
|
||||
38
RebuildBdd/Config/.env.exemple
Normal file
38
RebuildBdd/Config/.env.exemple
Normal file
@@ -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
|
||||
30
RebuildBdd/Config/Targets/prod.env.example
Normal file
30
RebuildBdd/Config/Targets/prod.env.example
Normal file
@@ -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
|
||||
42
RebuildBdd/Config/Targets/test.env.example
Normal file
42
RebuildBdd/Config/Targets/test.env.example
Normal file
@@ -0,0 +1,42 @@
|
||||
###############################################################################
|
||||
# config/targets/test.env.example
|
||||
###############################################################################
|
||||
|
||||
# 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
|
||||
|
||||
# Bootstrap
|
||||
TARGET_ENABLE_BOOTSTRAP=yes
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=yes
|
||||
|
||||
# Repo local cible
|
||||
TARGET_REPO_DIR=/home/backup_liot/RebuildBdd
|
||||
TARGET_ENV_FILE=/home/backup_liot/RebuildBdd/.env
|
||||
|
||||
# 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"
|
||||
|
||||
# Backup cible
|
||||
TARGET_BACKUP_SUBDIR=bdd-recette
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
566
RebuildBdd/README.md
Normal file
566
RebuildBdd/README.md
Normal file
@@ -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/<nom_cible>.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/<cible>.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
|
||||
<BACKUP_REMOTE_DIR>/<db>/<db>_*.dump
|
||||
```
|
||||
|
||||
* le dernier fichier rôles dans :
|
||||
|
||||
```bash
|
||||
<BACKUP_REMOTE_DIR>/<REMOTE_ROLES_DIR_NAME>/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/<cible>.env`
|
||||
2. lancer `bootstrap-target-host.sh --target <cible>`
|
||||
3. lancer ensuite `run-rebuild-bdd.sh --target <cible> ...`
|
||||
|
||||
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
|
||||
<user> ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||
<user> 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.
|
||||
|
||||
```
|
||||
579
RebuildBdd/bootstrap-target-host.sh
Executable file
579
RebuildBdd/bootstrap-target-host.sh
Executable file
@@ -0,0 +1,579 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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}"
|
||||
|
||||
TARGET_NAME="${TARGET_NAME:-}"
|
||||
CLI_TARGET=""
|
||||
JSON_ONLY="${JSON_ONLY:-no}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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)
|
||||
[[ $# -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
|
||||
|
||||
json_escape() {
|
||||
python3 - <<'PY' "$1"
|
||||
import json, sys
|
||||
print(json.dumps(sys.argv[1]))
|
||||
PY
|
||||
}
|
||||
|
||||
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":%s,"message":%s}\n' \
|
||||
"$(json_escape "error")" \
|
||||
"$(json_escape "$msg")"
|
||||
else
|
||||
echo "ERROR: $msg" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
success() {
|
||||
local msg="$1"
|
||||
if [[ "$JSON_ONLY" == "yes" ]]; then
|
||||
printf '{"status":%s,"message":%s}\n' \
|
||||
"$(json_escape "success")" \
|
||||
"$(json_escape "$msg")"
|
||||
else
|
||||
log "$msg"
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "%q" "$1"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "${TMP_ENV_FILE:-}"
|
||||
}
|
||||
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"
|
||||
|
||||
TARGET_ENV_SOURCE="${TARGETS_DIR}/${TARGET_NAME}.env"
|
||||
|
||||
[[ -f "$GLOBAL_ENV_FILE" ]] || fail "fichier global introuvable : $GLOBAL_ENV_FILE"
|
||||
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$GLOBAL_ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
source "$TARGET_ENV_SOURCE"
|
||||
set +a
|
||||
|
||||
BOOTSTRAP_HOST="${TARGET_HOST:-}"
|
||||
BOOTSTRAP_PORT="${TARGET_PORT:-22}"
|
||||
BOOTSTRAP_USER="${TARGET_BOOTSTRAP_USER:-}"
|
||||
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:-}"
|
||||
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:-}"
|
||||
|
||||
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 "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"
|
||||
|
||||
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"
|
||||
[[ -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 "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_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 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
|
||||
|
||||
run_root() {
|
||||
if [ \"\$(id -u)\" -eq 0 ]; then
|
||||
\"\$@\"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo \"\$@\" || {
|
||||
echo 'sudo 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_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")
|
||||
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"
|
||||
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)"
|
||||
|
||||
cat >"$TMP_ENV_FILE" <<EOF
|
||||
ENV_NAME=$(shell_quote "$TARGET_ENV_NAME_VALUE")
|
||||
PGHOST=$(shell_quote "$TARGET_PGHOST_VALUE")
|
||||
PGPORT=$(shell_quote "$TARGET_PGPORT_VALUE")
|
||||
PGUSER=$(shell_quote "$TARGET_PGUSER_VALUE")
|
||||
PGPASSWORD=$(shell_quote "$TARGET_PGPASSWORD_VALUE")
|
||||
DBS=$(shell_quote "$TARGET_DBS_VALUE")
|
||||
|
||||
BACKUP_REMOTE_USER=$(shell_quote "$TARGET_BACKUP_REMOTE_USER_VALUE")
|
||||
BACKUP_REMOTE_HOST=$(shell_quote "$TARGET_BACKUP_REMOTE_HOST_VALUE")
|
||||
BACKUP_REMOTE_DIR=$(shell_quote "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
||||
BACKUP_REMOTE_SSH_PORT=$(shell_quote "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE")
|
||||
|
||||
BACKUP_LOG_DIR=$(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
|
||||
LOCAL_RESTORE_BASE_DIR=$(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
||||
REMOTE_ROLES_DIR_NAME=$(shell_quote "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE")
|
||||
SSH_KEY=$(shell_quote "$TARGET_SSH_KEY_VALUE")
|
||||
|
||||
AUTO_INSTALL_POSTGRES=$(shell_quote "$TARGET_AUTO_INSTALL_POSTGRES_VALUE")
|
||||
AUTO_CREATE_PGUSER=$(shell_quote "$TARGET_AUTO_CREATE_PGUSER_VALUE")
|
||||
PGUSER_SUPERUSER=$(shell_quote "$TARGET_PGUSER_SUPERUSER_VALUE")
|
||||
AUTO_CONFIGURE_SUDOERS=$(shell_quote "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE")
|
||||
EXCLUDED_RESTORE_ROLES=$(shell_quote "$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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
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_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 >/dev/null 2>&1
|
||||
"
|
||||
|
||||
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 \"\$@\" || {
|
||||
echo 'sudo 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\" <<EOF
|
||||
${TARGET_RUNTIME_USER_VALUE} ALL=(root) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /usr/bin/systemctl
|
||||
${TARGET_RUNTIME_USER_VALUE} ALL=(postgres) NOPASSWD: /usr/bin/psql
|
||||
EOF
|
||||
|
||||
chmod 440 \"\$TMP_SUDOERS_FILE\"
|
||||
|
||||
visudo -cf \"\$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 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 minimal désactivée."
|
||||
fi
|
||||
|
||||
REMOTE_REPO_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
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_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_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_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_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
|
||||
"
|
||||
|
||||
log "Clone / mise à jour du dépôt distant"
|
||||
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
|
||||
command -v sudo >/dev/null 2>&1 || {
|
||||
echo 'sudo absent sur la cible' >&2
|
||||
exit 1
|
||||
}
|
||||
sudo /usr/bin/systemctl --version >/dev/null 2>&1 || {
|
||||
echo 'sudo indisponible pour systemctl' >&2
|
||||
exit 1
|
||||
}
|
||||
"
|
||||
|
||||
log "Validation initiale de sudo"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_ROOT_CMD" \
|
||||
|| fail "sudo invalide sur la cible"
|
||||
|
||||
REMOTE_RUN_CHECK_PG_CMD="
|
||||
set -euo pipefail
|
||||
|
||||
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") et TARGET_REPO_SUBDIR=$(shell_quote "$TARGET_REPO_SUBDIR")\" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -x \"\$CHECK_SCRIPT\" ]] || chmod 700 \"\$CHECK_SCRIPT\"
|
||||
|
||||
\"\$CHECK_SCRIPT\" --env-file \"\$ENV_FILE\" --non-interactive
|
||||
"
|
||||
|
||||
log "Préparation PostgreSQL via check-postgresql.sh"
|
||||
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
|
||||
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 -u postgres"
|
||||
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_VALIDATE_SUDO_POSTGRES_CMD" \
|
||||
|| fail "sudo -u postgres invalide sur la cible"
|
||||
|
||||
success "bootstrap initial terminé pour ${TARGET_NAME}"
|
||||
160
RebuildBdd/create-target-config.sh
Normal file
160
RebuildBdd/create-target-config.sh
Normal file
@@ -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" <<EOF
|
||||
TARGET_HOST=$(printf '%q' "$HOST")
|
||||
TARGET_PORT=$(printf '%q' "$PORT")
|
||||
TARGET_BOOTSTRAP_USER=$(printf '%q' "$BOOTSTRAP_USER")
|
||||
TARGET_BOOTSTRAP_SSH_KEY=$(printf '%q' "$BOOTSTRAP_SSH_KEY")
|
||||
TARGET_RUNTIME_USER=$(printf '%q' "$RUNTIME_USER")
|
||||
|
||||
TARGET_ENABLE_BOOTSTRAP=$(printf '%q' "$ENABLE_BOOTSTRAP")
|
||||
TARGET_BOOTSTRAP_ALLOW_PASSWORDLESS_SUDO=$(printf '%q' "$ALLOW_PASSWORDLESS_SUDO")
|
||||
|
||||
TARGET_REPO_DIR=$(printf '%q' "$REPO_DIR")
|
||||
TARGET_ENV_FILE=$(printf '%q' "$ENV_FILE")
|
||||
|
||||
TARGET_ENV_NAME=$(printf '%q' "$ENV_NAME")
|
||||
TARGET_PGHOST=$(printf '%q' "$PGHOST")
|
||||
TARGET_PGPORT=$(printf '%q' "$PGPORT")
|
||||
TARGET_PGUSER=$(printf '%q' "$PGUSER")
|
||||
TARGET_PGPASSWORD=$(printf '%q' "$PGPASSWORD")
|
||||
TARGET_DBS=$(printf '%q' "$DBS")
|
||||
|
||||
TARGET_BACKUP_SUBDIR=$(printf '%q' "$BACKUP_SUBDIR")
|
||||
|
||||
TARGET_BACKUP_LOG_DIR=$(printf '%q' "$BACKUP_LOG_DIR")
|
||||
TARGET_LOCAL_RESTORE_BASE_DIR=$(printf '%q' "$LOCAL_RESTORE_BASE_DIR")
|
||||
TARGET_SSH_KEY=$(printf '%q' "$SSH_KEY_TARGET_PATH")
|
||||
|
||||
TARGET_REMOTE_ROLES_DIR_NAME=$(printf '%q' "$REMOTE_ROLES_DIR_NAME")
|
||||
TARGET_EXCLUDED_RESTORE_ROLES=$(printf '%q' "$EXCLUDED_RESTORE_ROLES")
|
||||
TARGET_AUTO_INSTALL_POSTGRES=$(printf '%q' "$AUTO_INSTALL_POSTGRES")
|
||||
TARGET_AUTO_CREATE_PGUSER=$(printf '%q' "$AUTO_CREATE_PGUSER")
|
||||
TARGET_PGUSER_SUPERUSER=$(printf '%q' "$PGUSER_SUPERUSER")
|
||||
TARGET_AUTO_CONFIGURE_SUDOERS=$(printf '%q' "$AUTO_CONFIGURE_SUDOERS")
|
||||
EOF
|
||||
|
||||
chmod 600 "$TARGET_FILE" || fail "chmod impossible sur $TARGET_FILE"
|
||||
|
||||
echo "OK: ${TARGET_FILE}"
|
||||
501
RebuildBdd/rebuild-bdd-core.sh
Executable file
501
RebuildBdd/rebuild-bdd-core.sh
Executable file
@@ -0,0 +1,501 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
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 "${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 "${SCP_OPTS[@]}" "${REMOTE_SSH}:${remote_path}" "$local_path" >>"$LOG_FILE" 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}"
|
||||
BACKUP_REMOTE_SSH_PORT="${BACKUP_REMOTE_SSH_PORT:-22}"
|
||||
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
|
||||
}
|
||||
|
||||
[[ "$PGPORT" =~ ^[0-9]+$ ]] || fail "PGPORT invalide"
|
||||
[[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || fail "BACKUP_REMOTE_SSH_PORT invalide"
|
||||
|
||||
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 curl; do
|
||||
require_cmd "$cmd" || fail "commande requise absente : $cmd"
|
||||
done
|
||||
|
||||
CHECK_SCRIPT="${SCRIPT_DIR}/Checkup/check-postgresql.sh"
|
||||
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"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "$SSH_KEY"
|
||||
-p "$BACKUP_REMOTE_SSH_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
|
||||
-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"
|
||||
[[ "${#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"
|
||||
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"
|
||||
download_remote_file "$LAST_REMOTE_ROLES_FILE" "$LOCAL_ROLES_FILE" \
|
||||
|| 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
|
||||
|
||||
# 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" \
|
||||
> "$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
|
||||
|
||||
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
|
||||
428
RebuildBdd/run-rebuild-bdd.sh
Executable file
428
RebuildBdd/run-rebuild-bdd.sh
Executable file
@@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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}"
|
||||
|
||||
CLI_TARGET=""
|
||||
CLI_DB=""
|
||||
CLI_OVERWRITE=""
|
||||
CLI_RESTORE_ROLES=""
|
||||
CLI_REQUEST_ID=""
|
||||
NON_INTERACTIVE="${NON_INTERACTIVE:-no}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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)
|
||||
[[ $# -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
|
||||
;;
|
||||
*)
|
||||
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 || 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
|
||||
}
|
||||
|
||||
is_tty() {
|
||||
[[ -t 0 && -t 1 ]]
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "%q" "$1"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "${BOOTSTRAP_JSON:-}" "${REMOTE_RESULT_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 "$GLOBAL_ENV_FILE"
|
||||
set +a
|
||||
|
||||
require_cmd ssh
|
||||
require_cmd git
|
||||
require_cmd python3
|
||||
|
||||
TARGET="${CLI_TARGET:-${TARGET:-}}"
|
||||
REQUESTED_DB="${CLI_DB:-${REQUESTED_DB:-}}"
|
||||
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"
|
||||
|
||||
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
|
||||
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 <= ${#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_ENV_SOURCE="${TARGETS_DIR}/${TARGET}.env"
|
||||
[[ -f "$TARGET_ENV_SOURCE" ]] || fail "fichier cible introuvable : $TARGET_ENV_SOURCE"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$TARGET_ENV_SOURCE"
|
||||
set +a
|
||||
|
||||
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_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}}"
|
||||
|
||||
[[ -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"
|
||||
|
||||
[[ -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_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
|
||||
|
||||
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
|
||||
else
|
||||
fail "REQUESTED_DB manquante et aucune interaction terminal disponible"
|
||||
fi
|
||||
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_PORT"
|
||||
-o IdentitiesOnly=yes
|
||||
-o BatchMode=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_SCRIPT_DIR}/rebuild-bdd-core.sh"
|
||||
REMOTE_RESULT_JSON="/tmp/run_rebuild_bdd_${REQUEST_ID}.json"
|
||||
|
||||
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")
|
||||
CORE_SCRIPT=$(shell_quote "$TARGET_CORE_SCRIPT")
|
||||
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")
|
||||
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 \"\$CLONE_DIR\")\"
|
||||
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\" >/dev/null 2>&1
|
||||
else
|
||||
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; }
|
||||
[[ -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_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\" 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
|
||||
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\" \"\$PRECHECK_STDERR\" \"\$CORE_JSON\" \"\$CORE_STDERR\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f \"\$PRECHECK_JSON\" \"\$PRECHECK_STDERR\"
|
||||
|
||||
\"\$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 >\"\$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 \
|
||||
|| {
|
||||
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
|
||||
)" || {
|
||||
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"
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
# RecetteScripts
|
||||
# RecetteScripts
|
||||
|
||||
Scripts Bash permettant d’automatiser la gestion d’un environnement **PostgreSQL de recette**.
|
||||
|
||||
|
||||
0
RecetteScripts/backup-bdd-recette.sh
Normal file → Executable file
0
RecetteScripts/backup-bdd-recette.sh
Normal file → Executable file
0
RecetteScripts/check-statut-recette.sh
Normal file → Executable file
0
RecetteScripts/check-statut-recette.sh
Normal file → Executable file
Reference in New Issue
Block a user