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:
grandir66andCursor committed 2026-02-05 22:09:40 +01:00
commit 7bd3f755be
9 files changed
+3888

No files matched your search

+466
View File
@@ -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()