563 lines
22 KiB
Python
563 lines
22 KiB
Python
from fastapi import APIRouter, HTTPException, Path, Query
|
|
from typing import List, Optional
|
|
import logging
|
|
import os
|
|
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"
|
|
|
|
|
|
def determine_sync_status(node_name: str, last_restore_date: Optional[datetime] = None) -> str:
|
|
"""
|
|
Determina el estado de sincronización basado en la ubicación de los archivos ZIP del nodo.
|
|
|
|
Lógica:
|
|
- Si existe en D:\sftp -> "Recibido"
|
|
- Si existe en D:\BackupSFTP -> "Procesando"
|
|
- Si la fecha de última sincronización es mayor a la del archivo en D:\BackupSFTP -> "Actualizada"
|
|
- En otros casos sigue la lógica anterior basada en fechas
|
|
"""
|
|
try:
|
|
sftp_path = f"D:\\sftp\\{node_name}.zip"
|
|
backup_sftp_path = f"D:\\BackupSFTP\\{node_name}.zip"
|
|
|
|
# Verificar si existe en D:\sftp (prioridad más alta)
|
|
if os.path.exists(sftp_path):
|
|
return EstadoSincronizacion.RECIBIDO.value
|
|
|
|
# Verificar si existe en D:\BackupSFTP
|
|
if os.path.exists(backup_sftp_path):
|
|
# Si tenemos fecha de última restauración, comparar
|
|
if last_restore_date:
|
|
try:
|
|
# Obtener fecha de modificación del archivo
|
|
file_mod_time = datetime.fromtimestamp(os.path.getmtime(backup_sftp_path))
|
|
|
|
# Si la última sincronización es más reciente que el archivo
|
|
if last_restore_date > file_mod_time:
|
|
return EstadoSincronizacion.ACTUALIZADA.value
|
|
|
|
# Si la última restauración fue hace más de 5 horas, continuar
|
|
if datetime.now() - last_restore_date > timedelta(hours=5):
|
|
# Pasaron más de 5 horas, continuar el proceso
|
|
pass
|
|
else:
|
|
# Aún dentro de las 5 horas → seguir procesando
|
|
return EstadoSincronizacion.PROCESANDO.value
|
|
|
|
except OSError:
|
|
# Si hay error leyendo el archivo, asumir procesando
|
|
return EstadoSincronizacion.PROCESANDO.value
|
|
|
|
# Si no existe en ninguna ruta SFTP, usar lógica basada en fechas de restauración
|
|
if last_restore_date is None:
|
|
return EstadoSincronizacion.ERROR.value
|
|
elif last_restore_date < datetime.now() - timedelta(days=7):
|
|
return EstadoSincronizacion.ERROR.value
|
|
elif last_restore_date < datetime.now() - timedelta(hours=24):
|
|
return EstadoSincronizacion.ATENCION.value
|
|
else:
|
|
return EstadoSincronizacion.ACTUALIZADA.value
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error determinando estado de sincronización para {node_name}: {str(e)}")
|
|
# En caso de error, usar lógica de fechas como fallback
|
|
if last_restore_date is None:
|
|
return EstadoSincronizacion.ERROR.value
|
|
elif last_restore_date < datetime.now() - timedelta(days=7):
|
|
return EstadoSincronizacion.ERROR.value
|
|
elif last_restore_date < datetime.now() - timedelta(hours=24):
|
|
return EstadoSincronizacion.ATENCION.value
|
|
else:
|
|
return EstadoSincronizacion.ACTUALIZADA.value
|
|
|
|
|
|
@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 ubicación de archivos y fecha de restauración
|
|
estado = determine_sync_status(node_name, last_restore_date)
|
|
|
|
# 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 ubicación de archivos y fecha de restauración
|
|
estado_sync = determine_sync_status(nodo_name, last_restore)
|
|
|
|
# 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)}"
|
|
)
|