#!/usr/bin/env bash set -Eeuo pipefail trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR usage() { cat <<'EOF' Usage: ./scripts/update-proxmox-lxc.sh \ --proxmox-host 192.168.1.10 \ --proxmox-user root@pam \ --proxmox-password 'motdepasse' \ --ctid 120 ./scripts/update-proxmox-lxc.sh --local --ctid 120 Options principales: --proxmox-host IP ou nom DNS du serveur Proxmox --proxmox-user Utilisateur SSH Proxmox (defaut: root@pam) --proxmox-password Mot de passe SSH Proxmox --ssh-port Port SSH Proxmox (defaut: 22) --local Execute directement sur l'hote Proxmox local --ctid CTID du LXC a mettre a jour --hostname Nom du LXC si le CTID n'est pas fourni (defaut: chesscubing-web) --disk-gb Taille disque cible du LXC en Go si un agrandissement est necessaire --repo-url Depot Git principal a deployer --branch Branche Git a deployer (defaut: main) --ethan-repo-url Depot Git de l'application Ethan --ethan-branch Branche Git de l'application Ethan (defaut: main) --brice-repo-url Depot Git de l'application Brice --brice-branch Branche Git de l'application Brice (defaut: main) --public-base-url URL publique du site (ex: http://jeu.example.com) --web-port Port HTTP expose dans le LXC (defaut: conserve ou 80) --keycloak-admin-user Utilisateur admin Keycloak a forcer --keycloak-admin-password Mot de passe admin Keycloak a forcer -h, --help Affiche cette aide EOF } die() { printf 'Erreur: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "La commande '$1' est requise." } PROXMOX_HOST="" PROXMOX_USER="root@pam" PROXMOX_PASSWORD="${PROXMOX_PASSWORD:-}" PROXMOX_PORT="22" LOCAL_MODE="0" CTID="" LXC_HOSTNAME="chesscubing-web" TARGET_DISK_GB="" REPO_URL="https://git.jeannerot.fr/christophe/chesscubing.git" REPO_BRANCH="main" ETHAN_REPO_URL="https://git.jeannerot.fr/Mineloulou/Chesscubing.git" ETHAN_REPO_BRANCH="main" BRICE_REPO_URL="https://git.jeannerot.fr/Lescratcheur/ChessCubing.git" BRICE_REPO_BRANCH="main" PUBLIC_BASE_URL="" WEB_PORT="" KEYCLOAK_ADMIN_USER="" KEYCLOAK_ADMIN_PASSWORD="" while [[ $# -gt 0 ]]; do case "$1" in --proxmox-host) PROXMOX_HOST="${2:-}" shift 2 ;; --proxmox-user) PROXMOX_USER="${2:-}" shift 2 ;; --proxmox-password) PROXMOX_PASSWORD="${2:-}" shift 2 ;; --ssh-port) PROXMOX_PORT="${2:-}" shift 2 ;; --local) LOCAL_MODE="1" shift ;; --ctid) CTID="${2:-}" shift 2 ;; --hostname) LXC_HOSTNAME="${2:-}" shift 2 ;; --disk-gb) TARGET_DISK_GB="${2:-}" shift 2 ;; --repo-url) REPO_URL="${2:-}" shift 2 ;; --branch) REPO_BRANCH="${2:-}" shift 2 ;; --ethan-repo-url) ETHAN_REPO_URL="${2:-}" shift 2 ;; --ethan-branch) ETHAN_REPO_BRANCH="${2:-}" shift 2 ;; --brice-repo-url) BRICE_REPO_URL="${2:-}" shift 2 ;; --brice-branch) BRICE_REPO_BRANCH="${2:-}" shift 2 ;; --public-base-url) PUBLIC_BASE_URL="${2:-}" shift 2 ;; --web-port) WEB_PORT="${2:-}" shift 2 ;; --keycloak-admin-user) KEYCLOAK_ADMIN_USER="${2:-}" shift 2 ;; --keycloak-admin-password) KEYCLOAK_ADMIN_PASSWORD="${2:-}" shift 2 ;; -h | --help) usage exit 0 ;; *) die "Option inconnue: $1" ;; esac done if [[ "$LOCAL_MODE" != "1" && -z "$PROXMOX_HOST" ]]; then if command -v pct >/dev/null 2>&1 && command -v pveam >/dev/null 2>&1; then LOCAL_MODE="1" fi fi payload_script="$(mktemp)" cleanup() { rm -f "$payload_script" } trap cleanup EXIT cat >"$payload_script" <<'REMOTE' set -Eeuo pipefail trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR ctid="$1" lxc_hostname="$2" repo_url="$3" repo_branch="$4" ethan_repo_url="$5" ethan_repo_branch="$6" brice_repo_url="$7" brice_repo_branch="$8" public_base_url="$9" web_port="${10}" keycloak_admin_user="${11}" keycloak_admin_password="${12}" target_disk_gb="${13}" die() { printf 'Erreur: %s\n' "$*" >&2 exit 1 } size_to_mb() { local raw="${1^^}" case "$raw" in *T) echo $(( ${raw%T} * 1024 * 1024 )) ;; *G) echo $(( ${raw%G} * 1024 )) ;; *M) echo $(( ${raw%M} )) ;; *K) echo $(( ${raw%K} / 1024 )) ;; *) echo "$raw" ;; esac } get_current_rootfs_mb() { local size size="$(pct config "$ctid" | awk -F'[:,=]' '/^rootfs:/ { for (i = 1; i <= NF; i++) { if ($i == "size") { print $(i + 1); exit } } }')" [[ -n "$size" ]] || return 1 size_to_mb "$size" } ensure_rootfs_capacity() { local requested_gb="$1" local current_mb local requested_mb local delta_mb local delta_gb local current_gb [[ -n "$requested_gb" ]] || return 0 [[ "$requested_gb" =~ ^[0-9]+$ ]] || die "La valeur de --disk-gb doit etre un entier en Go." current_mb="$(get_current_rootfs_mb)" || die "Impossible de lire la taille actuelle du disque rootfs du LXC." requested_mb=$(( requested_gb * 1024 )) if (( current_mb >= requested_mb )); then return 0 fi delta_mb=$(( requested_mb - current_mb )) delta_gb=$(( (delta_mb + 1023) / 1024 )) current_gb=$(( (current_mb + 1023) / 1024 )) printf 'Agrandissement du disque rootfs du LXC: %sG -> %sG (+%sG).\n' "$current_gb" "$requested_gb" "$delta_gb" pct resize "$ctid" rootfs "+${delta_gb}G" >/dev/null } find_ctid_by_hostname() { local wanted="$1" local candidate="" local candidate_hostname="" while read -r candidate; do [[ -n "$candidate" ]] || continue candidate_hostname="$(pct config "$candidate" 2>/dev/null | awk -F ': ' '/^hostname:/ { print $2; exit }')" if [[ "$candidate_hostname" == "$wanted" ]]; then printf '%s\n' "$candidate" return 0 fi done < <(pct list | awk 'NR > 1 { print $1 }') return 1 } command -v pct >/dev/null 2>&1 || die "La commande 'pct' est absente sur Proxmox." if [[ -z "$ctid" ]]; then ctid="$(find_ctid_by_hostname "$lxc_hostname" || true)" fi [[ -n "$ctid" ]] || die "Impossible de retrouver le LXC. Passe --ctid ou --hostname." if ! pct status "$ctid" >/dev/null 2>&1; then die "Le conteneur $ctid est introuvable." fi detected_hostname="$(pct config "$ctid" 2>/dev/null | awk -F ': ' '/^hostname:/ { print $2; exit }')" if [[ -n "$detected_hostname" ]]; then lxc_hostname="$detected_hostname" fi ensure_rootfs_capacity "$target_disk_gb" if pct status "$ctid" | grep -q "running"; then pct stop "$ctid" fi pct set "$ctid" --features nesting=1,keyctl=1 >/dev/null pct start "$ctid" for _ in $(seq 1 20); do if pct exec "$ctid" -- true >/dev/null 2>&1; then break fi sleep 2 done pct exec "$ctid" -- true >/dev/null 2>&1 || die "Le LXC n'est pas joignable apres le redemarrage." ct_exec() { pct exec "$ctid" -- bash -lc "$1" } ct_exec "install -d -m 0755 /opt/chesscubing/repo /opt/chesscubing/ethan-repo /opt/chesscubing/brice-repo /opt/chesscubing/deploy /opt/chesscubing/config" ct_exec "cat > /usr/local/bin/update-chesscubing <<'SCRIPT' #!/usr/bin/env bash set -Eeuo pipefail trap 'printf \"Erreur: echec de la commande [%s] a la ligne %s.\\n\" \"\$BASH_COMMAND\" \"\$LINENO\" >&2' ERR main_repo_dir='/opt/chesscubing/repo' ethan_repo_dir='/opt/chesscubing/ethan-repo' brice_repo_dir='/opt/chesscubing/brice-repo' deploy_dir='/opt/chesscubing/deploy' config_dir='/opt/chesscubing/config' env_file=\"\$config_dir/chesscubing.env\" main_branch=\"\${1:-${repo_branch}}\" public_base_url_override=\"\${2:-}\" web_port_override=\"\${3:-}\" keycloak_admin_user_override=\"\${4:-}\" keycloak_admin_password_override=\"\${5:-}\" main_repo_url='${repo_url}' ethan_repo_url='${ethan_repo_url}' ethan_branch='${ethan_repo_branch}' brice_repo_url='${brice_repo_url}' brice_branch='${brice_repo_branch}' random_secret() { od -An -N24 -tx1 /dev/urandom | tr -d ' \n' } ensure_base_packages() { apt-get update apt-get install -y ca-certificates curl gpg git rsync } ensure_docker_stack() { ensure_base_packages if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then install -m 0755 -d /etc/apt/keyrings if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc fi if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian %s stable\n' \ \"\$(dpkg --print-architecture)\" \ \"\$(. /etc/os-release && printf '%s' \"\$VERSION_CODENAME\")\" > /etc/apt/sources.list.d/docker.list fi apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin fi systemctl enable docker >/dev/null 2>&1 || true systemctl restart docker } sync_git_repo() { local repo_dir=\"\$1\" local repo_url=\"\$2\" local branch=\"\$3\" local label=\"\$4\" if [[ -d \"\$repo_dir/.git\" ]]; then cd \"\$repo_dir\" if ! git diff --quiet || ! git diff --cached --quiet; then echo \"Le depot \${label} contient des modifications locales. Mise a jour annulee.\" >&2 exit 1 fi git fetch origin \"\$branch\" if git show-ref --verify --quiet \"refs/heads/\$branch\"; then git checkout \"\$branch\" else git checkout -b \"\$branch\" --track \"origin/\$branch\" fi git pull --ff-only origin \"\$branch\" return 0 fi [[ -n \"\$repo_url\" ]] || { echo \"Le depot \${label} est absent et aucune URL n'a ete fournie.\" >&2 exit 1 } rm -rf \"\$repo_dir\" git clone --branch \"\$branch\" --single-branch \"\$repo_url\" \"\$repo_dir\" } get_env_var() { local key=\"\$1\" [[ -f \"\$env_file\" ]] || return 0 awk -F= -v wanted=\"\$key\" '\$1 == wanted { print substr(\$0, length(wanted) + 2); exit }' \"\$env_file\" } set_env_var() { local key=\"\$1\" local value=\"\$2\" touch \"\$env_file\" if grep -q \"^\${key}=\" \"\$env_file\" 2>/dev/null; then sed -i \"s|^\${key}=.*|\${key}=\${value}|\" \"\$env_file\" else printf '%s=%s\n' \"\$key\" \"\$value\" >> \"\$env_file\" fi } ensure_env_default() { local key=\"\$1\" local fallback=\"\$2\" local value value=\"\$(get_env_var \"\$key\")\" if [[ -z \"\$value\" ]]; then value=\"\$fallback\" fi set_env_var \"\$key\" \"\$value\" } normalize_public_base_url() { local value=\"\$1\" printf '%s\n' \"\${value%/}\" } build_default_public_base_url() { local port=\"\$1\" local detected_ip detected_ip=\"\$(hostname -I | awk '{print \$1}')\" [[ -n \"\$detected_ip\" ]] || return 1 if [[ \"\$port\" == \"80\" ]]; then printf 'http://%s\n' \"\$detected_ip\" else printf 'http://%s:%s\n' \"\$detected_ip\" \"\$port\" fi } configure_env_file() { local current_value local effective_web_port local effective_public_base_url local effective_keycloak_admin_user local effective_keycloak_admin_password effective_web_port=\"\$web_port_override\" if [[ -z \"\$effective_web_port\" ]]; then effective_web_port=\"\$(get_env_var WEB_PORT)\" fi if [[ -z \"\$effective_web_port\" ]]; then effective_web_port='80' fi set_env_var WEB_PORT \"\$effective_web_port\" effective_public_base_url=\"\$public_base_url_override\" if [[ -z \"\$effective_public_base_url\" ]]; then effective_public_base_url=\"\$(get_env_var PUBLIC_BASE_URL)\" fi if [[ -z \"\$effective_public_base_url\" ]]; then effective_public_base_url=\"\$(build_default_public_base_url \"\$effective_web_port\" || true)\" fi if [[ -z \"\$effective_public_base_url\" ]]; then if [[ \"\$effective_web_port\" == \"80\" ]]; then effective_public_base_url='http://localhost' else effective_public_base_url=\"http://localhost:\$effective_web_port\" fi fi set_env_var PUBLIC_BASE_URL \"\$(normalize_public_base_url \"\$effective_public_base_url\")\" effective_keycloak_admin_user=\"\$keycloak_admin_user_override\" if [[ -z \"\$effective_keycloak_admin_user\" ]]; then effective_keycloak_admin_user=\"\$(get_env_var KEYCLOAK_ADMIN_USER)\" fi if [[ -z \"\$effective_keycloak_admin_user\" ]]; then effective_keycloak_admin_user='admin' fi set_env_var KEYCLOAK_ADMIN_USER \"\$effective_keycloak_admin_user\" effective_keycloak_admin_password=\"\$keycloak_admin_password_override\" if [[ -z \"\$effective_keycloak_admin_password\" ]]; then effective_keycloak_admin_password=\"\$(get_env_var KEYCLOAK_ADMIN_PASSWORD)\" fi if [[ -z \"\$effective_keycloak_admin_password\" ]]; then effective_keycloak_admin_password=\"\$(random_secret)\" fi set_env_var KEYCLOAK_ADMIN_PASSWORD \"\$effective_keycloak_admin_password\" ensure_env_default KEYCLOAK_DB_NAME keycloak ensure_env_default KEYCLOAK_DB_USER keycloak current_value=\"\$(get_env_var KEYCLOAK_DB_PASSWORD)\" if [[ -z \"\$current_value\" ]]; then current_value=\"\$(random_secret)\" fi set_env_var KEYCLOAK_DB_PASSWORD \"\$current_value\" ensure_env_default SITE_DB_NAME chesscubing_site ensure_env_default SITE_DB_USER chesscubing current_value=\"\$(get_env_var SITE_DB_PASSWORD)\" if [[ -z \"\$current_value\" ]]; then current_value=\"\$(random_secret)\" fi set_env_var SITE_DB_PASSWORD \"\$current_value\" current_value=\"\$(get_env_var SITE_DB_ROOT_PASSWORD)\" if [[ -z \"\$current_value\" ]]; then current_value=\"\$(random_secret)\" fi set_env_var SITE_DB_ROOT_PASSWORD \"\$current_value\" } sync_deploy_tree() { install -d -m 0755 \"\$deploy_dir\" \"\$config_dir\" rsync -a --delete \ --exclude='.git/' \ --exclude='bin/' \ --exclude='obj/' \ --exclude='.env' \ --exclude='node_modules/' \ \"\$main_repo_dir/\" \"\$deploy_dir/\" if [[ -d \"\$ethan_repo_dir/.git\" ]]; then rm -rf \"\$deploy_dir/ethan\" rsync -a --delete \ --exclude='.git/' \ --exclude='node_modules/' \ \"\$ethan_repo_dir/\" \"\$deploy_dir/ethan/\" fi if [[ -d \"\$brice_repo_dir/.git\" ]]; then rm -rf \"\$deploy_dir/brice\" rsync -a --delete \ --exclude='.git/' \ --exclude='node_modules/' \ \"\$brice_repo_dir/\" \"\$deploy_dir/brice/\" fi } disable_legacy_nginx() { systemctl disable --now nginx >/dev/null 2>&1 || true } prepare_disk_space() { cd \"\$deploy_dir\" docker compose down || true docker system prune -af || true apt-get clean || true rm -rf /var/lib/apt/lists/* || true } ensure_free_space_mb() { local required_mb=\"\$1\" local available_mb available_mb=\"\$(df -Pm / | awk 'NR == 2 { print \$4 }')\" if [[ -z \"\$available_mb\" ]]; then echo \"Impossible de mesurer l'espace disque libre dans le LXC.\" >&2 exit 1 fi if (( available_mb < required_mb )); then echo \"Espace disque insuffisant dans le LXC: \${available_mb} Mo libres, \${required_mb} Mo recommandes avant la reconstruction Docker.\" >&2 echo \"Relance le script avec --disk-gb 16 ou une valeur plus elevee si besoin.\" >&2 exit 1 fi } deploy_stack() { cp \"\$env_file\" \"\$deploy_dir/.env\" prepare_disk_space ensure_free_space_mb 6144 cd \"\$deploy_dir\" docker compose up -d --build docker compose ps } ensure_docker_stack sync_git_repo \"\$main_repo_dir\" \"\$main_repo_url\" \"\$main_branch\" 'principal' sync_git_repo \"\$ethan_repo_dir\" \"\$ethan_repo_url\" \"\$ethan_branch\" 'Ethan' sync_git_repo \"\$brice_repo_dir\" \"\$brice_repo_url\" \"\$brice_branch\" 'Brice' sync_deploy_tree configure_env_file disable_legacy_nginx deploy_stack SCRIPT chmod +x /usr/local/bin/update-chesscubing" ct_exec "/usr/local/bin/update-chesscubing '$repo_branch' '$public_base_url' '$web_port' '$keycloak_admin_user' '$keycloak_admin_password'" container_ip="$(pct exec "$ctid" -- bash -lc "hostname -I | awk '{print \$1}'" 2>/dev/null | tr -d '\r' || true)" public_url="$(pct exec "$ctid" -- bash -lc "awk -F= '/^PUBLIC_BASE_URL=/{print substr(\$0, 17); exit}' /opt/chesscubing/config/chesscubing.env" 2>/dev/null | tr -d '\r' || true)" cat <} - URL publique configuree: ${public_url:-http://${container_ip:-}} EOF REMOTE if [[ "$LOCAL_MODE" == "1" ]]; then printf 'Mise a jour du LXC ChessCubing en local sur cet hote Proxmox...\n' bash "$payload_script" \ "$CTID" \ "$LXC_HOSTNAME" \ "$REPO_URL" \ "$REPO_BRANCH" \ "$ETHAN_REPO_URL" \ "$ETHAN_REPO_BRANCH" \ "$BRICE_REPO_URL" \ "$BRICE_REPO_BRANCH" \ "$PUBLIC_BASE_URL" \ "$WEB_PORT" \ "$KEYCLOAK_ADMIN_USER" \ "$KEYCLOAK_ADMIN_PASSWORD" \ "$TARGET_DISK_GB" exit 0 fi [[ -n "$PROXMOX_HOST" ]] || die "Merci de fournir --proxmox-host." [[ -n "$PROXMOX_USER" ]] || die "Merci de fournir --proxmox-user." if [[ -z "$PROXMOX_PASSWORD" ]]; then read -rsp "Mot de passe SSH pour ${PROXMOX_USER}@${PROXMOX_HOST}: " PROXMOX_PASSWORD echo fi need_cmd ssh need_cmd sshpass printf 'Mise a jour du LXC ChessCubing sur %s...\n' "$PROXMOX_HOST" sshpass -p "$PROXMOX_PASSWORD" \ ssh \ -p "$PROXMOX_PORT" \ -o StrictHostKeyChecking=accept-new \ -o PreferredAuthentications=password \ -o PubkeyAuthentication=no \ "$PROXMOX_USER@$PROXMOX_HOST" \ bash -s -- \ "$CTID" \ "$LXC_HOSTNAME" \ "$REPO_URL" \ "$REPO_BRANCH" \ "$ETHAN_REPO_URL" \ "$ETHAN_REPO_BRANCH" \ "$BRICE_REPO_URL" \ "$BRICE_REPO_BRANCH" \ "$PUBLIC_BASE_URL" \ "$WEB_PORT" \ "$KEYCLOAK_ADMIN_USER" \ "$KEYCLOAK_ADMIN_PASSWORD" \ "$TARGET_DISK_GB" < "$payload_script"