Add interface information handling and update Zabbix formatting

- 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.
This commit is contained in:
Xavier Rodriguez
2026-03-09 15:21:48 +01:00
parent f12e236e82
commit 6782aa47d1
5 changed files with 160 additions and 50 deletions
Binary file not shown.
+5 -5
View File
@@ -150,11 +150,11 @@ class CiscoMacHTTPHandler(BaseHTTPRequestHandler):
if not mac_entries:
self.logger.warning(f"No se obtuvieron entradas de tabla MAC de {host}")
# Obtener la lista de interfaces detectadas
all_interfaces = getattr(mac_parser, 'detected_interfaces', [])
# Formatear para Zabbix incluyendo todas las interfaces
zabbix_data = zabbix_formatter.format_for_zabbix(mac_entries, all_interfaces)
# Formatear para Zabbix incluyendo información de interfaces
zabbix_data = zabbix_formatter.format_for_zabbix(
mac_entries,
mac_parser.interface_info
)
return zabbix_data
+140 -43
View File
@@ -36,6 +36,15 @@ class MacEntry:
self.port = port
class InterfaceInfo:
"""Información de interfaz - Compatible con Python 3.6"""
def __init__(self, name: str, status: str = "unknown", description: str = ""):
self.name = name
self.status = status # up, down, disabled, unknown
self.description = description
class CiscoSSHConnector:
"""Conector SSH para dispositivos Cisco"""
@@ -131,13 +140,14 @@ class CiscoMacTableParser:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.interface_info = {} # Dict[str, InterfaceInfo]
def get_mac_table(self, ssh_connector: CiscoSSHConnector) -> List[MacEntry]:
"""Obtiene la tabla MAC del dispositivo"""
try:
# Primero obtenemos la lista de interfaces
interfaces = self._get_interface_list(ssh_connector)
self.logger.info(f"Detectadas {len(interfaces)} interfaces")
# Primero obtenemos la información completa de interfaces (nombre, estado, descripción)
self._get_interface_info(ssh_connector)
self.logger.info(f"Detectadas {len(self.interface_info)} interfaces")
# CRUCIAL: Reconectar para dispositivos Cisco antiguos
# Algunos switches Cisco antiguos corrompen la sesión SSH después de comandos largos
@@ -154,9 +164,6 @@ class CiscoMacTableParser:
mac_entries = self._parse_mac_table_output(output)
self.logger.info(f"Obtenidas {len(mac_entries)} entradas de tabla MAC")
# Almacenar las interfaces para usarlas en el formatter
self.detected_interfaces = interfaces
return mac_entries
except Exception as e:
@@ -165,36 +172,36 @@ class CiscoMacTableParser:
self.logger.error(f"Traceback: {traceback.format_exc()}")
return []
def _get_interface_list(self, ssh_connector: CiscoSSHConnector) -> List[str]:
"""Obtiene la lista de todas las interfaces del dispositivo"""
def _get_interface_info(self, ssh_connector: CiscoSSHConnector):
"""Obtiene información completa de todas las interfaces del dispositivo"""
try:
# Comando para obtener interfaces (funciona en IOS 12.x y 15.x)
# Primero intentamos con show interface status (incluye estado)
command = "show interface status"
output = ssh_connector.execute_command(command)
status_output = ssh_connector.execute_command(command)
# Si no funciona, probar comando alternativo
if "Invalid input" in output or not output.strip():
if "Invalid input" in status_output or not status_output.strip():
command = "show ip interface brief"
output = ssh_connector.execute_command(command)
status_output = ssh_connector.execute_command(command)
interfaces = self._parse_interface_list(output)
# Parsear información de estado
self._parse_interface_status(status_output)
# Si aún no obtenemos interfaces, usar un comando más básico
if not interfaces:
# Si no obtuvimos interfaces, usar un comando más básico
if not self.interface_info:
command = "show interface | include line protocol"
output = ssh_connector.execute_command(command)
interfaces = self._parse_interface_line_protocol(output)
self._parse_interface_line_protocol_to_info(output)
return interfaces
# Ahora obtenemos las descripciones
if self.interface_info:
self._get_interface_descriptions(ssh_connector)
except Exception as e:
self.logger.warning(f"Error obteniendo interfaces: {str(e)}")
return []
self.logger.warning(f"Error obteniendo información de interfaces: {str(e)}")
def _parse_interface_list(self, output: str) -> List[str]:
def _parse_interface_status(self, output: str):
"""Parsea la salida de show interface status o show ip interface brief"""
interfaces = []
lines = output.split('\n')
for line in lines:
line = line.strip()
@@ -204,28 +211,105 @@ class CiscoMacTableParser:
# Patrón para show interface status
# Format: Port Name Status Vlan Duplex Speed Type
parts = line.split()
if len(parts) >= 2:
if len(parts) >= 3:
interface = parts[0]
# Filtrar solo interfaces físicas relevantes
if self._is_physical_interface(interface):
interfaces.append(interface)
return list(set(interfaces)) # Eliminar duplicados
# El estado generalmente está en la posición después del nombre/descripción
# Formato típico: Port Name Status Vlan...
# O: Port Status Vlan... (sin nombre)
status_raw = parts[1] if len(parts) > 1 else "unknown"
# Si parts[1] parece ser un estado válido, úsalo
# De lo contrario, asume que parts[1] es el nombre y parts[2] es el estado
if status_raw.lower() in ['connected', 'notconnect', 'disabled', 'up', 'down', 'admin']:
status = self._normalize_status(status_raw)
elif len(parts) > 2:
status = self._normalize_status(parts[2])
else:
status = "unknown"
self.interface_info[interface] = InterfaceInfo(
name=interface,
status=status,
description=""
)
def _parse_interface_line_protocol(self, output: str) -> List[str]:
def _parse_interface_line_protocol_to_info(self, output: str):
"""Parsea la salida de show interface | include line protocol"""
interfaces = []
lines = output.split('\n')
for line in lines:
# Format: FastEthernet0/1 is up, line protocol is up
if ' is ' in line and 'line protocol' in line:
interface = line.split(' is ')[0].strip()
parts = line.split(' is ')
interface = parts[0].strip()
if self._is_physical_interface(interface):
interfaces.append(interface)
# Extraer el estado
status = "unknown"
if len(parts) > 1:
status_part = parts[1].split(',')[0].strip()
status = self._normalize_status(status_part)
self.interface_info[interface] = InterfaceInfo(
name=interface,
status=status,
description=""
)
def _get_interface_descriptions(self, ssh_connector: CiscoSSHConnector):
"""Obtiene las descripciones de las interfaces"""
try:
command = "show interface description"
output = ssh_connector.execute_command(command)
# Si el comando no es válido, intentar alternativa
if "Invalid input" in output or not output.strip():
# Intentar obtener descripciones una por una (más lento pero funciona en IOS antiguos)
self.logger.debug("show interface description no disponible, usando método alternativo")
return
self._parse_interface_descriptions(output)
except Exception as e:
self.logger.warning(f"Error obteniendo descripciones de interfaces: {str(e)}")
def _parse_interface_descriptions(self, output: str):
"""Parsea la salida de show interface description"""
lines = output.split('\n')
for line in lines:
line_stripped = line.strip()
if not line_stripped or line_stripped.startswith('-') or 'Interface' in line_stripped:
continue
# Format: Interface Status Protocol Description
# Fa0/1 up up User Port
# Usamos split con máximo 3 splits para preservar la descripción completa
parts = line.split(None, 3) # Split en whitespace, máximo 4 partes
if len(parts) >= 1:
interface = parts[0]
description = ""
if len(parts) >= 4:
description = parts[3].strip()
# Si esta interfaz ya existe en nuestro diccionario, actualizar descripción
if interface in self.interface_info:
self.interface_info[interface].description = description
def _normalize_status(self, status_raw: str) -> str:
"""Normaliza el estado de la interfaz a up/down/disabled/unknown"""
status_lower = status_raw.lower()
return list(set(interfaces))
if status_lower in ['up', 'connected']:
return "up"
elif status_lower in ['down', 'notconnect', 'notconnected']:
return "down"
elif status_lower in ['disabled', 'admin', 'administratively']:
return "disabled"
else:
return "unknown"
def _is_physical_interface(self, interface: str) -> bool:
"""Verifica si una interfaz es física y relevante para monitorización"""
@@ -343,7 +427,7 @@ class ZabbixLLDFormatter:
self.device_hostname = device_hostname
self.logger = logging.getLogger(__name__)
def format_for_zabbix(self, mac_entries: List[MacEntry], all_interfaces: List[str] = None) -> Dict[str, Any]:
def format_for_zabbix(self, mac_entries: List[MacEntry], interface_info: Dict[str, Any] = None) -> Dict[str, Any]:
"""Formatea datos en formato JSON estructurado por puertos"""
discovery_data = []
# Usar timestamp Unix para compatibilidad con Zabbix
@@ -362,7 +446,9 @@ class ZabbixLLDFormatter:
"device_hostname": self.device_hostname,
"mac_addresses": [],
"mac_count": 0,
"vlans": set()
"vlans": set(),
"status": "unknown",
"description": ""
}
ports_data[port]["mac_addresses"].append({
@@ -372,17 +458,23 @@ class ZabbixLLDFormatter:
})
ports_data[port]["vlans"].add(entry.vlan)
# Añadir interfaces sin MACs si tenemos la lista de todas las interfaces
if all_interfaces:
for interface in all_interfaces:
if interface not in ports_data:
ports_data[interface] = {
"port_name": interface,
# Añadir todas las interfaces conocidas (con o sin MACs)
if interface_info:
for interface_name, iface_info in interface_info.items():
if interface_name not in ports_data:
ports_data[interface_name] = {
"port_name": interface_name,
"device_hostname": self.device_hostname,
"mac_addresses": [],
"mac_count": 0,
"vlans": set()
"vlans": set(),
"status": iface_info.status,
"description": iface_info.description
}
else:
# Actualizar estado y descripción para puertos que ya tienen MACs
ports_data[interface_name]["status"] = iface_info.status
ports_data[interface_name]["description"] = iface_info.description
# Crear entradas en el nuevo formato
for port, data in ports_data.items():
@@ -405,6 +497,8 @@ class ZabbixLLDFormatter:
"count": data["mac_count"],
"vlans": ",".join(data["vlans"]) if data["vlans"] else "",
"addresses": mac_addresses_text,
"status": data.get("status", "unknown"),
"description": data.get("description", ""),
"timestamp": unix_timestamp
}
}
@@ -520,8 +614,11 @@ Salida:
if not mac_entries:
logger.warning("No se obtuvieron entradas de tabla MAC")
# Formatear para Zabbix
zabbix_data = zabbix_formatter.format_for_zabbix(mac_entries)
# Formatear para Zabbix incluyendo información de interfaces
zabbix_data = zabbix_formatter.format_for_zabbix(
mac_entries,
mac_parser.interface_info
)
# Generar salida JSON
json_output = json.dumps(zabbix_data, indent=2, ensure_ascii=False)
+4
View File
@@ -1,5 +1,9 @@
#!/bin/bash
docker stop cisco-friends
docker rm cisco-friends
docker run -d --platform linux/amd64 \
--restart unless-stopped \
-p 127.0.0.1:8443:8443 \
+11 -2
View File
@@ -7,7 +7,7 @@ import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from cisco_mac_monitor import MacEntry, ZabbixLLDFormatter
from cisco_mac_monitor import MacEntry, InterfaceInfo, ZabbixLLDFormatter
import json
# Crear datos de prueba
@@ -17,9 +17,16 @@ test_mac_entries = [
MacEntry(vlan="151", mac_address="00:50:56:a6:91:69", type="DYNAMIC", port="Gi1/0/10"),
]
# Crear información de interfaces de prueba (incluye estado y descripción)
test_interface_info = {
"Gi1/0/1": InterfaceInfo(name="Gi1/0/1", status="up", description="User Port - Office"),
"Gi1/0/10": InterfaceInfo(name="Gi1/0/10", status="up", description="Server Link"),
"Gi1/0/11": InterfaceInfo(name="Gi1/0/11", status="down", description="Unused Port"),
}
# Crear formatter y formatear datos
formatter = ZabbixLLDFormatter("10.150.250.2")
result = formatter.format_for_zabbix(test_mac_entries, ["Gi1/0/1", "Gi1/0/10", "Gi1/0/11"])
result = formatter.format_for_zabbix(test_mac_entries, test_interface_info)
# Mostrar resultado
print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -39,5 +46,7 @@ for item in data:
print(f" - Count: {port_data['count']}")
print(f" - VLANs: '{port_data['vlans']}'")
print(f" - Addresses: '{port_data['addresses']}'")
print(f" - Status: '{port_data['status']}'")
print(f" - Description: '{port_data['description']}'")
print(f" - Timestamp: {port_data['timestamp']}")
print()