#!/usr/bin/env bash # ============================================================================== # install.sh — Admin Edu-Space (UniCABA) # Instalador automatizado para Debian 12 (LXC / Bare Metal) # Uso: sudo bash install.sh # ============================================================================== set -euo pipefail # ─── Colores ─────────────────────────────────────────────────────────────────── 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 ───────────────────────────────────────────────────────────────── # APP_DIR resuelto desde la ubicación del script (no del CWD) 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}" # ─── Root check ──────────────────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && err "Ejecutá este script con sudo: sudo bash install.sh" [[ ! -f "${APP_DIR}/app.py" ]] && err "No se encontró app.py en ${APP_DIR}. ¿Estás en el directorio correcto?" echo -e "\n${BOLD}${CYAN}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Admin Edu-Space (UniCABA) — Instalador Debian 12 ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${RESET}" info "Directorio del proyecto: ${APP_DIR}" info "Servicio systemd: ${SERVICE_NAME}" info "Usuario de servicio: ${APP_USER}" # ─── 1. Actualizar sistema e instalar base ────────────────────────────────────── hdr "1. Actualizando sistema" 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 ok "Paquetes base instalados" # ─── 2. Configurar locale UTF-8 ──────────────────────────────────────────────── hdr "2. Configurando locale" 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 update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 > /dev/null 2>&1 export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 ok "Locale configurado: en_US.UTF-8 / es_AR.UTF-8" # ─── 3. Instalar Python 3.10+ ─────────────────────────────────────────────────── hdr "3. Instalando 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 PostgreSQL ──────────────────────────────────────────────────── hdr "4. Instalando PostgreSQL" apt-get install -y -qq postgresql postgresql-client > /dev/null 2>&1 ok "PostgreSQL instalado: $(psql --version | awk '{print $3}')" # Iniciar PostgreSQL (LXC puede no tener systemd activo aún) pg_ctlcluster 15 main start 2>/dev/null \ || service postgresql start 2>/dev/null \ || systemctl start postgresql 2>/dev/null \ || true sleep 2 systemctl enable postgresql > /dev/null 2>&1 || true ok "PostgreSQL activo y habilitado al inicio" # ─── 5. Configurar .env ──────────────────────────────────────────────────────── hdr "5. Configurando variables de entorno" DB_NAME="classrooms_db" DB_USER="eduspace_user" echo "" echo -e " ${BOLD}Configuración de la base de datos PostgreSQL${RESET}" read -rp " Contraseña para el usuario de BD [default: eduspace2024]: " DB_PASS_INPUT DB_PASSWORD="${DB_PASS_INPUT:-eduspace2024}" if [[ -f "$APP_ENV" ]]; then ok ".env ya existe — se conserva sin modificar" # Leer variables del .env existente para usarlas en el script set -a # shellcheck disable=SC1090 source <(grep -v '^#' "$APP_ENV" | grep '=') set +a DB_USER="${DB_USERNAME:-$DB_USER}" DB_PASSWORD="${DB_PASSWORD:-eduspace2024}" DB_NAME=$(echo "${DATABASE_URL:-}" | sed 's|.*\/||') DB_NAME="${DB_NAME:-classrooms_db}" else SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') cat > "$APP_ENV" </dev/null \ || warn "El usuario '${DB_USER}' ya existe — se omite creación" sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" 2>/dev/null \ || warn "La base de datos '${DB_NAME}' ya existe — se omite creación" sudo -u postgres psql -d "${DB_NAME}" \ -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" \ -c "GRANT ALL ON SCHEMA public TO ${DB_USER};" > /dev/null 2>&1 || true sudo -u postgres psql -d "${DB_NAME}" \ -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${DB_USER};" > /dev/null 2>&1 || true ok "Base de datos '${DB_NAME}' lista con usuario '${DB_USER}'" # ─── 7. Entorno virtual Python + dependencias ─────────────────────────────────── hdr "7. Instalando dependencias Python" # Limpiar venv previo roto si existe if [[ -d "$VENV_DIR" ]]; then warn "Venv previo detectado — eliminando para recrear limpio..." rm -rf "$VENV_DIR" fi python3 -m venv "$VENV_DIR" > /dev/null 2>&1 "${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel --quiet "${VENV_DIR}/bin/pip" install gunicorn --quiet ok "Entorno virtual creado: Python $(${VENV_DIR}/bin/python --version | awk '{print $2}')" # Instalar requirements con fallback para Python 3.13+ info "Instalando requirements.txt..." if "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" --quiet 2>/dev/null; then ok "Dependencias instaladas correctamente" else warn "Instalación estándar falló — reintentando con --no-build-isolation (Python 3.13+)..." "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" \ --no-build-isolation --quiet ok "Dependencias instaladas (modo compatibilidad)" fi # ─── 8. Inicializar base de datos ─────────────────────────────────────────────── hdr "8. Inicializando base de datos" cd "$APP_DIR" set -a # shellcheck disable=SC1090 source "$APP_ENV" set +a if [[ -f "classrooms_db.sql" ]]; then info "Se detectó el archivo classrooms_db.sql. Restaurando volcado SQL..." # Eliminar DB si ya existe para cargar de 0 sudo -u postgres psql -c "DROP DATABASE IF EXISTS ${DB_NAME};" > /dev/null 2>&1 sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" > /dev/null 2>&1 if sudo -u postgres psql -d "${DB_NAME}" < classrooms_db.sql > /dev/null 2>&1; then ok "Base de datos restaurada correctamente desde classrooms_db.sql." else err "Error al restaurar la base de datos desde el archivo SQL." fi else LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ PYTHONUTF8=1 \ PYTHONIOENCODING=utf-8 \ FLASK_APP=app.py \ "${VENV_DIR}/bin/python" -X utf8 init_db.py ok "Esquema, roles, edificios y usuario admin inicializados" fi cd - > /dev/null # ─── 9. Usuario del sistema ───────────────────────────────────────────────────── hdr "9. Configurando usuario del sistema" if id "$APP_USER" &>/dev/null; then warn "El usuario '${APP_USER}' ya existe — se omite" else useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" ok "Usuario de sistema '${APP_USER}' creado (sin shell)" fi chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" chmod -R 755 "$APP_DIR" chmod 640 "$APP_ENV" mkdir -p "$LOG_DIR" chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" ok "Permisos aplicados en ${APP_DIR}" # ─── 10. Servicio systemd ─────────────────────────────────────────────────────── hdr "10. Configurando servicio systemd" GUNICORN_BIN="${VENV_DIR}/bin/gunicorn" cat > "/etc/systemd/system/${SERVICE_NAME}.service" < /dev/null 2>&1 systemctl start "${SERVICE_NAME}" sleep 3 if systemctl is-active --quiet "$SERVICE_NAME"; then ok "Servicio '${SERVICE_NAME}' corriendo y habilitado al inicio" else warn "El servicio no inició correctamente. Revisá los logs:" warn " journalctl -u ${SERVICE_NAME} -n 60 --no-pager" fi # ─── Resumen final ────────────────────────────────────────────────────────────── hdr "Instalación completada" IP=$(hostname -I | awk '{print $1}') echo -e " ${BOLD}URL de la aplicación:${RESET}" echo -e " ${CYAN}http://${IP}:5000${RESET}" echo "" echo -e " ${BOLD}Credenciales por defecto:${RESET}" echo -e " Email: ${CYAN}admin@edu-space.com${RESET}" echo -e " Password: ${CYAN}admin123${RESET}" echo "" echo -e " ${BOLD}Base de datos:${RESET}" echo -e " Host: localhost:5432" echo -e " Base: ${DB_NAME}" echo -e " Usuario: ${DB_USER}" echo "" echo -e " ${BOLD}Comandos de gestión:${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 "" warn "Cambiá la contraseña del admin desde la interfaz web." warn "Los archivos .env contienen credenciales sensibles — NO los incluyas en git." echo "" ok "¡Listo! La aplicación está corriendo en http://${IP}:5000" echo ""