#!/usr/bin/env bash # ============================================================================== # install.sh — Admin Edu-Space (UniCABA) # Instalador automatizado para Proxmox VE (LXC) / Debian 12 / Ubuntu (entorno venv) # Uso: sudo bash install.sh # ============================================================================== set -euo pipefail # ─── Colores y Helpers ───────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' ok() { echo -e "${GREEN} ✔ $*${RESET}"; } info() { echo -e "${CYAN} ▸ $*${RESET}"; } warn() { echo -e "${YELLOW} ⚠ $*${RESET}"; } err() { echo -e "${RED} ✘ $*${RESET}" >&2; exit 1; } hdr() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; } # ─── Variables Principales ───────────────────────────────────────────────────── APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VENV_DIR="${APP_DIR}/venv" APP_ENV="${APP_DIR}/.env" SERVICE_NAME="admin-edu-space" APP_USER="eduspace" PYTHON_REQUIRED_MAJOR=3 PYTHON_REQUIRED_MINOR=10 LOG_DIR="/var/log/${SERVICE_NAME}" SQL_BACKUP_FILE="${APP_DIR}/classrooms_db.sql" # ─── Verificación de Permisos Root ───────────────────────────────────────────── [[ $EUID -ne 0 ]] && err "Ejecutá este script con permisos de root: sudo bash install.sh" # Si el directorio está dentro de /root, lo trasladamos a /opt para evitar problemas de permisos con el usuario de servicio if [[ "${APP_DIR}" == /root/* ]]; then NEW_DIR="/opt/admin-edu-space" warn "Detectado directorio en ${APP_DIR}. Trasladando a ${NEW_DIR} para evitar bloqueos de permisos..." systemctl stop "${SERVICE_NAME}" 2>/dev/null || true # Limpiar cualquier subdirectorio anidado previo si quedó de un intento fallido rm -rf "${NEW_DIR}/admin-edu-space" 2>/dev/null || true mkdir -p "${NEW_DIR}" # Copiar recursivamente todo el contenido (incluyendo dotfiles) directamente a NEW_DIR cp -a "${APP_DIR}/." "${NEW_DIR}/" cd "${NEW_DIR}" rm -rf "${APP_DIR}" 2>/dev/null || true exec bash install.sh fi # Limpiar posible subdirectorio anidado residual en caso de ejecuciones previas rm -rf "${APP_DIR}/admin-edu-space" 2>/dev/null || true [[ ! -f "${APP_DIR}/wsgi.py" ]] && err "No se encontró wsgi.py en ${APP_DIR}. ¿Estás en el directorio raíz del proyecto?" echo -e "\n${BOLD}${CYAN}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Admin Edu-Space (UniCABA) — Proxmox LXC Installer ║" echo "║ Despliegue Nativo con venv + Motor PostgreSQL ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${RESET}" OS_NAME=$(grep -oP '(?<=^PRETTY_NAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || uname -s) info "Sistema Operativo: ${OS_NAME}" info "Directorio del proyecto: ${APP_DIR}" info "Servicio systemd: ${SERVICE_NAME}" info "Usuario de servicio: ${APP_USER}" # ─── 1. Actualizar sistema e instalar dependencias base ──────────────────────── hdr "1. Actualizando repositorios y paquetes base" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ curl wget git ca-certificates gnupg lsb-release \ build-essential libpq-dev \ locales \ > /dev/null 2>&1 ok "Paquetes base instalados" # ─── 2. Configurar Locale UTF-8 ──────────────────────────────────────────────── hdr "2. Configurando soporte de codificación (Locales UTF-8)" if [ -f /etc/locale.gen ]; then sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen sed -i 's/# es_AR.UTF-8 UTF-8/es_AR.UTF-8 UTF-8/' /etc/locale.gen locale-gen en_US.UTF-8 es_AR.UTF-8 > /dev/null 2>&1 || true fi update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 > /dev/null 2>&1 || true export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 ok "Locales configurados: en_US.UTF-8 / es_AR.UTF-8" # ─── 3. Instalar Python 3.10+ y entorno de desarrollo ────────────────────────── hdr "3. Verificando entorno Python" apt-get install -y -qq python3 python3-venv python3-pip python3-dev > /dev/null 2>&1 PY_VERSION=$(python3 --version 2>/dev/null | awk '{print $2}' || echo "0.0") PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) if [[ "$PY_MAJOR" -ge "$PYTHON_REQUIRED_MAJOR" && "$PY_MINOR" -ge "$PYTHON_REQUIRED_MINOR" ]]; then ok "Python $PY_VERSION disponible" else info "Python $PY_VERSION es menor a 3.10 — instalando Python 3.11..." apt-get install -y -qq python3.11 python3.11-venv python3.11-dev > /dev/null 2>&1 update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 > /dev/null 2>&1 PY_VERSION=$(python3 --version | awk '{print $2}') ok "Python $PY_VERSION instalado" fi # ─── 4. Instalar y Levantar Motor PostgreSQL ─────────────────────────────────── hdr "4. Instalando y configurando motor de base de datos PostgreSQL" apt-get install -y -qq postgresql postgresql-contrib postgresql-client > /dev/null 2>&1 PG_VER=$(psql --version 2>/dev/null | awk '{print $3}' || echo "instalado") ok "Motor PostgreSQL instalado: ${PG_VER}" # Levantar servicio de PostgreSQL asegurando compatibilidad con contenedores LXC y VMs info "Iniciando servicio de PostgreSQL..." systemctl start postgresql 2>/dev/null \ || service postgresql start 2>/dev/null \ || su - postgres -c "pg_ctlcluster $(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1) main start" 2>/dev/null \ || true # Esperar a que el socket de PostgreSQL esté completamente activo PG_READY=false for i in {1..15}; do if sudo -u postgres psql -c '\q' >/dev/null 2>&1; then PG_READY=true break fi sleep 1 done if [ "$PG_READY" = true ]; then # Configurar pg_hba.conf para permitir conexiones locales (socket y 127.0.0.1) sin fallos de autenticación PG_HBA=$(find /etc/postgresql/ -name pg_hba.conf 2>/dev/null | sort -V | tail -n1) if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then sed -i -E 's/^(local\s+all\s+all\s+)peer/\1trust/' "$PG_HBA" 2>/dev/null || true sed -i -E 's/^(host\s+all\s+all\s+127\.0\.0\.1\/32\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true sed -i -E 's/^(host\s+all\s+all\s+::1\/128\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true systemctl reload postgresql 2>/dev/null || true fi systemctl enable postgresql > /dev/null 2>&1 || true ok "PostgreSQL activo y aceptando conexiones" else err "No se pudo conectar al motor PostgreSQL. Verificá los registros del sistema." fi # ─── 5. Configurar Variables de Entorno (.env) ───────────────────────────────── hdr "5. Configurando variables de entorno (.env)" # Valores predeterminados (paridad con sistema activo) DB_NAME="classrooms_db" DB_USER="postgres" DB_PASSWORD="password" if [[ -f "$APP_ENV" ]]; then ok ".env ya existe — se preserva la configuración actual" set -a # shellcheck disable=SC1090 source <(grep -v '^#' "$APP_ENV" | grep '=') set +a # Extraer credenciales desde DATABASE_URL si está presente if [[ -n "${DATABASE_URL:-}" ]]; then URL_USER=$(echo "$DATABASE_URL" | sed -E 's|^.*://([^:@]+).*|\1|') URL_PASS=$(echo "$DATABASE_URL" | sed -E 's|^.*://[^:]+:([^@]+)@.*|\1|') URL_NAME=$(echo "$DATABASE_URL" | sed -E 's|^.*/([^?]+).*|\1|') if [[ "$URL_USER" != "$DATABASE_URL" && -n "$URL_USER" ]]; then DB_USER="$URL_USER" fi if [[ "$URL_PASS" != "$DATABASE_URL" && -n "$URL_PASS" ]]; then DB_PASSWORD="$URL_PASS" fi if [[ "$URL_NAME" != "$DATABASE_URL" && -n "$URL_NAME" ]]; then DB_NAME="$URL_NAME" fi fi DB_USER="${DB_USER:-${DB_USERNAME:-postgres}}" DB_PASSWORD="${DB_PASSWORD:-password}" DB_NAME="${DB_NAME:-classrooms_db}" else SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') cat > "$APP_ENV" </dev/null 2>&1 \ || sudo -u postgres psql -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \ || true sudo -u postgres psql -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true fi done sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true # Crear base de datos sudo -u postgres psql -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" >/dev/null 2>&1 \ || info "La base de datos '${DB_NAME}' ya existía." for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do if [[ -n "$u" ]]; then sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" >/dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" >/dev/null 2>&1 || true fi done ok "Motor PostgreSQL configurado (usuario: ${DB_USER}, base: ${DB_NAME})" # ─── 7. Crear Entorno Virtual e Instalar Dependencias ────────────────────────── hdr "7. Creando virtualenv e instalando dependencias Python" if [[ -d "$VENV_DIR" ]]; then info "Recreando entorno virtual para asegurar dependencias limpias..." rm -rf "$VENV_DIR" fi python3 -m venv "$VENV_DIR" > /dev/null 2>&1 "${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel --quiet info "Instalando paquetes desde requirements.txt..." if "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" --quiet 2>/dev/null; then ok "Dependencias instaladas correctamente" else warn "Reintentando instalación con --no-build-isolation (compatibilidad Python 3.12/3.13)..." "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" --no-build-isolation --quiet ok "Dependencias instaladas" fi # ─── 8. Despliegue de Datos: Restauración o Inicialización ───────────────────── hdr "8. Desplegando esquema y datos de la aplicación" cd "$APP_DIR" if [[ -f "$SQL_BACKUP_FILE" ]]; then BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}') info "Se detectó snapshot de base de datos '${SQL_BACKUP_FILE}' (${BACKUP_SIZE})." info "Restaurando volcado integral de aulas, comisiones, materias, reservas y usuarios..." # Recrear base de datos limpia para carga sin colisiones sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true sudo -u postgres psql -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1 # Restaurar backup filtrando directivas específicas de PG18 si existiesen if sed '/^\\restrict/d' "$SQL_BACKUP_FILE" | sudo -u postgres psql -d "${DB_NAME}" > /dev/null 2>&1; then ok "Volcado SQL restaurado exitosamente." else warn "Restauración completada con avisos menores — aplicando permisos..." fi # Otorgar propiedad y permisos absolutos sobre todas las tablas y secuencias al usuario de la app info "Garantizando permisos y secuencias para ${DB_USER}..." for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do if [[ -n "$u" ]]; then sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO \"${u}\";" > /dev/null 2>&1 || true fi done if [[ "${DB_USER}" != "postgres" ]]; then sudo -u postgres psql -d "${DB_NAME}" -c "ALTER SCHEMA public OWNER TO \"${DB_USER}\";" > /dev/null 2>&1 || true fi # Ejecutar init_db.py de forma idempotente para verificar roles y migraciones incrementales info "Validando coherencia de modelos con init_db.py..." LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py ok "Base de datos sincronizada y verificada" else info "No se encontró classrooms_db.sql. Creando esquema desde cero con init_db.py..." LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py ok "Esquema, roles RBAC, edificios y usuario admin creados" fi cd - > /dev/null # ─── 9. Usuario del Sistema y Permisos de Directorio ─────────────────────────── hdr "9. Configurando permisos y usuario de ejecución del sistema" if id "$APP_USER" &>/dev/null; then info "El usuario de sistema '${APP_USER}' ya existe." else useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" ok "Usuario de sistema '${APP_USER}' creado (sin shell)" fi mkdir -p "${APP_DIR}/app/static/uploads" chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" chmod -R 755 "$APP_DIR" chmod -R 775 "${APP_DIR}/app/static/uploads" chmod 640 "$APP_ENV" mkdir -p "$LOG_DIR" chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" chmod 755 "$LOG_DIR" ok "Permisos de seguridad aplicados en ${APP_DIR} y ${LOG_DIR}" # ─── 10. Servicio Systemd (Gunicorn de Producción) ───────────────────────────── hdr "10. Configurando servicio systemd de arranque automático" GUNICORN_BIN="${VENV_DIR}/bin/gunicorn" cat > "/etc/systemd/system/${SERVICE_NAME}.service" < /dev/null 2>&1 systemctl restart "${SERVICE_NAME}" sleep 3 if systemctl is-active --quiet "$SERVICE_NAME"; then ok "Servicio '${SERVICE_NAME}' activo y en ejecución" # Validar respuesta HTTP local info "Verificando respuesta del servidor web..." sleep 2 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/ || echo "000") if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "302" ]]; then ok "Servidor web respondiendo correctamente (HTTP ${HTTP_CODE})" else warn "El servidor web devolvió código HTTP ${HTTP_CODE}. Podés revisar los logs con: journalctl -u ${SERVICE_NAME} -n 30 --no-pager" fi else warn "El servicio tardó en arrancar. Podés revisar los logs con:" warn " journalctl -u ${SERVICE_NAME} -n 50 --no-pager" fi # ─── Resumen Final y Datos de Acceso ────────────────────────────────────────── hdr "¡Despliegue Completado con Éxito!" IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1") echo -e " ${BOLD}Acceso Web a la Aplicación:${RESET}" echo -e " ${CYAN}http://${IP}:5000${RESET} o ${CYAN}http://localhost:5000${RESET}" echo "" echo -e " ${BOLD}Credenciales Iniciales de Administrador:${RESET}" echo -e " Email: ${CYAN}admin@edu-space.com${RESET}" echo -e " Password: ${CYAN}admin123${RESET}" echo "" echo -e " ${BOLD}Base de Datos Desplegada:${RESET}" echo -e " Motor: PostgreSQL (${PG_VER})" echo -e " Base: ${DB_NAME}" echo -e " Usuario: ${DB_USER}" echo -e " Snapshot: $( [ -f "$SQL_BACKUP_FILE" ] && echo "Restaurada desde classrooms_db.sql" || echo "Inicializada limpia" )" echo "" echo -e " ${BOLD}Comandos de Gestión del Servicio:${RESET}" echo -e " Estado: ${CYAN}systemctl status ${SERVICE_NAME}${RESET}" echo -e " Logs: ${CYAN}journalctl -u ${SERVICE_NAME} -f${RESET}" echo -e " Reiniciar: ${CYAN}systemctl restart ${SERVICE_NAME}${RESET}" echo -e " Detener: ${CYAN}systemctl stop ${SERVICE_NAME}${RESET}" echo "" echo -e " ${BOLD}Generar Nuevos Backups:${RESET}" echo -e " ${CYAN}bash backup_db.sh${RESET}" echo "" ok "Despliegue finalizado. El sistema se encuentra 100% operativo." echo ""