mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-19 08:56:23 +00:00
311 lines
13 KiB
Python
311 lines
13 KiB
Python
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)} |