mirror of
https://github.com/grandir66/UnifiMultimirror.git
synced 2026-10-02 15:24:42 +02:00
Initial commit: UnifiMultimirror port mirroring tool
- TUI application (Textual) for interactive port selection - CLI application (Rich) as alternative interface - SSH manager for device connection - Config parser for running-config, interface status, port-channel - Script generator for Bash, Python, and command files - Support for multiple source ports and LAG interfaces Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
7bd3f755be
9 files changed
+3888
No files matched your search
@@ -0,0 +1,466 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ubiquiti Port Mirroring Manager
|
||||
|
||||
A tool to configure port mirroring on Ubiquiti UniFi/EdgeSwitch devices via SSH.
|
||||
Provides an interactive menu interface for viewing port status, configuring
|
||||
mirroring, and generating reusable configuration scripts.
|
||||
|
||||
Usage:
|
||||
python main.py
|
||||
|
||||
Requirements:
|
||||
pip install paramiko rich cryptography
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from rich import box
|
||||
|
||||
from ssh_manager import SSHManager, setup_config_interactive
|
||||
from config_parser import ConfigParser
|
||||
from ui import PortMirroringUI
|
||||
from script_generator import ScriptGenerator
|
||||
|
||||
|
||||
class PortMirroringManager:
|
||||
"""Main application controller."""
|
||||
|
||||
def __init__(self):
|
||||
self.console = Console()
|
||||
self.ssh_manager = SSHManager()
|
||||
self.parser = ConfigParser()
|
||||
self.ui = PortMirroringUI()
|
||||
self.script_gen = ScriptGenerator()
|
||||
|
||||
self.is_connected = False
|
||||
self.config_loaded = False
|
||||
|
||||
# Pending changes (not yet applied)
|
||||
self.pending_destination: Optional[str] = None
|
||||
self.pending_sources: List[str] = []
|
||||
self.has_pending_changes = False
|
||||
|
||||
# =========================================================================
|
||||
# Initialization
|
||||
# =========================================================================
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""Initialize the application - always ask for IP and password."""
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
"[bold]UBIQUITI PORT MIRRORING MANAGER[/bold]\n\n"
|
||||
"Gestione port mirroring per switch UniFi/EdgeSwitch",
|
||||
style="bold white on blue",
|
||||
box=box.DOUBLE
|
||||
))
|
||||
self.console.print()
|
||||
|
||||
# Always ask for connection details
|
||||
return self._ask_connection_details()
|
||||
|
||||
def _ask_connection_details(self) -> bool:
|
||||
"""Ask user for IP and password, then connect."""
|
||||
from getpass import getpass
|
||||
|
||||
self.console.print("[bold]CONNESSIONE AL DISPOSITIVO[/bold]")
|
||||
self.console.print()
|
||||
|
||||
# Check if we have saved config
|
||||
saved_ip = ""
|
||||
if self.ssh_manager.config_exists():
|
||||
success, _ = self.ssh_manager.load_config()
|
||||
if success:
|
||||
saved_ip = self.ssh_manager.hostname
|
||||
|
||||
# Ask for IP
|
||||
if saved_ip:
|
||||
ip_prompt = f"IP del dispositivo [{saved_ip}]"
|
||||
ip_input = Prompt.ask(ip_prompt, default=saved_ip)
|
||||
else:
|
||||
ip_input = Prompt.ask("IP del dispositivo")
|
||||
|
||||
if not ip_input or not ip_input.strip():
|
||||
self.console.print("[red]IP non valido[/red]")
|
||||
return False
|
||||
|
||||
ip_address = ip_input.strip()
|
||||
|
||||
# Username is always admin
|
||||
username = "admin"
|
||||
self.console.print(f"[dim]Username: {username}[/dim]")
|
||||
|
||||
# Ask for password
|
||||
self.console.print()
|
||||
password = getpass("Password: ")
|
||||
|
||||
if not password:
|
||||
self.console.print("[red]Password non valida[/red]")
|
||||
return False
|
||||
|
||||
# Save config
|
||||
self.ssh_manager.create_config(
|
||||
hostname=ip_address,
|
||||
username=username,
|
||||
password=password,
|
||||
port=22
|
||||
)
|
||||
|
||||
# Reload config
|
||||
success, msg = self.ssh_manager.load_config()
|
||||
if not success:
|
||||
self.console.print(f"[red]Errore: {msg}[/red]")
|
||||
return False
|
||||
|
||||
self.console.print()
|
||||
|
||||
# Connect
|
||||
return self.connect_to_device()
|
||||
|
||||
def connect_to_device(self) -> bool:
|
||||
"""Connect to the switch and load configuration."""
|
||||
self.console.print(f"\n[dim]Connessione a {self.ssh_manager.hostname}...[/dim]")
|
||||
|
||||
success, msg = self.ssh_manager.connect()
|
||||
if not success:
|
||||
self.console.print(f"[red]✗ {msg}[/red]")
|
||||
return False
|
||||
|
||||
self.is_connected = True
|
||||
self.console.print(f"[green]✓ {msg}[/green]")
|
||||
|
||||
# Load configuration
|
||||
return self.reload_configuration()
|
||||
|
||||
def reload_configuration(self) -> bool:
|
||||
"""Reload configuration from the switch."""
|
||||
if not self.is_connected:
|
||||
self.console.print("[red]Non connesso al dispositivo[/red]")
|
||||
return False
|
||||
|
||||
self.console.print("[dim]Recupero configurazione...[/dim]")
|
||||
|
||||
try:
|
||||
# Get running config
|
||||
running_config = self.ssh_manager.get_running_config()
|
||||
self.parser.parse_running_config(running_config)
|
||||
self.console.print("[green]✓[/green] Running configuration caricata")
|
||||
|
||||
# Get interface status
|
||||
iface_status = self.ssh_manager.get_interfaces_status()
|
||||
self.parser.parse_interfaces_status(iface_status)
|
||||
self.console.print("[green]✓[/green] Stato interfacce caricato")
|
||||
|
||||
# Get port-channel info
|
||||
pc_brief = self.ssh_manager.get_port_channel_brief()
|
||||
self.parser.parse_port_channel_brief(pc_brief)
|
||||
self.console.print("[green]✓[/green] Configurazione LAG caricata")
|
||||
|
||||
# Update UI
|
||||
self.ui.set_config(self.parser)
|
||||
self.ui.set_device_info(self.ssh_manager.hostname, self.ssh_manager.username)
|
||||
|
||||
self.config_loaded = True
|
||||
self.console.print()
|
||||
|
||||
# Show summary
|
||||
dest = self.parser.get_mirroring_destination()
|
||||
sources = self.parser.get_mirroring_sources()
|
||||
|
||||
if dest:
|
||||
self.console.print(
|
||||
f"[dim]Mirroring attivo: Destination=[bold]{dest}[/bold], "
|
||||
f"Sources={len(sources)} porte[/dim]"
|
||||
)
|
||||
else:
|
||||
self.console.print("[dim]Nessun mirroring configurato[/dim]")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Errore durante il caricamento: {e}[/red]")
|
||||
return False
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from the switch."""
|
||||
if self.is_connected:
|
||||
self.ssh_manager.disconnect()
|
||||
self.is_connected = False
|
||||
self.console.print("[dim]Disconnesso dal dispositivo[/dim]")
|
||||
|
||||
# =========================================================================
|
||||
# Main menu handlers
|
||||
# =========================================================================
|
||||
|
||||
def run(self) -> None:
|
||||
"""Main application loop."""
|
||||
try:
|
||||
if not self.initialize():
|
||||
self.console.print("\n[yellow]Inizializzazione fallita. Uscita.[/yellow]")
|
||||
return
|
||||
|
||||
while True:
|
||||
choice = self.ui.show_main_menu()
|
||||
|
||||
if choice == "0":
|
||||
# Exit
|
||||
if self.has_pending_changes:
|
||||
if not Confirm.ask(
|
||||
"[yellow]Ci sono modifiche non applicate. Uscire comunque?[/yellow]",
|
||||
default=False
|
||||
):
|
||||
continue
|
||||
break
|
||||
|
||||
elif choice == "1":
|
||||
# View port status
|
||||
self.ui.show_port_status()
|
||||
|
||||
elif choice == "2":
|
||||
# View mirroring status
|
||||
self.ui.show_mirroring_status()
|
||||
|
||||
elif choice == "3":
|
||||
# Configure mirroring
|
||||
self.handle_configure_mirroring()
|
||||
|
||||
elif choice == "4":
|
||||
# Generate script
|
||||
self.handle_generate_script()
|
||||
|
||||
elif choice == "5":
|
||||
# Apply configuration
|
||||
self.handle_apply_configuration()
|
||||
|
||||
elif choice == "6":
|
||||
# Reload configuration
|
||||
if self.is_connected:
|
||||
self.reload_configuration()
|
||||
self.ui.wait_for_enter()
|
||||
else:
|
||||
self.console.print("[yellow]Non connesso. Riconnessione...[/yellow]")
|
||||
if self.connect_to_device():
|
||||
self.ui.wait_for_enter()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.console.print("\n\n[yellow]Interrotto dall'utente[/yellow]")
|
||||
|
||||
finally:
|
||||
self.disconnect()
|
||||
self.console.print("\n[dim]Arrivederci![/dim]\n")
|
||||
|
||||
# =========================================================================
|
||||
# Configure mirroring
|
||||
# =========================================================================
|
||||
|
||||
def handle_configure_mirroring(self) -> None:
|
||||
"""Handle mirroring configuration."""
|
||||
if not self.config_loaded:
|
||||
self.console.print("[red]Configurazione non caricata[/red]")
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
|
||||
result = self.ui.configure_mirroring()
|
||||
|
||||
if result:
|
||||
destination, sources = result
|
||||
self.pending_destination = destination
|
||||
self.pending_sources = sources
|
||||
self.has_pending_changes = True
|
||||
|
||||
self.ui.clear_screen()
|
||||
self.ui.print_header("CONFIGURAZIONE COMPLETATA")
|
||||
|
||||
# Show commands
|
||||
commands = self.ui.generate_mirroring_commands(destination, sources)
|
||||
self.ui.show_command_preview(destination, sources)
|
||||
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
"[bold green]Configurazione salvata in memoria[/bold green]\n\n"
|
||||
"Usa le opzioni del menu per:\n"
|
||||
" [4] Genera Script - Crea script riutilizzabile\n"
|
||||
" [5] Applica Configurazione - Applica ora al device",
|
||||
box=box.ROUNDED
|
||||
))
|
||||
|
||||
self.ui.wait_for_enter()
|
||||
|
||||
# =========================================================================
|
||||
# Generate script
|
||||
# =========================================================================
|
||||
|
||||
def handle_generate_script(self) -> None:
|
||||
"""Handle script generation."""
|
||||
self.ui.clear_screen()
|
||||
self.ui.print_header("GENERA SCRIPT DI CONFIGURAZIONE")
|
||||
|
||||
# Check if we have pending changes or current config
|
||||
if self.has_pending_changes:
|
||||
destination = self.pending_destination
|
||||
sources = self.pending_sources
|
||||
self.console.print("[cyan]Usando configurazione in memoria (modifiche pending)[/cyan]\n")
|
||||
elif self.config_loaded:
|
||||
destination = self.parser.get_mirroring_destination()
|
||||
sources = self.parser.get_mirroring_sources()
|
||||
if not destination:
|
||||
self.console.print("[yellow]Nessun mirroring configurato sul device.[/yellow]")
|
||||
self.console.print("Usa prima [3] Configura Port Mirroring.\n")
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
self.console.print("[cyan]Usando configurazione attuale del device[/cyan]\n")
|
||||
else:
|
||||
self.console.print("[red]Nessuna configurazione disponibile[/red]")
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
|
||||
# Generate commands
|
||||
commands = self.ui.generate_mirroring_commands(destination, sources)
|
||||
|
||||
# Show preview
|
||||
self.ui.show_command_preview(destination, sources)
|
||||
self.console.print()
|
||||
|
||||
if not Confirm.ask("Generare gli script?", default=True):
|
||||
return
|
||||
|
||||
# Generate all formats
|
||||
try:
|
||||
description = f"Mirroring: {destination} <- {len(sources)} sources"
|
||||
|
||||
paths = self.script_gen.generate_all(
|
||||
commands=commands,
|
||||
device_ip=self.ssh_manager.hostname,
|
||||
username=self.ssh_manager.username,
|
||||
port=self.ssh_manager.port,
|
||||
config=self.parser.config if self.config_loaded else None,
|
||||
description=description
|
||||
)
|
||||
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
"[bold green]Script generati con successo![/bold green]\n\n"
|
||||
f"[bold]Bash script:[/bold]\n {paths['bash']}\n\n"
|
||||
f"[bold]Python script:[/bold]\n {paths['python']}\n\n"
|
||||
f"[bold]File comandi:[/bold]\n {paths['commands']}",
|
||||
title="FILE GENERATI",
|
||||
box=box.ROUNDED
|
||||
))
|
||||
|
||||
self.console.print()
|
||||
self.console.print("[dim]Per eseguire lo script bash:[/dim]")
|
||||
self.console.print(f" [bold]chmod +x {paths['bash'].name}[/bold]")
|
||||
self.console.print(f" [bold]./{paths['bash'].name}[/bold]")
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Errore durante la generazione: {e}[/red]")
|
||||
|
||||
self.ui.wait_for_enter()
|
||||
|
||||
# =========================================================================
|
||||
# Apply configuration
|
||||
# =========================================================================
|
||||
|
||||
def handle_apply_configuration(self) -> None:
|
||||
"""Handle applying configuration to device."""
|
||||
self.ui.clear_screen()
|
||||
self.ui.print_header("APPLICA CONFIGURAZIONE")
|
||||
|
||||
if not self.is_connected:
|
||||
self.console.print("[red]Non connesso al dispositivo[/red]")
|
||||
if Confirm.ask("Vuoi riconnetterti?", default=True):
|
||||
if not self.connect_to_device():
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
else:
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
|
||||
# Check if we have pending changes
|
||||
if not self.has_pending_changes:
|
||||
self.console.print("[yellow]Nessuna modifica in sospeso.[/yellow]")
|
||||
self.console.print("Usa prima [3] Configura Port Mirroring.\n")
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
|
||||
destination = self.pending_destination
|
||||
sources = self.pending_sources
|
||||
|
||||
# Generate and show commands
|
||||
commands = self.ui.generate_mirroring_commands(destination, sources)
|
||||
|
||||
self.console.print("[bold]Configurazione da applicare:[/bold]\n")
|
||||
self.ui.show_command_preview(destination, sources)
|
||||
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
"[bold yellow]ATTENZIONE[/bold yellow]\n\n"
|
||||
"Questa operazione modificherà la configurazione del port mirroring.\n"
|
||||
"Le modifiche saranno attive immediatamente ma potrebbero essere\n"
|
||||
"sovrascritte dal controller UniFi al prossimo provisioning.",
|
||||
box=box.ROUNDED,
|
||||
style="yellow"
|
||||
))
|
||||
|
||||
self.console.print()
|
||||
if not Confirm.ask("[bold]Applicare la configurazione?[/bold]", default=False):
|
||||
self.console.print("[dim]Operazione annullata[/dim]")
|
||||
self.ui.wait_for_enter()
|
||||
return
|
||||
|
||||
# Apply configuration
|
||||
self.console.print()
|
||||
self.console.print("[dim]Applicazione in corso...[/dim]")
|
||||
|
||||
try:
|
||||
success, output = self.ssh_manager.apply_mirroring_config(commands)
|
||||
|
||||
if success:
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
"[bold green]Configurazione applicata con successo![/bold green]",
|
||||
box=box.DOUBLE,
|
||||
style="green"
|
||||
))
|
||||
|
||||
# Clear pending changes
|
||||
self.has_pending_changes = False
|
||||
self.pending_destination = None
|
||||
self.pending_sources = []
|
||||
|
||||
# Offer to reload
|
||||
self.console.print()
|
||||
if Confirm.ask("Ricaricare la configurazione per verificare?", default=True):
|
||||
self.reload_configuration()
|
||||
|
||||
else:
|
||||
self.console.print()
|
||||
self.console.print(Panel(
|
||||
f"[bold red]Errore durante l'applicazione[/bold red]\n\n{output}",
|
||||
box=box.ROUNDED,
|
||||
style="red"
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Errore: {e}[/red]")
|
||||
|
||||
self.ui.wait_for_enter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry point
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
"""Application entry point."""
|
||||
app = PortMirroringManager()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user