@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Разбор текстового вывода 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,
|
||||
}
|
||||
Reference in New Issue
Block a user