""" Разбор текстового вывода traceroute / tracepath (IPv4) для отчёта в админке. Форматы ориентируем на типичный вывод Linux: номер хопа, хост/IP, строки с * ms. """ from __future__ import annotations import re from typing import Any _IPV4 = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3})\b") _MS = re.compile(r"(\d+(?:\.\d+)?)\s*ms") _HOP_START = re.compile(r"^\s*(\d+)\s+") def _octets(ip: str) -> tuple[int, int, int, int] | None: parts = ip.split(".") if len(parts) != 4: return None try: a, b, c, d = (int(p) for p in parts) return (a, b, c, d) except ValueError: return None def is_private_ipv4(ip: str) -> bool: o = _octets(ip) if not o: return False a, b, c, d = o if a == 10: return True if a == 172 and 16 <= b <= 31: return True if a == 192 and b == 168: return True if a == 127: return True if a == 169 and b == 254: return True if a == 100 and 64 <= b <= 127: return True return False def parse_traceroute_text(text: str, *, max_chars: int = 512_000) -> dict[str, Any]: """ Возвращает: hops: [{ num, ip, delays, avg_ms, network_type }] stats: { total_hops, private_count, public_count, max_delay_ms, total_time_ms } """ raw = (text or "")[:max_chars] lines = raw.splitlines() hops: list[dict[str, Any]] = [] current: dict[str, Any] | None = None for raw_line in lines: line = raw_line.rstrip() if not line.strip(): continue hop_m = _HOP_START.match(line) if hop_m: if current and current.get("ip"): hops.append(_finalize_hop(current)) hop_num = int(hop_m.group(1)) ips = _IPV4.findall(line) ip = ips[0] if ips else "" delays = [float(x) for x in _MS.findall(line)] current = {"num": hop_num, "ip": ip, "delays": delays} elif current is not None: delays = [float(x) for x in _MS.findall(line)] if delays: current["delays"].extend(delays) ips = _IPV4.findall(line) if ips and not current.get("ip"): current["ip"] = ips[0] if current and current.get("ip"): hops.append(_finalize_hop(current)) private_count = 0 public_count = 0 max_delay = 0.0 total_time = 0.0 for h in hops: nt = h.get("network_type") if nt == "private": private_count += 1 elif nt == "public": public_count += 1 for d in h.get("delays") or []: if d > max_delay: max_delay = d if h.get("delays"): total_time += sum(h["delays"]) return { "hops": hops, "stats": { "total_hops": len(hops), "private_count": private_count, "public_count": public_count, "max_delay_ms": round(max_delay, 3) if max_delay else 0.0, "total_time_ms": round(total_time, 3) if total_time else 0.0, }, } def _finalize_hop(current: dict[str, Any]) -> dict[str, Any]: delays: list[float] = list(current.get("delays") or []) ip = str(current.get("ip") or "").strip() avg: float | None = None if delays: avg = round(sum(delays) / len(delays), 3) if ip and _octets(ip): nt = "private" if is_private_ipv4(ip) else "public" else: nt = "unknown" return { "num": int(current.get("num") or 0), "ip": ip, "delays": delays, "avg_ms": avg, "network_type": nt, }