mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-19 08:56:23 +00:00
Update backend monitor
This commit is contained in:
311
AppImage/scripts/network_monitor.py
Normal file
311
AppImage/scripts/network_monitor.py
Normal file
@@ -0,0 +1,311 @@
|
||||
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)}
|
||||
Reference in New Issue
Block a user