Files
pvemanager/backend/app/proxmox/net_parser.py
markmorado d99e67a60e
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m22s
Deploy to Production / Trigger production update (push) Failing after 10s
SonarCloud / SonarCloud Analysis (push) Failing after 2m22s
feat: add network topology page
Interactive infrastructure map (panel -> cluster -> node -> guest) rendered
with React Flow, backed by a single aggregated /api/network/topology endpoint
instead of fanning out over servers/nodes/SDN/VMs. Adds a netN NIC parser for
QEMU/LXC guest configs, status/threshold filters, canvas tools, and PNG/SVG
export.
2026-08-20 01:31:37 +05:00

140 lines
4.7 KiB
Python

"""Parser for Proxmox ``netN`` guest NIC strings.
Proxmox stores every guest network card as one comma-separated key=value
string in the guest config (``net0``, ``net1``, …). Two dialects exist:
* QEMU — ``virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=100,firewall=1``
The NIC model is itself the key holding the MAC address.
* LXC — ``name=eth0,bridge=vmbr0,hwaddr=AA:BB:CC:DD:EE:FF,ip=10.0.0.5/24,``
``gw=10.0.0.1,type=veth,firewall=1``
The repository only ever *builds* these strings (``api/proxmox/lxc.py``,
``proxmox/mixins/vm.py``); this module reads them back so callers can learn
which bridge/VLAN a guest actually sits on.
Deliberately free of any ``proxmoxer`` import so both the API layer and the
client mixins can use it. Parsing never raises: an unparsable token is skipped
rather than failing the whole request.
"""
from dataclasses import dataclass, field, asdict
from typing import Any, Dict, List, Optional
import re
# NIC models QEMU accepts. The model name doubles as the key carrying the MAC,
# which is the only way to tell "virtio=AA:.." from an ordinary option.
QEMU_MODELS = {
"virtio", "e1000", "e1000e", "e1000-82540em", "e1000-82544gc",
"e1000-82545em", "rtl8139", "vmxnet3", "ne2k_pci", "ne2k_isa",
"pcnet", "i82551", "i82557b", "i82559er",
}
_NET_KEY_RE = re.compile(r"^net(\d+)$")
@dataclass
class ParsedNic:
"""One guest network interface, normalised across the QEMU/LXC dialects."""
key: str # "net0"
index: int # 0
model: Optional[str] = None # virtio / e1000 / … (QEMU only)
mac: Optional[str] = None # uppercase, from model= or hwaddr=
bridge: Optional[str] = None # vmbr0 or an SDN vnet
vlan_tag: Optional[int] = None
trunks: List[int] = field(default_factory=list)
firewall: bool = False
link_down: bool = False
name: Optional[str] = None # LXC: name=eth0
ip: Optional[str] = None # LXC: "10.0.0.5/24" or "dhcp"
ip6: Optional[str] = None
gw: Optional[str] = None
gw6: Optional[str] = None
rate: Optional[float] = None # MB/s
mtu: Optional[int] = None
queues: Optional[int] = None
raw: str = ""
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
def _as_int(value: str) -> Optional[int]:
try:
return int(value)
except (TypeError, ValueError):
return None
def _as_float(value: str) -> Optional[float]:
try:
return float(value)
except (TypeError, ValueError):
return None
def _as_bool(value: str) -> bool:
return str(value).strip().lower() in ("1", "true", "yes", "on")
def parse_net_string(key: str, value: str) -> ParsedNic:
"""Parse a single ``netN`` value. Never raises."""
match = _NET_KEY_RE.match(key or "")
nic = ParsedNic(key=key or "", index=int(match.group(1)) if match else 0, raw=value or "")
for token in (value or "").split(","):
token = token.strip()
if not token or "=" not in token:
# Bare tokens (rare, e.g. a stray flag) carry nothing we need.
continue
opt, _, raw_val = token.partition("=")
opt = opt.strip().lower()
raw_val = raw_val.strip()
if opt in QEMU_MODELS:
nic.model = opt
nic.mac = raw_val.upper() or None
elif opt == "hwaddr":
nic.mac = raw_val.upper() or None
elif opt == "bridge":
nic.bridge = raw_val or None
elif opt == "tag":
nic.vlan_tag = _as_int(raw_val)
elif opt == "trunks":
nic.trunks = [v for v in (_as_int(p) for p in raw_val.split(";")) if v is not None]
elif opt == "firewall":
nic.firewall = _as_bool(raw_val)
elif opt == "link_down":
nic.link_down = _as_bool(raw_val)
elif opt == "name":
nic.name = raw_val or None
elif opt == "ip":
nic.ip = raw_val or None
elif opt == "ip6":
nic.ip6 = raw_val or None
elif opt == "gw":
nic.gw = raw_val or None
elif opt == "gw6":
nic.gw6 = raw_val or None
elif opt == "rate":
nic.rate = _as_float(raw_val)
elif opt == "mtu":
nic.mtu = _as_int(raw_val)
elif opt == "queues":
nic.queues = _as_int(raw_val)
return nic
def parse_guest_nics(config: Dict[str, Any]) -> List[ParsedNic]:
"""Every ``netN`` entry of a QEMU/LXC config, ordered by interface index."""
nics: List[ParsedNic] = []
for key, value in (config or {}).items():
if not isinstance(key, str) or not _NET_KEY_RE.match(key):
continue
if not isinstance(value, str):
continue
nics.append(parse_net_string(key, value))
nics.sort(key=lambda n: n.index)
return nics