mirror of
https://github.com/grandir66/UnifiMultimirror.git
synced 2026-10-02 15:24:42 +02:00
- 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>
467 lines
17 KiB
Python
467 lines
17 KiB
Python
#!/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()
|