From 733ef04eeb607aa7ed9b4e618c046d134d2bcf5a Mon Sep 17 00:00:00 2001 From: ernestohc21 Date: Thu, 6 Nov 2025 09:50:10 -0700 Subject: [PATCH] Initial commit: SYNC_API project --- sync_api/.dockerignore | 24 + sync_api/.env | 32 ++ sync_api/DOCKER_README.md | 109 ++++ sync_api/Dockerfile | 43 ++ sync_api/README.md | 274 ++++++++++ sync_api/__pycache__/main.cpython-312.pyc | Bin 0 -> 10878 bytes sync_api/build-and-run.sh | 62 +++ sync_api/config/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 147 bytes .../__pycache__/settings.cpython-312.pyc | Bin 0 -> 1607 bytes sync_api/config/settings.py | 45 ++ sync_api/database/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 149 bytes .../__pycache__/connection.cpython-312.pyc | Bin 0 -> 7720 bytes sync_api/database/connection.py | 132 +++++ sync_api/docker-build.bat | 24 + sync_api/docker-compose.yml | 61 +++ sync_api/docker-run.bat | 37 ++ sync_api/main.py | 275 ++++++++++ sync_api/models/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 147 bytes .../__pycache__/sync_models.cpython-312.pyc | Bin 0 -> 6786 bytes sync_api/models/sync_models.py | 83 +++ sync_api/nginx_proxy.conf | 18 + sync_api/requirements.txt | 11 + sync_api/routers/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 148 bytes .../__pycache__/health.cpython-312.pyc | Bin 0 -> 6649 bytes .../routers/__pycache__/nodes.cpython-312.pyc | Bin 0 -> 18799 bytes sync_api/routers/health.py | 176 ++++++ sync_api/routers/nodes.py | 510 ++++++++++++++++++ sync_api/run_dev.bat | 9 + sync_api/run_prod.bat | 7 + sync_api/run_prod_https.bat | 26 + sync_api/run_prod_server.bat | 20 + sync_api/ssl/certificate.crt | 73 +++ sync_api/ssl/private.key | 27 + sync_api/ssl/test_key.pem | 52 ++ sync_api/start_https_9443.py | 67 +++ sync_api/start_https_9443_alt.py | 48 ++ sync_api/test_api.bat | 23 + sync_api/verify_installation.py | 108 ++++ 42 files changed, 2380 insertions(+) create mode 100644 sync_api/.dockerignore create mode 100644 sync_api/.env create mode 100644 sync_api/DOCKER_README.md create mode 100644 sync_api/Dockerfile create mode 100644 sync_api/README.md create mode 100644 sync_api/__pycache__/main.cpython-312.pyc create mode 100644 sync_api/build-and-run.sh create mode 100644 sync_api/config/__init__.py create mode 100644 sync_api/config/__pycache__/__init__.cpython-312.pyc create mode 100644 sync_api/config/__pycache__/settings.cpython-312.pyc create mode 100644 sync_api/config/settings.py create mode 100644 sync_api/database/__init__.py create mode 100644 sync_api/database/__pycache__/__init__.cpython-312.pyc create mode 100644 sync_api/database/__pycache__/connection.cpython-312.pyc create mode 100644 sync_api/database/connection.py create mode 100644 sync_api/docker-build.bat create mode 100644 sync_api/docker-compose.yml create mode 100644 sync_api/docker-run.bat create mode 100644 sync_api/main.py create mode 100644 sync_api/models/__init__.py create mode 100644 sync_api/models/__pycache__/__init__.cpython-312.pyc create mode 100644 sync_api/models/__pycache__/sync_models.cpython-312.pyc create mode 100644 sync_api/models/sync_models.py create mode 100644 sync_api/nginx_proxy.conf create mode 100644 sync_api/requirements.txt create mode 100644 sync_api/routers/__init__.py create mode 100644 sync_api/routers/__pycache__/__init__.cpython-312.pyc create mode 100644 sync_api/routers/__pycache__/health.cpython-312.pyc create mode 100644 sync_api/routers/__pycache__/nodes.cpython-312.pyc create mode 100644 sync_api/routers/health.py create mode 100644 sync_api/routers/nodes.py create mode 100644 sync_api/run_dev.bat create mode 100644 sync_api/run_prod.bat create mode 100644 sync_api/run_prod_https.bat create mode 100644 sync_api/run_prod_server.bat create mode 100644 sync_api/ssl/certificate.crt create mode 100644 sync_api/ssl/private.key create mode 100644 sync_api/ssl/test_key.pem create mode 100644 sync_api/start_https_9443.py create mode 100644 sync_api/start_https_9443_alt.py create mode 100644 sync_api/test_api.bat create mode 100644 sync_api/verify_installation.py 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 0000000000000000000000000000000000000000..2a40d5f14c0f9b7fe72e8f13e821fe9a372598e3 GIT binary patch literal 10878 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a1bfaa291a9aee7a8877cea825f31de421a52566 GIT binary patch literal 147 zcmX@j%ge<81fHKcG8KUIV-N=&d}aZPOlPQM&}8&m$xy@uKEC3J4B9Q<9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6c0d29a63048b668086cebc6880a5f0dd5e9d8af GIT binary patch literal 1607 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 Z^9(__H{lQy$_pg&)~|XRe(6$7{tupl0FwX! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ee179e80cba018e3322f4f0309098706d7dcef7 GIT binary patch literal 147 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 QaZE9R{RbKyrpf&He-)#|)Bpeg literal 0 HcmV?d00001 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)