Files
los-vecinos-de-cisco/old/cisco_mac_monitor.py
T
2025-12-09 12:45:15 +01:00

423 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Cisco MAC Address Monitor for Zabbix LLD
========================================
Script para monitorizar direcciones MAC conectadas a puertos de switches Cisco.
Compatible con modelos C2960, C3560, C3560E, C2960X, C3560CX y Catalyst series.
Genera salida JSON compatible con Zabbix Low Level Discovery.
Autor: Sistema de Monitorización de Red
Fecha: Octubre 2024
"""
import json
import logging
import time
import yaml
import paramiko
import re
import sys
from datetime import datetime
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
@dataclass
class DeviceConfig:
"""Configuración de dispositivo de red"""
hostname: str
ip: str
username: str
password: str
port: int = 22
device_type: str = "cisco_ios"
timeout: int = 30
@dataclass
class MacEntry:
"""Entrada de tabla MAC"""
vlan: str
mac_address: str
type: str
port: str
class CiscoSSHConnector:
"""Conector SSH para dispositivos Cisco"""
def __init__(self, device_config: DeviceConfig):
self.device_config = device_config
self.ssh_client = None
self.logger = logging.getLogger(__name__)
def connect(self) -> bool:
"""Establece conexión SSH con el dispositivo"""
try:
self.ssh_client = paramiko.SSHClient()
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.logger.info(f"Conectando a {self.device_config.hostname} ({self.device_config.ip})")
self.ssh_client.connect(
hostname=self.device_config.ip,
port=self.device_config.port,
username=self.device_config.username,
password=self.device_config.password,
timeout=self.device_config.timeout,
look_for_keys=False,
allow_agent=False
)
self.logger.info(f"Conexión exitosa a {self.device_config.hostname}")
return True
except Exception as e:
self.logger.error(f"Error conectando a {self.device_config.hostname}: {str(e)}")
return False
def execute_command(self, command: str) -> str:
"""Ejecuta comando en el dispositivo"""
if not self.ssh_client:
raise Exception("No hay conexión SSH activa")
try:
self.logger.debug(f"Ejecutando comando: {command}")
stdin, stdout, stderr = self.ssh_client.exec_command(command)
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
if error:
self.logger.warning(f"Error en comando '{command}': {error}")
return output
except Exception as e:
self.logger.error(f"Error ejecutando comando '{command}': {str(e)}")
raise
def disconnect(self):
"""Cierra la conexión SSH"""
if self.ssh_client:
self.ssh_client.close()
self.logger.info(f"Desconectado de {self.device_config.hostname}")
class CiscoMacTableParser:
"""Parser para tabla MAC de dispositivos Cisco"""
def __init__(self):
self.logger = logging.getLogger(__name__)
def get_mac_table(self, ssh_connector: CiscoSSHConnector) -> List[MacEntry]:
"""Obtiene la tabla MAC del dispositivo"""
try:
# Comando para obtener tabla MAC (compatible con IOS 12.x y 15.x)
command = "show mac address-table"
output = ssh_connector.execute_command(command)
mac_entries = self._parse_mac_table_output(output)
self.logger.info(f"Obtenidas {len(mac_entries)} entradas de tabla MAC")
return mac_entries
except Exception as e:
self.logger.error(f"Error obteniendo tabla MAC: {str(e)}")
return []
def _parse_mac_table_output(self, output: str) -> List[MacEntry]:
"""Parsea la salida del comando show mac address-table"""
mac_entries = []
# Patrones para diferentes formatos de salida de IOS
# Formato típico: VLAN MAC Address Type Ports
patterns = [
# IOS 12.x y 15.x estándar
r'^\s*(\d+)\s+([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4})\s+(\w+)\s+(.+)$',
# Formato alternativo con guiones
r'^\s*(\d+)\s+([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})\s+(\w+)\s+(.+)$',
# Formato con dos puntos
r'^\s*(\d+)\s+([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})\s+(\w+)\s+(.+)$'
]
lines = output.split('\n')
for line in lines:
line = line.strip()
if not line or line.startswith('-') or 'VLAN' in line.upper() or 'MAC Address' in line:
continue
for pattern in patterns:
match = re.match(pattern, line)
if match:
vlan = match.group(1)
mac_address = self._normalize_mac_address(match.group(2))
mac_type = match.group(3)
port = match.group(4).strip()
# Filtrar MACs del CPU y otras no relevantes
if not self._is_valid_port(port):
continue
mac_entries.append(MacEntry(
vlan=vlan,
mac_address=mac_address,
type=mac_type,
port=port
))
break
return mac_entries
def _normalize_mac_address(self, mac: str) -> str:
"""Normaliza formato de dirección MAC"""
# Remover todos los separadores y convertir a minúsculas
clean_mac = re.sub(r'[.:-]', '', mac.lower())
# Formatear como XX:XX:XX:XX:XX:XX
if len(clean_mac) == 12:
return ':'.join([clean_mac[i:i+2] for i in range(0, 12, 2)])
return mac
def _is_valid_port(self, port: str) -> bool:
"""Verifica si el puerto es válido para monitorización"""
# Excluir puertos del CPU, uplinks management, etc.
exclude_patterns = [
r'cpu',
r'switch',
r'router',
r'null',
r'drop',
r'punt'
]
port_lower = port.lower()
for pattern in exclude_patterns:
if re.search(pattern, port_lower):
return False
return True
class ZabbixLLDFormatter:
"""Formateador de datos para Zabbix Low Level Discovery"""
def __init__(self):
self.logger = logging.getLogger(__name__)
def format_for_zabbix(self, device_data: Dict[str, List[MacEntry]]) -> Dict[str, Any]:
"""Formatea datos para Zabbix LLD"""
discovery_data = []
timestamp = datetime.now().isoformat()
for hostname, mac_entries in device_data.items():
# Agrupar MACs por puerto
ports_data = {}
for entry in mac_entries:
port = entry.port
if port not in ports_data:
ports_data[port] = {
"port_name": port,
"device_hostname": hostname,
"mac_addresses": [],
"mac_count": 0,
"vlans": set()
}
ports_data[port]["mac_addresses"].append({
"mac": entry.mac_address,
"vlan": entry.vlan,
"type": entry.type
})
ports_data[port]["vlans"].add(entry.vlan)
# Crear entradas para LLD
for port, data in ports_data.items():
data["mac_count"] = len(data["mac_addresses"])
data["vlans"] = list(data["vlans"])
discovery_item = {
"{#DEVICE_HOSTNAME}": hostname,
"{#PORT_NAME}": port,
"{#MAC_COUNT}": data["mac_count"],
"{#VLANS}": ",".join(data["vlans"]),
"{#MAC_ADDRESSES}": json.dumps(data["mac_addresses"]),
"{#LAST_UPDATE}": timestamp
}
discovery_data.append(discovery_item)
result = {
"data": discovery_data,
"timestamp": timestamp,
"total_devices": len(device_data),
"total_ports": len(discovery_data)
}
self.logger.info(f"Formateados datos para {len(device_data)} dispositivos, {len(discovery_data)} puertos")
return result
class CiscoMacMonitor:
"""Monitor principal para dispositivos Cisco"""
def __init__(self, config_file: str = "config.yaml"):
self.config_file = config_file
self.config = None
self.devices = []
self.logger = self._setup_logging()
self.mac_parser = CiscoMacTableParser()
self.zabbix_formatter = ZabbixLLDFormatter()
def _setup_logging(self) -> logging.Logger:
"""Configura el sistema de logging"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/tmp/cisco_mac_monitor.log'),
logging.StreamHandler(sys.stdout)
]
)
return logging.getLogger(__name__)
def load_config(self) -> bool:
"""Carga configuración desde archivo YAML"""
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
# Validar configuración
if 'devices' not in self.config:
raise ValueError("No se encontró sección 'devices' en configuración")
if 'monitoring' not in self.config:
raise ValueError("No se encontró sección 'monitoring' en configuración")
# Crear objetos DeviceConfig
self.devices = []
for device_data in self.config['devices']:
device = DeviceConfig(**device_data)
self.devices.append(device)
self.logger.info(f"Configuración cargada: {len(self.devices)} dispositivos")
return True
except Exception as e:
self.logger.error(f"Error cargando configuración: {str(e)}")
return False
def monitor_single_device(self, device: DeviceConfig) -> List[MacEntry]:
"""Monitoriza un dispositivo individual"""
ssh_connector = CiscoSSHConnector(device)
mac_entries = []
try:
if ssh_connector.connect():
mac_entries = self.mac_parser.get_mac_table(ssh_connector)
except Exception as e:
self.logger.error(f"Error monitorizando {device.hostname}: {str(e)}")
finally:
ssh_connector.disconnect()
return mac_entries
def monitor_all_devices(self) -> Dict[str, List[MacEntry]]:
"""Monitoriza todos los dispositivos configurados"""
all_data = {}
for device in self.devices:
self.logger.info(f"Monitorizando dispositivo: {device.hostname}")
mac_entries = self.monitor_single_device(device)
all_data[device.hostname] = mac_entries
return all_data
def run_monitoring_cycle(self) -> Dict[str, Any]:
"""Ejecuta un ciclo completo de monitorización"""
self.logger.info("Iniciando ciclo de monitorización")
# Monitorizar todos los dispositivos
device_data = self.monitor_all_devices()
# Formatear para Zabbix
zabbix_data = self.zabbix_formatter.format_for_zabbix(device_data)
self.logger.info("Ciclo de monitorización completado")
return zabbix_data
def run_continuous_monitoring(self):
"""Ejecuta monitorización continua según configuración"""
if not self.config:
self.logger.error("No hay configuración cargada")
return
interval = self.config['monitoring'].get('interval_minutes', 5)
output_file = self.config['monitoring'].get('output_file', '/tmp/cisco_mac_data.json')
self.logger.info(f"Iniciando monitorización continua cada {interval} minutos")
while True:
try:
# Ejecutar ciclo de monitorización
zabbix_data = self.run_monitoring_cycle()
# Guardar datos
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(zabbix_data, f, indent=2, ensure_ascii=False)
self.logger.info(f"Datos guardados en: {output_file}")
# Esperar hasta el siguiente ciclo
time.sleep(interval * 60)
except KeyboardInterrupt:
self.logger.info("Monitorización detenida por usuario")
break
except Exception as e:
self.logger.error(f"Error en ciclo de monitorización: {str(e)}")
time.sleep(60) # Esperar 1 minuto antes de reintentar
def main():
"""Función principal"""
import argparse
parser = argparse.ArgumentParser(description='Monitor de direcciones MAC para dispositivos Cisco')
parser.add_argument('--config', '-c', default='config.yaml', help='Archivo de configuración')
parser.add_argument('--single-run', '-s', action='store_true', help='Ejecutar una sola vez')
parser.add_argument('--output', '-o', help='Archivo de salida para modo single-run')
args = parser.parse_args()
monitor = CiscoMacMonitor(args.config)
if not monitor.load_config():
sys.exit(1)
if args.single_run:
# Ejecución única
zabbix_data = monitor.run_monitoring_cycle()
output_file = args.output or '/tmp/cisco_mac_data.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(zabbix_data, f, indent=2, ensure_ascii=False)
print(f"Datos guardados en: {output_file}")
else:
# Monitorización continua
monitor.run_continuous_monitoring()
if __name__ == '__main__':
main()