Some checks failed
CI / sonarqube (push) Failing after 52s
Sur le serveur prod, l'utilisateur 'bot' (cree par Ansible) a UID 1001 alors que l'utilisateur 'appuser' du conteneur PHP etait hardcode a UID 1000, ce qui causait des erreurs 'Unable to write in var/cache/prod' apres restore ou deploiement: les fichiers du host appartenaient a bot:bot (1001) et appuser (1000) ne pouvait pas y ecrire. docker/php/prod/Dockerfile: - appuser UID/GID passe de 1000 a 1001 pour matcher bot - dev/Dockerfile reste a 1000 (les users dev sont generalement 1000) ansible/deploy.yml: - chown des dossiers public/uploads/* et var/payouts passe de 1000 a 1001 restore.sh: - Nouvelle option --owner USER:GRP (defaut: bot:bot, override via RESTORE_OWNER) - Chown automatique en fin de restore sur var/, public/uploads/, config/cert/, .env.local - Purge automatique de var/cache (sera reconstruit par PHP) - Verification root au demarrage (sudo requis pour chown) - Verification de l'existence du user et group cibles - Option --skip-chown pour bypass (deconseille) - Etapes post-migration mises a jour (ajout de make build_prod en premier) Procedure de migration sur le serveur apres ce commit: cd /var/www/e-ticket git pull origin master make build_prod # rebuild image PHP avec UID 1001 make stop_prod sudo chown -R bot:bot var public/uploads config/cert .env.local sudo rm -rf var/cache make start_prod make clear_prod Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
328 lines
12 KiB
Bash
Executable File
328 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# E-Ticket - Script d'import pour migration de serveur
|
|
# Restaure un bundle de migration genere par backup.sh sur un nouveau serveur.
|
|
#
|
|
# Le bundle est attendu au format e_ticket_migration_<DATE>.tar.gz et contient:
|
|
# db.sql.gz, public_uploads.tar.gz, var_state.tar.gz, env.local, cert/, manifest.txt
|
|
#
|
|
# A la fin, le script chown automatiquement les fichiers restaures vers RESTORE_OWNER
|
|
# (defaut: bot) afin que l'utilisateur PHP du conteneur puisse y ecrire, et purge
|
|
# var/cache pour eviter les fichiers obsoletes appartenant a un autre utilisateur.
|
|
#
|
|
# Usage:
|
|
# ./restore.sh <bundle.tar.gz> [options]
|
|
#
|
|
# Variables d'environnement supportees:
|
|
# COMPOSE_FILE : fichier docker-compose a utiliser (defaut: docker-compose-prod.yml)
|
|
# DB_SERVICE : service Docker de la base (defaut: db-master)
|
|
# DB_USER : utilisateur PostgreSQL (defaut: e-ticket)
|
|
# DB_NAME : base de donnees a restaurer (defaut: e-ticket)
|
|
# RESTORE_OWNER : user:group pour le chown final (defaut: bot:bot)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
COMPOSE_FILE="${COMPOSE_FILE:-${SCRIPT_DIR}/docker-compose-prod.yml}"
|
|
DB_SERVICE="${DB_SERVICE:-db-master}"
|
|
DB_USER="${DB_USER:-e-ticket}"
|
|
DB_NAME="${DB_NAME:-e-ticket}"
|
|
RESTORE_OWNER="${RESTORE_OWNER:-bot:bot}"
|
|
|
|
BUNDLE=""
|
|
ASSUME_YES="false"
|
|
SKIP_DB="false"
|
|
SKIP_UPLOADS="false"
|
|
SKIP_VAR="false"
|
|
SKIP_SECRETS="false"
|
|
SKIP_CHOWN="false"
|
|
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
|
}
|
|
|
|
err() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERREUR: $*" >&2
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
sudo ./restore.sh <bundle.tar.gz> [options]
|
|
|
|
Options:
|
|
--yes, -y Mode non interactif (pas de confirmation)
|
|
--skip-db Ne pas restaurer la base de donnees
|
|
--skip-uploads Ne pas restaurer public/uploads
|
|
--skip-var Ne pas restaurer var/ (payouts, billets, invoices, unsubscribed.json)
|
|
--skip-secrets Ne pas restaurer .env.local et config/cert/
|
|
--skip-chown Ne pas faire le chown final (deconseille)
|
|
--owner USER:GRP Utilisateur:groupe pour le chown final (defaut: bot:bot)
|
|
-h, --help Affiche cette aide
|
|
|
|
Variables d'environnement (alternatives aux flags):
|
|
RESTORE_OWNER Equivalent de --owner
|
|
|
|
ATTENTION: La restauration ECRASE les donnees existantes !
|
|
ATTENTION: Le chown final necessite root (sudo).
|
|
EOF
|
|
}
|
|
|
|
# --- Parsing des arguments ---
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--yes|-y) ASSUME_YES="true"; shift ;;
|
|
--skip-db) SKIP_DB="true"; shift ;;
|
|
--skip-uploads) SKIP_UPLOADS="true"; shift ;;
|
|
--skip-var) SKIP_VAR="true"; shift ;;
|
|
--skip-secrets) SKIP_SECRETS="true"; shift ;;
|
|
--skip-chown) SKIP_CHOWN="true"; shift ;;
|
|
--owner)
|
|
if [ $# -lt 2 ]; then
|
|
err "--owner requiert une valeur (ex: --owner bot:bot)"
|
|
exit 1
|
|
fi
|
|
RESTORE_OWNER="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--*) err "Option inconnue: $1"; usage; exit 1 ;;
|
|
*)
|
|
if [ -z "${BUNDLE}" ]; then
|
|
BUNDLE="$1"
|
|
else
|
|
err "Argument en trop: $1"
|
|
usage
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# --- Verification root pour le chown final ---
|
|
if [ "${SKIP_CHOWN}" = "false" ] && [ "$(id -u)" != "0" ]; then
|
|
err "Ce script doit etre execute en root (sudo) pour le chown final vers ${RESTORE_OWNER}"
|
|
err "Soit relancer avec sudo, soit ajouter --skip-chown (deconseille)"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Verification que le user/group cible existe ---
|
|
if [ "${SKIP_CHOWN}" = "false" ]; then
|
|
OWNER_USER="${RESTORE_OWNER%%:*}"
|
|
OWNER_GROUP="${RESTORE_OWNER##*:}"
|
|
if ! id -u "${OWNER_USER}" >/dev/null 2>&1; then
|
|
err "Utilisateur '${OWNER_USER}' introuvable sur le systeme"
|
|
exit 1
|
|
fi
|
|
if ! getent group "${OWNER_GROUP}" >/dev/null 2>&1; then
|
|
err "Groupe '${OWNER_GROUP}' introuvable sur le systeme"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${BUNDLE}" ]; then
|
|
err "Aucun bundle specifie"
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "${BUNDLE}" ]; then
|
|
err "Bundle introuvable: ${BUNDLE}"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Extraction du bundle dans un dossier temporaire ---
|
|
TMP_DIR="$(mktemp -d -t e_ticket_restore_XXXXXX)"
|
|
cleanup() {
|
|
rm -rf "${TMP_DIR}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
log "=== Restauration du bundle de migration E-Ticket ==="
|
|
log "Bundle: ${BUNDLE}"
|
|
log "Extraction dans ${TMP_DIR}..."
|
|
|
|
if ! tar -xzf "${BUNDLE}" -C "${TMP_DIR}"; then
|
|
err "Echec d'extraction du bundle"
|
|
exit 1
|
|
fi
|
|
|
|
# Trouve le dossier extrait (e_ticket_migration_<DATE>)
|
|
EXTRACTED_DIR="$(find "${TMP_DIR}" -maxdepth 1 -mindepth 1 -type d -name 'e_ticket_migration_*' | head -n1)"
|
|
if [ -z "${EXTRACTED_DIR}" ] || [ ! -d "${EXTRACTED_DIR}" ]; then
|
|
err "Bundle invalide: dossier e_ticket_migration_* introuvable"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Verification du manifest et des checksums ---
|
|
if [ ! -f "${EXTRACTED_DIR}/manifest.txt" ]; then
|
|
err "Manifest manquant dans le bundle"
|
|
exit 1
|
|
fi
|
|
|
|
log "Verification des checksums..."
|
|
(cd "${EXTRACTED_DIR}" && grep -E '^[a-f0-9]{64} ' manifest.txt | sha256sum -c --quiet) || {
|
|
err "Checksums invalides - bundle corrompu"
|
|
exit 1
|
|
}
|
|
log " OK - bundle integre"
|
|
|
|
echo
|
|
echo "================================================================"
|
|
echo " Manifest du bundle:"
|
|
echo "================================================================"
|
|
grep -E '^(bundle_name|created_at|source_host|git_commit|git_branch|db_name|skip_secrets):' "${EXTRACTED_DIR}/manifest.txt" || true
|
|
echo "================================================================"
|
|
|
|
# --- Verification des composants disponibles ---
|
|
HAS_DB="false"
|
|
HAS_UPLOADS="false"
|
|
HAS_VAR="false"
|
|
HAS_ENV="false"
|
|
HAS_CERT="false"
|
|
[ -f "${EXTRACTED_DIR}/db.sql.gz" ] && HAS_DB="true"
|
|
[ -f "${EXTRACTED_DIR}/public_uploads.tar.gz" ] && HAS_UPLOADS="true"
|
|
[ -f "${EXTRACTED_DIR}/var_state.tar.gz" ] && HAS_VAR="true"
|
|
[ -f "${EXTRACTED_DIR}/env.local" ] && HAS_ENV="true"
|
|
[ -d "${EXTRACTED_DIR}/cert" ] && HAS_CERT="true"
|
|
|
|
echo
|
|
echo " Plan de restauration:"
|
|
echo "----------------------------------------------------------------"
|
|
[ "${HAS_DB}" = "true" ] && [ "${SKIP_DB}" = "false" ] && echo " [X] Base de donnees -> ${DB_NAME} (ECRASE)" || echo " [ ] Base de donnees"
|
|
[ "${HAS_UPLOADS}" = "true" ] && [ "${SKIP_UPLOADS}" = "false" ] && echo " [X] public/uploads/ (ECRASE, sauvegarde de securite)" || echo " [ ] public/uploads/"
|
|
[ "${HAS_VAR}" = "true" ] && [ "${SKIP_VAR}" = "false" ] && echo " [X] var/ (payouts, billets, invoices, unsubscribed.json)" || echo " [ ] var/"
|
|
[ "${HAS_ENV}" = "true" ] && [ "${SKIP_SECRETS}" = "false" ] && echo " [X] .env.local (ECRASE)" || echo " [ ] .env.local"
|
|
[ "${HAS_CERT}" = "true" ] && [ "${SKIP_SECRETS}" = "false" ] && echo " [X] config/cert/ (ECRASE)" || echo " [ ] config/cert/"
|
|
echo "----------------------------------------------------------------"
|
|
echo
|
|
|
|
if [ "${ASSUME_YES}" != "true" ]; then
|
|
read -r -p "Confirmer la restauration ? (tapez 'oui' pour continuer) : " CONFIRM
|
|
if [ "${CONFIRM}" != "oui" ]; then
|
|
log "Restauration annulee par l'utilisateur"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# --- 1. Secrets (en premier pour que la stack puisse demarrer si besoin) ---
|
|
if [ "${SKIP_SECRETS}" = "false" ]; then
|
|
if [ "${HAS_ENV}" = "true" ]; then
|
|
log "[secrets] Restauration de .env.local..."
|
|
if [ -f "${SCRIPT_DIR}/.env.local" ]; then
|
|
cp -p "${SCRIPT_DIR}/.env.local" "${SCRIPT_DIR}/.env.local.before_restore_$(date +%Y%m%d_%H%M%S)"
|
|
fi
|
|
cp -p "${EXTRACTED_DIR}/env.local" "${SCRIPT_DIR}/.env.local"
|
|
chmod 600 "${SCRIPT_DIR}/.env.local"
|
|
log " OK"
|
|
fi
|
|
if [ "${HAS_CERT}" = "true" ]; then
|
|
log "[secrets] Restauration de config/cert/..."
|
|
mkdir -p "${SCRIPT_DIR}/config/cert"
|
|
cp -rp "${EXTRACTED_DIR}/cert/." "${SCRIPT_DIR}/config/cert/"
|
|
chmod -R go-rwx "${SCRIPT_DIR}/config/cert"
|
|
log " OK"
|
|
fi
|
|
fi
|
|
|
|
# --- 2. Base de donnees ---
|
|
if [ "${HAS_DB}" = "true" ] && [ "${SKIP_DB}" = "false" ]; then
|
|
log "[db] Verification du service ${DB_SERVICE}..."
|
|
if [ ! -f "${COMPOSE_FILE}" ]; then
|
|
err "Fichier docker-compose introuvable: ${COMPOSE_FILE}"
|
|
exit 1
|
|
fi
|
|
if ! docker compose -f "${COMPOSE_FILE}" ps --services --status running 2>/dev/null | grep -qx "${DB_SERVICE}"; then
|
|
err "Le service '${DB_SERVICE}' n'est pas demarre. Lancer: make start_prod"
|
|
exit 1
|
|
fi
|
|
|
|
log "[db] Restauration de la base ${DB_NAME}..."
|
|
if ! gunzip -c "${EXTRACTED_DIR}/db.sql.gz" \
|
|
| docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
|
|
psql -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 >/dev/null; then
|
|
err "Echec de la restauration PostgreSQL"
|
|
exit 1
|
|
fi
|
|
log " OK"
|
|
fi
|
|
|
|
# --- 3. public/uploads ---
|
|
if [ "${HAS_UPLOADS}" = "true" ] && [ "${SKIP_UPLOADS}" = "false" ]; then
|
|
log "[uploads] Restauration de public/uploads..."
|
|
if [ -d "${SCRIPT_DIR}/public/uploads" ]; then
|
|
SAFETY="${SCRIPT_DIR}/public/uploads.before_restore_$(date +%Y%m%d_%H%M%S)"
|
|
log " Sauvegarde de securite: ${SAFETY}"
|
|
mv "${SCRIPT_DIR}/public/uploads" "${SAFETY}"
|
|
fi
|
|
mkdir -p "${SCRIPT_DIR}/public"
|
|
if ! tar -xzf "${EXTRACTED_DIR}/public_uploads.tar.gz" -C "${SCRIPT_DIR}/public"; then
|
|
err "Echec d'extraction de public_uploads.tar.gz"
|
|
exit 1
|
|
fi
|
|
log " OK"
|
|
fi
|
|
|
|
# --- 4. var/ (PDFs generes, unsubscribed.json...) ---
|
|
if [ "${HAS_VAR}" = "true" ] && [ "${SKIP_VAR}" = "false" ]; then
|
|
log "[var] Restauration de var/ (payouts, billets, invoices, unsubscribed.json)..."
|
|
mkdir -p "${SCRIPT_DIR}/var"
|
|
# Sauvegarde de securite des elements existants qui seront ecrases
|
|
SAFETY_VAR="${SCRIPT_DIR}/var/.before_restore_$(date +%Y%m%d_%H%M%S)"
|
|
mkdir -p "${SAFETY_VAR}"
|
|
for item in payouts billets invoices unsubscribed.json; do
|
|
if [ -e "${SCRIPT_DIR}/var/${item}" ]; then
|
|
mv "${SCRIPT_DIR}/var/${item}" "${SAFETY_VAR}/"
|
|
fi
|
|
done
|
|
# Si rien n'a ete sauvegarde, on supprime le dossier vide
|
|
if [ -z "$(ls -A "${SAFETY_VAR}")" ]; then
|
|
rmdir "${SAFETY_VAR}"
|
|
else
|
|
log " Sauvegarde de securite: ${SAFETY_VAR}"
|
|
fi
|
|
if ! tar -xzf "${EXTRACTED_DIR}/var_state.tar.gz" -C "${SCRIPT_DIR}/var"; then
|
|
err "Echec d'extraction de var_state.tar.gz"
|
|
exit 1
|
|
fi
|
|
log " OK"
|
|
fi
|
|
|
|
# --- 5. Purge du cache Symfony ---
|
|
# Indispensable: var/cache contient des fichiers compiles avec les anciens
|
|
# uid/gid et peut empecher PHP d'ecrire au demarrage suivant.
|
|
if [ -d "${SCRIPT_DIR}/var/cache" ]; then
|
|
log "[cache] Purge de var/cache (sera reconstruit par PHP au demarrage)..."
|
|
rm -rf "${SCRIPT_DIR}/var/cache"
|
|
log " OK"
|
|
fi
|
|
|
|
# --- 6. Chown final pour matcher l'utilisateur PHP du conteneur ---
|
|
if [ "${SKIP_CHOWN}" = "false" ]; then
|
|
log "[chown] Application de '${RESTORE_OWNER}' sur les fichiers restaures..."
|
|
CHOWN_TARGETS=()
|
|
[ -d "${SCRIPT_DIR}/var" ] && CHOWN_TARGETS+=("${SCRIPT_DIR}/var")
|
|
[ -d "${SCRIPT_DIR}/public/uploads" ] && CHOWN_TARGETS+=("${SCRIPT_DIR}/public/uploads")
|
|
[ -d "${SCRIPT_DIR}/config/cert" ] && CHOWN_TARGETS+=("${SCRIPT_DIR}/config/cert")
|
|
[ -f "${SCRIPT_DIR}/.env.local" ] && CHOWN_TARGETS+=("${SCRIPT_DIR}/.env.local")
|
|
|
|
if [ ${#CHOWN_TARGETS[@]} -gt 0 ]; then
|
|
chown -R "${RESTORE_OWNER}" "${CHOWN_TARGETS[@]}"
|
|
log " OK - cibles: ${CHOWN_TARGETS[*]}"
|
|
else
|
|
log " Aucune cible a chown"
|
|
fi
|
|
else
|
|
log "[chown] Saut du chown final (--skip-chown)"
|
|
log " ATTENTION: PHP pourrait ne pas avoir les droits d'ecriture sur var/, public/uploads/..."
|
|
fi
|
|
|
|
log "=== Migration terminee avec succes ==="
|
|
log
|
|
log "Etapes recommandees post-migration:"
|
|
log " 1. make build_prod (si l'image PHP n'a pas ete reconstruite avec le bon UID)"
|
|
log " 2. make start_prod (si pas deja fait)"
|
|
log " 3. make migrate_prod (appliquer les migrations Doctrine eventuelles)"
|
|
log " 4. make clear_prod (vider le cache)"
|
|
log " 5. Verifier le bon fonctionnement de l'application"
|