Files
ProxMenux/AppImage/scripts/network_monitor.py

311 lines
13 KiB
Python
Raw Normal View History

2026-01-29 17:47:10 +01:00
import os
import re
import json
import socket
import psutil
import subprocess
from system_monitor import get_proxmox_node_name
def extract_vmid_from_interface(interface_name):
"""
Extrae el ID de la VM del nombre de la interfaz.
Ejemplo: veth100i0 -> 100 (LXC), tap105i0 -> 105 (VM)
"""
try:
match = re.match(r'(veth|tap)(\d+)i\d+', interface_name)
if match:
vmid = int(match.group(2))
interface_type = 'lxc' if match.group(1) == 'veth' else 'vm'
return vmid, interface_type
return None, None
except Exception:
return None, None
def get_vm_lxc_names():
"""
Crea un mapa de VMIDs a nombres (ej: 100 -> 'Servidor-Web').
Ayuda a identificar qué interfaz pertenece a qué máquina.
"""
vm_lxc_map = {}
try:
local_node = get_proxmox_node_name()
# Consultamos pvesh para obtener la lista de VMs
result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
resources = json.loads(result.stdout)
for resource in resources:
if resource.get('node') == local_node:
vmid = resource.get('vmid')
if vmid:
vm_lxc_map[vmid] = {
'name': resource.get('name', f'VM-{vmid}'),
'type': 'lxc' if resource.get('type') == 'lxc' else 'vm',
'status': resource.get('status', 'unknown')
}
except Exception:
pass
return vm_lxc_map
def get_interface_type(interface_name):
"""
Clasifica la interfaz de red en tipos manejables.
"""
if interface_name == 'lo': return 'skip'
if interface_name.startswith(('veth', 'tap')): return 'vm_lxc'
if interface_name.startswith(('tun', 'vnet', 'docker', 'virbr')): return 'skip'
if interface_name.startswith('bond'): return 'bond'
if interface_name.startswith(('vmbr', 'br')): return 'bridge'
if '.' in interface_name: return 'vlan'
# Verificar si es una interfaz física real
if os.path.exists(f'/sys/class/net/{interface_name}/device'): return 'physical'
# Fallback por nombre común
if interface_name.startswith(('enp', 'eth', 'eno', 'ens', 'enx', 'wlan', 'wlp', 'wlo', 'usb')): return 'physical'
return 'skip'
def get_bond_info(bond_name):
"""Obtiene detalles de una interfaz Bond (agregación de enlaces)."""
info = {'mode': 'unknown', 'slaves': [], 'active_slave': None}
try:
path = f'/proc/net/bonding/{bond_name}'
if os.path.exists(path):
with open(path, 'r') as f:
content = f.read()
for line in content.split('\n'):
if 'Bonding Mode:' in line: info['mode'] = line.split(':', 1)[1].strip()
elif 'Slave Interface:' in line: info['slaves'].append(line.split(':', 1)[1].strip())
elif 'Currently Active Slave:' in line: info['active_slave'] = line.split(':', 1)[1].strip()
except Exception: pass
return info
def get_bridge_info(bridge_name):
"""
Obtiene los miembros de un Bridge (puente).
Intenta identificar la interfaz física real detrás del puente.
"""
info = {'members': [], 'physical_interface': None, 'physical_duplex': 'unknown', 'bond_slaves': []}
try:
brif_path = f'/sys/class/net/{bridge_name}/brif'
if os.path.exists(brif_path):
members = os.listdir(brif_path)
info['members'] = members
for member in members:
# Si el puente usa un bond
if member.startswith('bond'):
info['physical_interface'] = member
bond_info = get_bond_info(member)
info['bond_slaves'] = bond_info['slaves']
if bond_info['active_slave']:
try:
stats = psutil.net_if_stats().get(bond_info['active_slave'])
if stats:
info['physical_duplex'] = 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown'
except: pass
break
# Si el puente usa una interfaz física directa
elif member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan')):
info['physical_interface'] = member
try:
stats = psutil.net_if_stats().get(member)
if stats:
info['physical_duplex'] = 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown'
except: pass
break
except Exception: pass
return info
def get_network_info():
"""
Obtiene información completa y detallada de TODA la red.
"""
data = {
'interfaces': [], 'physical_interfaces': [], 'bridge_interfaces': [], 'vm_lxc_interfaces': [],
'traffic': {}, 'hostname': get_proxmox_node_name(), 'domain': None, 'dns_servers': []
}
# Leer configuración DNS
try:
with open('/etc/resolv.conf', 'r') as f:
for line in f:
if line.startswith('nameserver'): data['dns_servers'].append(line.split()[1])
elif line.startswith('domain'): data['domain'] = line.split()[1]
elif line.startswith('search') and not data['domain']:
parts = line.split()
if len(parts) > 1: data['domain'] = parts[1]
except: pass
vm_map = get_vm_lxc_names()
stats = psutil.net_if_stats()
addrs = psutil.net_if_addrs()
io_counters = psutil.net_io_counters(pernic=True)
# Contadores
counts = {'physical': {'active':0, 'total':0}, 'bridge': {'active':0, 'total':0}, 'vm': {'active':0, 'total':0}}
for name, stat in stats.items():
itype = get_interface_type(name)
if itype == 'skip': continue
info = {
'name': name, 'type': itype, 'status': 'up' if stat.isup else 'down',
'speed': stat.speed, 'mtu': stat.mtu,
'duplex': 'full' if stat.duplex == 2 else 'half' if stat.duplex == 1 else 'unknown',
'addresses': []
}
# IPs
if name in addrs:
for addr in addrs[name]:
if addr.family == socket.AF_INET: # IPv4
info['addresses'].append({'ip': addr.address, 'netmask': addr.netmask})
elif addr.family == 17: # MAC
info['mac_address'] = addr.address
# Tráfico
if name in io_counters:
io = io_counters[name]
# Si es VM, invertimos perspectiva (tx host = rx vm)
if itype == 'vm_lxc':
info.update({'bytes_sent': io.bytes_recv, 'bytes_recv': io.bytes_sent,
'packets_sent': io.packets_recv, 'packets_recv': io.packets_sent})
else:
info.update({'bytes_sent': io.bytes_sent, 'bytes_recv': io.bytes_recv,
'packets_sent': io.packets_sent, 'packets_recv': io.packets_recv})
info.update({'errors_in': io.errin, 'errors_out': io.errout,
'drops_in': io.dropin, 'drops_out': io.dropout})
# Clasificación
if itype == 'vm_lxc':
counts['vm']['total'] += 1
if stat.isup: counts['vm']['active'] += 1
vmid, _ = extract_vmid_from_interface(name)
if vmid and vmid in vm_map:
info.update({'vmid': vmid, 'vm_name': vm_map[vmid]['name'],
'vm_type': vm_map[vmid]['type'], 'vm_status': vm_map[vmid]['status']})
elif vmid:
info.update({'vmid': vmid, 'vm_name': f'VM/LXC {vmid}', 'vm_status': 'unknown'})
data['vm_lxc_interfaces'].append(info)
elif itype == 'physical':
counts['physical']['total'] += 1
if stat.isup: counts['physical']['active'] += 1
data['physical_interfaces'].append(info)
elif itype == 'bridge':
counts['bridge']['total'] += 1
if stat.isup: counts['bridge']['active'] += 1
b_info = get_bridge_info(name)
info['bridge_members'] = b_info['members']
info['bridge_physical_interface'] = b_info['physical_interface']
if b_info['physical_duplex'] != 'unknown':
info['duplex'] = b_info['physical_duplex']
data['bridge_interfaces'].append(info)
elif itype == 'bond':
bond_info = get_bond_info(name)
info.update({'bond_mode': bond_info['mode'], 'bond_slaves': bond_info['slaves'],
'bond_active_slave': bond_info['active_slave']})
data['interfaces'].append(info)
# Tráfico global
g_io = psutil.net_io_counters()
data['traffic'] = {
'bytes_sent': g_io.bytes_sent, 'bytes_recv': g_io.bytes_recv,
'packets_sent': g_io.packets_sent, 'packets_recv': g_io.packets_recv,
'packet_loss_in': 0, 'packet_loss_out': 0
}
tin = g_io.packets_recv + g_io.dropin
if tin > 0: data['traffic']['packet_loss_in'] = round((g_io.dropin / tin) * 100, 2)
tout = g_io.packets_sent + g_io.dropout
if tout > 0: data['traffic']['packet_loss_out'] = round((g_io.dropout / tout) * 100, 2)
data.update({
'physical_active_count': counts['physical']['active'], 'physical_total_count': counts['physical']['total'],
'bridge_active_count': counts['bridge']['active'], 'bridge_total_count': counts['bridge']['total'],
'vm_lxc_active_count': counts['vm']['active'], 'vm_lxc_total_count': counts['vm']['total']
})
return data
def get_network_summary():
"""Resumen rápido de red."""
net_io = psutil.net_io_counters()
stats = psutil.net_if_stats()
addrs = psutil.net_if_addrs()
phys_ifaces = []
bridge_ifaces = []
counts = {'phys_active':0, 'phys_total':0, 'br_active':0, 'br_total':0}
for name, stat in stats.items():
if name in ['lo', 'docker0'] or name.startswith(('veth', 'tap', 'fw')): continue
is_up = stat.isup
addresses = []
if name in addrs:
for addr in addrs[name]:
if addr.family == socket.AF_INET:
addresses.append({'ip': addr.address, 'netmask': addr.netmask})
info = {'name': name, 'status': 'up' if is_up else 'down', 'addresses': addresses}
if name.startswith(('enp', 'eth', 'eno', 'ens', 'wlan')):
counts['phys_total'] += 1
if is_up: counts['phys_active'] += 1
phys_ifaces.append(info)
elif name.startswith(('vmbr', 'br')):
counts['br_total'] += 1
if is_up: counts['br_active'] += 1
bridge_ifaces.append(info)
return {
'physical_active_count': counts['phys_active'], 'physical_total_count': counts['phys_total'],
'bridge_active_count': counts['br_active'], 'bridge_total_count': counts['br_total'],
'physical_interfaces': phys_ifaces, 'bridge_interfaces': bridge_ifaces,
'traffic': {'bytes_sent': net_io.bytes_sent, 'bytes_recv': net_io.bytes_recv,
'packets_sent': net_io.packets_sent, 'packets_recv': net_io.packets_recv}
}
def get_interface_metrics(interface_name, timeframe='day'):
"""Obtiene métricas RRD históricas para una interfaz."""
local_node = get_proxmox_node_name()
itype = get_interface_type(interface_name)
rrd_data = []
try:
# Si es VM/LXC, sacamos datos del contenedor/VM
if itype == 'vm_lxc':
vmid, vm_type = extract_vmid_from_interface(interface_name)
if vmid:
res = subprocess.run(['pvesh', 'get', f'/nodes/{local_node}/{vm_type}/{vmid}/rrddata',
'--timeframe', timeframe, '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
if res.returncode == 0:
data = json.loads(res.stdout)
for point in data:
item = {'time': point.get('time')}
if 'netin' in point: item['netin'] = point['netin']
if 'netout' in point: item['netout'] = point['netout']
rrd_data.append(item)
else:
# Si es física/bridge, sacamos datos del nodo (tráfico total del nodo)
res = subprocess.run(['pvesh', 'get', f'/nodes/{local_node}/rrddata',
'--timeframe', timeframe, '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
if res.returncode == 0:
data = json.loads(res.stdout)
for point in data:
item = {'time': point.get('time')}
if 'netin' in point: item['netin'] = point['netin']
if 'netout' in point: item['netout'] = point['netout']
rrd_data.append(item)
return {'interface': interface_name, 'type': itype, 'timeframe': timeframe, 'data': rrd_data}
except Exception as e:
return {'error': str(e)}