Files
admin-edu-space/install.sh
T

355 lines
16 KiB
Bash

#!/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 movemos a /opt para evitar problemas de permisos
if [[ "${APP_DIR}" == /root/* ]]; then
NEW_DIR="/opt/admin-edu-space"
warn "Detectado directorio en ${APP_DIR}. Moviendo a ${NEW_DIR} para evitar errores de permisos..."
mkdir -p /opt
mv "${APP_DIR}" "${NEW_DIR}"
cd "${NEW_DIR}"
exec bash install.sh
fi
[[ ! -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
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)"
# Configuración idéntica al entorno del sistema activo (100% automático sin interacción)
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
DB_USER="${DB_USERNAME:-$DB_USER}"
DB_PASSWORD="${DB_PASSWORD:-password}"
DB_NAME=$(echo "${DATABASE_URL:-}" | sed -E 's|.*/([^?]+).*|\1|')
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"
# Asignar contraseña al usuario postgres
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true
# Crear usuario eduspace_user con la misma contraseña como alternativa
sudo -u postgres psql -c "CREATE USER eduspace_user WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| sudo -u postgres psql -c "ALTER USER eduspace_user WITH PASSWORD '${DB_PASSWORD}';" >/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."
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO postgres, eduspace_user;" >/dev/null 2>&1 || true
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO postgres, eduspace_user;" >/dev/null 2>&1 || true
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} y eduspace_user..."
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO postgres, eduspace_user;" > /dev/null 2>&1 || true
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres, eduspace_user;" > /dev/null 2>&1 || true
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO postgres, eduspace_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 postgres, eduspace_user;" > /dev/null 2>&1 || true
sudo -u postgres psql -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres, eduspace_user;" > /dev/null 2>&1 || true
# 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 > /dev/null 2>&1 || true
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
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"
chmod 750 "$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 ${LOG_DIR}/error.log \
--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"
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 ""