commit 733ef04eeb607aa7ed9b4e618c046d134d2bcf5a Author: ernestohc21 Date: Thu Nov 6 09:50:10 2025 -0700 Initial commit: SYNC_API project diff --git a/sync_api/.dockerignore b/sync_api/.dockerignore new file mode 100644 index 0000000..eb260f2 --- /dev/null +++ b/sync_api/.dockerignore @@ -0,0 +1,24 @@ +# Archivos a ignorar en Docker +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +.env.local +.env.development +.env.test +.venv/ +venv/ +.git/ +.gitignore +README.md +*.log +.pytest_cache/ +.coverage +htmlcov/ +.mypy_cache/ +.DS_Store +Thumbs.db +*.bat +run_*.bat +test_*.bat diff --git a/sync_api/.env b/sync_api/.env new file mode 100644 index 0000000..b1b70a5 --- /dev/null +++ b/sync_api/.env @@ -0,0 +1,32 @@ +# Variables de entorno para conexión SQL Server - CONTROLDESK +DATABASE_SERVER=104.192.7.202 +DATABASE_NAME=controldesk +DATABASE_USER=sa +DATABASE_PASSWORD=Soluciones01 +DATABASE_DRIVER=ODBC Driver 17 for SQL Server +DATABASE_PORT=1433 + +# Variables de entorno para servidor de respaldos +BACKUP_SERVER=104.192.7.152 +BACKUP_DATABASE=master +BACKUP_USER=sa +BACKUP_PASSWORD=Soluciones01 +BACKUP_DRIVER=ODBC Driver 17 for SQL Server +BACKUP_PORT=1433 + +# Configuración de la API +API_HOST=0.0.0.0 +API_PORT=9443 +DEBUG_MODE=False + +# Configuración de logging +LOG_LEVEL=INFO + +# Configuración opcional de caché +CACHE_TTL=300 +ENABLE_CACHE=False + +# Configuración de seguridad +API_USERNAME=admin +API_PASSWORD=Aduanasoft2025! + diff --git a/sync_api/DOCKER_README.md b/sync_api/DOCKER_README.md new file mode 100644 index 0000000..48d22e1 --- /dev/null +++ b/sync_api/DOCKER_README.md @@ -0,0 +1,109 @@ +# API FastAPI - Contenedor Docker + +## 🐳 Construcción y Ejecución con Docker + +### Prerrequisitos +- Docker Desktop instalado y ejecutándose +- Docker Compose disponible + +### 🚀 Inicio Rápido + +1. **Construir la imagen:** + ```bash + docker-build.bat + ``` + +2. **Ejecutar el contenedor:** + ```bash + docker-run.bat + ``` + +### 🔧 Comandos Docker Manuales + +#### Construir imagen +```bash +docker build -t aduanasoft-sync-api:latest . +``` + +#### Ejecutar con Docker Compose +```bash +docker-compose up -d +``` + +#### Ejecutar contenedor individual +```bash +docker run -d \ + -p 9443:9443 \ + --name sync-api \ + -e DATABASE_SERVER=104.192.7.202 \ + -e BACKUP_SERVER=104.192.7.152 \ + -e API_USERNAME=admin \ + -e API_PASSWORD=Aduanasoft2025! \ + aduanasoft-sync-api:latest +``` + +### 📊 Verificación y Monitoreo + +#### Ver logs +```bash +docker-compose logs -f +``` + +#### Estado del contenedor +```bash +docker-compose ps +``` + +#### Health check +```bash +curl -k -u admin:Aduanasoft2025! https://localhost:9443/api/health +``` + +### 🌐 URLs de Acceso + +- **Documentación:** https://cpanel-a24.aduanasoft.com:9443/docs +- **Health Check:** https://cpanel-a24.aduanasoft.com:9443/api/health +- **Nodos:** https://cpanel-a24.aduanasoft.com:9443/api/nodes + +### 🔐 Autenticación + +- **Usuario:** admin +- **Password:** Aduanasoft2025! + +### 🛑 Detener Servicios + +```bash +docker-compose down +``` + +### 🔄 Actualizar Contenedor + +1. Detener contenedor actual: + ```bash + docker-compose down + ``` + +2. Reconstruir imagen: + ```bash + docker-compose build --no-cache + ``` + +3. Iniciar nuevamente: + ```bash + docker-compose up -d + ``` + +### 📁 Estructura de Archivos + +``` +sync_api/ +├── Dockerfile # Definición de imagen Docker +├── docker-compose.yml # Orquestación de servicios +├── docker-build.bat # Script de construcción +├── docker-run.bat # Script de ejecución +├── .dockerignore # Archivos a ignorar +├── ssl/ # Certificados SSL +│ ├── certificate.crt +│ └── private.key +└── ... # Código fuente de la API +``` diff --git a/sync_api/Dockerfile b/sync_api/Dockerfile new file mode 100644 index 0000000..458a18f --- /dev/null +++ b/sync_api/Dockerfile @@ -0,0 +1,43 @@ +# Usar imagen base de Python +FROM python:3.12-slim + +# Establecer directorio de trabajo +WORKDIR /app + +# Instalar dependencias del sistema necesarias para SQL Server +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + unixodbc \ + unixodbc-dev \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ + && apt-get update \ + && ACCEPT_EULA=Y apt-get install -y msodbcsql17 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copiar requirements +COPY requirements.txt . + +# Instalar dependencias Python +RUN pip install --no-cache-dir -r requirements.txt + +# Copiar código fuente +COPY . . + +# Crear directorio SSL y copiar certificados +RUN mkdir -p ssl +COPY ssl/ ssl/ + +# Exponer puerto +EXPOSE 9443 + +# Variables de entorno por defecto +ENV API_HOST=0.0.0.0 +ENV API_PORT=9443 +ENV DEBUG_MODE=False +ENV LOG_LEVEL=INFO + +# Comando para ejecutar la aplicación +CMD ["python", "main.py"] diff --git a/sync_api/README.md b/sync_api/README.md new file mode 100644 index 0000000..f890621 --- /dev/null +++ b/sync_api/README.md @@ -0,0 +1,274 @@ +# API de Monitoreo de Sincronización - Aduanasoft + +Sistema de monitoreo de bases de datos SQL Server con API REST desarrollada en FastAPI. + +## Características + +- ✅ API REST completa con FastAPI +- ✅ Conexión robusta a SQL Server con pyodbc +- ✅ Endpoints para monitoreo de nodos individuales y generales +- ✅ Health checks del sistema +- ✅ Información de backups (opcional) +- ✅ Configuración CORS para integración PHP +- ✅ Logging completo +- ✅ Documentación automática con Swagger/OpenAPI +- ✅ Manejo de errores robusto +- ✅ Validación de datos con Pydantic + +## Estructura del Proyecto + +``` +sync_api/ +├── main.py # Aplicación principal FastAPI +├── requirements.txt # Dependencias Python +├── .env # Variables de entorno +├── models/ +│ └── sync_models.py # Modelos Pydantic para respuestas +├── database/ +│ └── connection.py # Conexión y queries SQL Server +├── routers/ +│ ├── nodes.py # Endpoints de nodos +│ └── health.py # Endpoints de salud +└── config/ + └── settings.py # Configuración de la aplicación +``` + +## Configuración + +### 1. Variables de Entorno + +Editar el archivo `.env` con tus credenciales de SQL Server: + +```bash +DATABASE_SERVER=tu_servidor +DATABASE_NAME=tu_base_datos +DATABASE_USER=tu_usuario +DATABASE_PASSWORD=tu_password +DATABASE_DRIVER=ODBC Driver 17 for SQL Server +``` + +### 2. Instalación de Dependencias + +```bash +pip install -r requirements.txt +``` + +### 3. Verificar Driver ODBC + +Asegúrate de tener instalado el driver ODBC 17 para SQL Server: +- [Microsoft ODBC Driver 17 for SQL Server](https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server) + +## Ejecución + +### Desarrollo +```bash +python main.py +``` + +### Producción con Uvicorn +```bash +uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +### Producción con Gunicorn (Linux) +```bash +gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +## Endpoints Disponibles + +### 📊 Monitoreo de Nodos + +- **GET /api/nodes** - Lista todos los nodos +- **GET /api/nodes/{node_name}/status** - Estado específico de un nodo +- **GET /api/nodes/{node_name}/backup-info** - Información de respaldos + +### 🏥 Health Checks + +- **GET /api/health** - Estado general del sistema +- **GET /api/ping** - Ping simple + +### 📖 Documentación + +- **GET /docs** - Documentación Swagger UI +- **GET /redoc** - Documentación ReDoc +- **GET /** - Información general de la API + +## Ejemplos de Uso + +### Estado de un Nodo Específico +```bash +curl http://localhost:8000/api/nodes/NODO001/status +``` + +Respuesta: +```json +{ + "nodo_sub_nodo": "NODO001", + "estado_sincronizacion": "Actualizada", + "ultima_sincronizacion": "2025-11-03T10:30:00", + "total_clientes": 45, + "clientes_activos": 42, + "clientes_inactivos": 3, + "tiempo_desde_ultima_sync": "2 horas" +} +``` + +### Lista de Todos los Nodos +```bash +curl http://localhost:8000/api/nodes +``` + +### Health Check del Sistema +```bash +curl http://localhost:8000/api/health +``` + +Respuesta: +```json +{ + "status": "healthy", + "database_status": "healthy", + "total_nodes": 15, + "nodes_healthy": 12, + "nodes_attention": 2, + "nodes_error": 1, + "last_check": "2025-11-03T12:00:00", + "uptime": "2h 15m", + "version": "1.0.0" +} +``` + +## Integración con PHP + +### Ejemplo AJAX básico: + +```javascript +// Obtener estado de un nodo +fetch('http://localhost:8000/api/nodes/NODO001/status') + .then(response => response.json()) + .then(data => { + console.log('Estado del nodo:', data); + // Actualizar dashboard + }) + .catch(error => console.error('Error:', error)); + +// Health check periódico +setInterval(() => { + fetch('http://localhost:8000/api/health') + .then(response => response.json()) + .then(data => { + updateHealthIndicator(data.status); + }); +}, 30000); // Cada 30 segundos +``` + +### Ejemplo PHP con cURL: + +```php +function getNodeStatus($nodeName) { + $url = "http://localhost:8000/api/nodes/{$nodeName}/status"; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + return json_decode($response, true); + } + + return null; +} + +// Uso +$nodeStatus = getNodeStatus('NODO001'); +if ($nodeStatus) { + echo "Estado: " . $nodeStatus['estado_sincronizacion']; +} +``` + +## Estados de Sincronización + +| Estado | Descripción | Acción Recomendada | +|--------|-------------|-------------------| +| **Actualizada** | Funcionando correctamente | Monitoreo normal | +| **Atención** | Requiere revisión | Investigar causa | +| **Error** | Problema crítico | Acción inmediata | + +## Logging + +Los logs se muestran en consola y incluyen: +- Peticiones HTTP entrantes y salientes +- Tiempos de procesamiento +- Errores de conexión a BD +- Estadísticas de consultas + +Nivel de logging configurable via `LOG_LEVEL` en `.env`: +- `DEBUG`: Información detallada +- `INFO`: Información general (por defecto) +- `WARNING`: Solo advertencias y errores +- `ERROR`: Solo errores + +## Monitoreo y Alertas + +### Métricas Clave a Monitorear: +- Estado de conectividad de la API (`/api/ping`) +- Estado general del sistema (`/api/health`) +- Tiempo de respuesta de endpoints +- Errores de conexión a base de datos +- Distribución de estados de nodos + +### Alertas Sugeridas: +- API no responde en `/api/ping` +- Base de datos desconectada +- Más del 20% de nodos en estado "Error" +- Tiempo de respuesta > 5 segundos +- Nodo sin sincronizar > 24 horas + +## Solución de Problemas + +### Error de Conexión a SQL Server +1. Verificar credenciales en `.env` +2. Confirmar que SQL Server acepta conexiones +3. Verificar firewall y puertos +4. Comprobar driver ODBC instalado + +### API no responde +1. Verificar que el puerto 8000 esté disponible +2. Revisar logs de la aplicación +3. Confirmar dependencias instaladas + +### Errores de CORS +1. Agregar el dominio PHP a `allow_origins` en `main.py` +2. Verificar headers permitidos +3. Comprobar métodos HTTP utilizados + +## Desarrollo y Extensiones + +### Agregar Nuevos Endpoints: +1. Crear función en router apropiado (`routers/nodes.py` o `routers/health.py`) +2. Definir modelo de respuesta en `models/sync_models.py` +3. Documentar endpoint con docstrings + +### Optimizaciones Futuras: +- Cache con Redis para consultas frecuentes +- Paginación avanzada para listas grandes +- Filtros adicionales por fecha/cliente +- Websockets para actualizaciones en tiempo real +- Autenticación y autorización + +## Soporte + +Para soporte técnico o mejoras, contactar al equipo de desarrollo de Aduanasoft. + +--- + +**Versión:** 1.0.0 +**Desarrollado con:** FastAPI + Python 3.12 +**Base de Datos:** SQL Server +**Licencia:** Propietaria - Aduanasoft diff --git a/sync_api/__pycache__/main.cpython-312.pyc b/sync_api/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..2a40d5f Binary files /dev/null and b/sync_api/__pycache__/main.cpython-312.pyc differ diff --git a/sync_api/build-and-run.sh b/sync_api/build-and-run.sh new file mode 100644 index 0000000..c786e95 --- /dev/null +++ b/sync_api/build-and-run.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Script para construir y ejecutar el contenedor Docker + +echo "================================================" +echo " SCRIPT DE CONSTRUCCIÓN DOCKER - SYNC API" +echo "================================================" +echo "" + +# Verificar Docker +echo "🔍 Verificando Docker..." +if ! command -v docker &> /dev/null; then + echo "❌ Docker no está instalado o no está en el PATH" + echo " Instala Docker Desktop desde: https://www.docker.com/products/docker-desktop/" + exit 1 +fi + +if ! docker info &> /dev/null; then + echo "❌ Docker no está ejecutándose" + echo " Inicia Docker Desktop y ejecuta este script nuevamente" + exit 1 +fi + +echo "✅ Docker está disponible" +echo "" + +# Construir imagen +echo "🏗️ Construyendo imagen Docker..." +docker build -t aduanasoft-sync-api:latest . + +if [ $? -eq 0 ]; then + echo "" + echo "✅ Imagen construida exitosamente" + echo "" + + # Ejecutar con Docker Compose + echo "🚀 Iniciando contenedor con Docker Compose..." + docker-compose up -d + + if [ $? -eq 0 ]; then + echo "" + echo "🎉 ¡CONTENEDOR INICIADO EXITOSAMENTE!" + echo "" + echo "🌐 URLs disponibles:" + echo " - https://cpanel-a24.aduanasoft.com:9443/docs" + echo " - https://104.192.7.152:9443/docs" + echo " - https://localhost:9443/docs" + echo "" + echo "🔐 Credenciales de acceso:" + echo " - Usuario: admin" + echo " - Password: Aduanasoft2025!" + echo "" + echo "📊 Comandos útiles:" + echo " - Ver logs: docker-compose logs -f" + echo " - Estado: docker-compose ps" + echo " - Detener: docker-compose down" + echo " - Health check: curl -k -u admin:Aduanasoft2025! https://localhost:9443/api/health" + else + echo "❌ Error al iniciar el contenedor" + fi +else + echo "❌ Error al construir la imagen Docker" +fi diff --git a/sync_api/config/__init__.py b/sync_api/config/__init__.py new file mode 100644 index 0000000..026b09a --- /dev/null +++ b/sync_api/config/__init__.py @@ -0,0 +1 @@ +# Config module initialization diff --git a/sync_api/config/__pycache__/__init__.cpython-312.pyc b/sync_api/config/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a1bfaa2 Binary files /dev/null and b/sync_api/config/__pycache__/__init__.cpython-312.pyc differ diff --git a/sync_api/config/__pycache__/settings.cpython-312.pyc b/sync_api/config/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..6c0d29a Binary files /dev/null and b/sync_api/config/__pycache__/settings.cpython-312.pyc differ diff --git a/sync_api/config/settings.py b/sync_api/config/settings.py new file mode 100644 index 0000000..40d38ac --- /dev/null +++ b/sync_api/config/settings.py @@ -0,0 +1,45 @@ +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + # Configuración de base de datos CONTROLDESK + database_server: str = "localhost" + database_name: str + database_user: str + database_password: str + database_driver: str = "ODBC Driver 17 for SQL Server" + database_port: int = 1433 + + # Configuración de servidor de respaldos + backup_server: str + backup_database: str + backup_user: str + backup_password: str + backup_driver: str = "ODBC Driver 17 for SQL Server" + backup_port: int = 1433 + + # Configuración de la API + api_host: str = "0.0.0.0" + api_port: int = 8000 + debug_mode: bool = True + + # Configuración de logging + log_level: str = "INFO" + + # Configuración de caché + cache_ttl: int = 300 + enable_cache: bool = False + + # Configuración de seguridad + api_username: str = "admin" + api_password: str = "password" + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = False + + +# Instancia global de configuración +settings = Settings() diff --git a/sync_api/database/__init__.py b/sync_api/database/__init__.py new file mode 100644 index 0000000..ca2ebad --- /dev/null +++ b/sync_api/database/__init__.py @@ -0,0 +1 @@ +# Database module initialization diff --git a/sync_api/database/__pycache__/__init__.cpython-312.pyc b/sync_api/database/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f21ccc4 Binary files /dev/null and b/sync_api/database/__pycache__/__init__.cpython-312.pyc differ diff --git a/sync_api/database/__pycache__/connection.cpython-312.pyc b/sync_api/database/__pycache__/connection.cpython-312.pyc new file mode 100644 index 0000000..0d49685 Binary files /dev/null and b/sync_api/database/__pycache__/connection.cpython-312.pyc differ diff --git a/sync_api/database/connection.py b/sync_api/database/connection.py new file mode 100644 index 0000000..7ad6e98 --- /dev/null +++ b/sync_api/database/connection.py @@ -0,0 +1,132 @@ +import pyodbc +import logging +from typing import Optional, Dict, Any, List +from contextlib import contextmanager +from config.settings import settings +import time + + +class SQLServerConnection: + def __init__(self): + self.logger = logging.getLogger(__name__) + + def _build_connection_string(self, server_type="main") -> str: + """Construye la cadena de conexión para SQL Server""" + if server_type == "backup": + return ( + f"DRIVER={{{settings.backup_driver}}};" + f"SERVER={settings.backup_server},{settings.backup_port};" + f"DATABASE={settings.backup_database};" + f"UID={settings.backup_user};" + f"PWD={settings.backup_password};" + f"TrustServerCertificate=yes;" + f"Connection Timeout=30;" + ) + else: # main server (CONTROLDESK) + return ( + f"DRIVER={{{settings.database_driver}}};" + f"SERVER={settings.database_server},{settings.database_port};" + f"DATABASE={settings.database_name};" + f"UID={settings.database_user};" + f"PWD={settings.database_password};" + f"TrustServerCertificate=yes;" + f"Connection Timeout=30;" + ) + + @contextmanager + def get_connection(self, server_type="main"): + """Context manager para manejar conexiones con cleanup automático""" + connection = None + try: + server_name = "CONTROLDESK" if server_type == "main" else "BACKUP" + self.logger.info(f"Estableciendo conexión con servidor {server_name}...") + connection_string = self._build_connection_string(server_type) + connection = pyodbc.connect( + connection_string, + timeout=30, + autocommit=True + ) + yield connection + except pyodbc.Error as e: + self.logger.error(f"Error de conexión SQL Server: {str(e)}") + raise + except Exception as e: + self.logger.error(f"Error inesperado en conexión: {str(e)}") + raise + finally: + if connection: + try: + connection.close() + self.logger.debug("Conexión cerrada correctamente") + except Exception as e: + self.logger.warning(f"Error al cerrar conexión: {str(e)}") + + def execute_query(self, query: str, params: Optional[tuple] = None, server_type="main") -> List[Dict[str, Any]]: + """Ejecuta una consulta SELECT y retorna los resultados como lista de diccionarios""" + try: + with self.get_connection(server_type) as conn: + cursor = conn.cursor() + + start_time = time.time() + if params: + cursor.execute(query, params) + else: + cursor.execute(query) + + # Obtener nombres de columnas + columns = [column[0] for column in cursor.description] if cursor.description else [] + + # Obtener filas y convertir a diccionarios + rows = cursor.fetchall() + results = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + row_dict[columns[i]] = value + results.append(row_dict) + + execution_time = time.time() - start_time + self.logger.info(f"Consulta ejecutada en {execution_time:.3f}s. Filas: {len(results)}") + + return results + + except pyodbc.Error as e: + self.logger.error(f"Error ejecutando consulta SQL: {str(e)}") + self.logger.error(f"Query: {query}") + if params: + self.logger.error(f"Params: {params}") + raise + except Exception as e: + self.logger.error(f"Error inesperado ejecutando consulta: {str(e)}") + raise + + def test_connection(self, server_type="main") -> Dict[str, Any]: + """Prueba la conexión y retorna información del servidor""" + try: + with self.get_connection(server_type) as conn: + cursor = conn.cursor() + + # Obtener información del servidor + cursor.execute("SELECT @@VERSION as ServerVersion, DB_NAME() as DatabaseName, GETDATE() as CurrentTime") + result = cursor.fetchone() + + return { + "status": "connected", + "server_version": result.ServerVersion if result else "Unknown", + "database_name": result.DatabaseName if result else "Unknown", + "server_time": result.CurrentTime.isoformat() if result and result.CurrentTime else None, + "connection_successful": True + } + + except Exception as e: + self.logger.error(f"Error probando conexión: {str(e)}") + return { + "status": "error", + "error": str(e), + "connection_successful": False + } + + +# Instancia global de la conexión +db_connection = SQLServerConnection() diff --git a/sync_api/docker-build.bat b/sync_api/docker-build.bat new file mode 100644 index 0000000..8ff8256 --- /dev/null +++ b/sync_api/docker-build.bat @@ -0,0 +1,24 @@ +@echo off +echo ========================================== +echo CONSTRUYENDO CONTENEDOR DOCKER +echo ========================================== +echo. + +REM Construir imagen Docker +echo Construyendo imagen Docker... +docker build -t aduanasoft-sync-api:latest . + +if %ERRORLEVEL% EQU 0 ( + echo. + echo ✅ Imagen construida exitosamente + echo. + echo Para ejecutar el contenedor, usa: + echo docker-compose up -d + echo. + echo O para ejecutar directamente: + echo docker run -d -p 9443:9443 --name sync-api aduanasoft-sync-api:latest +) else ( + echo ❌ Error al construir la imagen +) + +pause diff --git a/sync_api/docker-compose.yml b/sync_api/docker-compose.yml new file mode 100644 index 0000000..90ba8de --- /dev/null +++ b/sync_api/docker-compose.yml @@ -0,0 +1,61 @@ +version: '3.8' + +services: + sync-api: + build: + context: . + dockerfile: Dockerfile + container_name: aduanasoft-sync-api + ports: + - "9443:9443" # Puerto HTTPS + - "8000:8000" # Puerto HTTP (opcional) + environment: + # Configuración de base de datos CONTROLDESK + - DATABASE_SERVER=104.192.7.202 + - DATABASE_NAME=controldesk + - DATABASE_USER=sa + - DATABASE_PASSWORD=Soluciones01 + - DATABASE_DRIVER=ODBC Driver 17 for SQL Server + - DATABASE_PORT=1433 + + # Configuración de servidor de respaldos + - BACKUP_SERVER=104.192.7.152 + - BACKUP_DATABASE=master + - BACKUP_USER=sa + - BACKUP_PASSWORD=Soluciones01 + - BACKUP_DRIVER=ODBC Driver 17 for SQL Server + - BACKUP_PORT=1433 + + # Configuración de la API + - API_HOST=0.0.0.0 + - API_PORT=9443 + - DEBUG_MODE=false + - LOG_LEVEL=INFO + + # Configuración de seguridad + - API_USERNAME=admin + - API_PASSWORD=Aduanasoft2025! + + # Configuración de caché + - CACHE_TTL=300 + - ENABLE_CACHE=false + + volumes: + # Montar certificados SSL desde el host + - ./ssl:/app/ssl:ro + + restart: unless-stopped + + networks: + - sync-network + + healthcheck: + test: ["CMD", "curl", "-k", "https://localhost:9443/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + sync-network: + driver: bridge diff --git a/sync_api/docker-run.bat b/sync_api/docker-run.bat new file mode 100644 index 0000000..a6899a2 --- /dev/null +++ b/sync_api/docker-run.bat @@ -0,0 +1,37 @@ +@echo off +echo ========================================== +echo EJECUTANDO CONTENEDOR DOCKER +echo ========================================== +echo. + +REM Verificar si Docker está ejecutándose +docker --version >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo ❌ Docker no está ejecutándose o no está instalado + pause + exit /b 1 +) + +echo Iniciando contenedor con Docker Compose... +docker-compose up -d + +if %ERRORLEVEL% EQU 0 ( + echo. + echo ✅ Contenedor iniciado exitosamente + echo. + echo 🌐 URLs disponibles: + echo - https://104.192.7.152:9443/docs + echo - https://cpanel-a24.aduanasoft.com:9443/docs + echo - https://localhost:9443/docs + echo. + echo 🔐 Credenciales: + echo - Usuario: admin + echo - Password: Aduanasoft2025! + echo. + echo Para ver logs: docker-compose logs -f + echo Para detener: docker-compose down +) else ( + echo ❌ Error al iniciar el contenedor +) + +pause diff --git a/sync_api/main.py b/sync_api/main.py new file mode 100644 index 0000000..ee7dd77 --- /dev/null +++ b/sync_api/main.py @@ -0,0 +1,275 @@ +from fastapi import FastAPI, HTTPException, Request, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from fastapi.security import HTTPBasic, HTTPBasicCredentials +import logging +import sys +import secrets +from datetime import datetime +from contextlib import asynccontextmanager + +# Importar routers +from routers import nodes, health +from config.settings import settings + + +# Configurar logging +def setup_logging(): + """Configurar sistema de logging""" + log_level = getattr(logging, settings.log_level.upper(), logging.INFO) + + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + # Opcionalmente agregar FileHandler para logs en archivo + # logging.FileHandler('sync_api.log') + ] + ) + + # Reducir logs de uvicorn y otras librerías si no estamos en debug + if not settings.debug_mode: + logging.getLogger("uvicorn").setLevel(logging.WARNING) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Gestión del ciclo de vida de la aplicación""" + # Startup + logger = logging.getLogger(__name__) + logger.info("🚀 Iniciando API de Monitoreo de Sincronización...") + + # Probar conexión a la base de datos + try: + from database.connection import db_connection + test_result = db_connection.test_connection() + if test_result.get("connection_successful", False): + logger.info("✅ Conexión a base de datos SQL Server exitosa") + logger.info(f"📊 Base de datos: {test_result.get('database_name', 'N/A')}") + else: + logger.warning("⚠️ No se pudo conectar a la base de datos al iniciar") + except Exception as e: + logger.error(f"❌ Error probando conexión inicial: {str(e)}") + + logger.info(f"🌐 API disponible en: http://{settings.api_host}:{settings.api_port}") + logger.info(f"📖 Documentación en: http://{settings.api_host}:{settings.api_port}/docs") + + yield # La aplicación está ejecutándose + + # Shutdown + logger.info("🛑 Cerrando API de Monitoreo de Sincronización...") + + +# Configurar logging antes de crear la app +setup_logging() + +# Crear la aplicación FastAPI +app = FastAPI( + title="API de Monitoreo de Sincronización", + description=""" + API REST para monitoreo de sincronización de bases de datos SQL Server. + """, + version="1.0.0", + contact={ + "name": "Sistema de Monitoreo Aduanasoft", + "url": "https://cpanel-a24.aduanasoft.com/ASDATABASES/", + }, + license_info={ + "name": "AduanaSoft", + }, + lifespan=lifespan +) + +# Configurar CORS para integración con PHP +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost", + "http://localhost:80", + "http://localhost:8080", + "http://127.0.0.1", + "http://127.0.0.1:80", + "http://127.0.0.1:8080", + # Dominios de producción Aduanasoft + "https://cpanel-a24.aduanasoft.com", + "http://cpanel-a24.aduanasoft.com", + "https://cpanel-a24.aduanasoft.com:8000", + "http://cpanel-a24.aduanasoft.com:8000", + ], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["*"], +) + +# Middleware de seguridad para hosts confiables (opcional) +if not settings.debug_mode: + app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=[ + "localhost", + "127.0.0.1", + "*.aduanasoft.com", + "cpanel-a24.aduanasoft.com" + ] + ) + +# Logger para la aplicación +logger = logging.getLogger(__name__) + +# Configurar seguridad HTTP Basic Auth +security = HTTPBasic() + +def authenticate_user(credentials: HTTPBasicCredentials = Depends(security)): + """Autenticación básica HTTP""" + correct_username = secrets.compare_digest(credentials.username, settings.api_username) + correct_password = secrets.compare_digest(credentials.password, settings.api_password) + + if not (correct_username and correct_password): + raise HTTPException( + status_code=401, + detail="Credenciales incorrectas", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +# Manejador de excepciones global +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """Manejador global de excepciones no capturadas""" + logger.error(f"Error no capturado en {request.url}: {str(exc)}") + logger.error(f"Tipo de error: {type(exc).__name__}") + + return JSONResponse( + status_code=500, + content={ + "error": "Error interno del servidor", + "detail": "Ha ocurrido un error inesperado. Consulte los logs para más detalles.", + "timestamp": datetime.now().isoformat(), + "path": str(request.url) + } + ) + + +# Middleware para logging de requests +@app.middleware("http") +async def log_requests(request: Request, call_next): + """Middleware para registrar todas las peticiones""" + start_time = datetime.now() + + # Registrar petición entrante + logger.info(f"📨 {request.method} {request.url}") + + # Procesar petición + response = await call_next(request) + + # Calcular tiempo de procesamiento + process_time = (datetime.now() - start_time).total_seconds() + + # Registrar respuesta + logger.info(f"📤 {request.method} {request.url} - {response.status_code} - {process_time:.3f}s") + + # Agregar header con tiempo de procesamiento + response.headers["X-Process-Time"] = str(process_time) + + return response + + +# Registrar routers +app.include_router(nodes.router) +app.include_router(health.router) + + +# Endpoint raíz +@app.get("/", + summary="Información de la API", + description="Endpoint raíz que proporciona información básica de la API") +async def root(): + """ + Endpoint raíz con información básica de la API. + Útil para verificar que la API está funcionando correctamente. + """ + return { + "message": "API de Monitoreo de Sincronización - Aduanasoft", + "version": "1.0.0", + "status": "running", + "timestamp": datetime.now().isoformat(), + "endpoints": { + "health": "/api/health", + "nodes_list": "/api/nodes", + "node_status": "/api/nodes/{node_name}/status", + "node_backup": "/api/nodes/{node_name}/backup-info", + "documentation": "/docs", + "openapi_spec": "/openapi.json" + }, + "features": [ + "Monitoreo de estado de nodos", + "Información de sincronización en tiempo real", + "Health checks del sistema", + "Información de backups", + "Integración AJAX/PHP ready" + ] + } + + +# Endpoint para obtener información del servidor +@app.get("/info", + summary="Información del servidor", + description="Información técnica del servidor y configuración") +async def server_info(): + """ + Información técnica del servidor y configuración actual. + Útil para debugging y verificación de configuración. + """ + return { + "server_config": { + "host": settings.api_host, + "port": settings.api_port, + "debug_mode": settings.debug_mode, + "log_level": settings.log_level + }, + "database_config": { + "server": settings.database_server, + "database": settings.database_name, + "driver": settings.database_driver, + "port": settings.database_port + }, + "cache_config": { + "enabled": settings.enable_cache, + "ttl": settings.cache_ttl + }, + "timestamp": datetime.now().isoformat() + } + + +if __name__ == "__main__": + import uvicorn + + logger.info("🚀 Iniciando servidor HTTPS en puerto 9443...") + logger.info("🔐 Configurando SSL...") + + # Configuración específica para puerto 9443 con SSL + try: + uvicorn.run( + "main:app", + host="0.0.0.0", + port=9443, + ssl_keyfile="ssl/private.key", + ssl_certfile="ssl/certificate.crt", + reload=False, + log_level="info", + access_log=True + ) + except Exception as e: + logger.error(f"❌ Error con SSL: {e}") + logger.info("🔧 Iniciando sin SSL como fallback...") + uvicorn.run( + "main:app", + host="0.0.0.0", + port=9443, + reload=False, + log_level="info" + ) diff --git a/sync_api/models/__init__.py b/sync_api/models/__init__.py new file mode 100644 index 0000000..7b45f56 --- /dev/null +++ b/sync_api/models/__init__.py @@ -0,0 +1 @@ +# Models module initialization diff --git a/sync_api/models/__pycache__/__init__.cpython-312.pyc b/sync_api/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4ee179e Binary files /dev/null and b/sync_api/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/sync_api/models/__pycache__/sync_models.cpython-312.pyc b/sync_api/models/__pycache__/sync_models.cpython-312.pyc new file mode 100644 index 0000000..9a0f20b Binary files /dev/null and b/sync_api/models/__pycache__/sync_models.cpython-312.pyc differ diff --git a/sync_api/models/sync_models.py b/sync_api/models/sync_models.py new file mode 100644 index 0000000..bd33f3f --- /dev/null +++ b/sync_api/models/sync_models.py @@ -0,0 +1,83 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime +from enum import Enum + + +class EstadoSincronizacion(str, Enum): + """Estados posibles de sincronización""" + ACTUALIZADA = "Actualizada" + ATENCION = "Atención" + ERROR = "Error" + + +class NodeStatus(BaseModel): + """Estado de un nodo específico""" + nodo_sub_nodo: str = Field(..., description="Nombre del nodo") + estado_sincronizacion: EstadoSincronizacion = Field(..., description="Estado actual de sincronización") + ultima_sincronizacion: Optional[datetime] = Field(None, description="Timestamp de la última sincronización") + total_clientes: int = Field(0, description="Total de clientes en el nodo") + clientes_activos: int = Field(0, description="Número de clientes activos") + clientes_inactivos: int = Field(0, description="Número de clientes inactivos") + tiempo_desde_ultima_sync: Optional[str] = Field(None, description="Tiempo transcurrido desde última sincronización") + + class Config: + json_encoders = { + datetime: lambda v: v.isoformat() if v else None + } + + +class NodeSummary(BaseModel): + """Resumen básico de un nodo para listados""" + nodo_sub_nodo: str = Field(..., description="Nombre del nodo") + estado_sincronizacion: EstadoSincronizacion = Field(..., description="Estado actual") + ultima_sincronizacion: Optional[datetime] = Field(None, description="Última sincronización") + total_clientes: int = Field(0, description="Total de clientes") + clientes_activos: int = Field(0, description="Clientes activos") + + +class NodesList(BaseModel): + """Lista de todos los nodos""" + nodes: List[NodeSummary] = Field(default_factory=list, description="Lista de nodos") + total_nodes: int = Field(0, description="Número total de nodos") + nodes_by_status: Dict[str, int] = Field(default_factory=dict, description="Conteo por estado") + last_updated: datetime = Field(default_factory=datetime.now, description="Timestamp de la consulta") + + +class BackupInfo(BaseModel): + """Información de respaldos de un nodo""" + nodo_sub_nodo: str = Field(..., description="Nombre del nodo") + ultimo_backup: Optional[datetime] = Field(None, description="Fecha del último respaldo") + tipo_backup: Optional[str] = Field(None, description="Tipo de respaldo (Full, Differential, Log)") + tamano_backup: Optional[str] = Field(None, description="Tamaño del respaldo") + estado_backup: Optional[str] = Field(None, description="Estado del respaldo") + ruta_backup: Optional[str] = Field(None, description="Ruta donde se almacena el respaldo") + dias_desde_backup: Optional[int] = Field(None, description="Días desde el último respaldo") + + +class HealthCheck(BaseModel): + """Estado general del sistema""" + status: str = Field(..., description="Estado general (healthy, degraded, unhealthy)") + database_status: str = Field(..., description="Estado de la base de datos") + total_nodes: int = Field(0, description="Total de nodos monitoreados") + nodes_healthy: int = Field(0, description="Nodos en estado saludable") + nodes_attention: int = Field(0, description="Nodos que requieren atención") + nodes_error: int = Field(0, description="Nodos con errores") + last_check: datetime = Field(default_factory=datetime.now, description="Timestamp del chequeo") + uptime: Optional[str] = Field(None, description="Tiempo de actividad del servicio") + version: str = Field("1.0.0", description="Versión de la API") + + +class ErrorResponse(BaseModel): + """Respuesta de error estándar""" + error: str = Field(..., description="Mensaje de error") + detail: Optional[str] = Field(None, description="Detalle adicional del error") + timestamp: datetime = Field(default_factory=datetime.now, description="Timestamp del error") + path: Optional[str] = Field(None, description="Ruta que causó el error") + + +class SuccessResponse(BaseModel): + """Respuesta exitosa estándar""" + message: str = Field(..., description="Mensaje de éxito") + data: Optional[Dict[str, Any]] = Field(None, description="Datos adicionales") + timestamp: datetime = Field(default_factory=datetime.now, description="Timestamp de la respuesta") diff --git a/sync_api/nginx_proxy.conf b/sync_api/nginx_proxy.conf new file mode 100644 index 0000000..5f7d0f5 --- /dev/null +++ b/sync_api/nginx_proxy.conf @@ -0,0 +1,18 @@ +# Configuración nginx para proxy inverso +server { + listen 8000; + server_name cpanel-a24.aduanasoft.com; + + location / { + proxy_pass http://104.192.7.152:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Para WebSockets si es necesario + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/sync_api/requirements.txt b/sync_api/requirements.txt new file mode 100644 index 0000000..bab16ad --- /dev/null +++ b/sync_api/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 +pyodbc==5.0.1 +sqlalchemy==2.0.23 +python-multipart==0.0.6 +python-dotenv==1.0.0 +redis==5.0.1 +asyncpg==0.29.0 +httpx==0.25.2 diff --git a/sync_api/routers/__init__.py b/sync_api/routers/__init__.py new file mode 100644 index 0000000..4aebcc7 --- /dev/null +++ b/sync_api/routers/__init__.py @@ -0,0 +1 @@ +# Routers module initialization diff --git a/sync_api/routers/__pycache__/__init__.cpython-312.pyc b/sync_api/routers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c51ec95 Binary files /dev/null and b/sync_api/routers/__pycache__/__init__.cpython-312.pyc differ diff --git a/sync_api/routers/__pycache__/health.cpython-312.pyc b/sync_api/routers/__pycache__/health.cpython-312.pyc new file mode 100644 index 0000000..e402067 Binary files /dev/null and b/sync_api/routers/__pycache__/health.cpython-312.pyc differ diff --git a/sync_api/routers/__pycache__/nodes.cpython-312.pyc b/sync_api/routers/__pycache__/nodes.cpython-312.pyc new file mode 100644 index 0000000..9ea0f3b Binary files /dev/null and b/sync_api/routers/__pycache__/nodes.cpython-312.pyc differ diff --git a/sync_api/routers/health.py b/sync_api/routers/health.py new file mode 100644 index 0000000..0403dd5 --- /dev/null +++ b/sync_api/routers/health.py @@ -0,0 +1,176 @@ +from fastapi import APIRouter, HTTPException +import logging +from datetime import datetime, timedelta +from database.connection import db_connection +from models.sync_models import HealthCheck, EstadoSincronizacion +import time + +router = APIRouter(prefix="/api", tags=["health"]) +logger = logging.getLogger(__name__) + +# Variable global para tracking del tiempo de inicio +start_time = time.time() + + +def get_uptime() -> str: + """Calcula el tiempo de actividad del servicio""" + uptime_seconds = time.time() - start_time + + if uptime_seconds < 60: + return f"{int(uptime_seconds)} segundos" + elif uptime_seconds < 3600: + minutes = int(uptime_seconds / 60) + seconds = int(uptime_seconds % 60) + return f"{minutes}m {seconds}s" + elif uptime_seconds < 86400: + hours = int(uptime_seconds / 3600) + minutes = int((uptime_seconds % 3600) / 60) + return f"{hours}h {minutes}m" + else: + days = int(uptime_seconds / 86400) + hours = int((uptime_seconds % 86400) / 3600) + return f"{days}d {hours}h" + + +@router.get("/health", + response_model=HealthCheck, + summary="Verificar salud del sistema", + description="Retorna el estado general de salud del sistema de monitoreo") +async def health_check(): + """ + Endpoint de health check que proporciona información sobre: + - Estado general del sistema + - Conectividad con la base de datos + - Estadísticas generales de nodos + - Tiempo de actividad del servicio + - Versión de la API + """ + try: + logger.info("Ejecutando health check del sistema") + + # Probar conexión a ambas bases de datos + main_db_status = "healthy" + backup_db_status = "healthy" + + try: + main_db_test = db_connection.test_connection("main") + if not main_db_test.get("connection_successful", False): + main_db_status = "unhealthy" + except Exception as e: + logger.error(f"Error en conexión a BD principal durante health check: {str(e)}") + main_db_status = "unhealthy" + + try: + backup_db_test = db_connection.test_connection("backup") + if not backup_db_test.get("connection_successful", False): + backup_db_status = "unhealthy" + except Exception as e: + logger.error(f"Error en conexión a BD backup durante health check: {str(e)}") + backup_db_status = "unhealthy" + + # Estado general de BD (ambas deben estar healthy) + db_status = "healthy" if main_db_status == "healthy" and backup_db_status == "healthy" else "unhealthy" + + # Obtener estadísticas de nodos si la BD está disponible + total_nodes = 0 + nodes_healthy = 0 + nodes_attention = 0 + nodes_error = 0 + + if db_status == "healthy": + try: + # Consultar estadísticas de nodos por estado + stats_query = """ + SELECT + CASE + WHEN Activo = 1 THEN 'Actualizada' + ELSE 'Atención' + END as EstadoSincronizacion, + COUNT(DISTINCT NodoSubNodo) as NodeCount + FROM BasesdeDatos + GROUP BY Activo + """ + + results = db_connection.execute_query(stats_query, server_type="main") + + status_mapping = { + EstadoSincronizacion.ACTUALIZADA.value: 'healthy', + EstadoSincronizacion.ATENCION.value: 'attention', + EstadoSincronizacion.ERROR.value: 'error' + } + + for row in results: + estado = row.get('EstadoSincronizacion', '') + count = row.get('NodeCount', 0) + total_nodes += count + + if estado == EstadoSincronizacion.ACTUALIZADA.value: + nodes_healthy += count + elif estado == EstadoSincronizacion.ATENCION.value: + nodes_attention += count + elif estado == EstadoSincronizacion.ERROR.value: + nodes_error += count + + logger.info(f"Estadísticas de nodos - Total: {total_nodes}, " + f"Healthy: {nodes_healthy}, Atención: {nodes_attention}, Error: {nodes_error}") + + except Exception as e: + logger.error(f"Error obteniendo estadísticas de nodos: {str(e)}") + # Mantener valores por defecto si hay error + + # Determinar estado general del sistema + overall_status = "healthy" + + if db_status != "healthy": + overall_status = "unhealthy" + elif nodes_error > 0 or (total_nodes > 0 and nodes_healthy / total_nodes < 0.5): + overall_status = "degraded" + elif nodes_attention > 0: + overall_status = "degraded" + + # Crear respuesta del health check + health_response = HealthCheck( + status=overall_status, + database_status=db_status, + total_nodes=total_nodes, + nodes_healthy=nodes_healthy, + nodes_attention=nodes_attention, + nodes_error=nodes_error, + last_check=datetime.now(), + uptime=get_uptime(), + version="1.0.0" + ) + + logger.info(f"Health check completado - Estado: {overall_status}") + return health_response + + except Exception as e: + logger.error(f"Error crítico durante health check: {str(e)}") + + # Retornar estado de error crítico + return HealthCheck( + status="unhealthy", + database_status="error", + total_nodes=0, + nodes_healthy=0, + nodes_attention=0, + nodes_error=0, + last_check=datetime.now(), + uptime=get_uptime(), + version="1.0.0" + ) + + +@router.get("/ping", + summary="Ping simple", + description="Endpoint simple para verificar que la API está respondiendo") +async def ping(): + """ + Endpoint simple de ping para verificación básica de disponibilidad. + Útil para load balancers y monitoreo básico. + """ + return { + "message": "pong", + "timestamp": datetime.now().isoformat(), + "status": "ok" + } diff --git a/sync_api/routers/nodes.py b/sync_api/routers/nodes.py new file mode 100644 index 0000000..e9599d7 --- /dev/null +++ b/sync_api/routers/nodes.py @@ -0,0 +1,510 @@ +from fastapi import APIRouter, HTTPException, Path, Query +from typing import List, Optional +import logging +from datetime import datetime, timedelta +from database.connection import db_connection +from models.sync_models import ( + NodeStatus, NodesList, NodeSummary, BackupInfo, + ErrorResponse, EstadoSincronizacion +) + +router = APIRouter(prefix="/api/nodes", tags=["nodes"]) +logger = logging.getLogger(__name__) + + +def calculate_time_since_sync(last_sync: Optional[datetime]) -> Optional[str]: + """Calcula el tiempo transcurrido desde la última sincronización""" + if not last_sync: + return None + + now = datetime.now() + if last_sync.tzinfo is None: + # Si last_sync no tiene timezone, asumimos que está en la zona local + diff = now - last_sync + else: + # Si tiene timezone, convertimos now a timezone-aware + from datetime import timezone + now = now.replace(tzinfo=timezone.utc) + diff = now - last_sync + + total_seconds = int(diff.total_seconds()) + + if total_seconds < 60: + return f"{total_seconds} segundos" + elif total_seconds < 3600: + minutes = total_seconds // 60 + return f"{minutes} minutos" + elif total_seconds < 86400: + hours = total_seconds // 3600 + return f"{hours} horas" + else: + days = total_seconds // 86400 + return f"{days} días" + + +@router.get("/{node_name}/status", + response_model=NodeStatus, + summary="Obtener estado específico de un nodo", + description="Retorna información detallada del estado de sincronización de un nodo específico") +async def get_node_status( + node_name: str = Path(..., description="Nombre del nodo a consultar", example="NODO001") +): + """ + Obtiene el estado detallado de sincronización de un nodo específico. + + Incluye: + - Estado de sincronización actual + - Fecha de última sincronización + - Contadores de clientes (total, activos, inactivos) + - Tiempo transcurrido desde última sincronización + """ + try: + logger.info(f"Consultando estado del nodo: {node_name}") + + # Paso 1: Obtener información del cliente desde CONTROLDESK + client_query = """ + SELECT + NodoSubNodo, + Nombre, + BDName, + Activo, + CorreoNotificacion + FROM BasesdeDatos + WHERE NodoSubNodo = ? OR BDName = ? + """ + + client_results = db_connection.execute_query(client_query, (node_name, node_name), server_type="main") + + if not client_results: + raise HTTPException( + status_code=404, + detail=f"Nodo '{node_name}' no encontrado en CONTROLDESK" + ) + + client_info = client_results[0] + + # Paso 2: Obtener información de restauración desde servidor de backup + restore_query = """ + SELECT + d.name AS database_name, + MAX(rh.restore_date) AS last_restore_date, + SUM(mf.size * 8 / 1024) AS size_mb + FROM sys.databases d + LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id + LEFT JOIN msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name + WHERE mf.type = 0 AND d.name != 'tempdb' + AND (d.name = ? OR d.name LIKE '%' + ? + '%') + GROUP BY d.name + """ + + restore_results = db_connection.execute_query(restore_query, (client_info['BDName'], node_name), server_type="backup") + + # Combinar resultados + last_restore_date = None + size_mb = 0 + + if restore_results: + restore_info = restore_results[0] + last_restore_date = restore_info.get('last_restore_date') + size_mb = restore_info.get('size_mb', 0) + + # Determinar estado basado en fecha de restauración + if last_restore_date is None: + estado = 'Error' + elif last_restore_date < datetime.now() - timedelta(days=7): + estado = 'Error' + elif last_restore_date < datetime.now() - timedelta(hours=24): + estado = 'Atención' + else: + estado = 'Actualizada' + + # Calcular tiempo desde última sincronización + time_since_sync = calculate_time_since_sync(last_restore_date) + + node_status = NodeStatus( + nodo_sub_nodo=client_info['NodoSubNodo'], + estado_sincronizacion=EstadoSincronizacion(estado), + ultima_sincronizacion=last_restore_date, + total_clientes=1, + clientes_activos=1 if client_info.get('Activo', 0) == 1 else 0, + clientes_inactivos=0 if client_info.get('Activo', 0) == 1 else 1, + tiempo_desde_ultima_sync=time_since_sync + ) + + logger.info(f"Estado del nodo {node_name} obtenido exitosamente") + return node_status + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error obteniendo estado del nodo {node_name}: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error interno del servidor al consultar nodo: {str(e)}" + ) + + +@router.get("", + response_model=NodesList, + summary="Listar todos los nodos", + description="Retorna una lista de todos los nodos con su estado de sincronización") +async def get_all_nodes( + estado: Optional[EstadoSincronizacion] = Query(None, description="Filtrar por estado de sincronización"), + activos_solo: bool = Query(False, description="Mostrar solo clientes activos"), + limit: int = Query(100, ge=1, le=1000, description="Límite de resultados"), + offset: int = Query(0, ge=0, description="Offset para paginación") +): + """ + Obtiene una lista de todos los nodos con información resumida. + + Parámetros opcionales: + - estado: Filtrar por estado específico (Actualizada, Atención, Error) + - activos_solo: Si es True, solo considera clientes activos en los conteos + - limit: Número máximo de nodos a retornar + - offset: Número de nodos a saltar (para paginación) + """ + try: + logger.info(f"Consultando lista de nodos - Estado: {estado}, Activos solo: {activos_solo}") + + # Paso 1: Obtener todos los clientes desde CONTROLDESK + clients_query = """ + SELECT + NodoSubNodo, + Nombre, + BDName, + Activo, + CorreoNotificacion + FROM BasesdeDatos + ORDER BY NodoSubNodo + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + """ + + client_results = db_connection.execute_query(clients_query, (offset, limit), server_type="main") + + # Paso 2: Obtener información de restauración para todas las bases de datos + restore_query = """ + SELECT + d.name AS database_name, + MAX(rh.restore_date) AS last_restore_date + FROM sys.databases d + LEFT JOIN msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name + WHERE d.name != 'tempdb' + GROUP BY d.name + """ + + restore_results = db_connection.execute_query(restore_query, server_type="backup") + + # Crear mapa de restauraciones para búsqueda rápida + restore_map = {row['database_name']: row['last_restore_date'] for row in restore_results} + + # Combinar información de clientes con restauraciones + nodes = [] + for client in client_results: + # Buscar información de restauración + bd_name = client['BDName'] + nodo_name = client['NodoSubNodo'] + + # Buscar por nombre de BD o nodo + last_restore = restore_map.get(bd_name) or restore_map.get(nodo_name) + + # Determinar estado basado en fecha de restauración + if last_restore is None: + estado_sync = 'Error' + elif last_restore < datetime.now() - timedelta(days=7): + estado_sync = 'Error' + elif last_restore < datetime.now() - timedelta(hours=24): + estado_sync = 'Atención' + else: + estado_sync = 'Actualizada' + + # Aplicar filtros + if estado and estado_sync != estado.value: + continue + if activos_solo and client.get('Activo', 0) == 0: + continue + + node_summary = NodeSummary( + nodo_sub_nodo=client['NodoSubNodo'], + estado_sincronizacion=EstadoSincronizacion(estado_sync), + ultima_sincronizacion=last_restore, + total_clientes=1, + clientes_activos=1 if client.get('Activo', 0) == 1 else 0 + ) + nodes.append(node_summary) + + # Calcular conteos por estado desde los nodos procesados + nodes_by_status = {} + for node in nodes: + estado_str = node.estado_sincronizacion.value + nodes_by_status[estado_str] = nodes_by_status.get(estado_str, 0) + 1 + + node_list = NodesList( + nodes=nodes, + total_nodes=len(nodes), + nodes_by_status=nodes_by_status, + last_updated=datetime.now() + ) + + logger.info(f"Lista de nodos obtenida exitosamente. Total: {len(nodes)}") + return node_list + + except Exception as e: + logger.error(f"Error obteniendo lista de nodos: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error interno del servidor al consultar nodos: {str(e)}" + ) + + +@router.get("/databases/restore-status", + summary="Obtener estado de restauración de todas las bases de datos", + description="Retorna información de restauración similar al panel PHP") +async def get_databases_restore_status(): + """ + Obtiene el estado de restauración de todas las bases de datos del servidor, + replicando la funcionalidad del panel PHP. + """ + try: + logger.info("Consultando estado de restauración de bases de datos") + + # Consulta exacta del archivo PHP + query = """ + SELECT + d.name AS visible_name, + REVERSE(SUBSTRING(REVERSE(mf.physical_name), 1, CHARINDEX('\\', REVERSE(mf.physical_name)) - 1)) AS original_name, + SUM(mf.size * 8 / 1024) AS size_mb, + MAX(rh.restore_date) AS last_restore_date + FROM + sys.databases d + LEFT JOIN + sys.master_files mf ON d.database_id = mf.database_id + LEFT JOIN + msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name + WHERE + mf.type = 0 AND d.name != 'tempdb' + GROUP BY + d.name, mf.physical_name + ORDER BY d.name + """ + + results = db_connection.execute_query(query, server_type="backup") + + # Procesar resultados + databases = [] + restored_count = 0 + not_restored_count = 0 + + for row in results: + last_restore = row.get('last_restore_date') + is_recent = False + + if last_restore: + # Verificar si fue restaurada en las últimas 24 horas + if isinstance(last_restore, datetime): + is_recent = last_restore > datetime.now() - timedelta(days=1) + else: + # Si es string, intentar parsear + try: + restore_dt = datetime.fromisoformat(str(last_restore).replace('Z', '+00:00')) + is_recent = restore_dt > datetime.now() - timedelta(days=1) + except: + is_recent = False + + if is_recent: + restored_count += 1 + else: + not_restored_count += 1 + + # Calcular tiempo desde última restauración + time_since_restore = None + if last_restore: + time_since_restore = calculate_time_since_sync(last_restore) + + database_info = { + "visible_name": row['visible_name'], + "original_name": row.get('original_name', 'N/A'), + "size_mb": round(row.get('size_mb', 0), 2), + "last_restore_date": last_restore.isoformat() if last_restore else None, + "is_recent_restore": is_recent, + "time_since_restore": time_since_restore, + "status": "Actualizada" if is_recent else ("Atención" if last_restore else "Error") + } + databases.append(database_info) + + response = { + "databases": databases, + "summary": { + "total_databases": len(databases), + "restored_recent": restored_count, + "not_restored_recent": not_restored_count, + "last_check": datetime.now().isoformat() + } + } + + logger.info(f"Estado de restauración obtenido. Total: {len(databases)} bases de datos") + return response + + except Exception as e: + logger.error(f"Error obteniendo estado de restauración: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error interno del servidor: {str(e)}" + ) + + +@router.get("/{node_name}/client-info", + summary="Obtener información del cliente por nodo", + description="Retorna información detallada del cliente asociado al nodo") +async def get_node_client_info( + node_name: str = Path(..., description="Nombre del nodo", example="NODO001") +): + """ + Obtiene información detallada del cliente asociado a un nodo específico. + """ + try: + logger.info(f"Consultando información del cliente para nodo: {node_name}") + + # Consulta para obtener información completa del cliente + query = """ + SELECT + bd.ID, + bd.Nombre, + bd.NodoSubNodo, + bd.BDName, + bd.CorreoNotificacion, + bd.Activo, + u.ClienteAutoridad, + u.Usuario, + u.BD_Shelter + FROM BasesdeDatos bd + LEFT JOIN Usuarios u ON bd.NodoSubNodo = u.Usuario + WHERE bd.NodoSubNodo = ? OR bd.BDName = ? + """ + + results = db_connection.execute_query(query, (node_name, node_name), server_type="main") + + if not results: + raise HTTPException( + status_code=404, + detail=f"No se encontró información para el nodo '{node_name}'" + ) + + row = results[0] + + response = { + "id": row.get('ID'), + "nombre_cliente": row.get('Nombre'), + "nodo_sub_nodo": row.get('NodoSubNodo'), + "bd_name": row.get('BDName'), + "correo_notificacion": row.get('CorreoNotificacion'), + "activo": bool(row.get('Activo', False)), + "cliente_autoridad": row.get('ClienteAutoridad'), + "usuario": row.get('Usuario'), + "bd_shelter": row.get('BD_Shelter'), + "estado": "Activo" if row.get('Activo') else "Inactivo" + } + + logger.info(f"Información del cliente para {node_name} obtenida exitosamente") + return response + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error obteniendo información del cliente para {node_name}: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error interno del servidor: {str(e)}" + ) + + +@router.get("/{node_name}/backup-info", + response_model=BackupInfo, + summary="Obtener información de respaldos de un nodo", + description="Retorna información sobre los respaldos de un nodo específico") +async def get_node_backup_info( + node_name: str = Path(..., description="Nombre del nodo", example="NODO001") +): + """ + Obtiene información sobre los respaldos de un nodo específico. + + NOTA: Este endpoint es opcional y requiere configuración adicional + de las tablas de respaldo en SQL Server. + """ + try: + logger.info(f"Consultando información de backup para nodo: {node_name}") + + # Query básica para información de backup (requiere tablas adicionales) + # Esta es una implementación básica que asume una estructura de backup + query = """ + SELECT TOP 1 + ? as NodoSubNodo, + backup_start_date as UltimoBackup, + type as TipoBackup, + compressed_backup_size / 1024.0 / 1024.0 as TamanoBackupMB, + 'Completed' as EstadoBackup, + physical_device_name as RutaBackup + FROM msdb.dbo.backupset bs + INNER JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id + WHERE database_name IN ( + SELECT DISTINCT BDName + FROM Clientes + WHERE NodoSubNodo = ? + ) + ORDER BY backup_start_date DESC + """ + + try: + results = db_connection.execute_query(query, (node_name, node_name)) + except Exception: + # Si no existe la información de backup, devolver información básica + logger.warning(f"No se encontró información de backup para {node_name}") + return BackupInfo( + nodo_sub_nodo=node_name, + ultimo_backup=None, + tipo_backup="No disponible", + tamano_backup="No disponible", + estado_backup="No configurado", + ruta_backup="No disponible", + dias_desde_backup=None + ) + + if not results: + return BackupInfo( + nodo_sub_nodo=node_name, + ultimo_backup=None, + tipo_backup="Sin respaldos", + tamano_backup="0 MB", + estado_backup="Sin datos", + ruta_backup="N/A", + dias_desde_backup=None + ) + + row = results[0] + ultimo_backup = row.get('UltimoBackup') + + # Calcular días desde el último backup + dias_desde_backup = None + if ultimo_backup: + dias_desde_backup = (datetime.now() - ultimo_backup).days + + backup_info = BackupInfo( + nodo_sub_nodo=node_name, + ultimo_backup=ultimo_backup, + tipo_backup=row.get('TipoBackup', 'Unknown'), + tamano_backup=f"{row.get('TamanoBackupMB', 0):.2f} MB", + estado_backup=row.get('EstadoBackup', 'Unknown'), + ruta_backup=row.get('RutaBackup', 'Unknown'), + dias_desde_backup=dias_desde_backup + ) + + logger.info(f"Información de backup para {node_name} obtenida exitosamente") + return backup_info + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error obteniendo información de backup para {node_name}: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error interno del servidor al consultar backup: {str(e)}" + ) diff --git a/sync_api/run_dev.bat b/sync_api/run_dev.bat new file mode 100644 index 0000000..0272414 --- /dev/null +++ b/sync_api/run_dev.bat @@ -0,0 +1,9 @@ +# Scripts de utilidad para la API de Monitoreo + +# Ejecutar en desarrollo +@echo off +echo Iniciando API de Monitoreo de Sincronizacion - Modo Desarrollo +echo. +cd /d "%~dp0" +C:\Python312\python.exe main.py +pause diff --git a/sync_api/run_prod.bat b/sync_api/run_prod.bat new file mode 100644 index 0000000..c7b085b --- /dev/null +++ b/sync_api/run_prod.bat @@ -0,0 +1,7 @@ +# Script para ejecutar en producción con Uvicorn +@echo off +echo Iniciando API de Monitoreo de Sincronizacion - Modo Produccion +echo. +cd /d "%~dp0" +C:\Python312\python.exe -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 +pause diff --git a/sync_api/run_prod_https.bat b/sync_api/run_prod_https.bat new file mode 100644 index 0000000..2c26838 --- /dev/null +++ b/sync_api/run_prod_https.bat @@ -0,0 +1,26 @@ +@echo off +echo ============================================== +echo API FASTAPI - SERVIDOR PRODUCCION HTTPS +echo ============================================== +echo. +echo Iniciando servidor con SSL... +echo URL: https://104.192.7.152:9000 +echo URL: https://cpanel-a24.aduanasoft.com:9000 +echo Docs: https://cpanel-a24.aduanasoft.com:9000/docs +echo. + +REM Configurar variables para producción con SSL +set API_HOST=0.0.0.0 +set API_PORT=9000 +set DEBUG_MODE=False +set LOG_LEVEL=INFO + +echo Usando certificados SSL... +echo Certificado: ssl/certificate.crt +echo Llave: ssl/private.key +echo. + +REM Ejecutar con SSL usando los certificados de XAMPP +python -m uvicorn main:app --host 0.0.0.0 --port 9000 --ssl-keyfile=ssl/private.key --ssl-certfile=ssl/certificate.crt --workers 1 + +pause diff --git a/sync_api/run_prod_server.bat b/sync_api/run_prod_server.bat new file mode 100644 index 0000000..1a35d72 --- /dev/null +++ b/sync_api/run_prod_server.bat @@ -0,0 +1,20 @@ +@echo off +echo ============================================== +echo API FASTAPI - SERVIDOR PRODUCCION +echo ============================================== +echo. +echo Iniciando servidor en modo produccion... +echo URL: http://cpanel-a24.aduanasoft.com:8000 +echo Docs: http://cpanel-a24.aduanasoft.com:8000/docs +echo. + +REM Configurar variables para producción +set API_HOST=0.0.0.0 +set API_PORT=8000 +set DEBUG_MODE=False +set LOG_LEVEL=INFO + +REM Ejecutar con configuración de producción +python -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 + +pause diff --git a/sync_api/ssl/certificate.crt b/sync_api/ssl/certificate.crt new file mode 100644 index 0000000..87825dd --- /dev/null +++ b/sync_api/ssl/certificate.crt @@ -0,0 +1,73 @@ +-----BEGIN CERTIFICATE----- +MIIGWzCCBUOgAwIBAgIQMwJQx6EVxkBFLAjTY5w+mDANBgkqhkiG9w0BAQsFADC +BjzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA +4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDV +QQDEy5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVy +IENBMB4XDTI1MDIxNzAwMDAwMFoXDTI2MDIxNzIzNTk1OVowJDEiMCAGA1UEAxM +ZY3BhbmVsLWEyNC5hZHVhbmFzb2Z0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADgg +EPADCCAQoCggEBAOLOReR8GHVdBXpq7pdM4vaZoYKb8LIJY9eFuMM81VTjTYDmk +CVVQzCiw2A0cvjOYyqX2yZJzsNib2rq5d5CIQmC3cyn020f/5GNqUocPRa3osR3 +WLJlSg/8zd0aoxqXNTdKd0GF38Magi7kkWF/756iOCcT20g0jw9lvYQEKUklfqW +BeMZ3qcxy2wYhiD1I20aoUQb+KAoA6e7TUNDtXY+CqMNnxDmyOtviOH3kFt7dSP +QriH9PoBSc4IRNY+w0eT4akCu1SxbR6AZ45bgXIMQ5PmypbT6bk/fP7+mmYlyNJ +92jRGcFTsbrs923avgdR6bDXXPTQ6f7FckXGUffkQ8CAwEAAaOCAxswggMXMB8G +A1UdIwQYMBaAFI2MXsRUrYrhd+mb+ZsF4bgBjWHhMB0GA1UdDgQWBBTn5xvIXuj +20RXIXjecTyb7Q5myzzAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBg +NVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEA +bIxAQICBzAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzAI +BgZngQwBAgEwgYQGCCsGAQUFBwEBBHgwdjBPBggrBgEFBQcwAoZDaHR0cDovL2N +ydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZV +NlcnZlckNBLmNydDAjBggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb +20wQwYDVR0RBDwwOoIZY3BhbmVsLWEyNC5hZHVhbmFzb2Z0LmNvbYIdd3d3LmNw +YW5lbC1hMjQuYWR1YW5hc29mdC5jb20wggF/BgorBgEEAdZ5AgQCBIIBbwSCAWs +BaQB2AJaXZL9VWJet90OHaDcIQnfp8DrV9qTzNm5GpD8PyqnGAAABlRVAa0MAAA +QDAEcwRQIgBAHpoJtGOpfZlmNbE8xE89m3UR8MNyDFQpcFtwmD518CIQDjFpJpq +v3rzs23erihOCFA96FyaMPWmUJq4vLe8e6EdQB2ABmG1Mcoqm/+ugNveCpNAZGq +zi1yMQ+uzl1wQS0lTMfUAAABlRVAavcAAAQDAEcwRQIgZHZIm5kx5Fry90zCcUG +WcRaJqm6Vw/piWgGVcSmw584CIQC6CkhmLFzhVJZB0Nuscxqb4aWxp7CJsiKOVj +IQGooAlQB3AMs49xWJfIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABlRVAa +wQAAAQDAEgwRgIhAM7ExSwmiuh7nnDRAw28dEydc8pZceKeAG6NwJwyZB0SAiEA +7GGIsMbo+iOPBAglaO6GIZoS3pCUhI0MyRvc+cXKsS8wDQYJKoZIhvcNAQELBQA +DggEBAFrGlDTmf7bwr5w9j1Q5la6GghTeFqUi1gDDLkeLu8aUtnB8Jm6LsYymdJ +okZRpptpS5Ky8b8pYT1xDkIDoJNaAKbNVQT+VC0iP/67MOm2CIe7evEbpDuX/NU +W4J9aAeaVOn8DA6c+dnI1TEU8ARS23doAkhvXqbgbgNUHwBvU73cW2geUX0enJq +Fh2vY20KniAMkVo8M/PJtUssPP0skkJVkDStCZMeZ6NyPcDIwVIII3MYF4E/TaL +wvD0ymOXOZIgMsNrXP2OELiHRbK7hLD8XgUQC/GyvSkj8fc9oyTM68omCOOPQ+f +wkT0X3MbPFDGQiwNlGjHUk14eBYzFkOdc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADC +BiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0 +plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsB +gNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcN +MTgxMTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzA +ZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMB +YGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb +21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/ +LEpgPLT+NTQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHx +YyQdqAJrkjeocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPA +XJpz9OTTG0EoKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCS +o1yfu4iYCBskHaswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDC +g/r8uoydajotYuK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbj +CCAWowHwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEF +I2MXsRUrYrhd+mb+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8E +CDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSA +EFDASMAYGBFUdIAAwCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly +9jcmwudXNlcnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0a +G9yaXR5LmNybDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9j +cnQudXNlcnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBgg +rBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQ +wFAAOCAgEAMr9hvQ5Iw0/HukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvV +KqJjUAXaPgREHzSyrHxVYbH7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5RO +Vm+yezu/Coa9zcV3HAO4OLGiH19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg +6F0AnwnJOKf0hPHzPE/uWLMUxRP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg +6s1SQ36yOoeNDT5++SR2RiOSLvxvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGj +mO+wea03KbNIaiGCpXZLoUmGv38sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9 +p5v//cWoaasm56ekBYdbqbe4oyALl6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3v +zxhFoYt/jmPQT2BVPi7Fp2RBgvGQq6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qH +SihrZe68Zk6s+go/lunrotEbaGmAhYLcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a +6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgS +abuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K00u/I5sUKUErmgQfky3xxzlIPK1aEn8 += +-----END CERTIFICATE----- diff --git a/sync_api/ssl/private.key b/sync_api/ssl/private.key new file mode 100644 index 0000000..8623059 --- /dev/null +++ b/sync_api/ssl/private.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4s5F5HwYdV0Femrul0zi9pmhgpvwsglj14W4wzzVVONNgOaQ +JVVDMKLDYDRy+M5jKpfbJknOw2Jvaurl3kIhCYLdzKfTbR//kY2pShw9FreixHdY +smVKD/zN3RqjGpc1N0p3QYXfwxqCLuSRYX/vnqI4JxPbSDSPD2W9hAQpSSV+pYF4 +xnepzHLbBiGIPUjbRqhRBv4oCgDp7tNQ0O1dj4Kow2fEObI62+I4feQW3t1I9CuI +f0+gFJzghE1j7DR5PhqQK7VLFtHoBnjluBcgxDk+bKltPpuT98/v6aZiXI0n3aNE +ZwVOxuuz3bdq+B1HpsNdc9NDp/sVyRcZR9+RDwIDAQABAoIBAHZRn5qTtnq7fVkg +oblijeCRwvUM/uMSDPjpNLr9aFkxf0Qog7hGI5giwQPWOMhdetREhjoSNQAuwOsp +AaQSIYQBtk3Gm4CJOd0n5tHXq3LHd+1ASZA/UQTIIGkhNUdw7H0cptwSx+nTUWi/ +KqyPfmTmQZ75N+MSMDObIuPU++jA4ET36RCw6iuvb+msBaT41Dkc3XxGqkAYxTgI +Mb9vliiuRWvz9Ggh+DA28uS63VAH9g/zVI6un0JUHkBrsrakY+NGgyAuDW5BqV5v +EoLB+o4OTBatco+sSOg0OsHqr5hZQVrSFKOKYRD/dIaPKAnchjj+Ngs5jrLuLQOl ++r/poZkCgYEA85fKAZYNQPyOtqyMdaVqm/pc0MD1bFlAJ/wkgfbab4t0Bi/wiKrS +jaCUmLGO7bBdzRnN2V/yrKVpdXMz3V7e8jM+aCVQ/32B1TKCvk5LDprH2yjf7Oc0 +/Lo5RkMGOjHNU9Z8mZy1dNWTP3Wlc4Y1jLAKU892oDjW5lDlFAJn88sCgYEA7luY +hpobH0zKWX1tRc1hPLcVyWQJW7LjBt/iqaAWBItrDlqEr32p+rbCxv/IdwpaNXnW +gr4HKexi6K91lkJIgJU9IpsqUf27F4HTHnd1bWpRd1rO3F2Mo65+8RF87jYUWFoA +CqyblAqa/L57TOFm0ecIWwQcd/irpb2OCPicF00CgYAekFoRTuGE2OuTMh1BmarB +vW0t6DgjCyPIoJN4FyAbRNUe/Z7hlwVqLc7XRdgYLLvoQyeX8bkCPpL34KeOTBwP +KazX5m6+T+IdfQP4mtxCDdurvPozwGLenwMWtMwwOxV3Qn9JyubdrPqXyMwkrZ9E +csYo8+v/eIJGTbogg5jFdwKBgQDoZoBpxPnq47lr1cJkS6KiSzS/3nzNXrbCbiqX +DtJI7hKctR8n6FOCcw2ob55qirp352qCoXtkoemyS9lPfyMn09r+0eFKfiUb/2X0 +D6971izGPdDxxXip4yo8K7vumuXtWxstTSyIBGsnQSYBSMgyoptf+rszMk9R02cY +UiYHBQKBgBvci1hX/BxASSYmEx3/6CxtivyY0UiHNcFL7q2MYOtCHOP9u6F7ILrg +aQhNQirLq+Kvy5GGB7K1hC5nwFcEPMo4WBHTdIy/tcUUox7rpzKPh/yn6NWo6tfS +Eg8VyA/u1zwT9fm8x8KMV77oe2Oi7Sw2Rq7osTKtzUPHnwnO8134 +-----END RSA PRIVATE KEY----- diff --git a/sync_api/ssl/test_key.pem b/sync_api/ssl/test_key.pem new file mode 100644 index 0000000..2de68a3 --- /dev/null +++ b/sync_api/ssl/test_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +6B01dMcbwHLqTan0+atfQK5/EQzJqQ== +-----END PRIVATE KEY----- diff --git a/sync_api/start_https_9443.py b/sync_api/start_https_9443.py new file mode 100644 index 0000000..159d418 --- /dev/null +++ b/sync_api/start_https_9443.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Servidor HTTPS para API FastAPI en puerto 9443 +""" +import uvicorn +import ssl +import sys +import os +from pathlib import Path + +def main(): + # Configuración SSL + ssl_keyfile = "ssl/private.key" + ssl_certfile = "ssl/certificate.crt" + + # Verificar que existan los archivos SSL + if not os.path.exists(ssl_keyfile): + print(f"❌ Error: No se encuentra {ssl_keyfile}") + sys.exit(1) + + if not os.path.exists(ssl_certfile): + print(f"❌ Error: No se encuentra {ssl_certfile}") + sys.exit(1) + + print("🔐 Iniciando servidor HTTPS en puerto 9443...") + print(f"📁 Certificado: {ssl_certfile}") + print(f"🔑 Llave privada: {ssl_keyfile}") + print("🌐 URLs disponibles:") + print(" - https://104.192.7.152:9443/docs") + print(" - https://cpanel-a24.aduanasoft.com:9443/docs") + print(" - https://104.192.7.152:9443/api/health") + print(" - https://104.192.7.152:9443/api/nodes") + + try: + # Configurar SSL context + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) + + # Iniciar servidor + uvicorn.run( + "main:app", + host="0.0.0.0", + port=9443, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + reload=False, + workers=1, + access_log=True, + log_level="info" + ) + + except Exception as e: + print(f"❌ Error al iniciar servidor HTTPS: {e}") + print("\n🔧 Diagnóstico:") + + # Diagnóstico SSL + try: + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(ssl_certfile, ssl_keyfile) + print("✅ Certificados SSL válidos") + except Exception as ssl_error: + print(f"❌ Error en certificados SSL: {ssl_error}") + + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/sync_api/start_https_9443_alt.py b/sync_api/start_https_9443_alt.py new file mode 100644 index 0000000..cec860e --- /dev/null +++ b/sync_api/start_https_9443_alt.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Servidor HTTPS para API FastAPI en puerto 9443 - Versión alternativa +""" +import uvicorn +import ssl +import sys +import os + +def main(): + print("🔐 Iniciando servidor HTTPS en puerto 9443 (versión alternativa)...") + + try: + # Método alternativo: dejar que uvicorn maneje SSL internamente + config = uvicorn.Config( + "main:app", + host="0.0.0.0", + port=9443, + ssl_keyfile="ssl/private.key", + ssl_certfile="ssl/certificate.crt", + reload=False, + access_log=True, + log_level="debug" + ) + + server = uvicorn.Server(config) + print("✅ Configuración creada, iniciando servidor...") + server.run() + + except Exception as e: + print(f"❌ Error: {e}") + + # Fallback: intentar sin SSL en puerto 9443 + print("\n🔧 Intentando sin SSL como fallback...") + try: + uvicorn.run( + "main:app", + host="0.0.0.0", + port=9443, + reload=False, + log_level="info" + ) + except Exception as fallback_error: + print(f"❌ Error en fallback: {fallback_error}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/sync_api/test_api.bat b/sync_api/test_api.bat new file mode 100644 index 0000000..ea926fb --- /dev/null +++ b/sync_api/test_api.bat @@ -0,0 +1,23 @@ +# Script de pruebas para verificar endpoints +@echo off +echo Ejecutando pruebas de la API... +echo. + +echo Probando endpoint raiz... +curl -s http://localhost:8000/ || echo Error: API no responde + +echo. +echo Probando ping... +curl -s http://localhost:8000/api/ping || echo Error: Ping fallo + +echo. +echo Probando health check... +curl -s http://localhost:8000/api/health || echo Error: Health check fallo + +echo. +echo Probando lista de nodos... +curl -s http://localhost:8000/api/nodes || echo Error: Lista de nodos fallo + +echo. +echo Pruebas completadas. +pause diff --git a/sync_api/verify_installation.py b/sync_api/verify_installation.py new file mode 100644 index 0000000..4e4892c --- /dev/null +++ b/sync_api/verify_installation.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Script de verificación e instalación para la API de Monitoreo +""" +import sys +import subprocess +import importlib +import os + +def check_python_version(): + """Verificar versión de Python""" + version = sys.version_info + if version.major < 3 or (version.major == 3 and version.minor < 8): + print("❌ Error: Se requiere Python 3.8 o superior") + return False + print(f"✅ Python {version.major}.{version.minor}.{version.micro} detectado") + return True + +def check_package(package_name): + """Verificar si un paquete está instalado""" + try: + importlib.import_module(package_name) + print(f"✅ {package_name} instalado correctamente") + return True + except ImportError: + print(f"❌ {package_name} no encontrado") + return False + +def install_requirements(): + """Instalar dependencias desde requirements.txt""" + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) + print("✅ Dependencias instaladas correctamente") + return True + except subprocess.CalledProcessError: + print("❌ Error instalando dependencias") + return False + +def check_env_file(): + """Verificar archivo .env""" + if os.path.exists('.env'): + print("✅ Archivo .env encontrado") + with open('.env', 'r') as f: + content = f.read() + if 'DATABASE_SERVER=' in content and 'DATABASE_NAME=' in content: + print("✅ Variables de entorno básicas configuradas") + return True + else: + print("⚠️ Archivo .env existe pero faltan variables importantes") + return False + else: + print("❌ Archivo .env no encontrado") + return False + +def main(): + """Función principal de verificación""" + print("🔍 Verificando instalación de API de Monitoreo de Sincronización...") + print("=" * 60) + + all_ok = True + + # Verificar Python + if not check_python_version(): + all_ok = False + + # Verificar paquetes principales + packages = ['fastapi', 'uvicorn', 'pydantic', 'pyodbc'] + for package in packages: + if not check_package(package): + print(f"Intentando instalar {package}...") + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + if check_package(package): + print(f"✅ {package} instalado exitosamente") + else: + all_ok = False + except: + all_ok = False + + # Verificar archivo .env + if not check_env_file(): + print("ℹ️ Asegúrate de configurar las variables de base de datos en .env") + + print("=" * 60) + + if all_ok: + print("🎉 ¡Verificación completada exitosamente!") + print("\n🚀 Para iniciar la API ejecuta:") + print(" python -m uvicorn main:app --host 127.0.0.1 --port 8000 --reload") + print("\n📖 Documentación disponible en:") + print(" http://127.0.0.1:8000/docs") + + # Intentar importar la configuración + try: + from config.settings import settings + print(f"\n⚙️ Configuración cargada para BD: {settings.database_name}") + except Exception as e: + print(f"\n⚠️ Error cargando configuración: {e}") + + else: + print("❌ Se encontraron problemas. Revisa los errores anteriores.") + + return all_ok + +if __name__ == "__main__": + success = main() + if not success: + sys.exit(1)