diff --git a/__pycache__/cisco_mac_monitor.cpython-314.pyc b/__pycache__/cisco_mac_monitor.cpython-314.pyc new file mode 100644 index 0000000..817a313 Binary files /dev/null and b/__pycache__/cisco_mac_monitor.cpython-314.pyc differ diff --git a/cisco_mac_http_service.py b/cisco_mac_http_service.py index 0e93d67..9006ec4 100755 --- a/cisco_mac_http_service.py +++ b/cisco_mac_http_service.py @@ -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 diff --git a/cisco_mac_monitor.py b/cisco_mac_monitor.py index fde7aed..7d08bfb 100755 --- a/cisco_mac_monitor.py +++ b/cisco_mac_monitor.py @@ -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) diff --git a/run.sh b/run.sh index ddeba6d..9b74804 100755 --- a/run.sh +++ b/run.sh @@ -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 \ diff --git a/test_new_format.py b/test_new_format.py index 625c330..0bdfb5e 100755 --- a/test_new_format.py +++ b/test_new_format.py @@ -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() \ No newline at end of file