first commit
This commit is contained in:
@@ -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
@@ -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** 🚀
|
||||
@@ -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
|
||||
Executable
+423
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Executable
+302
@@ -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 "$@"
|
||||
@@ -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
|
||||
}
|
||||
Executable
+348
@@ -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 "$@"
|
||||
Executable
+132
@@ -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 "$@"
|
||||
@@ -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()}>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()}>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()}>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)}>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()}<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}"
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user