fix(lxc): resolver 500 propagando permisos PostgreSQL universales y logging en journald

This commit is contained in:
Carlos Tello
2026-09-04 23:15:02 -03:00
parent b367b106bf
commit 4e49311208
3 changed files with 107 additions and 24 deletions
+18
View File
@@ -384,3 +384,21 @@
* **Dependencias ([requirements.txt](file:///c:/Workspace/admin-edu-space/requirements.txt)):** * **Dependencias ([requirements.txt](file:///c:/Workspace/admin-edu-space/requirements.txt)):**
* Se incorporó formalmente `gunicorn>=21.2.0` al listado de dependencias del proyecto. * 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://`.
+7 -1
View File
@@ -1,10 +1,16 @@
import os import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
load_dotenv(os.path.join(basedir, '.env'))
class Config: class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' 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 SQLALCHEMY_TRACK_MODIFICATIONS = False
API_BASE_URL = os.environ.get('API_BASE_URL') or 'http://localhost:8080/api' API_BASE_URL = os.environ.get('API_BASE_URL') or 'http://localhost:8080/api'
+82 -23
View File
@@ -131,6 +131,14 @@ for i in {1..15}; do
done done
if [ "$PG_READY" = true ]; then 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 systemctl enable postgresql > /dev/null 2>&1 || true
ok "PostgreSQL activo y aceptando conexiones" ok "PostgreSQL activo y aceptando conexiones"
else else
@@ -140,7 +148,7 @@ fi
# ─── 5. Configurar Variables de Entorno (.env) ───────────────────────────────── # ─── 5. Configurar Variables de Entorno (.env) ─────────────────────────────────
hdr "5. Configurando 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_NAME="classrooms_db"
DB_USER="postgres" DB_USER="postgres"
DB_PASSWORD="password" DB_PASSWORD="password"
@@ -151,9 +159,26 @@ if [[ -f "$APP_ENV" ]]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source <(grep -v '^#' "$APP_ENV" | grep '=') source <(grep -v '^#' "$APP_ENV" | grep '=')
set +a 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_PASSWORD="${DB_PASSWORD:-password}"
DB_NAME=$(echo "${DATABASE_URL:-}" | sed -E 's|.*/([^?]+).*|\1|')
DB_NAME="${DB_NAME:-classrooms_db}" DB_NAME="${DB_NAME:-classrooms_db}"
else else
SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') 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 ─────────────────────── # ─── 6. Configurar Usuario y Base de Datos en PostgreSQL ───────────────────────
hdr "6. Configurando motor PostgreSQL y credenciales de base de datos" hdr "6. Configurando motor PostgreSQL y credenciales de base de datos"
# Asignar contraseña al usuario postgres # Configurar usuarios en PostgreSQL con privilegios completos
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true 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 "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true
sudo -u postgres psql -c "CREATE USER eduspace_user WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \ sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true
|| sudo -u postgres psql -c "ALTER USER eduspace_user WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \
|| true
# Crear base de datos # 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." || 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 for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do
sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO postgres, eduspace_user;" >/dev/null 2>&1 || true 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})" 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..." info "Restaurando volcado integral de aulas, comisiones, materias, reservas y usuarios..."
# Recrear base de datos limpia para carga sin colisiones # 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 "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 "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1
# Restaurar backup filtrando directivas específicas de PG18 si existiesen # 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 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 fi
# Otorgar propiedad y permisos absolutos sobre todas las tablas y secuencias al usuario de la app # 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..." info "Garantizando permisos y secuencias para ${DB_USER}..."
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
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 if [[ -n "$u" ]]; then
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 "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" 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 postgres, eduspace_user;" > /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 "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES 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 \"${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 # Ejecutar init_db.py de forma idempotente para verificar roles y migraciones incrementales
info "Validando coherencia de modelos con init_db.py..." 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 \ 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" ok "Base de datos sincronizada y verificada"
else else
info "No se encontró classrooms_db.sql. Creando esquema desde cero con init_db.py..." 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)" ok "Usuario de sistema '${APP_USER}' creado (sin shell)"
fi fi
mkdir -p "${APP_DIR}/app/static/uploads"
chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" chown -R "${APP_USER}:${APP_USER}" "$APP_DIR"
chmod -R 755 "$APP_DIR" chmod -R 755 "$APP_DIR"
chmod -R 775 "${APP_DIR}/app/static/uploads"
chmod 640 "$APP_ENV" chmod 640 "$APP_ENV"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
chown -R "${APP_USER}:${APP_USER}" "$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}" ok "Permisos de seguridad aplicados en ${APP_DIR} y ${LOG_DIR}"
# ─── 10. Servicio Systemd (Gunicorn de Producción) ───────────────────────────── # ─── 10. Servicio Systemd (Gunicorn de Producción) ─────────────────────────────
@@ -307,7 +354,9 @@ ExecStart=${GUNICORN_BIN} \
--bind 0.0.0.0:5000 \ --bind 0.0.0.0:5000 \
--timeout 120 \ --timeout 120 \
--access-logfile ${LOG_DIR}/access.log \ --access-logfile ${LOG_DIR}/access.log \
--error-logfile ${LOG_DIR}/error.log \ --error-logfile - \
--capture-output \
--enable-stdio-inheritance \
--log-level info \ --log-level info \
wsgi:app wsgi:app
Restart=always Restart=always
@@ -329,6 +378,16 @@ sleep 3
if systemctl is-active --quiet "$SERVICE_NAME"; then if systemctl is-active --quiet "$SERVICE_NAME"; then
ok "Servicio '${SERVICE_NAME}' activo y en ejecución" 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 else
warn "El servicio tardó en arrancar. Podés revisar los logs con:" warn "El servicio tardó en arrancar. Podés revisar los logs con:"
warn " journalctl -u ${SERVICE_NAME} -n 50 --no-pager" warn " journalctl -u ${SERVICE_NAME} -n 50 --no-pager"