130 lines
3.7 KiB
Python
130 lines
3.7 KiB
Python
"""
|
|
Разбор текстового вывода 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,
|
|
}
|