diff --git a/install.sh b/install.sh index f9db300..a312ba5 100644 --- a/install.sh +++ b/install.sh @@ -1,83 +1,274 @@ #!/bin/bash -# --- SCRIPT DE INSTALACIÓN FINAL (Arquitectura Web + Worker) --- +# ============================================================================== +# SCRIPT DE REINSTALACIÓN PARA APLICACIÓN RSS (MODO ACCESO WEB DIRECTO) +# +# CARGA LOS DATOS INICIALES DESDE LOS ARCHIVOS .sql EN EL DIRECTORIO. +# SIRVE LA APLICACIÓN DIRECTAMENTE EN EL PUERTO 8000 USANDO GUNICORN. +# +# ACCIONES DESTRUCTIVAS: +# - DETIENE y ELIMINA todos los servicios systemd que empiecen por "rss". +# - ELIMINA (DROP) la base de datos y el usuario de la base de datos. +# +# USO: +# 1. Clona tu repositorio y entra en su directorio. +# 2. Asegúrate de tener los archivos .sql (categorias.sql, etc.) en la raíz. +# 3. Dale permisos de ejecución a este script: chmod +x install.sh +# 4. Ejecútalo con sudo: sudo ./install.sh +# ============================================================================== -set -e +set -e # Termina el script si un comando falla # ========= CONFIGURACIÓN ========= APP_NAME="rss" DB_NAME="rss" DB_USER="rss" -APP_USER="x" -APP_DIR="/home/$APP_USER/$APP_NAME" +APP_USER="x" # El usuario del sistema que ejecutará la aplicación +APP_DIR=$(pwd) # Asume que el directorio de la app es el directorio actual PYTHON_ENV="$APP_DIR/venv" WSGI_APP_ENTRY="app:app" +WEB_PORT=8000 # Puerto en el que la aplicación será accesible -# ========= 0. COMPROBACIONES INICIALES Y CREACIÓN DE USUARIO ========= -echo "🟢 Paso 0: Verificando usuario y pidiendo contraseña de la BD..." +# ========= 0. COMPROBACIONES Y CONFIRMACIÓN DE SEGURIDAD ========= +echo "🟢 Paso 0: Verificaciones y confirmación de seguridad" + +if [[ $EUID -ne 0 ]]; then + echo "❌ Este script debe ser ejecutado como root (usa sudo)." + exit 1 +fi + +echo "------------------------------------------------------------------" +echo "⚠️ ADVERTENCIA: Este script realizará las siguientes acciones DESTRUCTIVAS:" +echo " - Eliminará TODOS los servicios systemd que empiecen por '$APP_NAME'." +echo " - Eliminará PERMANENTEMENTE la base de datos '$DB_NAME'." +echo " - Eliminará PERMANENTEMENTE el usuario de base de datos '$DB_USER'." +echo "------------------------------------------------------------------" +read -p "Estás seguro de que quieres continuar? (escribe 'si' para confirmar): " CONFIRM +if [ "$CONFIRM" != "si" ]; then + echo "Operación cancelada por el usuario." + exit 0 +fi + +read -sp "🔑 Introduce la contraseña para el usuario de la base de datos '$DB_USER' (se creará de nuevo): " DB_PASS +echo +if [ -z "$DB_PASS" ]; then + echo "❌ La contraseña no puede estar vacía. Abortando." + exit 1 +fi + +# ========= 0.5: LIMPIEZA DE LA INSTALACIÓN ANTERIOR ========= +echo "🧹 Paso 0.5: Limpiando instalación anterior..." +echo " -> Buscando y eliminando servicios systemd antiguos..." +for service in $(systemctl list-unit-files | grep "^$APP_NAME" | cut -d' ' -f1); do + echo " -> Deteniendo y deshabilitando $service" + systemctl stop "$service" || true + systemctl disable "$service" || true +done +rm -f /etc/systemd/system/$APP_NAME* +systemctl daemon-reload +echo " -> Servicios systemd limpiados." + +# ========= 1. INSTALAR DEPENDENCIAS DEL SISTEMA ========= +echo "🟢 Paso 1: Instalando dependencias del sistema (PostgreSQL, Python, Gunicorn...)" +apt-get update +apt-get install -y wget ca-certificates postgresql postgresql-contrib python3-venv python3-pip python3-dev libpq-dev gunicorn + +# ========= 2. RECREAR LA BASE DE DATOS Y EL USUARIO ========= +echo "🔥 Paso 2: Eliminando y recreando la base de datos y el usuario..." +sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" +sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" +echo " -> Entidades de BD anteriores eliminadas." +sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';" +sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" +echo "✅ Base de datos y usuario recreados con éxito." + +# ========= 3. PREPARAR ENTORNO DE LA APP ========= +echo "🐍 Paso 3: Configurando el entorno de la aplicación..." if ! id "$APP_USER" &>/dev/null; then echo "👤 Creando usuario del sistema '$APP_USER'..." sudo useradd -m -s /bin/bash "$APP_USER" + echo "✅ Usuario '$APP_USER' creado." else echo "✅ Usuario del sistema '$APP_USER' ya existe." fi - -if [ -z "$DB_PASS" ]; then - read -sp "🔑 Introduce la contraseña para el usuario de la base de datos '$DB_USER': " DB_PASS - echo - if [ -z "DB\_PASS" \]; then -echo "❌ La contraseña no puede estar vacía\. Abortando\." -exit 1 -fi -fi -\# \=\=\=\=\=\=\=\=\= 1\. INSTALAR DEPENDENCIAS DEL SISTEMA \=\=\=\=\=\=\=\=\= -echo "🟢 Paso 1\: Instalando dependencias del sistema\.\.\." -sudo apt\-get update -sudo apt\-get install \-y wget ca\-certificates postgresql postgresql\-contrib python3\-venv python3\-pip python3\-dev libpq\-dev -\# \=\=\=\=\=\=\=\=\= 2\. CONFIGURAR POSTGRESQL \=\=\=\=\=\=\=\=\= -echo "🛠️ Paso 2\: Configurando PostgreSQL\.\.\." -sudo \-u postgres psql \-c <7\>"DO \\$\\ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_user WHERE usename = '$DB_USER') THEN CREATE USER $DB_USER WITH PASSWORD '$DB_PASS'; ELSE ALTER USER $DB_USER WITH PASSWORD 'DB\_PASS'; END IF; END \\$\\;" -if ! sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME"; then - sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" - echo "Base de datos '$DB_NAME' creada." -else - echo "✅ Base de datos '$DB_NAME' ya existe." -fi - -# ========= 3. PREPARAR DIRECTORIO Y ENTORNO DE LA APP ========= -echo "🐍 Paso 3: Configurando el directorio de la aplicación y el entorno virtual..." -sudo mkdir -p "$APP_DIR" -sudo chown -R "$APP_USER":"$APP_USER" "$APP_DIR" +chown -R "$APP_USER":"$APP_USER" "$APP_DIR" sudo -u "$APP_USER" bash < Creando entorno virtual en $PYTHON_ENV" +rm -rf "$PYTHON_ENV" python3 -m venv "$PYTHON_ENV" -echo "📦 Instalando dependencias desde requirements.txt..." +echo " -> Instalando dependencias desde requirements.txt..." "$PYTHON_ENV/bin/python" -m pip install --upgrade pip if [ -f "requirements.txt" ]; then "$PYTHON_ENV/bin/python" -m pip install -r "requirements.txt" else - echo "⚠️ ADVERTENCIA: No se encontró requirements.txt." + echo "⚠️ ADVERTENCIA: No se encontró requirements.txt. La aplicación podría no funcionar." fi EOF +echo "✅ Entorno de Python configurado." -# ========= 4. CREAR TABLAS Y CONFIGURAR BÚSQUEDA DE TEXTO COMPLETO ========= -echo "📐 Paso 4: Creando/verificando tablas y configurando Full-Text Search..." +# ========= 4. CREAR ESQUEMA Y SEMBRAR DATOS DESDE ARCHIVOS SQL ========= +echo "📐 Paso 4: Creando esquema de BD, configurando FTS y sembrando datos desde archivos .sql..." export PGPASSWORD="$DB_PASS" -# --- Creación de tablas base completas --- -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TABLE IF NOT EXISTS continentes (id SERIAL PRIMARY KEY, nombre VARCHAR(50) NOT NULL UNIQUE);" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TABLE IF NOT EXISTS categorias (id SERIAL PRIMARY KEY, nombre VARCHAR(100) NOT NULL UNIQUE);" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TABLE IF NOT EXISTS paises (id SERIAL PRIMARY KEY, nombre VARCHAR(100) NOT NULL UNIQUE, continente_id INTEGER REFERENCES continentes(id) ON DELETE SET NULL);" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TABLE IF NOT EXISTS feeds (id SERIAL PRIMARY KEY, nombre VARCHAR(255), descripcion TEXT, url TEXT NOT NULL UNIQUE, categoria_id INTEGER REFERENCES categorias(id) ON DELETE SET NULL, pais_id INTEGER REFERENCES paises(id) ON DELETE SET NULL, idioma CHAR(2), activo BOOLEAN DEFAULT TRUE, fallos INTEGER DEFAULT 0, last_etag TEXT, last_modified TEXT);" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TABLE IF NOT EXISTS noticias (id VARCHAR(32) PRIMARY KEY, titulo TEXT, resumen TEXT, url TEXT NOT NULL UNIQUE, fecha TIMESTAMP, imagen_url TEXT, categoria_id INTEGER REFERENCES categorias(id) ON DELETE SET NULL, pais_id INTEGER REFERENCES paises(id) ON DELETE SET NULL, tsv tsvector);" -# --- Configuración de FTS --- -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "ALTER TABLE noticias ADD COLUMN IF NOT EXISTS tsv tsvector;" -psql -U "$DB_USER" -h localhost -d "DB\_NAME" \-c "CREATE OR REPLACE FUNCTION noticias\_tsv\_trigger\(\) RETURNS trigger AS \\$\\$ begin new\.tsv \:\= setweight\(to\_tsvector\('spanish', coalesce\(new\.titulo,''\)\), 'A'\) \|\| setweight\(to\_tsvector\('spanish', coalesce\(new\.resumen,''\)\), 'B'\); return new; end \\$\\ LANGUAGE plpgsql;" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "DROP TRIGGER IF EXISTS tsvectorupdate ON noticias;" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE TRIGGER tsvectorupdate BEFORE INSERT ON noticias FOR EACH ROW EXECUTE PROCEDURE noticias_tsv_trigger();" -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "CREATE INDEX IF NOT EXISTS noticias_tsv_idx ON noticias USING gin(tsv);" -echo "🔄 Actualizando índice de búsqueda para noticias existentes (puede tardar)..." -psql -U "$DB_USER" -h localhost -d "$DB_NAME" -c "UPDATE noticias SET tsv = setweight(to_tsvector('spanish', coalesce(titulo,'')), 'A') || setweight(to_tsvector('spanish', coalesce(resumen,'')), 'B') WHERE tsv IS NULL;" -# --- Cargar datos iniciales --- -for sql_file in continentes.sql paises.sql + +# Crear las tablas primero +psql -U "$DB_USER" -h localhost -d "$DB_NAME" < Buscando archivos .sql para sembrar datos..." +if [ -f "continentes.sql" ]; then + echo " -> Cargando continentes.sql..." + psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "continentes.sql" +else + echo " -> ADVERTENCIA: No se encontró continentes.sql" +fi + +if [ -f "categorias.sql" ]; then + echo " -> Cargando categorias.sql..." + psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "categorias.sql" +else + echo " -> ADVERTENCIA: No se encontró categorias.sql" +fi + +if [ -f "paises.sql" ]; then + echo " -> Cargando paises.sql..." + psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "paises.sql" +else + echo " -> ADVERTENCIA: No se encontró paises.sql" +fi + +# Reiniciar las secuencias para que los nuevos INSERTs no colisionen +echo " -> Actualizando contadores de secuencias de la base de datos..." +psql -U "$DB_USER" -h localhost -d "$DB_NAME" < "$APP_DIR/worker.py" +import sys +import os +import logging +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +try: + from app import app, fetch_and_store +except ImportError as e: + logging.basicConfig() + logging.critical(f"No se pudo importar la aplicación Flask. Error: {e}") + sys.exit(1) +if __name__ == "__main__": + with app.app_context(): + fetch_and_store() +EOF +chown "$APP_USER":"$APP_USER" "$APP_DIR/worker.py" +echo "✅ Script del worker creado/actualizado." + +# ========= 6. CREAR SERVICIOS SYSTEMD ========= +echo "⚙️ Paso 6: Creando nuevos archivos de servicio systemd..." + +# --- Servicio para la aplicación web (Gunicorn) --- +cat < /etc/systemd/system/$APP_NAME.service +[Unit] +Description=Gunicorn instance to serve $APP_NAME +After=network.target + +[Service] +User=$APP_USER +Group=$APP_USER +WorkingDirectory=$APP_DIR +Environment="PATH=$PYTHON_ENV/bin" +Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')" +Environment="DB_HOST=localhost" +Environment="DB_PORT=5432" +Environment="DB_NAME=$DB_NAME" +Environment="DB_USER=$DB_USER" +Environment="DB_PASS=$DB_PASS" + +# --- LÍNEA CLAVE --- +# Gunicorn escucha en todas las IPs (0.0.0.0) en el puerto especificado +ExecStart=$PYTHON_ENV/bin/gunicorn --workers 3 --bind 0.0.0.0:$WEB_PORT $WSGI_APP_ENTRY + +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +# --- Servicio para el worker --- +cat < /etc/systemd/system/$APP_NAME-worker.service +[Unit] +Description=$APP_NAME Feed Fetcher Worker +After=postgresql.service +[Service] +Type=oneshot +User=$APP_USER +WorkingDirectory=$APP_DIR +Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')" +Environment="DB_HOST=localhost" +Environment="DB_PORT=5432" +Environment="DB_NAME=$DB_NAME" +Environment="DB_USER=$DB_USER" +Environment="DB_PASS=$DB_PASS" +ExecStart=$PYTHON_ENV/bin/python $APP_DIR/worker.py +EOF + +# --- Timer para el worker --- +cat < /etc/systemd/system/$APP_NAME-worker.timer +[Unit] +Description=Run $APP_NAME worker every 15 minutes +[Timer] +OnBootSec=5min +OnUnitActiveSec=15min +Unit=$APP_NAME-worker.service +[Install] +WantedBy=timers.target +EOF +echo "✅ Archivos de servicio y timer creados." + +# ========= 7. HABILITAR, ARRANCAR SERVICIOS Y ABRIR FIREWALL ========= +echo "🚀 Paso 7: Recargando, habilitando, arrancando servicios y configurando firewall..." +systemctl daemon-reload +systemctl enable $APP_NAME.service +systemctl start $APP_NAME.service +systemctl enable $APP_NAME-worker.timer +systemctl start $APP_NAME-worker.timer + +# Abre el puerto en el firewall (UFW), si está activo +if command -v ufw &> /dev/null && ufw status | grep -q 'Status: active'; then + echo " -> Firewall UFW detectado. Abriendo puerto $WEB_PORT..." + ufw allow $WEB_PORT/tcp + ufw reload +fi + +echo "" +echo "🎉 ¡REINSTALACIÓN COMPLETADA!" +echo "--------------------------------" +echo "" +echo "✅ La aplicación web está ahora accesible en:" +echo " http://:$WEB_PORT" +echo "" +echo "Puedes verificar el estado de los servicios con:" +echo "sudo systemctl status $APP_NAME.service" +echo "sudo systemctl status $APP_NAME-worker.timer" +echo "" +echo "Para ver los logs de la aplicación web:" +echo "sudo journalctl -u $APP_NAME.service -f" + diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..4e183c2 --- /dev/null +++ b/worker.py @@ -0,0 +1,13 @@ +import sys +import os +import logging +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +try: + from app import app, fetch_and_store +except ImportError as e: + logging.basicConfig() + logging.critical(f"No se pudo importar la aplicación Flask. Error: {e}") + sys.exit(1) +if __name__ == "__main__": + with app.app_context(): + fetch_and_store()