Initial commit: SYNC_API project
This commit is contained in:
24
sync_api/.dockerignore
Normal file
24
sync_api/.dockerignore
Normal 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
32
sync_api/.env
Normal 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
109
sync_api/DOCKER_README.md
Normal 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
43
sync_api/Dockerfile
Normal 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
274
sync_api/README.md
Normal 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
|
||||||
BIN
sync_api/__pycache__/main.cpython-312.pyc
Normal file
BIN
sync_api/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
62
sync_api/build-and-run.sh
Normal file
62
sync_api/build-and-run.sh
Normal 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
|
||||||
1
sync_api/config/__init__.py
Normal file
1
sync_api/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Config module initialization
|
||||||
BIN
sync_api/config/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
sync_api/config/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sync_api/config/__pycache__/settings.cpython-312.pyc
Normal file
BIN
sync_api/config/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
45
sync_api/config/settings.py
Normal file
45
sync_api/config/settings.py
Normal 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()
|
||||||
1
sync_api/database/__init__.py
Normal file
1
sync_api/database/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Database module initialization
|
||||||
BIN
sync_api/database/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
sync_api/database/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sync_api/database/__pycache__/connection.cpython-312.pyc
Normal file
BIN
sync_api/database/__pycache__/connection.cpython-312.pyc
Normal file
Binary file not shown.
132
sync_api/database/connection.py
Normal file
132
sync_api/database/connection.py
Normal 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
24
sync_api/docker-build.bat
Normal 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
|
||||||
61
sync_api/docker-compose.yml
Normal file
61
sync_api/docker-compose.yml
Normal 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
37
sync_api/docker-run.bat
Normal 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
275
sync_api/main.py
Normal 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"
|
||||||
|
)
|
||||||
1
sync_api/models/__init__.py
Normal file
1
sync_api/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Models module initialization
|
||||||
BIN
sync_api/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
sync_api/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sync_api/models/__pycache__/sync_models.cpython-312.pyc
Normal file
BIN
sync_api/models/__pycache__/sync_models.cpython-312.pyc
Normal file
Binary file not shown.
83
sync_api/models/sync_models.py
Normal file
83
sync_api/models/sync_models.py
Normal 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
18
sync_api/nginx_proxy.conf
Normal 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
11
sync_api/requirements.txt
Normal 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
|
||||||
1
sync_api/routers/__init__.py
Normal file
1
sync_api/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Routers module initialization
|
||||||
BIN
sync_api/routers/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
sync_api/routers/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sync_api/routers/__pycache__/health.cpython-312.pyc
Normal file
BIN
sync_api/routers/__pycache__/health.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sync_api/routers/__pycache__/nodes.cpython-312.pyc
Normal file
BIN
sync_api/routers/__pycache__/nodes.cpython-312.pyc
Normal file
Binary file not shown.
176
sync_api/routers/health.py
Normal file
176
sync_api/routers/health.py
Normal 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
510
sync_api/routers/nodes.py
Normal 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
9
sync_api/run_dev.bat
Normal 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
7
sync_api/run_prod.bat
Normal 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
|
||||||
26
sync_api/run_prod_https.bat
Normal file
26
sync_api/run_prod_https.bat
Normal 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
|
||||||
20
sync_api/run_prod_server.bat
Normal file
20
sync_api/run_prod_server.bat
Normal 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
|
||||||
73
sync_api/ssl/certificate.crt
Normal file
73
sync_api/ssl/certificate.crt
Normal 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
27
sync_api/ssl/private.key
Normal 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
52
sync_api/ssl/test_key.pem
Normal 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-----
|
||||||
67
sync_api/start_https_9443.py
Normal file
67
sync_api/start_https_9443.py
Normal 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()
|
||||||
48
sync_api/start_https_9443_alt.py
Normal file
48
sync_api/start_https_9443_alt.py
Normal 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
23
sync_api/test_api.bat
Normal 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
|
||||||
108
sync_api/verify_installation.py
Normal file
108
sync_api/verify_installation.py
Normal 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)
|
||||||
Reference in New Issue
Block a user