- Introduced InterfaceInfo class to encapsulate interface details. - Modified CiscoMacTableParser to gather and store interface information. - Updated ZabbixLLDFormatter to include interface status and descriptions in the output. - Adjusted test_new_format.py to validate new interface information integration. - Enhanced run.sh for improved container management.
440 lines
16 KiB
Python
Executable File
440 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Cisco MAC Monitor HTTPS Service
|
|
===============================
|
|
|
|
Servicio HTTPS que expone la funcionalidad de monitorización de MACs vía REST API.
|
|
Mantiene todos los logs informativos pero devuelve solo JSON limpio en las respuestas.
|
|
Usa POST para credenciales seguras y soporte SSL/TLS.
|
|
|
|
Endpoints:
|
|
POST /mac-monitor - Body JSON con credenciales
|
|
GET /health - Health check
|
|
GET /metrics - Métricas del servicio
|
|
|
|
Autor: Sistema de Monitorización de Red
|
|
Fecha: Noviembre 2024
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
import ssl
|
|
import os
|
|
from datetime import datetime
|
|
from typing import Dict, Any
|
|
from urllib.parse import parse_qs, urlparse
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
import threading
|
|
import signal
|
|
import time
|
|
|
|
# Importar las clases existentes del cisco_mac_monitor
|
|
from cisco_mac_monitor import CiscoSSHConnector, CiscoMacTableParser, ZabbixLLDFormatter
|
|
|
|
|
|
class CiscoMacHTTPHandler(BaseHTTPRequestHandler):
|
|
"""Handler HTTP para el servicio de monitorización MAC"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.logger = logging.getLogger(__name__)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def do_GET(self):
|
|
"""Maneja peticiones GET - Solo endpoints públicos"""
|
|
try:
|
|
if self.path == '/health' or self.path == '/':
|
|
self._handle_health()
|
|
elif self.path == '/metrics':
|
|
self._handle_metrics()
|
|
elif self.path.startswith('/mac-monitor'):
|
|
self._send_error(405, "Método no permitido. Use POST con JSON body para /mac-monitor")
|
|
else:
|
|
self._send_error(404, "Endpoint no encontrado")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error procesando petición GET: {str(e)}")
|
|
self._send_error(500, f"Error interno: {str(e)}")
|
|
|
|
def do_POST(self):
|
|
"""Maneja peticiones POST - Endpoint seguro de monitorización"""
|
|
try:
|
|
if self.path == '/mac-monitor':
|
|
self._handle_mac_monitor_post()
|
|
else:
|
|
self._send_error(404, "Endpoint no encontrado")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error procesando petición POST: {str(e)}")
|
|
self._send_error(500, f"Error interno: {str(e)}")
|
|
|
|
def _handle_mac_monitor_post(self):
|
|
"""Maneja el endpoint POST de monitorización MAC con JSON body"""
|
|
try:
|
|
# Leer Content-Length
|
|
content_length = int(self.headers.get('Content-Length', 0))
|
|
if content_length == 0:
|
|
self._send_error(400, "Body JSON requerido")
|
|
return
|
|
|
|
# Leer body JSON
|
|
post_data = self.rfile.read(content_length)
|
|
|
|
try:
|
|
json_data = json.loads(post_data.decode('utf-8'))
|
|
except json.JSONDecodeError as e:
|
|
self._send_error(400, f"JSON inválido: {str(e)}")
|
|
return
|
|
|
|
# Validar parámetros requeridos
|
|
required_fields = ['host', 'username', 'password']
|
|
missing_fields = []
|
|
|
|
for field in required_fields:
|
|
if field not in json_data or not json_data[field]:
|
|
missing_fields.append(field)
|
|
|
|
if missing_fields:
|
|
self._send_error(400, f"Campos faltantes en JSON: {', '.join(missing_fields)}")
|
|
return
|
|
|
|
# Extraer parámetros
|
|
host = json_data['host']
|
|
username = json_data['username']
|
|
password = json_data['password']
|
|
hostname = json_data.get('hostname', host)
|
|
port = int(json_data.get('port', 22))
|
|
timeout = int(json_data.get('timeout', 30))
|
|
|
|
self.logger.info(f"Iniciando monitorización para {host}")
|
|
|
|
# Ejecutar monitorización
|
|
result = self._execute_mac_monitoring(host, username, password, hostname, port, timeout)
|
|
|
|
if result:
|
|
self._send_json_response(result)
|
|
self.logger.info(f"Monitorización completada para {host}")
|
|
else:
|
|
self._send_error(500, "Error obteniendo datos del dispositivo")
|
|
|
|
except ValueError as e:
|
|
self._send_error(400, f"Parámetro inválido: {str(e)}")
|
|
except Exception as e:
|
|
self.logger.error(f"Error en monitorización POST: {str(e)}")
|
|
self._send_error(500, f"Error de monitorización: {str(e)}")
|
|
|
|
def _execute_mac_monitoring(self, host: str, username: str, password: str,
|
|
hostname: str, port: int, timeout: int) -> Dict[str, Any]:
|
|
"""Ejecuta el proceso de monitorización MAC"""
|
|
# Inicializar componentes
|
|
ssh_connector = CiscoSSHConnector(
|
|
host=host,
|
|
username=username,
|
|
password=password,
|
|
port=port,
|
|
timeout=timeout
|
|
)
|
|
|
|
mac_parser = CiscoMacTableParser()
|
|
zabbix_formatter = ZabbixLLDFormatter(hostname)
|
|
|
|
try:
|
|
# Conectar al dispositivo
|
|
if not ssh_connector.connect():
|
|
self.logger.error(f"No se pudo establecer conexión SSH con {host}")
|
|
return None
|
|
|
|
# Obtener tabla MAC
|
|
mac_entries = mac_parser.get_mac_table(ssh_connector)
|
|
|
|
if not mac_entries:
|
|
self.logger.warning(f"No se obtuvieron entradas de tabla MAC de {host}")
|
|
|
|
# Formatear para Zabbix incluyendo información de interfaces
|
|
zabbix_data = zabbix_formatter.format_for_zabbix(
|
|
mac_entries,
|
|
mac_parser.interface_info
|
|
)
|
|
|
|
return zabbix_data
|
|
|
|
finally:
|
|
ssh_connector.disconnect()
|
|
|
|
def _handle_health(self):
|
|
"""Endpoint de health check"""
|
|
health_data = {
|
|
"status": "healthy",
|
|
"service": "cisco-mac-monitor-https",
|
|
"timestamp": datetime.now().isoformat(),
|
|
"version": "2.1-https",
|
|
"ssl_enabled": hasattr(self.server, 'socket') and isinstance(self.server.socket, ssl.SSLSocket)
|
|
}
|
|
self._send_json_response(health_data)
|
|
|
|
def _handle_metrics(self):
|
|
"""Endpoint básico de métricas"""
|
|
metrics_data = {
|
|
"service": "cisco-mac-monitor-https",
|
|
"uptime_seconds": time.time() - start_time,
|
|
"requests_total": getattr(self.server, 'request_count', 0),
|
|
"timestamp": datetime.now().isoformat(),
|
|
"ssl_enabled": hasattr(self.server, 'socket') and isinstance(self.server.socket, ssl.SSLSocket)
|
|
}
|
|
self._send_json_response(metrics_data)
|
|
|
|
def _send_json_response(self, data: Dict[str, Any]):
|
|
"""Envía respuesta JSON"""
|
|
json_data = json.dumps(data, indent=2, ensure_ascii=False)
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
self.send_header('Content-Length', len(json_data.encode('utf-8')))
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
|
|
self.wfile.write(json_data.encode('utf-8'))
|
|
|
|
def _send_error(self, code: int, message: str):
|
|
"""Envía respuesta de error"""
|
|
error_data = {
|
|
"error": message,
|
|
"code": code,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
json_data = json.dumps(error_data, indent=2, ensure_ascii=False)
|
|
|
|
self.send_response(code)
|
|
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
self.send_header('Content-Length', len(json_data.encode('utf-8')))
|
|
self.end_headers()
|
|
|
|
self.wfile.write(json_data.encode('utf-8'))
|
|
|
|
self.logger.warning(f"Error {code}: {message}")
|
|
|
|
def log_message(self, format, *args):
|
|
"""Sobrescribir para evitar logs HTTP automáticos"""
|
|
# Los logs HTTP automáticos se manejan mediante nuestro logger
|
|
pass
|
|
|
|
|
|
class CiscoMacHTTPSServer:
|
|
"""Servidor HTTPS para el servicio de monitorización MAC"""
|
|
|
|
def __init__(self, host='0.0.0.0', port=8443, ssl_cert=None, ssl_key=None):
|
|
self.host = host
|
|
self.port = port
|
|
self.ssl_cert = ssl_cert
|
|
self.ssl_key = ssl_key
|
|
self.httpd = None
|
|
self.server_thread = None
|
|
self.logger = logging.getLogger(__name__)
|
|
self.running = False
|
|
|
|
def _generate_self_signed_cert(self):
|
|
"""Genera certificado autofirmado si no se proporciona"""
|
|
cert_file = '/tmp/cisco_mac_server.crt'
|
|
key_file = '/tmp/cisco_mac_server.key'
|
|
|
|
if os.path.exists(cert_file) and os.path.exists(key_file):
|
|
self.logger.info("Usando certificado autofirmado existente")
|
|
return cert_file, key_file
|
|
|
|
self.logger.info("Generando certificado autofirmado...")
|
|
|
|
try:
|
|
import subprocess
|
|
|
|
# Generar key privada
|
|
result = subprocess.run([
|
|
'openssl', 'genrsa', '-out', key_file, '2048'
|
|
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
if result.returncode != 0:
|
|
self.logger.error(f"Error generando key: {result.stderr.decode()}")
|
|
return None, None
|
|
|
|
# Generar certificado autofirmado
|
|
result = subprocess.run([
|
|
'openssl', 'req', '-new', '-x509', '-key', key_file,
|
|
'-out', cert_file, '-days', '365', '-subj',
|
|
'/C=ES/ST=Madrid/L=Madrid/O=CiscoMonitor/CN=cisco-mac-monitor'
|
|
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
if result.returncode != 0:
|
|
self.logger.error(f"Error generando certificado: {result.stderr.decode()}")
|
|
return None, None
|
|
|
|
# Cambiar permisos
|
|
os.chmod(key_file, 0o600)
|
|
os.chmod(cert_file, 0o644)
|
|
|
|
self.logger.info(f"Certificado autofirmado generado: {cert_file}")
|
|
return cert_file, key_file
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
self.logger.error(f"Error generando certificado: {e}")
|
|
return None, None
|
|
except Exception as e:
|
|
self.logger.error(f"Error en subprocess: {e}")
|
|
return None, None
|
|
except FileNotFoundError:
|
|
self.logger.warning("OpenSSL no disponible, funcionando en modo HTTP")
|
|
return None, None
|
|
|
|
def start(self):
|
|
"""Inicia el servidor HTTPS"""
|
|
try:
|
|
self.httpd = HTTPServer((self.host, self.port), CiscoMacHTTPHandler)
|
|
self.httpd.request_count = 0
|
|
|
|
# Configurar SSL si está disponible
|
|
ssl_enabled = False
|
|
if self.ssl_cert and self.ssl_key:
|
|
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
ssl_context.load_cert_chain(self.ssl_cert, self.ssl_key)
|
|
self.httpd.socket = ssl_context.wrap_socket(self.httpd.socket, server_side=True)
|
|
ssl_enabled = True
|
|
self.logger.info(f"SSL habilitado con certificados personalizados")
|
|
else:
|
|
# Intentar generar certificado autofirmado
|
|
cert_file, key_file = self._generate_self_signed_cert()
|
|
if cert_file and key_file:
|
|
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
ssl_context.load_cert_chain(cert_file, key_file)
|
|
self.httpd.socket = ssl_context.wrap_socket(self.httpd.socket, server_side=True)
|
|
ssl_enabled = True
|
|
self.logger.info(f"SSL habilitado con certificado autofirmado")
|
|
|
|
protocol = "https" if ssl_enabled else "http"
|
|
self.logger.info(f"Iniciando servidor {protocol.upper()} en {self.host}:{self.port}")
|
|
|
|
# Configurar shutdown graceful
|
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
|
|
self.running = True
|
|
self.logger.info(f"Servidor disponible en {protocol}://{self.host}:{self.port}")
|
|
self.logger.info("Endpoints disponibles:")
|
|
self.logger.info(f" POST /mac-monitor - Body JSON con credenciales")
|
|
self.logger.info(f" GET /health")
|
|
self.logger.info(f" GET /metrics")
|
|
|
|
if ssl_enabled:
|
|
self.logger.info("⚠️ Usando certificado autofirmado - ignorar warnings SSL")
|
|
|
|
# Iniciar servidor
|
|
self.httpd.serve_forever()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error iniciando servidor: {str(e)}")
|
|
raise
|
|
|
|
def stop(self):
|
|
"""Detiene el servidor HTTP"""
|
|
if self.httpd and self.running:
|
|
self.logger.info("Deteniendo servidor HTTP...")
|
|
self.running = False
|
|
self.httpd.shutdown()
|
|
self.httpd.server_close()
|
|
self.logger.info("Servidor HTTP detenido")
|
|
|
|
def _signal_handler(self, signum, frame):
|
|
"""Maneja señales de sistema para shutdown graceful"""
|
|
self.logger.info(f"Recibida señal {signum}, deteniendo servidor...")
|
|
self.stop()
|
|
|
|
|
|
def setup_logging(debug: bool = False) -> logging.Logger:
|
|
"""Configura el sistema de logging"""
|
|
level = logging.DEBUG if debug else logging.INFO
|
|
|
|
# Formato de logging
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
# Handler para stderr
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
handler.setFormatter(formatter)
|
|
|
|
# Configurar logger principal
|
|
logger = logging.getLogger()
|
|
logger.setLevel(level)
|
|
logger.addHandler(handler)
|
|
|
|
# Reducir verbosidad de paramiko si no está en debug
|
|
if not debug:
|
|
logging.getLogger('paramiko').setLevel(logging.WARNING)
|
|
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
def main():
|
|
"""Función principal"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Cisco MAC Monitor HTTPS Service',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Ejemplos de uso:
|
|
%(prog)s --host 0.0.0.0 --port 8443
|
|
%(prog)s --host 127.0.0.1 --port 8443 --debug
|
|
%(prog)s --ssl-cert /path/to/cert.pem --ssl-key /path/to/key.pem
|
|
|
|
Endpoints:
|
|
POST /mac-monitor - Body JSON: {"host": "192.168.1.10", "username": "admin", "password": "cisco123"}
|
|
GET /health
|
|
GET /metrics
|
|
|
|
Ejemplo de petición:
|
|
curl -k -X POST "https://localhost:8443/mac-monitor" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"host":"192.168.1.10","username":"admin","password":"cisco123"}'
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('--host', default='0.0.0.0',
|
|
help='IP de escucha del servidor (default: 0.0.0.0)')
|
|
parser.add_argument('--port', '-p', type=int, default=8443,
|
|
help='Puerto de escucha del servidor (default: 8443)')
|
|
parser.add_argument('--ssl-cert',
|
|
help='Archivo de certificado SSL (opcional)')
|
|
parser.add_argument('--ssl-key',
|
|
help='Archivo de clave privada SSL (opcional)')
|
|
parser.add_argument('--debug', '-d', action='store_true',
|
|
help='Habilitar modo debug')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Configurar logging
|
|
logger = setup_logging(args.debug)
|
|
|
|
# Variable global para métricas
|
|
global start_time
|
|
start_time = time.time()
|
|
|
|
# Crear y iniciar servidor
|
|
server = CiscoMacHTTPSServer(
|
|
host=args.host,
|
|
port=args.port,
|
|
ssl_cert=args.ssl_cert,
|
|
ssl_key=args.ssl_key
|
|
)
|
|
|
|
try:
|
|
server.start()
|
|
except KeyboardInterrupt:
|
|
logger.info("Interrupción por teclado recibida")
|
|
except Exception as e:
|
|
logger.error(f"Error fatal: {str(e)}")
|
|
sys.exit(1)
|
|
finally:
|
|
server.stop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |