286 lines
11 KiB
Bash
286 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
###############################################################################
|
|
# bootstrap-target-host.sh
|
|
#
|
|
# Bootstrap initial d'une machine cible neuve ou quasi neuve.
|
|
#
|
|
# Ce script est lancé depuis la machine de pilotage et :
|
|
# 1. charge le .env local ;
|
|
# 2. récupère la configuration bootstrap de la cible ;
|
|
# 3. teste la connexion SSH de bootstrap ;
|
|
# 4. installe le socle minimal sur la cible ;
|
|
# 5. crée les dossiers de travail ;
|
|
# 6. génère le .env cible ;
|
|
# 7. clone ou met à jour le dépôt distant.
|
|
#
|
|
# À ce stade, il ne gère pas encore :
|
|
# - la clé SSH backup ;
|
|
# - known_hosts backup ;
|
|
# - sudoers PostgreSQL ;
|
|
# - le lancement du rebuild.
|
|
###############################################################################
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
DEFAULT_ENV_FILE="${SCRIPT_DIR}/.env"
|
|
|
|
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
|
|
TARGET_NAME="${TARGET_NAME:-}"
|
|
CLI_TARGET=""
|
|
JSON_ONLY="${JSON_ONLY:-no}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--env-file)
|
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --env-file" >&2; exit 1; }
|
|
ENV_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--target)
|
|
[[ $# -ge 2 ]] || { echo "Argument manquant pour --target" >&2; exit 1; }
|
|
CLI_TARGET="$2"
|
|
shift 2
|
|
;;
|
|
--json-only)
|
|
JSON_ONLY="yes"
|
|
shift
|
|
;;
|
|
*)
|
|
echo "Argument inconnu : $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
print_stdout() {
|
|
[[ "$JSON_ONLY" == "yes" ]] || echo "$*"
|
|
}
|
|
|
|
log() {
|
|
print_stdout "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
|
}
|
|
|
|
fail() {
|
|
local msg="$1"
|
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
|
printf '{"status":"error","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')"
|
|
else
|
|
echo "ERROR: $msg" >&2
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
success() {
|
|
local msg="$1"
|
|
if [[ "$JSON_ONLY" == "yes" ]]; then
|
|
printf '{"status":"success","message":"%s"}\n' "$(printf '%s' "$msg" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | sed 's/^"//;s/"$//')"
|
|
else
|
|
log "$msg"
|
|
fi
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1"
|
|
}
|
|
|
|
sanitize_key() {
|
|
local value="${1:-}"
|
|
value="${value//[^a-zA-Z0-9_]/_}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
get_env_var() {
|
|
local var_name="$1"
|
|
printf '%s' "${!var_name:-}"
|
|
}
|
|
|
|
shell_quote() {
|
|
printf "%q" "$1"
|
|
}
|
|
|
|
[[ -f "$ENV_FILE" ]] || fail "fichier .env introuvable : $ENV_FILE"
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
set +a
|
|
|
|
TARGET_NAME="${CLI_TARGET:-${TARGET_NAME:-}}"
|
|
[[ -n "$TARGET_NAME" ]] || fail "target manquante"
|
|
|
|
SAFE_TARGET="$(sanitize_key "$TARGET_NAME")"
|
|
|
|
BOOTSTRAP_HOST="$(get_env_var "TARGET_HOST_${SAFE_TARGET}")"
|
|
BOOTSTRAP_PORT="$(get_env_var "TARGET_PORT_${SAFE_TARGET}")"
|
|
BOOTSTRAP_USER="$(get_env_var "TARGET_BOOTSTRAP_USER_${SAFE_TARGET}")"
|
|
BOOTSTRAP_SSH_KEY="$(get_env_var "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET}")"
|
|
|
|
TARGET_REPO_URL="$(get_env_var "TARGET_REPO_URL_${SAFE_TARGET}")"
|
|
TARGET_REPO_BRANCH="$(get_env_var "TARGET_REPO_BRANCH_${SAFE_TARGET}")"
|
|
TARGET_REPO_DIR="$(get_env_var "TARGET_REPO_DIR_${SAFE_TARGET}")"
|
|
TARGET_ENV_FILE_PATH="$(get_env_var "TARGET_ENV_FILE_${SAFE_TARGET}")"
|
|
|
|
TARGET_ENV_NAME_VALUE="$(get_env_var "TARGET_ENV_NAME_${SAFE_TARGET}")"
|
|
TARGET_PGHOST_VALUE="$(get_env_var "TARGET_PGHOST_${SAFE_TARGET}")"
|
|
TARGET_PGPORT_VALUE="$(get_env_var "TARGET_PGPORT_${SAFE_TARGET}")"
|
|
TARGET_PGUSER_VALUE="$(get_env_var "TARGET_PGUSER_${SAFE_TARGET}")"
|
|
TARGET_PGPASSWORD_VALUE="$(get_env_var "TARGET_PGPASSWORD_${SAFE_TARGET}")"
|
|
TARGET_DBS_VALUE="$(get_env_var "TARGET_DBS_${SAFE_TARGET}")"
|
|
|
|
TARGET_BACKUP_REMOTE_USER_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET}")"
|
|
TARGET_BACKUP_REMOTE_HOST_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET}")"
|
|
TARGET_BACKUP_REMOTE_DIR_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET}")"
|
|
TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="$(get_env_var "TARGET_BACKUP_REMOTE_SSH_PORT_${SAFE_TARGET}")"
|
|
TARGET_BACKUP_LOG_DIR_VALUE="$(get_env_var "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET}")"
|
|
|
|
TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="$(get_env_var "TARGET_LOCAL_RESTORE_BASE_DIR_${SAFE_TARGET}")"
|
|
TARGET_REMOTE_ROLES_DIR_NAME_VALUE="$(get_env_var "TARGET_REMOTE_ROLES_DIR_NAME_${SAFE_TARGET}")"
|
|
TARGET_SSH_KEY_VALUE="$(get_env_var "TARGET_SSH_KEY_${SAFE_TARGET}")"
|
|
TARGET_AUTO_INSTALL_POSTGRES_VALUE="$(get_env_var "TARGET_AUTO_INSTALL_POSTGRES_${SAFE_TARGET}")"
|
|
TARGET_AUTO_CREATE_PGUSER_VALUE="$(get_env_var "TARGET_AUTO_CREATE_PGUSER_${SAFE_TARGET}")"
|
|
TARGET_PGUSER_SUPERUSER_VALUE="$(get_env_var "TARGET_PGUSER_SUPERUSER_${SAFE_TARGET}")"
|
|
TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="$(get_env_var "TARGET_AUTO_CONFIGURE_SUDOERS_${SAFE_TARGET}")"
|
|
TARGET_EXCLUDED_RESTORE_ROLES_VALUE="$(get_env_var "TARGET_EXCLUDED_RESTORE_ROLES_${SAFE_TARGET}")"
|
|
|
|
[[ -n "$BOOTSTRAP_HOST" ]] || fail "TARGET_HOST_${SAFE_TARGET} manquante"
|
|
[[ -n "$BOOTSTRAP_PORT" ]] || BOOTSTRAP_PORT="22"
|
|
[[ -n "$BOOTSTRAP_USER" ]] || fail "TARGET_BOOTSTRAP_USER_${SAFE_TARGET} manquante"
|
|
[[ -n "$BOOTSTRAP_SSH_KEY" ]] || fail "TARGET_BOOTSTRAP_SSH_KEY_${SAFE_TARGET} manquante"
|
|
|
|
[[ -f "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap introuvable : $BOOTSTRAP_SSH_KEY"
|
|
[[ -r "$BOOTSTRAP_SSH_KEY" ]] || fail "clé bootstrap non lisible : $BOOTSTRAP_SSH_KEY"
|
|
|
|
[[ -n "$TARGET_REPO_URL" ]] || fail "TARGET_REPO_URL_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_REPO_BRANCH" ]] || fail "TARGET_REPO_BRANCH_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_REPO_DIR" ]] || fail "TARGET_REPO_DIR_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_ENV_FILE_PATH" ]] || fail "TARGET_ENV_FILE_${SAFE_TARGET} manquante"
|
|
|
|
[[ -n "$TARGET_ENV_NAME_VALUE" ]] || fail "TARGET_ENV_NAME_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_PGHOST_VALUE" ]] || fail "TARGET_PGHOST_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_PGPORT_VALUE" ]] || fail "TARGET_PGPORT_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_PGUSER_VALUE" ]] || fail "TARGET_PGUSER_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_PGPASSWORD_VALUE" ]] || fail "TARGET_PGPASSWORD_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_DBS_VALUE" ]] || fail "TARGET_DBS_${SAFE_TARGET} manquante"
|
|
|
|
[[ -n "$TARGET_BACKUP_REMOTE_USER_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_USER_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_BACKUP_REMOTE_HOST_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_HOST_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_BACKUP_REMOTE_DIR_VALUE" ]] || fail "TARGET_BACKUP_REMOTE_DIR_${SAFE_TARGET} manquante"
|
|
[[ -n "$TARGET_BACKUP_LOG_DIR_VALUE" ]] || fail "TARGET_BACKUP_LOG_DIR_${SAFE_TARGET} manquante"
|
|
|
|
[[ -n "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE" ]] || TARGET_BACKUP_REMOTE_SSH_PORT_VALUE="22"
|
|
[[ -n "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE" ]] || TARGET_LOCAL_RESTORE_BASE_DIR_VALUE="${TARGET_REPO_DIR}/restore_tmp"
|
|
[[ -n "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE" ]] || TARGET_REMOTE_ROLES_DIR_NAME_VALUE="user"
|
|
[[ -n "$TARGET_SSH_KEY_VALUE" ]] || TARGET_SSH_KEY_VALUE="/home/${BOOTSTRAP_USER}/.ssh/id_ed25519_backup_readonly"
|
|
[[ -n "$TARGET_AUTO_INSTALL_POSTGRES_VALUE" ]] || TARGET_AUTO_INSTALL_POSTGRES_VALUE="yes"
|
|
[[ -n "$TARGET_AUTO_CREATE_PGUSER_VALUE" ]] || TARGET_AUTO_CREATE_PGUSER_VALUE="yes"
|
|
[[ -n "$TARGET_PGUSER_SUPERUSER_VALUE" ]] || TARGET_PGUSER_SUPERUSER_VALUE="no"
|
|
[[ -n "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE" ]] || TARGET_AUTO_CONFIGURE_SUDOERS_VALUE="no"
|
|
[[ -n "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE" ]] || TARGET_EXCLUDED_RESTORE_ROLES_VALUE="postgres"
|
|
|
|
require_cmd ssh
|
|
require_cmd scp
|
|
require_cmd python3
|
|
|
|
SSH_OPTS=(
|
|
-i "$BOOTSTRAP_SSH_KEY"
|
|
-p "$BOOTSTRAP_PORT"
|
|
-o IdentitiesOnly=yes
|
|
-o BatchMode=yes
|
|
-o StrictHostKeyChecking=accept-new
|
|
-o ConnectTimeout=8
|
|
)
|
|
|
|
REMOTE="${BOOTSTRAP_USER}@${BOOTSTRAP_HOST}"
|
|
|
|
log "Test de connexion SSH bootstrap vers ${REMOTE}:${BOOTSTRAP_PORT}"
|
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "exit 0" >/dev/null 2>&1 \
|
|
|| fail "connexion SSH bootstrap impossible vers ${REMOTE}"
|
|
|
|
REMOTE_SETUP_CMD="
|
|
set -euo pipefail
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
sudo apt-get update
|
|
sudo apt-get install -y bash git python3 sudo curl openssh-client ca-certificates
|
|
else
|
|
apt-get update
|
|
apt-get install -y bash git python3 sudo curl openssh-client ca-certificates
|
|
fi
|
|
else
|
|
echo 'apt-get absent sur la cible' >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p $(shell_quote "$(dirname "$TARGET_REPO_DIR")")
|
|
mkdir -p $(shell_quote "$(dirname "$TARGET_ENV_FILE_PATH")")
|
|
mkdir -p $(shell_quote "$TARGET_BACKUP_LOG_DIR_VALUE")
|
|
mkdir -p $(shell_quote "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
|
mkdir -p $(shell_quote "$(dirname "$TARGET_SSH_KEY_VALUE")")
|
|
"
|
|
|
|
log "Installation du socle minimal sur la cible"
|
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_SETUP_CMD" \
|
|
|| fail "échec de préparation système distante"
|
|
|
|
TMP_ENV_FILE="$(mktemp)"
|
|
cleanup() {
|
|
rm -f "$TMP_ENV_FILE"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cat >"$TMP_ENV_FILE" <<EOF
|
|
ENV_NAME=$(printf '%s\n' "$TARGET_ENV_NAME_VALUE")
|
|
PGHOST=$(printf '%s\n' "$TARGET_PGHOST_VALUE")
|
|
PGPORT=$(printf '%s\n' "$TARGET_PGPORT_VALUE")
|
|
PGUSER=$(printf '%s\n' "$TARGET_PGUSER_VALUE")
|
|
PGPASSWORD=$(printf '%s\n' "$TARGET_PGPASSWORD_VALUE")
|
|
DBS=$(printf '%s\n' "$TARGET_DBS_VALUE")
|
|
|
|
BACKUP_REMOTE_USER=$(printf '%s\n' "$TARGET_BACKUP_REMOTE_USER_VALUE")
|
|
BACKUP_REMOTE_HOST=$(printf '%s\n' "$TARGET_BACKUP_REMOTE_HOST_VALUE")
|
|
BACKUP_REMOTE_DIR=$(printf '%s\n' "$TARGET_BACKUP_REMOTE_DIR_VALUE")
|
|
BACKUP_REMOTE_SSH_PORT=$(printf '%s\n' "$TARGET_BACKUP_REMOTE_SSH_PORT_VALUE")
|
|
|
|
BACKUP_LOG_DIR=$(printf '%s\n' "$TARGET_BACKUP_LOG_DIR_VALUE")
|
|
LOCAL_RESTORE_BASE_DIR=$(printf '%s\n' "$TARGET_LOCAL_RESTORE_BASE_DIR_VALUE")
|
|
REMOTE_ROLES_DIR_NAME=$(printf '%s\n' "$TARGET_REMOTE_ROLES_DIR_NAME_VALUE")
|
|
SSH_KEY=$(printf '%s\n' "$TARGET_SSH_KEY_VALUE")
|
|
|
|
AUTO_INSTALL_POSTGRES=$(printf '%s\n' "$TARGET_AUTO_INSTALL_POSTGRES_VALUE")
|
|
AUTO_CREATE_PGUSER=$(printf '%s\n' "$TARGET_AUTO_CREATE_PGUSER_VALUE")
|
|
PGUSER_SUPERUSER=$(printf '%s\n' "$TARGET_PGUSER_SUPERUSER_VALUE")
|
|
AUTO_CONFIGURE_SUDOERS=$(printf '%s\n' "$TARGET_AUTO_CONFIGURE_SUDOERS_VALUE")
|
|
EXCLUDED_RESTORE_ROLES=$(printf '%s\n' "$TARGET_EXCLUDED_RESTORE_ROLES_VALUE")
|
|
EOF
|
|
|
|
log "Copie du .env cible"
|
|
scp "${SSH_OPTS[@]}" "$TMP_ENV_FILE" "${REMOTE}:$(printf '%q' "$TARGET_ENV_FILE_PATH")" >/dev/null 2>&1 \
|
|
|| fail "échec de copie du .env cible"
|
|
|
|
REMOTE_REPO_CMD="
|
|
set -euo pipefail
|
|
|
|
if [[ ! -d $(shell_quote "${TARGET_REPO_DIR}/.git") ]]; then
|
|
rm -rf $(shell_quote "$TARGET_REPO_DIR")
|
|
git clone --branch $(shell_quote "$TARGET_REPO_BRANCH") --single-branch $(shell_quote "$TARGET_REPO_URL") $(shell_quote "$TARGET_REPO_DIR")
|
|
else
|
|
git -C $(shell_quote "$TARGET_REPO_DIR") fetch --prune origin
|
|
git -C $(shell_quote "$TARGET_REPO_DIR") checkout -f $(shell_quote "$TARGET_REPO_BRANCH")
|
|
git -C $(shell_quote "$TARGET_REPO_DIR") reset --hard origin/$(shell_quote "$TARGET_REPO_BRANCH")
|
|
fi
|
|
|
|
chmod 700 $(shell_quote "$TARGET_REPO_DIR/run-rebuild-bdd.sh") 2>/dev/null || true
|
|
chmod 700 $(shell_quote "$TARGET_REPO_DIR/rebuild-bdd-core.sh") 2>/dev/null || true
|
|
chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-postgresql.sh") 2>/dev/null || true
|
|
chmod 700 $(shell_quote "$TARGET_REPO_DIR/Checkup/check-target-readiness.sh") 2>/dev/null || true
|
|
"
|
|
|
|
log "Clone / mise à jour du dépôt distant"
|
|
ssh "${SSH_OPTS[@]}" "$REMOTE" "$REMOTE_REPO_CMD" \
|
|
|| fail "échec de synchronisation du dépôt sur la cible"
|
|
|
|
success "bootstrap initial terminé pour ${TARGET_NAME}" |