"""Sondeo de disponibilidad ICMP de un host via el binario `ping` (Linux). Funcion IMPURA: ejecuta `ping -c -w ` como subprocess y parsea la salida (resumen de paquetes y linea rtt). Un host inalcanzable o con ICMP filtrado NO es error: se reporta `status:"ok"` con `loss_pct=100` y rtt None. Solo es error si el binario falla, el host esta vacio o hay timeout duro del subprocess. El campo `raw` siempre esta presente. """ import re import subprocess _LOSS_RE = re.compile(r"([\d.]+)%\s+packet\s+loss") _TX_RX_RE = re.compile(r"(\d+)\s+packets\s+transmitted,\s+(\d+)\s+(?:packets\s+)?received") _RTT_RE = re.compile( r"(?:rtt|round-trip)\s+min/avg/max(?:/mdev)?\s*=\s*" r"([\d.]+)/([\d.]+)/([\d.]+)" ) def ping_host(host: str, count: int = 4, timeout_s: int = 30) -> dict: """Hace ping ICMP a un host y parsea el resumen de paquetes y latencia. Args: host: Hostname o IP a sondear (ej. ``"1.1.1.1"`` o ``"google.com"``). count: Numero de echo requests a enviar (`ping -c`). timeout_s: Deadline total del comando ping (`ping -w`) y a la vez el timeout duro del subprocess (este ultimo con +5s de margen). Returns: Dict de estado. En exito (incluido host inalcanzable):: { "status": "ok", "host": , "packets_sent": , "packets_recv": , "loss_pct": , "rtt_avg_ms": , "rtt_min_ms": , "rtt_max_ms": , "raw": , } En fallo (binario ausente, host vacio, timeout duro):: {"status": "error", "error": , "host": , "raw": } """ if not host or not host.strip(): return {"status": "error", "error": "ping_host: host vacio", "host": host, "raw": ""} host = host.strip() hard_timeout = float(timeout_s) + 5.0 try: proc = subprocess.run( ["ping", "-c", str(count), "-w", str(timeout_s), host], capture_output=True, text=True, timeout=hard_timeout, ) except FileNotFoundError: return { "status": "error", "error": "ping_host: binario `ping` no encontrado en PATH (paquete iputils-ping)", "host": host, "raw": "", } except subprocess.TimeoutExpired as exc: partial = exc.stdout or "" if isinstance(partial, bytes): partial = partial.decode(errors="replace") return { "status": "error", "error": f"ping_host: timeout duro del subprocess tras {hard_timeout}s", "host": host, "raw": partial, } raw = proc.stdout or "" packets_sent: int | None = None packets_recv: int | None = None loss_pct: float = 100.0 rtt_min = rtt_avg = rtt_max = None m_txrx = _TX_RX_RE.search(raw) if m_txrx: packets_sent = int(m_txrx.group(1)) packets_recv = int(m_txrx.group(2)) m_loss = _LOSS_RE.search(raw) if m_loss: loss_pct = float(m_loss.group(1)) m_rtt = _RTT_RE.search(raw) if m_rtt: rtt_min = float(m_rtt.group(1)) rtt_avg = float(m_rtt.group(2)) rtt_max = float(m_rtt.group(3)) return { "status": "ok", "host": host, "packets_sent": packets_sent, "packets_recv": packets_recv, "loss_pct": loss_pct, "rtt_avg_ms": rtt_avg, "rtt_min_ms": rtt_min, "rtt_max_ms": rtt_max, "raw": raw, } if __name__ == "__main__": try: result = ping_host("1.1.1.1", count=3, timeout_s=10) print(result["status"]) if result["status"] == "ok": print( f"loss={result['loss_pct']}% " f"recv={result['packets_recv']}/{result['packets_sent']} " f"avg={result['rtt_avg_ms']}ms" ) print("--- raw ---") print(result["raw"]) else: print("error:", result.get("error")) except Exception as exc: # smoke: tolera cualquier fallo de red sin romper print("smoke fallo (tolerado):", exc)