diff --git a/install.sh b/install.sh index 55745a7..d38a953 100644 --- a/install.sh +++ b/install.sh @@ -1,111 +1,124 @@ #!/usr/bin/env bash # ============================================================================== -# install.sh — Instalador automático de Admin Edu-Space -# Sistema operativo: Debian 12 (Bookworm) — LXC / Bare Metal +# install.sh — Admin Edu-Space (UniCABA) +# Instalador automatizado para Debian 12 (LXC / Bare Metal) # Uso: sudo bash install.sh -# Debe ejecutarse desde el directorio raíz del proyecto (donde está app.py) # ============================================================================== + set -euo pipefail -# ── Colores ───────────────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' +# ─── Colores ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' -step() { echo -e "\n${CYAN}${BOLD}▶ $*${NC}"; } -ok() { echo -e "${GREEN} ✔ $*${NC}"; } -warn() { echo -e "${YELLOW} ⚠ $*${NC}"; } -die() { echo -e "${RED} ✘ $*${NC}" >&2; exit 1; } +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"; } -# ── Verificaciones previas ─────────────────────────────────────────────────── -[[ $EUID -ne 0 ]] && die "Este script debe ejecutarse como root. Usá: sudo bash install.sh" -[[ ! -f "app.py" ]] && die "No se encontró app.py. Ejecutá el script desde el directorio raíz del proyecto." - -APP_DIR="$(pwd)" -APP_USER="eduspace" +# ─── 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" -VENV_DIR="$APP_DIR/venv" -PYTHON_BIN="python3" +APP_USER="eduspace" +PYTHON_REQUIRED_MAJOR=3 +PYTHON_REQUIRED_MINOR=10 +LOG_DIR="/var/log/${SERVICE_NAME}" -# ── Banner ─────────────────────────────────────────────────────────────────── -echo -e "${BOLD}" +# ─── 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 — Instalador para Debian 12 ║" +echo "║ Admin Edu-Space (UniCABA) — Instalador Debian 12 ║" echo "╚══════════════════════════════════════════════════════════════╝" -echo -e "${NC}" -echo " Directorio de instalación: ${CYAN}$APP_DIR${NC}" +echo -e "${RESET}" +info "Directorio del proyecto: ${APP_DIR}" +info "Servicio systemd: ${SERVICE_NAME}" +info "Usuario de servicio: ${APP_USER}" -# ── 1. Locale UTF-8 ───────────────────────────────────────────────────────── -step "Configurando locale UTF-8..." +# ─── 1. Actualizar sistema e instalar base ────────────────────────────────────── +hdr "1. Actualizando sistema" export DEBIAN_FRONTEND=noninteractive -apt-get install -y locales > /dev/null 2>&1 +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 -export LC_ALL=en_US.UTF-8 -export PYTHONUTF8=1 -export PYTHONIOENCODING=utf-8 -ok "Locale configurado: en_US.UTF-8" +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" -# ── 2. Dependencias del sistema ────────────────────────────────────────────── -step "Actualizando repositorios e instalando dependencias del sistema..." -apt-get update -qq -apt-get install -y \ - python3 \ - python3-venv \ - python3-pip \ - python3-dev \ - build-essential \ - libpq-dev \ - postgresql \ - postgresql-client \ - curl \ - git \ - > /dev/null 2>&1 -ok "Dependencias instaladas (Python3 + PostgreSQL + build tools)" +# ─── 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 -# ── 3. Iniciar PostgreSQL ──────────────────────────────────────────────────── -step "Iniciando servicio PostgreSQL..." -# Intentar iniciar por distintos métodos (LXC puede no tener systemd activo aún) +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 -# Habilitar para inicio automático (ignorar error si ya está habilitado) systemctl enable postgresql > /dev/null 2>&1 || true -ok "PostgreSQL activo" +ok "PostgreSQL activo y habilitado al inicio" -# ── 4. Configurar variables de entorno (.env) ──────────────────────────────── -step "Configurando variables de entorno..." +# ─── 5. Configurar .env ──────────────────────────────────────────────────────── +hdr "5. Configurando variables de entorno" -SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') - -echo "" -echo -e " ${BOLD}Configuración de la base de datos PostgreSQL${NC}" -read -rp " Contraseña para el usuario de BD [default: eduspace2024]: " DB_PASS_INPUT -DB_PASSWORD="${DB_PASS_INPUT:-eduspace2024}" DB_NAME="classrooms_db" DB_USER="eduspace_user" -if [[ -f "$APP_DIR/.env" ]]; then - warn ".env ya existe — se conserva sin modificar" - # Leer variables del .env existente - source <(grep -v '^#' "$APP_DIR/.env" | grep '=' | sed 's/ *= */=/;s/^/export /') - # Reasignar desde el .env leído +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 - cat > "$APP_DIR/.env" < "$APP_ENV" </dev/null \ || warn "El usuario '${DB_USER}' ya existe — se omite creación" @@ -133,45 +148,47 @@ sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}'; 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 +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}'" -# ── 6. Entorno virtual Python + dependencias ───────────────────────────────── -step "Creando entorno virtual Python e instalando dependencias..." +# ─── 7. Entorno virtual Python + dependencias ─────────────────────────────────── +hdr "7. Instalando dependencias Python" -# Eliminar venv roto si existe de una instalación anterior fallida +# Limpiar venv previo roto si existe if [[ -d "$VENV_DIR" ]]; then warn "Venv previo detectado — eliminando para recrear limpio..." rm -rf "$VENV_DIR" fi -PY_VERSION=$($PYTHON_BIN -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') -ok "Versión de Python: $PY_VERSION" +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}')" -$PYTHON_BIN -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 - -# Instalar dependencias — con fallback para Python 3.13+ donde algunos paquetes -# requieren --no-build-isolation por incompatibilidades con setuptools nuevo -if "$VENV_DIR/bin/pip" install -r "$APP_DIR/requirements.txt" --quiet 2>/dev/null; then +# 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..." - "$VENV_DIR/bin/pip" install -r "$APP_DIR/requirements.txt" --no-build-isolation --quiet + 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 -ok "Virtualenv listo con todas las dependencias + Gunicorn" -# ── 7. Inicializar base de datos ────────────────────────────────────────────── -step "Inicializando esquema y datos iniciales (init_db.py)..." +# ─── 8. Inicializar base de datos ─────────────────────────────────────────────── +hdr "8. Inicializando base de datos" cd "$APP_DIR" -# Exportar variables del .env para que Flask/SQLAlchemy las use set -a -source "$APP_DIR/.env" +# shellcheck disable=SC1090 +source "$APP_ENV" set +a LANG=en_US.UTF-8 \ @@ -179,36 +196,37 @@ LC_ALL=en_US.UTF-8 \ PYTHONUTF8=1 \ PYTHONIOENCODING=utf-8 \ FLASK_APP=app.py \ -"$VENV_DIR/bin/python" -X utf8 init_db.py +"${VENV_DIR}/bin/python" -X utf8 init_db.py -ok "Esquema y datos iniciales cargados correctamente" +ok "Esquema, roles, edificios y usuario admin inicializados" cd - > /dev/null -# ── 8. Usuario del sistema ──────────────────────────────────────────────────── -step "Creando usuario del sistema '${APP_USER}'..." +# ─── 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 '${APP_USER}' creado" + ok "Usuario de sistema '${APP_USER}' creado (sin shell)" fi -chown -R "$APP_USER":"$APP_USER" "$APP_DIR" +chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" chmod -R 755 "$APP_DIR" -# El .env debe ser legible sólo por el owner -chmod 640 "$APP_DIR/.env" -ok "Permisos aplicados en $APP_DIR" +chmod 640 "$APP_ENV" -# ── 9. Servicio systemd ─────────────────────────────────────────────────────── -step "Registrando servicio systemd '${SERVICE_NAME}'..." +mkdir -p "$LOG_DIR" +chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" +ok "Permisos aplicados en ${APP_DIR}" -mkdir -p "/var/log/${SERVICE_NAME}" -chown -R "$APP_USER":"$APP_USER" "/var/log/${SERVICE_NAME}" +# ─── 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" +systemctl enable "${SERVICE_NAME}" > /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 para el arranque automático" + ok "Servicio '${SERVICE_NAME}' corriendo y habilitado al inicio" else - warn "El servicio no inició correctamente. Revisá con:" + warn "El servicio no inició correctamente. Revisá los logs:" warn " journalctl -u ${SERVICE_NAME} -n 60 --no-pager" fi -# ── 10. Resumen final ───────────────────────────────────────────────────────── +# ─── 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}${GREEN}" -echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ ✔ Instalación completada con éxito ║" -echo "╚══════════════════════════════════════════════════════════════╝" -echo -e "${NC}" -echo -e " ${BOLD}URL de la aplicación:${NC} http://${IP}:5000" +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}Credenciales por defecto:${NC}" -echo -e " Email: admin@edu-space.com" -echo -e " Password: admin123" -echo "" -echo -e " ${BOLD}Base de datos:${NC}" +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:${NC}" -echo -e " Ver estado: ${CYAN}systemctl status ${SERVICE_NAME}${NC}" -echo -e " Ver logs: ${CYAN}journalctl -u ${SERVICE_NAME} -f${NC}" -echo -e " Reiniciar: ${CYAN}systemctl restart ${SERVICE_NAME}${NC}" -echo -e " Detener: ${CYAN}systemctl stop ${SERVICE_NAME}${NC}" +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 "" -echo -e " ${YELLOW}⚠ Cambiá la contraseña del admin desde la interfaz web${NC}" +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 ""