feat(deploy): mover install.sh a la raiz con soporte unificado para backend, frontend y seleccion de base de datos postgres

This commit is contained in:
2026-09-23 14:12:09 -03:00
parent 902e30a261
commit 6bfaab634f
3 changed files with 647 additions and 475 deletions
+25 -1
View File
@@ -17,7 +17,8 @@ Fase 1: Paridad Legacy y Estabilización [████████████
Fase 2: Co-docencia y Matriz Conflictos [████████████████████] 100% (Completado) Fase 2: Co-docencia y Matriz Conflictos [████████████████████] 100% (Completado)
Fase 3: Hitos Evaluativos y Calificaciones[████████████████████] 100% (Completado) Fase 3: Hitos Evaluativos y Calificaciones[████████████████████] 100% (Completado)
Fase 4: UI/UX Drag & Drop e Impersonación[████████████████████] 100% (Completado) Fase 4: UI/UX Drag & Drop e Impersonación[████████████████████] 100% (Completado)
Fase 5: Despliegue Producción e Integrac.[░░░░░░░░░░░░░░░░░░░░] 0% (Planificada) Fase 5: Integración Auth, Email & Moodle [████████████████████] 100% (Completado)
Fase 6: Despliegue Producción e Integrac.[████░░░░░░░░░░░░░░░░] 25% (En Curso)
``` ```
--- ---
@@ -153,6 +154,29 @@ node src/app.js
--- ---
## 📦 Despliegue Automatizado en Producción (Linux / Proxmox LXC)
Para servidores Debian 12 / Ubuntu en Proxmox LXC o VPS, el repositorio incluye el instalador unificado [install.sh](install.sh) en la raíz:
```bash
# Despliegue interactivo (pregunta si mantener o restaurar la BD si ya existe):
sudo bash install.sh
# Despliegue desatendido manteniendo la base de datos existente:
sudo bash install.sh --keep-db
# Despliegue desatendido recreando la base de datos desde cero (DROP y restore snapshot):
sudo bash install.sh --fresh-db
```
El script configura automáticamente:
1. Entorno Python 3.10+ y virtualenv en `backend/venv` con Gunicorn y servicio `admin-edu-space-backend.service` (puerto 5000).
2. Entorno Node.js 20 LTS en `frontend/` con servicio `admin-edu-space-frontend.service` (puerto 3000).
3. Motor PostgreSQL local con configuración de usuario y opciones seguras para preservar o restaurar la base `classrooms_db`.
4. Servicio maestro unificado `admin-edu-space.service` gestionable vía `systemctl {status|restart|stop} admin-edu-space`.
---
## 🧪 Pruebas Automatizadas ## 🧪 Pruebas Automatizadas
Para ejecutar la suite completa de pruebas unitarias de regresión (Fases 2, 3 y 4): Para ejecutar la suite completa de pruebas unitarias de regresión (Fases 2, 3 y 4):
-474
View File
@@ -1,474 +0,0 @@
#!/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}"
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
# Verificar e instalar git si no está presente en el sistema
if ! command -v git &>/dev/null; then
info "Git no está instalado. Instalando paquete git..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1 || true
apt-get install -y -qq git >/dev/null 2>&1 || apt-get install -y git
ok "Git instalado correctamente"
fi
# Configurar safe.directory en Git para evitar errores de propiedad cruzada (root vs eduspace)
git config --global --add safe.directory '*' 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 \
sudo curl wget git ca-certificates gnupg lsb-release \
build-essential libpq-dev \
locales \
> /dev/null 2>&1
ok "Paquetes base instalados"
# Función auxiliar para ejecutar comandos psql como superusuario postgres de forma universal (sin depender de sudo)
pg_cmd() {
if command -v sudo >/dev/null 2>&1; then
sudo -u postgres psql "$@"
elif command -v runuser >/dev/null 2>&1; then
runuser -u postgres -- psql "$@"
else
su - postgres -c "psql $(printf '%q ' "$@")"
fi
}
# ─── 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")
PG_MAJOR=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1 || echo "")
[[ -z "$PG_MAJOR" ]] && PG_MAJOR=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1 || echo "")
[[ -z "$PG_MAJOR" ]] && PG_MAJOR="15"
ok "Motor PostgreSQL instalado: ${PG_VER}"
# Asegurar directorios de socket y permisos para PostgreSQL en contenedores LXC
mkdir -p /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql
chown -R postgres:postgres /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql
chmod 2775 /run/postgresql /var/run/postgresql
# Verificar si existe el cluster 'main'. Si no existe (común en Debian si falló la creación automática), crearlo
if ! pg_lsclusters 2>/dev/null | grep -q 'main'; then
info "Cluster PostgreSQL no detectado — inicializando cluster ${PG_MAJOR} main..."
pg_createcluster "${PG_MAJOR}" main --locale en_US.UTF-8 --start >/dev/null 2>&1 \
|| pg_createcluster "${PG_MAJOR}" main --locale C.UTF-8 --start >/dev/null 2>&1 \
|| true
fi
# Levantar servicio de PostgreSQL asegurando compatibilidad con contenedores LXC y VMs
info "Iniciando servicio de PostgreSQL..."
pg_ctlcluster "${PG_MAJOR}" main start >/dev/null 2>&1 || true
systemctl start "postgresql@${PG_MAJOR}-main" 2>/dev/null || true
systemctl start postgresql 2>/dev/null || true
service postgresql start 2>/dev/null || true
# Esperar a que el socket de PostgreSQL esté completamente activo
PG_READY=false
for i in {1..20}; do
if pg_cmd -c '\q' >/dev/null 2>&1; then
PG_READY=true
break
fi
if [[ $i -eq 3 || $i -eq 8 ]]; then
pg_ctlcluster "${PG_MAJOR}" main start 2>/dev/null || true
systemctl restart "postgresql@${PG_MAJOR}-main" 2>/dev/null || true
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@${PG_MAJOR}-main" 2>/dev/null || systemctl reload postgresql 2>/dev/null || true
fi
systemctl enable "postgresql@${PG_MAJOR}-main" > /dev/null 2>&1 || true
systemctl enable postgresql > /dev/null 2>&1 || true
ok "PostgreSQL activo y aceptando conexiones"
else
warn "Diagnóstico de arranque de PostgreSQL en LXC:"
pg_lsclusters 2>/dev/null || true
systemctl status "postgresql@${PG_MAJOR}-main" --no-pager -n 10 2>/dev/null || true
journalctl -u postgresql -n 15 --no-pager 2>/dev/null || true
info "Prueba directa de conexión psql:"
pg_cmd -c '\q' || true
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" <<ENVEOF
# Admin Edu-Space — Configuración de producción
# Generado automáticamente el $(date '+%Y-%m-%d %H:%M:%S')
FLASK_APP=wsgi.py
FLASK_ENV=production
SECRET_KEY=${SECRET_KEY}
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}
DB_USERNAME=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
API_BASE_URL=http://localhost:8080/api
BABEL_DEFAULT_LOCALE=es
BABEL_DEFAULT_TIMEZONE=America/Argentina/Buenos_Aires
ENVEOF
chmod 640 "$APP_ENV"
ok ".env generado automáticamente con configuración de producción"
fi
# ─── 6. Configurar Usuario y Base de Datos en PostgreSQL ───────────────────────
hdr "6. Configurando motor PostgreSQL y credenciales de base de datos"
# Configurar usuarios en PostgreSQL con privilegios completos
for u in "${DB_USER}" "${DB_USERNAME:-}" eduspace_user; do
if [[ -n "$u" && "$u" != "postgres" ]]; then
pg_cmd -c "CREATE USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| pg_cmd -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| true
pg_cmd -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true
fi
done
pg_cmd -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true
pg_cmd -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true
# Crear base de datos
pg_cmd -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
pg_cmd -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" >/dev/null 2>&1 || true
pg_cmd -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
pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true
pg_cmd -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" | pg_cmd -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
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -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
pg_cmd -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" <<SVCEOF
[Unit]
Description=Admin Edu-Space — Flask/Gunicorn (UniCABA)
Documentation=https://gitea.oemspot.com.ar/carlostellocba/admin-edu-space
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${APP_ENV}
Environment="LANG=en_US.UTF-8"
Environment="LC_ALL=en_US.UTF-8"
Environment="PYTHONUTF8=1"
Environment="PYTHONIOENCODING=utf-8"
Environment="FLASK_ENV=production"
Environment="HOME=${APP_DIR}"
ExecStart=${GUNICORN_BIN} \
--workers 3 \
--bind 0.0.0.0:5000 \
--timeout 120 \
--access-logfile ${LOG_DIR}/access.log \
--error-logfile - \
--capture-output \
--enable-stdio-inheritance \
--log-level info \
wsgi:app
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
SVCEOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}" > /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 ""
Executable
+622
View File
@@ -0,0 +1,622 @@
#!/usr/bin/env bash
# ==============================================================================
# install.sh — Admin Edu-Space (UniCABA)
# Instalador automatizado para Proxmox VE (LXC) / Debian 12 / Ubuntu
# Despliegue unificado: Backend Flask (Gunicorn) + Frontend Node.js (BFF) + PostgreSQL
# Uso: sudo bash install.sh [--keep-db | --fresh-db]
# ==============================================================================
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"; }
# ─── Argumentos de Línea de Comandos ───────────────────────────────────────────
ARG_DB_ACTION=""
for arg in "$@"; do
case "$arg" in
--keep-db)
ARG_DB_ACTION="keep"
;;
--fresh-db|--restore-db)
ARG_DB_ACTION="fresh"
;;
-h|--help)
echo "Uso: sudo bash install.sh [OPCIONES]"
echo "Opciones:"
echo " --keep-db Mantiene la base de datos existente si ya contiene datos."
echo " --fresh-db Restaura la base de datos desde cero (DROP y recarga snapshot)."
echo " -h, --help Muestra esta ayuda."
exit 0
;;
*)
warn "Opción desconocida ignorada: $arg"
;;
esac
done
# ─── Variables Principales ─────────────────────────────────────────────────────
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="${ROOT_DIR}/backend"
FRONTEND_DIR="${ROOT_DIR}/frontend"
VENV_DIR="${BACKEND_DIR}/venv"
BACKEND_ENV="${BACKEND_DIR}/.env"
FRONTEND_ENV="${FRONTEND_DIR}/.env"
SERVICE_BACKEND="admin-edu-space-backend"
SERVICE_FRONTEND="admin-edu-space-frontend"
SERVICE_META="admin-edu-space"
APP_USER="eduspace"
PYTHON_REQUIRED_MAJOR=3
PYTHON_REQUIRED_MINOR=10
LOG_DIR="/var/log/${SERVICE_META}"
# Ubicación del snapshot de base de datos
SQL_BACKUP_FILE="${BACKEND_DIR}/classrooms_db.sql"
if [[ ! -f "$SQL_BACKUP_FILE" && -f "${ROOT_DIR}/classrooms_db.sql" ]]; then
SQL_BACKUP_FILE="${ROOT_DIR}/classrooms_db.sql"
fi
# ─── 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, trasladarlo a /opt para evitar problemas de permisos con el usuario de servicio
if [[ "${ROOT_DIR}" == /root/* ]]; then
NEW_DIR="/opt/admin-edu-space"
warn "Detectado directorio en ${ROOT_DIR}. Trasladando a ${NEW_DIR} para evitar bloqueos de permisos..."
systemctl stop "${SERVICE_BACKEND}" 2>/dev/null || true
systemctl stop "${SERVICE_FRONTEND}" 2>/dev/null || true
systemctl stop "${SERVICE_META}" 2>/dev/null || true
mkdir -p "${NEW_DIR}"
cp -a "${ROOT_DIR}/." "${NEW_DIR}/"
cd "${NEW_DIR}"
exec bash install.sh "$@"
fi
# Validar estructura básica del repositorio
[[ ! -f "${BACKEND_DIR}/wsgi.py" ]] && err "No se encontró wsgi.py en ${BACKEND_DIR}. Verificá la estructura del proyecto."
[[ ! -f "${FRONTEND_DIR}/package.json" ]] && err "No se encontró package.json en ${FRONTEND_DIR}. Verificá la estructura del proyecto."
# Verificar e instalar git si no está presente en el sistema
if ! command -v git &>/dev/null; then
info "Git no está instalado. Instalando paquete git..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1 || true
apt-get install -y -qq git >/dev/null 2>&1 || apt-get install -y git
ok "Git instalado correctamente"
fi
git config --global --add safe.directory '*' 2>/dev/null || true
echo -e "\n${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Admin Edu-Space (UniCABA) — Instalador ║"
echo "║ Backend (Flask/Gunicorn) + Frontend (Node.js) + 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 Raíz: ${ROOT_DIR}"
info "Directorio Backend: ${BACKEND_DIR}"
info "Directorio Frontend: ${FRONTEND_DIR}"
info "Usuario de Servicio: ${APP_USER}"
# ─── 1. Paquetes Base del Sistema ──────────────────────────────────────────────
hdr "1. Actualizando repositorios y paquetes base"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
sudo curl wget git ca-certificates gnupg lsb-release \
build-essential libpq-dev \
locales \
> /dev/null 2>&1
ok "Paquetes base instalados"
pg_cmd() {
if command -v sudo >/dev/null 2>&1; then
sudo -u postgres psql "$@"
elif command -v runuser >/dev/null 2>&1; then
runuser -u postgres -- psql "$@"
else
su - postgres -c "psql $(printf '%q ' "$@")"
fi
}
# ─── 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 virtual ────────────────────────────────
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 Node.js y npm para el Frontend BFF ───────────────────────────
hdr "4. Verificando entorno Node.js para el Frontend BFF"
NODE_INSTALLED=false
if command -v node &>/dev/null; then
NODE_VER_NUM=$(node -v | tr -d 'v' | cut -d. -f1 || echo "0")
if [[ "$NODE_VER_NUM" -ge 18 ]]; then
NODE_INSTALLED=true
ok "Node.js $(node -v) y npm $(npm -v 2>/dev/null || echo '') ya disponibles"
fi
fi
if [ "$NODE_INSTALLED" = false ]; then
info "Instalando Node.js 20 LTS vía repositorio oficial NodeSource..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg --yes 2>/dev/null || true
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
apt-get update -qq >/dev/null 2>&1 || true
apt-get install -y -qq nodejs >/dev/null 2>&1 || apt-get install -y nodejs
ok "Node.js $(node -v) instalado exitosamente"
fi
# ─── 5. Instalar y Levantar Motor PostgreSQL ───────────────────────────────────
hdr "5. 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")
PG_MAJOR=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1 || echo "")
[[ -z "$PG_MAJOR" ]] && PG_MAJOR=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1 || echo "")
[[ -z "$PG_MAJOR" ]] && PG_MAJOR="15"
ok "Motor PostgreSQL instalado: ${PG_VER}"
mkdir -p /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql
chown -R postgres:postgres /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql
chmod 2775 /run/postgresql /var/run/postgresql
if ! pg_lsclusters 2>/dev/null | grep -q 'main'; then
info "Cluster PostgreSQL no detectado — inicializando cluster ${PG_MAJOR} main..."
pg_createcluster "${PG_MAJOR}" main --locale en_US.UTF-8 --start >/dev/null 2>&1 \
|| pg_createcluster "${PG_MAJOR}" main --locale C.UTF-8 --start >/dev/null 2>&1 \
|| true
fi
info "Iniciando servicio de PostgreSQL..."
pg_ctlcluster "${PG_MAJOR}" main start >/dev/null 2>&1 || true
systemctl start "postgresql@${PG_MAJOR}-main" 2>/dev/null || true
systemctl start postgresql 2>/dev/null || true
service postgresql start 2>/dev/null || true
PG_READY=false
for i in {1..20}; do
if pg_cmd -c '\q' >/dev/null 2>&1; then
PG_READY=true
break
fi
sleep 1
done
if [ "$PG_READY" = true ]; then
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@${PG_MAJOR}-main" 2>/dev/null || systemctl reload postgresql 2>/dev/null || true
fi
systemctl enable "postgresql@${PG_MAJOR}-main" > /dev/null 2>&1 || true
systemctl enable postgresql > /dev/null 2>&1 || true
ok "PostgreSQL activo y aceptando conexiones"
else
err "No se pudo conectar al motor PostgreSQL local. Verificá los registros del sistema."
fi
# ─── 6. Configurar Variables de Entorno (.env) ─────────────────────────────────
hdr "6. Configurando variables de entorno (Backend y Frontend)"
DB_NAME="classrooms_db"
DB_USER="postgres"
DB_PASSWORD="password"
if [[ -f "$BACKEND_ENV" ]]; then
ok "backend/.env ya existe — se preserva la configuración actual"
set -a
# shellcheck disable=SC1090
source <(grep -v '^#' "$BACKEND_ENV" | grep '=')
set +a
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|')
[[ "$URL_USER" != "$DATABASE_URL" && -n "$URL_USER" ]] && DB_USER="$URL_USER"
[[ "$URL_PASS" != "$DATABASE_URL" && -n "$URL_PASS" ]] && DB_PASSWORD="$URL_PASS"
[[ "$URL_NAME" != "$DATABASE_URL" && -n "$URL_NAME" ]] && DB_NAME="$URL_NAME"
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 > "$BACKEND_ENV" <<ENVEOF
# Admin Edu-Space — Backend Flask Configuración de Producción
# Generado automáticamente el $(date '+%Y-%m-%d %H:%M:%S')
FLASK_APP=wsgi.py
FLASK_ENV=production
SECRET_KEY=${SECRET_KEY}
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}
DB_USERNAME=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
API_BASE_URL=http://localhost:5000/api/v1
BABEL_DEFAULT_LOCALE=es
BABEL_DEFAULT_TIMEZONE=America/Argentina/Buenos_Aires
ENVEOF
chmod 640 "$BACKEND_ENV"
ok "backend/.env generado con configuración de producción"
fi
# Configurar frontend/.env
if [[ -f "$FRONTEND_ENV" ]]; then
ok "frontend/.env ya existe — se preserva la configuración actual"
else
cat > "$FRONTEND_ENV" <<FRONTENVEOF
# Admin Edu-Space — Frontend Node.js BFF Configuración de Producción
# Generado automáticamente el $(date '+%Y-%m-%d %H:%M:%S')
PORT=3000
FLASK_API_URL=http://127.0.0.1:5000/api/v1
NODE_ENV=production
FRONTENVEOF
chmod 640 "$FRONTEND_ENV"
ok "frontend/.env generado con configuración conectada al backend local (puerto 3000)"
fi
# ─── 7. Gestión y Elección de Base de Datos PostgreSQL ─────────────────────────
hdr "7. Configuración de Base de Datos PostgreSQL (Existente vs Nueva)"
# Asegurar roles de PostgreSQL
for u in "${DB_USER}" "${DB_USERNAME:-}" eduspace_user; do
if [[ -n "$u" && "$u" != "postgres" ]]; then
pg_cmd -c "CREATE USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| pg_cmd -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| true
pg_cmd -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true
fi
done
pg_cmd -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true
pg_cmd -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true
# Detectar existencia y tablas previas
DB_EXISTS=false
TABLE_COUNT=0
if pg_cmd -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "${DB_NAME}"; then
DB_EXISTS=true
TABLE_COUNT=$(pg_cmd -d "${DB_NAME}" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null || echo "0")
fi
DO_RESTORE_FRESH=false
if [ "$DB_EXISTS" = true ] && [ "$TABLE_COUNT" -gt 0 ]; then
info "Base de datos detectada: '${DB_NAME}' con ${TABLE_COUNT} tabla(s) existente(s)."
if [[ "$ARG_DB_ACTION" == "keep" ]]; then
info "Bandera --keep-db recibida: Se mantiene la base de datos existente intacta."
DO_RESTORE_FRESH=false
elif [[ "$ARG_DB_ACTION" == "fresh" ]]; then
warn "Bandera --fresh-db recibida: Se recreará la base de datos desde cero."
DO_RESTORE_FRESH=true
elif [ -t 0 ]; then
echo -e "\n${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} ¿Qué deseas hacer con la base de datos PostgreSQL existente?${RESET}"
echo -e " [1] ${GREEN}${BOLD}Mantener la base de datos actual${RESET} (Recomendado: preserva registros y sincroniza esquema)"
echo -e " [2] ${RED}${BOLD}Restaurar desde cero${RESET} (DROP DATABASE y recargar snapshot ${SQL_BACKUP_FILE##*/})"
echo -e "${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
DB_CHOICE=""
read -t 30 -p " Ingresá tu opción [1/2] (por defecto 1 en 30s): " DB_CHOICE || DB_CHOICE="1"
DB_CHOICE="${DB_CHOICE:-1}"
if [[ "$DB_CHOICE" == "2" ]]; then
warn "Opción [2] seleccionada: Restauración destructiva desde cero."
DO_RESTORE_FRESH=true
else
ok "Opción [1] seleccionada: Mantener base de datos existente."
DO_RESTORE_FRESH=false
fi
else
info "Ejecución no interactiva: se mantiene la base de datos existente por defecto."
DO_RESTORE_FRESH=false
fi
else
info "La base de datos '${DB_NAME}' no existe o está vacía. Se inicializará limpia."
DO_RESTORE_FRESH=true
fi
# Otorgar privilegios base
pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" >/dev/null 2>&1 || true
# ─── 8. Crear Entorno Virtual e Instalar Dependencias Backend ───────────────────
hdr "8. Configurando entorno virtual Python e instalando dependencias backend"
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 dependencias desde backend/requirements.txt..."
if "${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --quiet 2>/dev/null; then
ok "Dependencias de Python instaladas correctamente"
else
warn "Reintentando instalación con --no-build-isolation..."
"${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --no-build-isolation --quiet
ok "Dependencias de Python instaladas"
fi
# ─── 9. Ejecución de Despliegue de Datos (Restaurar o Sincronizar) ──────────────
hdr "9. Aplicando esquema y datos en la base de datos"
cd "$BACKEND_DIR"
if [ "$DO_RESTORE_FRESH" = true ]; then
if [[ -f "$SQL_BACKUP_FILE" ]]; then
BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}')
info "Restaurando snapshot '${SQL_BACKUP_FILE##*/}' (${BACKUP_SIZE})..."
pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true
pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1
sed '/^\\restrict/d' "$SQL_BACKUP_FILE" | pg_cmd -d "${DB_NAME}" > /dev/null 2>&1 || true
ok "Snapshot SQL restaurado exitosamente"
else
info "No se encontró classrooms_db.sql. Creando esquema limpio con init_db.py..."
fi
else
ok "Conservando tablas existentes en '${DB_NAME}'"
fi
# Otorgar permisos completos al usuario de la aplicación
for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do
if [[ -n "$u" ]]; then
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true
pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true
fi
done
# Sincronización idempotente con init_db.py (crea tablas o columnas faltantes sin borrar datos)
info "Sincronizando modelos y esquemas institucionales 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, usuarios y roles RBAC sincronizados con éxito"
cd "$ROOT_DIR"
# ─── 10. Instalar Dependencias del Frontend Node.js ────────────────────────────
hdr "10. Instalando dependencias de Node.js en frontend/"
cd "$FRONTEND_DIR"
info "Ejecutando npm install en frontend..."
if [ -f "package-lock.json" ]; then
npm ci --omit=dev --silent 2>/dev/null || npm install --omit=dev --silent
else
npm install --omit=dev --silent
fi
ok "Dependencias de Node.js instaladas en frontend/"
cd "$ROOT_DIR"
# ─── 11. Usuario del Sistema y Permisos de Directorio ───────────────────────────
hdr "11. Configurando usuario del sistema y permisos de seguridad"
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 interactiva)"
fi
mkdir -p "${BACKEND_DIR}/app/static/uploads"
mkdir -p "$LOG_DIR"
chown -R "${APP_USER}:${APP_USER}" "$ROOT_DIR"
chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR"
chmod -R 755 "$ROOT_DIR"
chmod -R 775 "${BACKEND_DIR}/app/static/uploads"
chmod 640 "$BACKEND_ENV" "$FRONTEND_ENV"
chmod 755 "$LOG_DIR"
ok "Permisos aplicados en ${ROOT_DIR} y ${LOG_DIR}"
# ─── 12. Servicios Systemd (Backend Flask y Frontend Node.js) ──────────────────
hdr "12. Configurando servicios systemd de arranque automático"
GUNICORN_BIN="${VENV_DIR}/bin/gunicorn"
NODE_BIN="$(command -v node)"
# 12.1 Servicio Backend
cat > "/etc/systemd/system/${SERVICE_BACKEND}.service" <<SVCEOF
[Unit]
Description=Admin Edu-Space — Backend Flask API (Gunicorn)
Documentation=https://gitea.oemspot.com.ar/carlostellocba/admin-edu-space
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${BACKEND_DIR}
EnvironmentFile=${BACKEND_ENV}
Environment="LANG=en_US.UTF-8"
Environment="LC_ALL=en_US.UTF-8"
Environment="PYTHONUTF8=1"
Environment="PYTHONIOENCODING=utf-8"
Environment="FLASK_ENV=production"
Environment="HOME=${BACKEND_DIR}"
ExecStart=${GUNICORN_BIN} \
--workers 3 \
--bind 0.0.0.0:5000 \
--timeout 120 \
--access-logfile ${LOG_DIR}/backend-access.log \
--error-logfile ${LOG_DIR}/backend-error.log \
--capture-output \
--enable-stdio-inheritance \
--log-level info \
wsgi:app
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_BACKEND}
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
SVCEOF
# 12.2 Servicio Frontend BFF
cat > "/etc/systemd/system/${SERVICE_FRONTEND}.service" <<SVCEOF
[Unit]
Description=Admin Edu-Space — Frontend Node.js BFF (Express)
Documentation=https://gitea.oemspot.com.ar/carlostellocba/admin-edu-space
After=network.target ${SERVICE_BACKEND}.service
Wants=${SERVICE_BACKEND}.service
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${FRONTEND_DIR}
EnvironmentFile=${FRONTEND_ENV}
Environment="NODE_ENV=production"
Environment="PORT=3000"
ExecStart=${NODE_BIN} src/app.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_FRONTEND}
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
SVCEOF
# 12.3 Servicio Meta Unificador
cat > "/etc/systemd/system/${SERVICE_META}.service" <<SVCEOF
[Unit]
Description=Admin Edu-Space — Plataforma Integral (Backend + Frontend)
Documentation=https://gitea.oemspot.com.ar/carlostellocba/admin-edu-space
After=${SERVICE_BACKEND}.service ${SERVICE_FRONTEND}.service
Wants=${SERVICE_BACKEND}.service ${SERVICE_FRONTEND}.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
[Install]
WantedBy=multi-user.target
SVCEOF
systemctl daemon-reload
systemctl enable "${SERVICE_BACKEND}" "${SERVICE_FRONTEND}" "${SERVICE_META}" > /dev/null 2>&1
info "Iniciando servicio backend..."
systemctl restart "${SERVICE_BACKEND}"
sleep 3
info "Iniciando servicio frontend..."
systemctl restart "${SERVICE_FRONTEND}"
systemctl restart "${SERVICE_META}"
sleep 3
# Verificar salud de los servicios
BACKEND_OK=false
if systemctl is-active --quiet "$SERVICE_BACKEND"; then
HTTP_BACK=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/api/v1/openapi.json || echo "000")
if [[ "$HTTP_BACK" == "200" ]]; then
ok "Backend Flask respondiendo activamente en puerto 5000 (OpenAPI HTTP 200)"
BACKEND_OK=true
else
warn "Backend activo pero devolvió HTTP ${HTTP_BACK} al consultar OpenAPI."
fi
else
warn "El servicio backend tardó en iniciar. Logs: journalctl -u ${SERVICE_BACKEND} -n 20 --no-pager"
fi
FRONTEND_OK=false
if systemctl is-active --quiet "$SERVICE_FRONTEND"; then
HTTP_FRONT=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/ || echo "000")
if [[ "$HTTP_FRONT" == "200" || "$HTTP_FRONT" == "302" ]]; then
ok "Frontend Node.js respondiendo activamente en puerto 3000 (HTTP ${HTTP_FRONT})"
FRONTEND_OK=true
else
warn "Frontend activo pero devolvió HTTP ${HTTP_FRONT} al consultar raíz."
fi
else
warn "El servicio frontend tardó en iniciar. Logs: journalctl -u ${SERVICE_FRONTEND} -n 20 --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 Plataforma (Frontend):${RESET}"
echo -e " ${GREEN}${BOLD}http://${IP}:3000${RESET} o ${GREEN}${BOLD}http://localhost:3000${RESET}"
echo ""
echo -e " ${BOLD}Acceso a la API REST (Backend Flask):${RESET}"
echo -e " ${CYAN}http://${IP}:5000/api/v1${RESET} (OpenAPI spec en /api/v1/openapi.json)"
echo ""
echo -e " ${BOLD}Credenciales de Administrador Iniciales:${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 " Modo: $( [ "$DO_RESTORE_FRESH" = true ] && echo "Restaurada desde cero" || echo "Base existente mantenida y sincronizada" )"
echo ""
echo -e " ${BOLD}Comandos de Gestión del Servicio:${RESET}"
echo -e " Estado General: ${CYAN}systemctl status ${SERVICE_META}${RESET}"
echo -e " Reiniciar Todo: ${CYAN}systemctl restart ${SERVICE_META}${RESET}"
echo -e " Logs Backend: ${CYAN}journalctl -u ${SERVICE_BACKEND} -f${RESET}"
echo -e " Logs Frontend: ${CYAN}journalctl -u ${SERVICE_FRONTEND} -f${RESET}"
echo ""
ok "Despliegue finalizado. Todos los servicios han sido configurados."
echo ""