first commit

This commit is contained in:
Xavier Rodriguez
2025-12-09 12:45:15 +01:00
commit 7fc616b5c3
44 changed files with 6981 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# Dockerfile para Cisco MAC Monitor
# Basado en Rocky Linux 8 con Python y dependencias SSH
FROM rockylinux:8
# Metadatos
LABEL maintainer="Equipo de Monitorización de Red"
LABEL description="Monitor de direcciones MAC para dispositivos Cisco - Compatible con Zabbix LLD"
LABEL version="1.0"
# Variables de entorno
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
# Actualizar sistema e instalar dependencias base
RUN dnf update -y && \
dnf install -y \
python3 \
python3-pip \
python3-devel \
openssh-clients \
git \
gcc \
openssl-devel \
libffi-devel \
curl \
vim \
procps-ng \
iproute \
iputils \
net-tools && \
dnf clean all
# Crear usuario no privilegiado para ejecutar la aplicación
RUN useradd -m -u 1000 -s /bin/bash monitor && \
mkdir -p /app /config /logs /data && \
chown -R monitor:monitor /app /config /logs /data
# Instalar dependencias Python
COPY requirements.txt /tmp/requirements.txt
RUN pip3 install --no-cache-dir --upgrade pip && \
pip3 install --no-cache-dir -r /tmp/requirements.txt && \
rm /tmp/requirements.txt
# Cambiar al usuario no privilegiado
USER monitor
WORKDIR /app
# Copiar archivos de la aplicación
COPY --chown=monitor:monitor cisco_mac_monitor.py /app/
COPY --chown=monitor:monitor config.yaml /config/config.yaml
# Crear directorios necesarios
RUN mkdir -p /app/scripts /app/utils
# Script de entrada personalizable
COPY --chown=monitor:monitor entrypoint.sh /app/entrypoint.sh
# Permisos de ejecución
USER root
RUN chmod +x /app/entrypoint.sh /app/cisco_mac_monitor.py
USER monitor
# Volúmenes para persistencia
VOLUME ["/config", "/logs", "/data"]
# Puertos expuestos (si fuera necesario para futuras extensiones)
EXPOSE 8080
# Variables de entorno por defecto
ENV CONFIG_FILE=/config/config.yaml
ENV LOG_LEVEL=INFO
ENV MONITORING_INTERVAL=5
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python3 /app/cisco_mac_monitor.py --config ${CONFIG_FILE} --single-run || exit 1
# Comando por defecto
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["continuous"]
+453
View File
@@ -0,0 +1,453 @@
# Cisco MAC Monitor para Zabbix LLD
## 📋 Descripción
**Cisco MAC Monitor** es una solución completa para monitorizar direcciones MAC conectadas a puertos de switches Cisco. Está diseñado específicamente para integrarse con **Zabbix Low Level Discovery (LLD)** y detectar cambios en dispositivos conectados a la red.
### Características principales:
- ✅ **Compatibilidad total** con modelos Cisco C2960, C3560, C3560E, C2960X, C3560CX y Catalyst series
- ✅ **Conexión SSH** segura a dispositivos de red
- ✅ **Formato JSON** optimizado para Zabbix LLD
- ✅ **Monitorización continua** configurable por intervalos
- ✅ **Contenedor Docker** basado en Rocky Linux 8
- ✅ **Detección automática** de cambios en tabla MAC
- ✅ **Filtros avanzados** para puertos y VLANs específicas
- ✅ **Logging completo** y manejo robusto de errores
---
## 🚀 Inicio Rápido
### 1. Preparar configuración
```bash
# Clonar o descargar los archivos
mkdir cisco-mac-monitor
cd cisco-mac-monitor
# Copiar archivos del proyecto
# - cisco_mac_monitor.py
# - config.yaml
# - Dockerfile
# - docker-compose.yml
# - requirements.txt
# - entrypoint.sh
```
### 2. Configurar dispositivos
Edita el archivo `config.yaml` con tus dispositivos:
```yaml
devices:
- hostname: "SW-PRINCIPAL-001"
ip: "192.168.1.10"
username: "admin"
password: "tu_password"
- hostname: "SW-SECUNDARIO-001"
ip: "192.168.1.11"
username: "admin"
password: "tu_password"
```
### 3. Construir y ejecutar
```bash
# Opción 1: Docker Compose (Recomendado)
docker-compose up --build -d
# Opción 2: Docker tradicional
docker build -t cisco-mac-monitor .
docker run -v $(pwd)/config:/config -v $(pwd)/data:/data cisco-mac-monitor
```
### 4. Verificar funcionamiento
```bash
# Ver logs en tiempo real
docker-compose logs -f
# Ejecutar una prueba única
docker-compose exec cisco-mac-monitor single-run
# Probar configuración
docker-compose exec cisco-mac-monitor test-config
```
---
## 📁 Estructura del Proyecto
```
cisco-mac-monitor/
├── cisco_mac_monitor.py # Script principal de Python
├── config.yaml # Configuración de dispositivos
├── requirements.txt # Dependencias de Python
├── Dockerfile # Imagen de contenedor
├── docker-compose.yml # Orquestación de contenedores
├── entrypoint.sh # Script de entrada del contenedor
├── example_output.json # Ejemplo de salida JSON
├── modelos.csv # Inventario de dispositivos (referencia)
└── README.md # Esta documentación
```
---
## ⚙️ Configuración Detallada
### Archivo `config.yaml`
```yaml
# Configuración de monitorización
monitoring:
interval_minutes: 5 # Intervalo entre monitoreos
output_file: "/data/cisco_mac_data.json" # Archivo de salida
log_level: "INFO" # Nivel de logging
log_file: "/logs/cisco_mac_monitor.log" # Archivo de log
# Dispositivos de red
devices:
- hostname: "SW-CORE-001"
ip: "192.168.1.5"
username: "admin"
password: "cisco123"
port: 22
timeout: 30
- hostname: "SW-ACCESS-001"
ip: "192.168.1.10"
username: "admin"
password: "cisco123"
# Filtros opcionales
filters:
exclude_ports:
- "Gi0/1" # Uplinks
- "Gi0/2"
exclude_vlans:
- "999" # VLAN de management
exclude_macs:
- "00:1a:2b:3c:4d:5e" # Servidores conocidos
# Configuración para Zabbix
zabbix:
custom_macros:
discovery_rule: "cisco.mac.discovery"
item_prefix: "cisco.mac"
```
### Variables de entorno del contenedor
| Variable | Default | Descripción |
|----------|---------|-------------|
| `CONFIG_FILE` | `/config/config.yaml` | Ruta al archivo de configuración |
| `LOG_LEVEL` | `INFO` | Nivel de logging (DEBUG, INFO, WARNING, ERROR) |
| `MONITORING_INTERVAL` | `5` | Intervalo en minutos (sobrescribe config.yaml) |
---
## 🐳 Uso con Docker
### Comandos básicos
```bash
# Monitorización continua (por defecto)
docker run -v $(pwd)/config:/config cisco-mac-monitor
# Ejecución única
docker run -v $(pwd)/config:/config cisco-mac-monitor single-run
# Probar configuración
docker run -v $(pwd)/config:/config cisco-mac-monitor test-config
# Probar conexiones SSH
docker run -v $(pwd)/config:/config cisco-mac-monitor test-ssh
# Shell interactivo para debugging
docker run -it -v $(pwd)/config:/config cisco-mac-monitor shell
```
### Volúmenes recomendados
```yaml
volumes:
- ./config:/config:ro # Configuración (solo lectura)
- ./logs:/logs # Logs persistentes
- ./data:/data # Datos de salida
```
---
## 📊 Integración con Zabbix
### Formato JSON de salida
El script genera JSON en el formato requerido por Zabbix LLD:
```json
{
"data": [
{
"{#DEVICE_HOSTNAME}": "SW-PB-001",
"{#PORT_NAME}": "Fa0/1",
"{#MAC_COUNT}": 2,
"{#VLANS}": "10,20",
"{#MAC_ADDRESSES}": "[{\"mac\": \"aa:bb:cc:dd:ee:01\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}]",
"{#LAST_UPDATE}": "2024-10-24T14:30:00.123456"
}
],
"timestamp": "2024-10-24T14:30:00.123456",
"total_devices": 3,
"total_ports": 15
}
```
### Configuración en Zabbix
1. **Crear Discovery Rule:**
- **Name:** `Cisco MAC Discovery`
- **Type:** `External check`
- **Key:** `cisco.mac.discovery`
- **Update interval:** `5m`
2. **Item prototypes:**
- `cisco.mac.count[{#DEVICE_HOSTNAME},{#PORT_NAME}]`
- `cisco.mac.list[{#DEVICE_HOSTNAME},{#PORT_NAME}]`
- `cisco.mac.vlans[{#DEVICE_HOSTNAME},{#PORT_NAME}]`
3. **Trigger prototypes:**
- MAC count changes
- New unauthorized MAC detected
- MAC disappeared from port
### Script de integración para Zabbix
```bash
#!/bin/bash
# /usr/lib/zabbix/externalscripts/cisco_mac_discovery.sh
docker exec cisco-mac-monitor single-run > /dev/null 2>&1
cat /data/cisco_mac_data.json
```
---
## 🔧 Compatibilidad de Dispositivos
### Modelos soportados (basado en `modelos.csv`):
| Modelo | IOS Version | Estado |
|--------|-------------|--------|
| C2960 | 12.2(35)SE5+ | ✅ Soportado |
| C2960X | 15.2(2)E3+ | ✅ Soportado |
| C2960S | 12.2(55)SE5+ | ✅ Soportado |
| C3560 | 12.2(25)SEB4+ | ✅ Soportado |
| C3560E | 12.2(55)SE3+ | ✅ Soportado |
| C3560CX | 15.2(4)E6+ | ✅ Soportado |
| Catalyst 3K | 16.6.5+ | ✅ Soportado |
### Comandos utilizados:
- `show mac address-table` - Obtener tabla MAC completa
- Compatible con IOS 12.x, 15.x y 16.x
---
## 🛠️ Desarrollo y Personalización
### Ejecutar en modo desarrollo
```bash
# Instalar dependencias
pip3 install -r requirements.txt
# Ejecutar directamente
python3 cisco_mac_monitor.py --config config.yaml --single-run
# Debug mode
python3 cisco_mac_monitor.py --config config.yaml --single-run --debug
```
### Estructura del código
```python
# Clases principales:
CiscoSSHConnector # Manejo de conexiones SSH
CiscoMacTableParser # Parsing de tabla MAC
ZabbixLLDFormatter # Formateo para Zabbix
CiscoMacMonitor # Orquestador principal
```
### Agregar nuevo modelo de dispositivo
1. Editar `CiscoMacTableParser._parse_mac_table_output()`
2. Agregar nuevo patrón regex para el formato de salida
3. Probar con `test-ssh` command
---
## 🔍 Troubleshooting
### Problemas comunes
**1. Error de conexión SSH:**
```bash
# Probar conectividad
docker-compose exec cisco-mac-monitor test-ssh
# Verificar credenciales en config.yaml
# Verificar conectividad de red del contenedor
```
**2. Formato JSON incorrecto:**
```bash
# Verificar salida
docker-compose exec cisco-mac-monitor single-run
cat data/cisco_mac_data.json | jq .
```
**3. No se detectan MACs:**
```bash
# Verificar comando en dispositivo
ssh admin@192.168.1.10
show mac address-table
# Verificar filtros en config.yaml
```
**4. Container no inicia:**
```bash
# Verificar logs
docker-compose logs cisco-mac-monitor
# Verificar configuración
docker-compose exec cisco-mac-monitor test-config
```
### Logs útiles
```bash
# Logs del contenedor
docker-compose logs -f cisco-mac-monitor
# Logs internos de la aplicación
docker-compose exec cisco-mac-monitor cat /logs/cisco_mac_monitor.log
# Debug completo
docker-compose exec cisco-mac-monitor tail -f /logs/cisco_mac_monitor.log
```
---
## 📈 Monitorización y Alertas
### Métricas disponibles
- **MAC Count por puerto:** Número de MACs conectadas
- **VLAN Distribution:** Distribución de VLANs por puerto
- **MAC Changes:** Cambios en tabla MAC
- **Device Connectivity:** Estado de conexión a dispositivos
### Alertas recomendadas
1. **Nuevo dispositivo conectado** (MAC no autorizada)
2. **Dispositivo desconectado** (MAC desaparecida)
3. **Puerto saturado** (demasiadas MACs)
4. **Error de conectividad** con switch
5. **Cambio en VLAN** de dispositivo conocido
---
## 🔒 Seguridad
### Mejores prácticas
1. **Credenciales:**
- Usar variables de entorno para passwords
- Configurar usuarios SSH específicos para monitorización
- Rotar credenciales regularmente
2. **Red:**
- Usar VLAN de management dedicada
- Configurar ACLs para acceso SSH
- Monitorizar accesos SSH en logs de switches
3. **Contenedor:**
- Ejecutar como usuario no privilegiado
- Montar volúmenes como solo lectura cuando sea posible
- Actualizar imagen base regularmente
### Configuración SSH en switches Cisco
```cisco
! Crear usuario para monitorización
username monitor privilege 1 password cisco123
! Configurar acceso SSH
line vty 0 15
login local
transport input ssh
! ACL para acceso (opcional)
access-list 99 permit 192.168.1.100
line vty 0 15
access-class 99 in
```
---
## 🤝 Contribución
### Cómo contribuir
1. Fork del repositorio
2. Crear branch para feature: `git checkout -b feature/nueva-funcionalidad`
3. Commit cambios: `git commit -am 'Agregar nueva funcionalidad'`
4. Push al branch: `git push origin feature/nueva-funcionalidad`
5. Crear Pull Request
### Reportar bugs
Incluir en el reporte:
- Versión del script
- Modelo y versión IOS del dispositivo
- Logs relevantes
- Configuración utilizada (sin credenciales)
---
## 📄 Licencia
Este proyecto está bajo licencia MIT. Ver archivo `LICENSE` para más detalles.
---
## 👥 Soporte
- **Documentación:** Este README
- **Logs:** `/logs/cisco_mac_monitor.log`
- **Ejemplos:** `example_output.json`
- **Issues:** GitHub Issues
---
## 🏷️ Versiones
### v1.0.0 (Actual)
- ✅ Soporte inicial para switches Cisco
- ✅ Integración con Zabbix LLD
- ✅ Contenedor Docker con Rocky Linux 8
- ✅ Monitorización continua configurable
- ✅ Sistema de filtros avanzado
### Roadmap v1.1.0
- 🔄 Soporte para SNMPv3
- 🔄 API REST para consultas
- 🔄 Dashboard web integrado
- 🔄 Soporte para múltiples vendors (HP, Juniper)
---
**Desarrollado para entornos de producción con switches Cisco** 🚀
+41
View File
@@ -0,0 +1,41 @@
# Archivos de la Versión Completa (OLD)
Esta carpeta contiene todos los archivos de la **versión completa original** del Cisco MAC Monitor, que manejaba múltiples dispositivos simultáneamente con configuración YAML.
## ⚠️ Versión Obsoleta
Estos archivos corresponden al prompt original que requería:
- Configuración YAML para múltiples dispositivos
- Sistema de configuración temporal
- Monitorización continua en un solo contenedor
- JSON grande con todos los dispositivos
## 📁 Archivos contenidos:
- **`cisco_mac_monitor.py`** - Script principal de la versión completa
- **`config.yaml`** - Archivo de configuración YAML con múltiples dispositivos
- **`Dockerfile`** - Imagen Docker completa con entrypoint
- **`docker-compose.yml`** - Compose para la versión completa
- **`entrypoint.sh`** - Script de entrada con múltiples modos
- **`example_output.json`** - Ejemplo de JSON con múltiples dispositivos
- **`README.md`** - Documentación completa de la versión original
- **`test_suite.sh`** - Suite de pruebas para la versión completa
- **`zabbix_integration.sh`** - Integración Zabbix para versión completa
- **`zabbix_template_examples.md`** - Plantillas detalladas de Zabbix
## 🔄 Versión Actual
La versión actual (en el directorio padre) es la **versión simplificada** que cumple con el prompt actualizado:
- Un dispositivo por ejecución
- Parámetros por línea de comandos
- JSON pequeño y manejable
- Contenedor ligero
## 🗂️ Si necesitas la versión completa
Puedes recuperar cualquier archivo de esta carpeta si en el futuro necesitas funcionalidades de la versión completa.
---
**Fecha de archivado:** 24 de octubre de 2024
**Motivo:** Cambio de requerimientos del prompt - Simplificación a un dispositivo por ejecución
+423
View File
@@ -0,0 +1,423 @@
#!/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()
+93
View File
@@ -0,0 +1,93 @@
# Configuración para Cisco MAC Monitor
# ===================================
# Configuración de monitorización
monitoring:
# Intervalo de monitorización en minutos
interval_minutes: 5
# Archivo donde se guardarán los datos JSON para Zabbix
output_file: "/tmp/cisco_mac_data.json"
# Nivel de logging (DEBUG, INFO, WARNING, ERROR)
log_level: "INFO"
# Archivo de log
log_file: "/tmp/cisco_mac_monitor.log"
# Configuración de dispositivos de red
devices:
# Switch Cisco Catalyst 2960 - Planta Baja
- hostname: "SW-PB-001"
ip: "192.168.1.10"
username: "admin"
password: "cisco123"
port: 22
device_type: "cisco_ios"
timeout: 30
# Switch Cisco Catalyst 2960 - Primera Planta
- hostname: "SW-P1-001"
ip: "192.168.1.11"
username: "admin"
password: "cisco123"
port: 22
device_type: "cisco_ios"
timeout: 30
# Switch Cisco Catalyst 3560 - Core
- hostname: "SW-CORE-001"
ip: "192.168.1.5"
username: "admin"
password: "cisco123"
port: 22
device_type: "cisco_ios"
timeout: 30
# Switch Cisco Catalyst 2960X - DMZ
- hostname: "SW-DMZ-001"
ip: "192.168.100.10"
username: "admin"
password: "cisco123"
port: 22
device_type: "cisco_ios"
timeout: 30
# Configuración de filtros (opcional)
filters:
# Excluir puertos específicos del monitoreo
exclude_ports:
- "Gi0/1" # Uplink al core
- "Gi0/2" # Uplink redundante
# Excluir VLANs específicas
exclude_vlans:
- "999" # VLAN de management
# Excluir MACs específicas (ejemplo: servidores conocidos)
exclude_macs:
- "00:1a:2b:3c:4d:5e" # Servidor principal
# Configuración específica para Zabbix
zabbix:
# Macros personalizadas para LLD
custom_macros:
discovery_rule: "cisco.mac.discovery"
item_prefix: "cisco.mac"
# Configuración de metadatos adicionales
metadata:
include_device_info: true
include_vlan_info: true
include_port_status: true
# Configuración de alertas (para futuro uso)
alerts:
# Habilitar detección de cambios en MACs
detect_mac_changes: true
# Habilitar detección de MACs no autorizadas
detect_unauthorized_macs: true
# Archivo con MACs autorizadas (opcional)
authorized_macs_file: "/config/authorized_macs.txt"
+79
View File
@@ -0,0 +1,79 @@
# Docker Compose para Cisco MAC Monitor
# ====================================
version: '3.8'
services:
cisco-mac-monitor:
build:
context: .
dockerfile: Dockerfile
image: cisco-mac-monitor:latest
container_name: cisco-mac-monitor
# Reiniciar automáticamente
restart: unless-stopped
# Variables de entorno
environment:
- CONFIG_FILE=/config/config.yaml
- LOG_LEVEL=INFO
- MONITORING_INTERVAL=5
- TZ=Europe/Madrid
# Volúmenes para persistencia
volumes:
# Configuración (REQUERIDO)
- ./config:/config:ro
# Logs (opcional)
- ./logs:/logs
# Datos de salida (opcional)
- ./data:/data
# Zona horaria del host
- /etc/localtime:/etc/localtime:ro
# Red del host para acceso a dispositivos locales
network_mode: host
# Límites de recursos
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
# Healthcheck
healthcheck:
test: ["CMD", "python3", "/app/cisco_mac_monitor.py", "--config", "/config/config.yaml", "--single-run"]
interval: 5m
timeout: 30s
retries: 3
start_period: 30s
# Logging del contenedor
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Redes (opcional si no usas host)
networks:
cisco-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# Volúmenes con nombre (alternativo)
volumes:
cisco-logs:
driver: local
cisco-data:
driver: local
+302
View File
@@ -0,0 +1,302 @@
#!/bin/bash
# Entrypoint script para Cisco MAC Monitor
# ========================================
set -e
# Colores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Función para logging
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1" >&2
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
}
success() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] SUCCESS:${NC} $1"
}
# Verificar archivo de configuración
check_config() {
if [[ ! -f "${CONFIG_FILE}" ]]; then
error "Archivo de configuración no encontrado: ${CONFIG_FILE}"
error "Asegúrese de montar el volumen /config con config.yaml"
exit 1
fi
log "Usando archivo de configuración: ${CONFIG_FILE}"
}
# Crear directorios necesarios
setup_directories() {
log "Configurando directorios..."
# Crear directorios si no existen
mkdir -p /logs /data /tmp
# Verificar permisos
if [[ ! -w /logs ]]; then
warn "Sin permisos de escritura en /logs, usando /tmp"
export LOG_DIR=/tmp
else
export LOG_DIR=/logs
fi
if [[ ! -w /data ]]; then
warn "Sin permisos de escritura en /data, usando /tmp"
export DATA_DIR=/tmp
else
export DATA_DIR=/data
fi
success "Directorios configurados correctamente"
}
# Validar conectividad de red
test_network() {
log "Verificando conectividad de red..."
# Verificar si podemos resolver DNS
if ! nslookup google.com > /dev/null 2>&1; then
warn "No se puede resolver DNS externo, verificando conectividad local..."
fi
# Extraer IPs de dispositivos del config para test básico
if command -v python3 > /dev/null 2>&1 && [[ -f "${CONFIG_FILE}" ]]; then
python3 -c "
import yaml
try:
with open('${CONFIG_FILE}', 'r') as f:
config = yaml.safe_load(f)
for device in config.get('devices', []):
ip = device.get('ip')
if ip:
print(f'Testing connectivity to {ip}...')
import subprocess
result = subprocess.run(['ping', '-c', '1', '-W', '3', ip],
capture_output=True, timeout=5)
if result.returncode == 0:
print(f'✓ {ip} reachable')
else:
print(f'✗ {ip} not reachable')
except Exception as e:
print(f'Error testing network connectivity: {e}')
"
fi
}
# Función para mostrar ayuda
show_help() {
cat << EOF
Cisco MAC Monitor - Contenedor Docker
USAGE:
docker run cisco-mac-monitor [COMMAND] [OPTIONS]
COMMANDS:
continuous Ejecutar monitorización continua (por defecto)
single-run Ejecutar una sola vez y salir
test-config Validar archivo de configuración
test-ssh Probar conexiones SSH a dispositivos
shell Abrir shell interactivo
help Mostrar esta ayuda
VARIABLES DE ENTORNO:
CONFIG_FILE Ruta al archivo de configuración (default: /config/config.yaml)
LOG_LEVEL Nivel de logging (default: INFO)
MONITORING_INTERVAL Intervalo de monitorización en minutos (default: 5)
VOLÚMENES:
/config Directorio de configuración (contiene config.yaml)
/logs Directorio de logs
/data Directorio de datos de salida
EJEMPLOS:
# Ejecutar monitorización continua
docker run -v /host/config:/config cisco-mac-monitor
# Ejecutar una sola vez
docker run -v /host/config:/config cisco-mac-monitor single-run
# Probar configuración
docker run -v /host/config:/config cisco-mac-monitor test-config
EOF
}
# Función para probar configuración
test_config() {
log "Validando configuración..."
if ! python3 -c "
import yaml
import sys
try:
with open('${CONFIG_FILE}', 'r') as f:
config = yaml.safe_load(f)
# Validar estructura básica
if 'devices' not in config:
print('ERROR: Sección devices no encontrada')
sys.exit(1)
if 'monitoring' not in config:
print('ERROR: Sección monitoring no encontrada')
sys.exit(1)
# Validar dispositivos
devices = config['devices']
if not devices:
print('ERROR: No hay dispositivos configurados')
sys.exit(1)
print(f'✓ Configuración válida: {len(devices)} dispositivos')
for i, device in enumerate(devices):
hostname = device.get('hostname', f'device-{i}')
ip = device.get('ip')
username = device.get('username')
if not ip:
print(f'ERROR: Device {hostname} sin IP')
sys.exit(1)
if not username:
print(f'ERROR: Device {hostname} sin username')
sys.exit(1)
print(f' - {hostname} ({ip})')
except Exception as e:
print(f'ERROR: {e}')
sys.exit(1)
"; then
error "Configuración inválida"
exit 1
fi
success "Configuración válida"
}
# Función para probar conexiones SSH
test_ssh() {
log "Probando conexiones SSH a dispositivos..."
python3 << EOF
import yaml
import paramiko
import sys
try:
with open('${CONFIG_FILE}', 'r') as f:
config = yaml.safe_load(f)
devices = config.get('devices', [])
success_count = 0
for device in devices:
hostname = device.get('hostname', 'unknown')
ip = device.get('ip')
username = device.get('username')
password = device.get('password')
port = device.get('port', 22)
print(f'Probando {hostname} ({ip})...', end=' ')
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname=ip,
port=port,
username=username,
password=password,
timeout=10,
look_for_keys=False,
allow_agent=False
)
ssh.close()
print('✓ CONECTADO')
success_count += 1
except Exception as e:
print(f'✗ ERROR: {e}')
print(f'\\nResultado: {success_count}/{len(devices)} dispositivos conectados')
if success_count == 0:
sys.exit(1)
except Exception as e:
print(f'ERROR: {e}')
sys.exit(1)
EOF
}
# Función principal
main() {
log "Iniciando Cisco MAC Monitor..."
# Configurar directorios
setup_directories
# Verificar configuración
check_config
# Procesar comando
COMMAND=${1:-continuous}
case "${COMMAND}" in
"continuous")
log "Modo: Monitorización continua"
test_config
warn "Iniciando monitorización continua - Presiona Ctrl+C para detener"
exec python3 /app/cisco_mac_monitor.py --config "${CONFIG_FILE}"
;;
"single-run")
log "Modo: Ejecución única"
test_config
exec python3 /app/cisco_mac_monitor.py --config "${CONFIG_FILE}" --single-run --output "${DATA_DIR}/cisco_mac_data.json"
;;
"test-config")
test_config
success "Configuración OK"
;;
"test-ssh")
test_config
test_ssh
;;
"test-network")
test_network
;;
"shell")
log "Abriendo shell interactivo..."
exec /bin/bash
;;
"help"|"--help"|"-h")
show_help
;;
*)
error "Comando desconocido: ${COMMAND}"
show_help
exit 1
;;
esac
}
# Manejo de señales
trap 'log "Recibida señal de terminación, cerrando..."; exit 0' SIGTERM SIGINT
# Ejecutar función principal
main "$@"
+39
View File
@@ -0,0 +1,39 @@
{
"data": [
{
"{#DEVICE_HOSTNAME}": "SW-PB-001",
"{#PORT_NAME}": "Fa0/1",
"{#MAC_COUNT}": 2,
"{#VLANS}": "10,20",
"{#MAC_ADDRESSES}": "[{\"mac\": \"aa:bb:cc:dd:ee:01\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}, {\"mac\": \"aa:bb:cc:dd:ee:02\", \"vlan\": \"20\", \"type\": \"DYNAMIC\"}]",
"{#LAST_UPDATE}": "2024-10-24T14:30:00.123456"
},
{
"{#DEVICE_HOSTNAME}": "SW-PB-001",
"{#PORT_NAME}": "Fa0/2",
"{#MAC_COUNT}": 1,
"{#VLANS}": "10",
"{#MAC_ADDRESSES}": "[{\"mac\": \"bb:cc:dd:ee:ff:01\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}]",
"{#LAST_UPDATE}": "2024-10-24T14:30:00.123456"
},
{
"{#DEVICE_HOSTNAME}": "SW-P1-001",
"{#PORT_NAME}": "Gi0/1",
"{#MAC_COUNT}": 3,
"{#VLANS}": "10,30",
"{#MAC_ADDRESSES}": "[{\"mac\": \"cc:dd:ee:ff:00:01\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}, {\"mac\": \"cc:dd:ee:ff:00:02\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}, {\"mac\": \"cc:dd:ee:ff:00:03\", \"vlan\": \"30\", \"type\": \"DYNAMIC\"}]",
"{#LAST_UPDATE}": "2024-10-24T14:30:00.123456"
},
{
"{#DEVICE_HOSTNAME}": "SW-CORE-001",
"{#PORT_NAME}": "Gi0/1",
"{#MAC_COUNT}": 5,
"{#VLANS}": "10,20,30",
"{#MAC_ADDRESSES}": "[{\"mac\": \"dd:ee:ff:00:11:01\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}, {\"mac\": \"dd:ee:ff:00:11:02\", \"vlan\": \"20\", \"type\": \"DYNAMIC\"}, {\"mac\": \"dd:ee:ff:00:11:03\", \"vlan\": \"30\", \"type\": \"DYNAMIC\"}, {\"mac\": \"dd:ee:ff:00:11:04\", \"vlan\": \"10\", \"type\": \"DYNAMIC\"}, {\"mac\": \"dd:ee:ff:00:11:05\", \"vlan\": \"20\", \"type\": \"DYNAMIC\"}]",
"{#LAST_UPDATE}": "2024-10-24T14:30:00.123456"
}
],
"timestamp": "2024-10-24T14:30:00.123456",
"total_devices": 3,
"total_ports": 4
}
+348
View File
@@ -0,0 +1,348 @@
#!/bin/bash
# Script de prueba para Cisco MAC Monitor
# ======================================
set -e
# Colores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Contadores
TESTS_PASSED=0
TESTS_FAILED=0
# Función para logging
log() {
echo -e "${BLUE}[TEST]${NC} $1"
}
success() {
echo -e "${GREEN}[PASS]${NC} $1"
((TESTS_PASSED++))
}
error() {
echo -e "${RED}[FAIL]${NC} $1"
((TESTS_FAILED++))
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
# Test 1: Verificar archivos necesarios
test_files() {
log "Verificando archivos necesarios..."
local files=(
"cisco_mac_monitor.py"
"config.yaml"
"requirements.txt"
"Dockerfile"
"docker-compose.yml"
"entrypoint.sh"
"README.md"
)
for file in "${files[@]}"; do
if [[ -f "$file" ]]; then
success "Archivo encontrado: $file"
else
error "Archivo faltante: $file"
fi
done
}
# Test 2: Validar sintaxis Python
test_python_syntax() {
log "Validando sintaxis de Python..."
if command -v python3 >/dev/null 2>&1; then
if python3 -m py_compile cisco_mac_monitor.py; then
success "Sintaxis de Python válida"
else
error "Error de sintaxis en Python"
fi
else
warn "Python3 no disponible para test de sintaxis"
fi
}
# Test 3: Validar archivo YAML
test_yaml_syntax() {
log "Validando sintaxis de YAML..."
if command -v python3 >/dev/null 2>&1; then
if python3 -c "
import yaml
try:
with open('config.yaml', 'r') as f:
yaml.safe_load(f)
print('YAML syntax valid')
except Exception as e:
print(f'YAML error: {e}')
exit(1)
"; then
success "Sintaxis de YAML válida"
else
error "Error de sintaxis en YAML"
fi
else
warn "Python3 no disponible para test de YAML"
fi
}
# Test 4: Validar Dockerfile
test_dockerfile() {
log "Validando Dockerfile..."
if command -v docker >/dev/null 2>&1; then
if docker build -t cisco-mac-monitor-test . >/dev/null 2>&1; then
success "Dockerfile válido - imagen construida"
# Limpiar imagen de test
docker rmi cisco-mac-monitor-test >/dev/null 2>&1 || true
else
error "Error en construcción de Dockerfile"
fi
else
warn "Docker no disponible para test de Dockerfile"
fi
}
# Test 5: Validar docker-compose
test_docker_compose() {
log "Validando docker-compose.yml..."
if command -v docker-compose >/dev/null 2>&1; then
if docker-compose config >/dev/null 2>&1; then
success "docker-compose.yml válido"
else
error "Error en docker-compose.yml"
fi
else
warn "docker-compose no disponible para test"
fi
}
# Test 6: Verificar dependencias Python
test_python_deps() {
log "Verificando dependencias de Python..."
if command -v pip3 >/dev/null 2>&1; then
# Crear entorno virtual temporal
local temp_venv=$(mktemp -d)
python3 -m venv "$temp_venv" >/dev/null 2>&1
if source "$temp_venv/bin/activate" && pip install -r requirements.txt >/dev/null 2>&1; then
success "Dependencias de Python instalables"
else
error "Error instalando dependencias de Python"
fi
# Limpiar entorno temporal
rm -rf "$temp_venv"
deactivate 2>/dev/null || true
else
warn "pip3 no disponible para test de dependencias"
fi
}
# Test 7: Validar permisos de entrypoint
test_entrypoint_permissions() {
log "Verificando permisos de entrypoint.sh..."
if [[ -x "entrypoint.sh" ]]; then
success "entrypoint.sh tiene permisos de ejecución"
else
warn "entrypoint.sh no tiene permisos de ejecución"
chmod +x entrypoint.sh
success "Permisos de ejecución agregados a entrypoint.sh"
fi
}
# Test 8: Validar estructura de configuración
test_config_structure() {
log "Validando estructura de configuración..."
if command -v python3 >/dev/null 2>&1; then
if python3 -c "
import yaml
try:
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
# Verificar secciones requeridas
required_sections = ['devices', 'monitoring']
for section in required_sections:
if section not in config:
print(f'Missing section: {section}')
exit(1)
# Verificar estructura de devices
devices = config['devices']
if not isinstance(devices, list) or len(devices) == 0:
print('Devices section must be a non-empty list')
exit(1)
# Verificar campos requeridos en devices
required_device_fields = ['hostname', 'ip', 'username', 'password']
for i, device in enumerate(devices):
for field in required_device_fields:
if field not in device:
print(f'Device {i} missing field: {field}')
exit(1)
print('Configuration structure valid')
except Exception as e:
print(f'Configuration error: {e}')
exit(1)
"; then
success "Estructura de configuración válida"
else
error "Error en estructura de configuración"
fi
else
warn "Python3 no disponible para test de configuración"
fi
}
# Test 9: Validar JSON de ejemplo
test_example_json() {
log "Validando JSON de ejemplo..."
if [[ -f "example_output.json" ]]; then
if command -v python3 >/dev/null 2>&1; then
if python3 -m json.tool example_output.json >/dev/null 2>&1; then
success "JSON de ejemplo válido"
else
error "JSON de ejemplo inválido"
fi
else
warn "Python3 no disponible para validación de JSON"
fi
else
warn "Archivo example_output.json no encontrado"
fi
}
# Test 10: Verificar documentación
test_documentation() {
log "Verificando documentación..."
if [[ -f "README.md" ]]; then
local readme_size=$(wc -c < README.md)
if [[ $readme_size -gt 1000 ]]; then
success "README.md presente y completo"
else
warn "README.md muy pequeño (${readme_size} bytes)"
fi
else
error "README.md faltante"
fi
if [[ -f "zabbix_template_examples.md" ]]; then
success "Documentación de Zabbix presente"
else
warn "Documentación de Zabbix faltante"
fi
}
# Test de integración básica
test_integration() {
log "Ejecutando test de integración básica..."
if command -v docker >/dev/null 2>&1; then
# Construir imagen
if docker build -t cisco-mac-monitor-test . >/dev/null 2>&1; then
success "Imagen Docker construida correctamente"
# Test de configuración en contenedor
if docker run --rm cisco-mac-monitor-test test-config >/dev/null 2>&1; then
success "Test de configuración en contenedor exitoso"
else
error "Test de configuración en contenedor falló"
fi
# Limpiar
docker rmi cisco-mac-monitor-test >/dev/null 2>&1 || true
else
error "Error construyendo imagen para test de integración"
fi
else
warn "Docker no disponible para test de integración"
fi
}
# Función principal
main() {
echo "=========================================="
echo " Cisco MAC Monitor - Test Suite"
echo "=========================================="
echo
# Verificar que estamos en el directorio correcto
if [[ ! -f "cisco_mac_monitor.py" ]]; then
error "No se encontró cisco_mac_monitor.py. ¿Estás en el directorio correcto?"
exit 1
fi
# Ejecutar tests
test_files
test_python_syntax
test_yaml_syntax
test_config_structure
test_entrypoint_permissions
test_example_json
test_documentation
# Tests que requieren herramientas externas
if command -v docker >/dev/null 2>&1; then
test_dockerfile
test_integration
else
warn "Docker no disponible - saltando tests de Docker"
fi
if command -v docker-compose >/dev/null 2>&1; then
test_docker_compose
else
warn "docker-compose no disponible - saltando test"
fi
if command -v pip3 >/dev/null 2>&1; then
test_python_deps
else
warn "pip3 no disponible - saltando test de dependencias"
fi
# Resumen
echo
echo "=========================================="
echo " Resumen de Tests"
echo "=========================================="
echo -e "${GREEN}Tests exitosos: $TESTS_PASSED${NC}"
echo -e "${RED}Tests fallidos: $TESTS_FAILED${NC}"
echo
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}🎉 Todos los tests pasaron exitosamente!${NC}"
echo
echo "El proyecto está listo para usar:"
echo "1. Edita config.yaml con tus dispositivos"
echo "2. Ejecuta: docker-compose up --build -d"
echo "3. Verifica logs: docker-compose logs -f"
exit 0
else
echo -e "${RED}❌ Algunos tests fallaron. Revisa los errores arriba.${NC}"
exit 1
fi
}
# Ejecutar tests
main "$@"
+132
View File
@@ -0,0 +1,132 @@
#!/bin/bash
# Script de integración para Zabbix External Scripts
# ==================================================
#
# Este script debe colocarse en:
# /usr/lib/zabbix/externalscripts/cisco_mac_discovery.sh
#
# Y configurar permisos:
# chmod +x /usr/lib/zabbix/externalscripts/cisco_mac_discovery.sh
# chown zabbix:zabbix /usr/lib/zabbix/externalscripts/cisco_mac_discovery.sh
# Configuración
CONTAINER_NAME="cisco-mac-monitor"
DATA_FILE="/data/cisco_mac_data.json"
TIMEOUT=60
# Función de logging
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >&2
}
error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2
exit 1
}
# Verificar que el contenedor existe y está corriendo
check_container() {
if ! docker ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
error "Contenedor ${CONTAINER_NAME} no está corriendo"
fi
}
# Ejecutar discovery y obtener datos
run_discovery() {
log "Ejecutando discovery en contenedor ${CONTAINER_NAME}"
# Ejecutar single-run en el contenedor
if ! timeout ${TIMEOUT} docker exec ${CONTAINER_NAME} single-run >/dev/null 2>&1; then
error "Falló la ejecución de discovery (timeout: ${TIMEOUT}s)"
fi
# Verificar que el archivo de datos existe
if ! docker exec ${CONTAINER_NAME} test -f ${DATA_FILE}; then
error "Archivo de datos no encontrado: ${DATA_FILE}"
fi
# Obtener y validar JSON
local json_data
json_data=$(docker exec ${CONTAINER_NAME} cat ${DATA_FILE})
if [[ -z "${json_data}" ]]; then
error "Archivo de datos está vacío"
fi
# Validar formato JSON básico
if ! echo "${json_data}" | python3 -m json.tool >/dev/null 2>&1; then
error "Datos no están en formato JSON válido"
fi
# Retornar datos
echo "${json_data}"
}
# Función principal
main() {
case "${1:-discovery}" in
"discovery"|"")
check_container
run_discovery
;;
"status")
if docker ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
echo "Container ${CONTAINER_NAME} is running"
exit 0
else
echo "Container ${CONTAINER_NAME} is not running"
exit 1
fi
;;
"logs")
docker logs --tail 50 ${CONTAINER_NAME}
;;
"help"|"-h"|"--help")
cat << EOF
Cisco MAC Discovery - Zabbix Integration Script
USAGE:
$0 [COMMAND]
COMMANDS:
discovery Ejecutar discovery y retornar JSON (default)
status Verificar estado del contenedor
logs Mostrar logs recientes del contenedor
help Mostrar esta ayuda
EJEMPLOS:
# Usar en Zabbix Discovery Rule
cisco_mac_discovery.sh
# Verificar estado
cisco_mac_discovery.sh status
# Ver logs para troubleshooting
cisco_mac_discovery.sh logs
CONFIGURACIÓN ZABBIX:
Discovery Rule:
- Name: Cisco MAC Discovery
- Type: External check
- Key: cisco_mac_discovery.sh
- Update interval: 5m
EOF
;;
*)
error "Comando desconocido: ${1}. Usa 'help' para ver opciones disponibles."
;;
esac
}
# Verificar dependencias
if ! command -v docker >/dev/null 2>&1; then
error "Docker no está instalado o no está en PATH"
fi
if ! command -v python3 >/dev/null 2>&1; then
error "Python3 no está instalado o no está en PATH"
fi
# Ejecutar función principal
main "$@"
+308
View File
@@ -0,0 +1,308 @@
# Plantilla de Zabbix para Cisco MAC Monitor
# ==========================================
## Discovery Rule
```xml
<discovery_rule>
<name>Cisco MAC Discovery</name>
<type>EXTERNAL</type>
<key>cisco_mac_discovery.sh</key>
<delay>5m</delay>
<description>Descubre puertos de switches Cisco con direcciones MAC conectadas</description>
<item_prototypes>
<item_prototype>
<name>MAC Count on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<type>CALCULATED</type>
<key>cisco.mac.count[{#DEVICE_HOSTNAME},{#PORT_NAME}]</key>
<delay>0</delay>
<value_type>UNSIGNED</value_type>
<params>{#MAC_COUNT}</params>
<description>Número de direcciones MAC en el puerto</description>
</item_prototype>
<item_prototype>
<name>MAC Addresses on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<type>CALCULATED</type>
<key>cisco.mac.addresses[{#DEVICE_HOSTNAME},{#PORT_NAME}]</key>
<delay>0</delay>
<value_type>TEXT</value_type>
<params>{#MAC_ADDRESSES}</params>
<description>Lista de direcciones MAC en formato JSON</description>
</item_prototype>
<item_prototype>
<name>VLANs on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<type>CALCULATED</type>
<key>cisco.mac.vlans[{#DEVICE_HOSTNAME},{#PORT_NAME}]</key>
<delay>0</delay>
<value_type>TEXT</value_type>
<params>{#VLANS}</params>
<description>VLANs presentes en el puerto</description>
</item_prototype>
<item_prototype>
<name>Last Update {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<type>CALCULATED</type>
<key>cisco.mac.lastupdate[{#DEVICE_HOSTNAME},{#PORT_NAME}]</key>
<delay>0</delay>
<value_type>TEXT</value_type>
<params>{#LAST_UPDATE}</params>
<description>Timestamp de última actualización</description>
</item_prototype>
</item_prototypes>
<trigger_prototypes>
<trigger_prototype>
<expression>{Template Cisco MAC:cisco.mac.count[{#DEVICE_HOSTNAME},{#PORT_NAME}].change()}&gt;0</expression>
<name>MAC count changed on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<priority>INFO</priority>
<description>El número de direcciones MAC en el puerto ha cambiado</description>
</trigger_prototype>
<trigger_prototype>
<expression>{Template Cisco MAC:cisco.mac.count[{#DEVICE_HOSTNAME},{#PORT_NAME}].last()}&gt;5</expression>
<name>Too many MACs on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<priority>WARNING</priority>
<description>Demasiadas direcciones MAC en un solo puerto (posible loop o hub)</description>
</trigger_prototype>
<trigger_prototype>
<expression>{Template Cisco MAC:cisco.mac.addresses[{#DEVICE_HOSTNAME},{#PORT_NAME}].change()}&gt;0</expression>
<name>MAC addresses changed on {#DEVICE_HOSTNAME} port {#PORT_NAME}</name>
<priority>INFO</priority>
<description>Las direcciones MAC en el puerto han cambiado</description>
</trigger_prototype>
</trigger_prototypes>
<graph_prototypes>
<graph_prototype>
<name>MAC Count - {#DEVICE_HOSTNAME} {#PORT_NAME}</name>
<graph_items>
<graph_item>
<item>cisco.mac.count[{#DEVICE_HOSTNAME},{#PORT_NAME}]</item>
</graph_item>
</graph_items>
</graph_prototype>
</graph_prototypes>
</discovery_rule>
```
## Items Adicionales (nivel de host)
```xml
<!-- Item para estadísticas generales -->
<item>
<name>Cisco MAC Monitor - Total Devices</name>
<type>DEPENDENT</type>
<key>cisco.mac.total_devices</key>
<master_item>cisco_mac_discovery.sh</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$.total_devices</params>
</step>
</preprocessing>
<description>Número total de dispositivos monitorizados</description>
</item>
<item>
<name>Cisco MAC Monitor - Total Ports</name>
<type>DEPENDENT</type>
<key>cisco.mac.total_ports</key>
<master_item>cisco_mac_discovery.sh</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$.total_ports</params>
</step>
</preprocessing>
<description>Número total de puertos con MACs</description>
</item>
<item>
<name>Cisco MAC Monitor - Last Timestamp</name>
<type>DEPENDENT</type>
<key>cisco.mac.timestamp</key>
<master_item>cisco_mac_discovery.sh</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$.timestamp</params>
</step>
</preprocessing>
<description>Timestamp de la última ejecución</description>
</item>
```
## Triggers de Host
```xml
<trigger>
<expression>{Template Cisco MAC:cisco.mac.timestamp.nodata(15m)}&gt;0</expression>
<name>Cisco MAC Monitor - No data received</name>
<priority>WARNING</priority>
<description>No se han recibido datos del monitor MAC en los últimos 15 minutos</description>
</trigger>
<trigger>
<expression>{Template Cisco MAC:cisco.mac.total_devices.last()}&lt;1</expression>
<name>Cisco MAC Monitor - No devices monitored</name>
<priority>HIGH</priority>
<description>No hay dispositivos siendo monitorizados</description>
</trigger>
```
## Macros de Template
```xml
<macros>
<macro>
<name>{$CISCO.MAC.MAX_PER_PORT}</name>
<value>5</value>
<description>Máximo número de MACs permitidas por puerto</description>
</macro>
<macro>
<name>{$CISCO.MAC.DATA_TIMEOUT}</name>
<value>15m</value>
<description>Timeout para considerar datos obsoletos</description>
</macro>
<macro>
<name>{$CISCO.MAC.DISCOVERY_INTERVAL}</name>
<value>5m</value>
<description>Intervalo de discovery</description>
</macro>
</macros>
```
## Scripts de Actions
### Script para notificaciones con detalles de MACs
```bash
#!/bin/bash
# Script para enviar detalles de cambios MAC
DEVICE="{ALERT.SUBJECT}"
PORT="{ITEM.KEY1}"
MAC_DATA="{ITEM.VALUE}"
# Formatear mensaje
MESSAGE="
Cambio detectado en tabla MAC:
Dispositivo: ${DEVICE}
Puerto: ${PORT}
Timestamp: $(date)
Direcciones MAC actuales:
${MAC_DATA}
"
# Enviar notificación (ejemplo con email)
echo "${MESSAGE}" | mail -s "Alerta MAC - ${DEVICE}" admin@empresa.com
```
### Script para análisis de MACs no autorizadas
```python
#!/usr/bin/env python3
import json
import sys
# Cargar lista de MACs autorizadas
authorized_macs = {
"aa:bb:cc:dd:ee:01": "Servidor Principal",
"aa:bb:cc:dd:ee:02": "Workstation Admin",
# ... más MACs autorizadas
}
# Obtener datos del item
mac_data = sys.argv[1] if len(sys.argv) > 1 else "{}"
try:
macs = json.loads(mac_data)
unauthorized = []
for mac_entry in macs:
mac = mac_entry.get('mac')
if mac and mac not in authorized_macs:
unauthorized.append({
'mac': mac,
'vlan': mac_entry.get('vlan'),
'type': mac_entry.get('type')
})
if unauthorized:
print(f"MACs no autorizadas detectadas: {len(unauthorized)}")
for mac in unauthorized:
print(f" - {mac['mac']} (VLAN {mac['vlan']})")
else:
print("Todas las MACs están autorizadas")
except Exception as e:
print(f"Error procesando datos MAC: {e}")
```
## Configuración de Web Scenarios
```xml
<web_scenario>
<name>Cisco MAC Monitor Container Health</name>
<delay>5m</delay>
<steps>
<step>
<name>Check Container Status</name>
<url>http://localhost:8080/health</url>
<status_codes>200</status_codes>
<required>OK</required>
</step>
</steps>
</web_scenario>
```
## Dashboard Widget
```json
{
"type": "graph",
"name": "Cisco MAC Activity",
"fields": {
"source_type": "1",
"itemid": {
"cisco.mac.total_ports": "Total Ports with MACs"
}
}
}
```
## Media Types para Alertas
### Webhook para Slack
```json
{
"name": "Cisco MAC Slack Alert",
"type": "webhook",
"parameters": {
"url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"channel": "#network-alerts",
"message": "🚨 Cisco MAC Alert\nDevice: {ALERT.SUBJECT}\nMessage: {ALERT.MESSAGE}\nTime: {DATE} {TIME}"
}
}
```
### Webhook para Teams
```json
{
"name": "Cisco MAC Teams Alert",
"type": "webhook",
"parameters": {
"url": "https://outlook.office.com/webhook/YOUR/TEAMS/WEBHOOK",
"title": "Cisco MAC Monitor Alert",
"text": "Device: {ALERT.SUBJECT}\\nStatus: {TRIGGER.STATUS}\\nMessage: {ALERT.MESSAGE}"
}
}
```