Initial commit: SYNC_API project

This commit is contained in:
2025-11-06 09:50:10 -07:00
commit 733ef04eeb
42 changed files with 2380 additions and 0 deletions

24
sync_api/.dockerignore Normal file
View File

@@ -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

32
sync_api/.env Normal file
View File

@@ -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!

109
sync_api/DOCKER_README.md Normal file
View File

@@ -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
```

43
sync_api/Dockerfile Normal file
View File

@@ -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"]

274
sync_api/README.md Normal file
View File

@@ -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

Binary file not shown.

62
sync_api/build-and-run.sh Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
# Config module initialization

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View File

@@ -0,0 +1 @@
# Database module initialization

Binary file not shown.

View File

@@ -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()

24
sync_api/docker-build.bat Normal file
View File

@@ -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

View File

@@ -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

37
sync_api/docker-run.bat Normal file
View File

@@ -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

275
sync_api/main.py Normal file
View File

@@ -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"
)

View File

@@ -0,0 +1 @@
# Models module initialization

Binary file not shown.

View File

@@ -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")

18
sync_api/nginx_proxy.conf Normal file
View File

@@ -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";
}
}

11
sync_api/requirements.txt Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
# Routers module initialization

Binary file not shown.

Binary file not shown.

Binary file not shown.

176
sync_api/routers/health.py Normal file
View File

@@ -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"
}

510
sync_api/routers/nodes.py Normal file
View File

@@ -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)}"
)

9
sync_api/run_dev.bat Normal file
View File

@@ -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

7
sync_api/run_prod.bat Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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-----

27
sync_api/ssl/private.key Normal file
View File

@@ -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-----

52
sync_api/ssl/test_key.pem Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDJf2pdY+/tSSsV
Pq69ONcImAgFEifMwZSY6Wrwi0DItkre7bxv1hSNWig2kAbhHsJF+RakubOgGxFi
BiF/BLdMFGXEJQjomBNjN0Diq2TAb4HRiAF5DsN9eYcsHTmta2FIO2JcNDuUXa22
7KJnUMPzXkRzqStbePD4WYoigS4bgmWLPkSyguvmwl/AHrOW9YZume2DJwq+ugdd
Jvsns2Yuj8try+tnWDoPYNNmSCwJl8wAKOcsunoXgv8O0SDEsQnNnKSsaJkylwhO
7FJT8EtG3Phe0InyeicZd+pu1BRpYWyx+JwvN0/FiItPSVheoUrOufiqP2UP6yem
Vf6citSfwIybTAyJTf5J3IjGms5rITiEU94rVVUXJgIDRgQCXROjvTdMqF5ulufF
d/uKUEnBlYjSSF28IILyP/4p7rEPSse4QdvXY8XDIRjMoJhcCUIFpVRys1XGxusy
x6wY6EMekr9pw/0U2f0/SNweoNF7cXVO9XwUzcczJ8K3w7p1tpX++8WfqLpbfrpK
xuQcNeuQHS+U6ZzhSTfBD+kP+JYhyuQCeYv0+SZ1cSoJM8FflohJJEfRVl2m3DAF
5xR9HBxjGKMSOGsGVzTUwuaQ5cV+X5b7Mi0iiRP8qddEyxux4RGYxkpL5c0ggC+X
0FFZHlhJ3jdo2vKcAtYcVuMevGvGqwIDAQABAoICACkjow9LOq6rLCkrwZhvlPOz
uptyb/7NnS21JnjTLZhtI48tYbZBsrODb0dok4b99TM9HoLmSUbMj/l///HDgDr/
xSFNYGrAteChZNWCodDqU0p45rgVLBOfrtnyY5bFIDkTqsrhfTKXB8MbwDKw2jkD
i51Dsk78O9Kw0VOnJUdHkbziqzr5IPndwJfkjM0q9tX5BnWrEHYNXDiIH2olJh0m
jaVGHptcd4oxPigZXyhcPjHgKAbJUekf/TxEtViDPtmkfDiZKIIcNP2MIWjy3COt
NOaC1E9V4krAYXRcR4iWdA9ID3dSZy6Q6MR42JqFetUHQj5cMCGWZOOAljnW3H8e
l6xDzSm4BmWsvmsHXWQOwQZLNtM89vEoO4txPCwvjmj/m3JS5vEJKvC1XyQyaRwS
kzT9FBQec4XpHiLTFAX+F6tyG7ZoJHYQ6nxac1eZFhSN9SIv0ktu1CJbxz2Kk8k2
gLq1CwrZ+TU/tcj/EjKJfc6oiHdjQ0ULFrqythBymJoyRuTRdln5zvOgziIkI5YV
ABp53zceURgDGgJaWpRrlml6gTfWCUP7gd5qKVyqt9xDctGpPKq6ooxmvxzrdXB1
+nrXC864z/f7xFSDgi4u7pmc02ZzSjwTRaVGj7OR5isy0iB2jpyQ9r93xWCYoJNE
t0v25ELXRc93P6ilxpzxAoIBAQDonUvSO5GfbkSsPq7KclxBiVPNcZ4XC/otlQ0Y
moHAltzK9e2wRC94TslqeKJ2iB6qoaejvAUXZ1ySn3cqwEtSHARffPQk7uUEStp1
JXbSxwcUaCaaDhv0lOhjkVHnyr64h+6VyGxl2Txs+RKudzn2/oLDYovQQepyg8tM
rfzCgLk2hQvyD8MCht60Vjhzma0L583YkQhWdQMt5hS3Rk0mQAGm8gpEsnNo2SCA
cKU1JR8FdMVlzUNzHSyXs49FpwhG2XFyr7l19A3bqcHIfFimcT0CJVBapnErA5Pm
LEPjKsk2diw6TIlALkqyOQrRM4CPBT4XSH7oXMygHkdjvnybAoIBAQDdwUfj72DA
w8doXU5km6Bwcox64o0dB7nj7lmkFnNUPe+cmdv5GhXKp6454c9q6sCH6DLREyWP
+nM8gemVv5gW1Gfsc0rBC/58m8uGDkSboS4IXM1Woz0/nCr1U5qSDSRMK4AVLQRE
9ZAubsGonOaxIMkSFFUoDu9pHPLik6/l/b5m8BD+pgO/WN2FsANhK9GqPV69D7F3
10/vbElgAFmIc87BRe3Rj6AFnwmM0NnVGV8ca/Wot0QObdAStbOAfXMDjUnEhgL9
E80AgV1FRzOmP7RiO8d0PPrnHOtFyJQ3XGfdZ2iPgC/KYhVYs0Bh6wmWoA9Ef0qF
3FXwvMnZjBcxAoIBACZVRZlvl5/eXBLGgad7ERj7uIOypYrGvoOkfHQBgkBqnBgW
TwUtN2VsFmUlXCCMbgFQgn/JDE2/CtJe1RSGe+McGtxhixpLBBKhAz8q3Vn+0nva
bgIVMSYtI97Eo+q+zo86V2b3kGV4VyzeHuYBX9wENShdpQy80BXvAiGwymaDMAJ8
CmvpjQltkGXHj6X9PTcpDBMUU3tr7QozUFajjzMueRc569dGUR6GKYUBCzim1WXg
yGbuXJ8rv52ASawAyqxiSP/NVZLKlT4Ny5FubG2BW7xuyb8mjp+4Bzd3M/jLpD0F
XGR/GF8rQcP9AntXMWhgRMsxdzhzkoUQeFNHoOECggEAR2DaahT4OLQL8JzwCdRK
LUfBziA0Od7tx99FvVp4nuocwXSI6MKrtwKIQxtbL1gGxvhHuyKFmbWM+DH85PW5
18dg+CwYKNFahfOD7E+VZ97WuVxdwABlS59R+IuDmnVedKIf5TELScMdOMTv0v1G
CEe8UPR4gJ9mEph92c6d9NLbQbkOT1lSpXnOPCTuwY1gPILqhsKRZjjJAxEBAUS7
jP9x0qZWqXiokoahhVEIfN5gzf42Iatz3Y0dR5hzAe3fYopM71QPjDSebQY25oDH
sECbKG+DgPfL7y5Q0LKCettkl1CcyGwDdgpiQp3YIGyONoyovWyXqUTF6Oprsnm0
UQKCAQEAkqvuCF81Ne9BDsW1r10v/RNCw+Vc3de6Ff27NXwtkIBmeS+iZquKlYYx
q4rWTRSKUfQnh3GhS8TZk1Q1AdeeEeSVADpN6gr6JxbiNfzJUMdJ0rGa1nolkOVx
wbKjsqU9wntxKxrH/Pv5ngUWI0EQv47yg448hu0M3igJMbNUONgjmJFHBRZ4MtBx
KhEQa338Dw/Th91SRhpZtVA3ubhqtGfjxOnIN+L2RyC6s5NAJ43VG/xXQk3t0aT3
EXehSkhReP17OfIjUOaynv72kBHO6FpYpmT65MyJWV0w2kiouiAuiv7IIxVeYsIi
6B01dMcbwHLqTan0+atfQK5/EQzJqQ==
-----END PRIVATE KEY-----

View File

@@ -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()

View File

@@ -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()

23
sync_api/test_api.bat Normal file
View File

@@ -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

View File

@@ -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)