From 4e49311208416443e912b7fa835b25f0bf8f7b20 Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Fri, 4 Sep 2026 23:15:02 -0300 Subject: [PATCH] fix(lxc): resolver 500 propagando permisos PostgreSQL universales y logging en journald --- CHANGELOG_SESSION.md | 18 ++++++++ config/config.py | 8 +++- install.sh | 105 +++++++++++++++++++++++++++++++++---------- 3 files changed, 107 insertions(+), 24 deletions(-) diff --git a/CHANGELOG_SESSION.md b/CHANGELOG_SESSION.md index 402c7ec..ef74033 100644 --- a/CHANGELOG_SESSION.md +++ b/CHANGELOG_SESSION.md @@ -384,3 +384,21 @@ * **Dependencias ([requirements.txt](file:///c:/Workspace/admin-edu-space/requirements.txt)):** * Se incorporó formalmente `gunicorn>=21.2.0` al listado de dependencias del proyecto. +--- + +## 23. Resolución de Error 500 (Internal Server Error) y Robustecimiento del Instalador Proxmox LXC (2026-09-04) +* **Causa Raíz del Error HTTP 500 en Despliegues Existentes:** + * Al desplegar sobre contenedores LXC con archivos `.env` preexistentes (por ejemplo, con usuario de base de datos personalizado `eduadmin` extraído de `DATABASE_URL`), el instalador anterior no otorgaba permisos de tablas ni secuencias al usuario dinámico (`GRANT` solo cubría `postgres` y `eduspace_user`), bloqueando las consultas de Flask a PostgreSQL con excepciones de privilegios insuficientes (`permission denied for relation ...`). + * Los registros de excepción de Gunicorn se canalizaban exclusivamente a un archivo sin `--capture-output`, impidiendo su visualización inmediata en `journalctl -u admin-edu-space -f`. +* **Solución y Mejoras en el Instalador ([install.sh](file:///c:/Workspace/admin-edu-space/install.sh)):** + * **Extracción Dinámica de Credenciales:** Parseo bidireccional de `DATABASE_URL` y variables individuales (`DB_USERNAME`, `DB_PASSWORD`, `DB_NAME`), garantizando compatibilidad absoluta con cualquier configuración de conexión. + * **Aprovisionamiento Universal de Roles PostgreSQL:** Creación y asignación de contraseña y privilegios `SUPERUSER CREATEDB` a todos los usuarios detectados (`DB_USER`, `DB_USERNAME`, `postgres`, `eduspace_user`). + * **Garantía Integral de Permisos DDL y DML:** Tras restaurar [classrooms_db.sql](file:///c:/Workspace/admin-edu-space/classrooms_db.sql), se ejecutan `GRANT ALL` en `DATABASE`, `SCHEMA public`, `ALL TABLES`, `ALL SEQUENCES`, `ALL FUNCTIONS` y `ALTER DEFAULT PRIVILEGES` para todos los usuarios. + * **Configuración de Autenticación Local (`pg_hba.conf`):** Ajuste automático en Debian/Ubuntu para que conexiones locales (`127.0.0.1`, `::1` y sockets UNIX) no fallen por discrepancias de método de cifrado (`scram-sha-256`/`md5`). + * **Logs en Tiempo Real en Systemd:** Reconfiguración de Gunicorn en el servicio con `--error-logfile -`, `--capture-output` y `--enable-stdio-inheritance`, permitiendo que cualquier excepción de Python/Flask se visualice en tiempo real vía `journalctl -u admin-edu-space -f`. + * **Healthcheck HTTP Automatizado:** Verificación al final del despliegue con `curl http://127.0.0.1:5000/`, validando que el servidor web responda exitosamente (HTTP 200/302) antes de concluir la instalación. +* **Carga Segura de Configuración ([config/config.py](file:///c:/Workspace/admin-edu-space/config/config.py)):** + * Integración explícita de `load_dotenv` para garantizar que scripts y workers carguen `.env` de forma confiable. + * Normalización automática de URIs con esquema heredado `postgres://` hacia el estándar `postgresql://`. + + diff --git a/config/config.py b/config/config.py index 2585172..4e62950 100644 --- a/config/config.py +++ b/config/config.py @@ -1,10 +1,16 @@ import os +from dotenv import load_dotenv basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +load_dotenv(os.path.join(basedir, '.env')) class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost:5432/classrooms_db' + + _db_url = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost:5432/classrooms_db' + if _db_url.startswith('postgres://'): + _db_url = _db_url.replace('postgres://', 'postgresql://', 1) + SQLALCHEMY_DATABASE_URI = _db_url SQLALCHEMY_TRACK_MODIFICATIONS = False API_BASE_URL = os.environ.get('API_BASE_URL') or 'http://localhost:8080/api' diff --git a/install.sh b/install.sh index e57d765..ab03761 100644 --- a/install.sh +++ b/install.sh @@ -131,6 +131,14 @@ for i in {1..15}; do 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 @@ -140,7 +148,7 @@ 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) +# Valores predeterminados (paridad con sistema activo) DB_NAME="classrooms_db" DB_USER="postgres" DB_PASSWORD="password" @@ -151,9 +159,26 @@ if [[ -f "$APP_ENV" ]]; then # shellcheck disable=SC1090 source <(grep -v '^#' "$APP_ENV" | grep '=') set +a - DB_USER="${DB_USERNAME:-$DB_USER}" + + # 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=$(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))') @@ -182,20 +207,29 @@ 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 +# Configurar usuarios en PostgreSQL con privilegios completos +for u in "${DB_USER}" "${DB_USERNAME:-}" eduspace_user; do + if [[ -n "$u" && "$u" != "postgres" ]]; then + sudo -u postgres psql -c "CREATE USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/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 -# 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 +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 \ +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 +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})" @@ -229,8 +263,8 @@ if [[ -f "$SQL_BACKUP_FILE" ]]; then 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 + 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 @@ -240,17 +274,28 @@ if [[ -f "$SQL_BACKUP_FILE" ]]; then 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 + 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 > /dev/null 2>&1 || true + 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..." @@ -269,13 +314,15 @@ else 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 750 "$LOG_DIR" +chmod 755 "$LOG_DIR" ok "Permisos de seguridad aplicados en ${APP_DIR} y ${LOG_DIR}" # ─── 10. Servicio Systemd (Gunicorn de Producción) ───────────────────────────── @@ -307,7 +354,9 @@ ExecStart=${GUNICORN_BIN} \ --bind 0.0.0.0:5000 \ --timeout 120 \ --access-logfile ${LOG_DIR}/access.log \ - --error-logfile ${LOG_DIR}/error.log \ + --error-logfile - \ + --capture-output \ + --enable-stdio-inheritance \ --log-level info \ wsgi:app Restart=always @@ -329,6 +378,16 @@ 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"