#!/usr/bin/env python3
"""Los experimentos de CDN de los apuntes, sobre cdn/cdn.clab.yml (make up-cdn).

  python3 cdn_demo.py dns        cada cliente pregunta el mismo nombre al mismo DNS
  python3 cdn_demo.py anycast    cada cliente va a la misma dirección y llega a su réplica
  python3 cdn_demo.py failover   se cae una réplica: qué pasa con las peticiones nuevas
  python3 cdn_demo.py cut        se cae una réplica en plena descarga: qué pasa con esa conexión
"""
import re
import subprocess
import sys
import time

from netlab import Lab, wait_until

CDN = Lab("cdn", "w13-cdn", "redes-w13-cdn")
ANYCAST = "10.213.50.1"
NS = "10.213.30.10"
NAME = "cdn.redes.test"


def start_services():
    """Levanta los servidores web de las réplicas y el DNS, si no están arriba."""
    for node in ("s1", "s2"):
        CDN.sh(node, "pkill", "-f", "http.server")
        CDN.daemon(node, "exec python3 -m http.server 80 --directory /www >/tmp/http.log 2>&1")
    CDN.sh("ns", "pkill", "-f", "geo_dns")
    CDN.daemon("ns", "exec python3 /geo_dns.py 53 >/tmp/dns.log 2>&1")
    time.sleep(1.0)


def converged():
    """Los dos clientes llegan a la dirección de anycast y al DNS."""
    return all(CDN.sh(node, "ping", "-c1", "-W1", target).returncode == 0
               for node in ("c1", "c2") for target in (ANYCAST, NS))


def rtt(node, target):
    output = CDN.out(node, "ping", "-c5", "-i0.2", "-q", target)
    match = re.search(r"= [\d.]+/([\d.]+)/", output)
    return float(match[1]) if match else None


def who(node, target=ANYCAST, wait="3"):
    done = CDN.sh(node, "curl", "-s", "--max-time", wait, f"http://{target}/quien.txt")
    return done.stdout.strip() or f"sin respuesta (curl {done.returncode})"


def dns():
    start_services()
    for node in ("c1", "c2"):
        answer = CDN.out(node, "dig", "+short", f"@{NS}", NAME, "A").strip()
        print(f"{node} pregunta {NAME} a {NS}: {answer}")
        print(f"   curl a esa dirección: {who(node, answer)}, RTT {rtt(node, answer):.1f} ms")
        other = "10.213.22.10" if answer == "10.213.21.10" else "10.213.21.10"
        print(f"   la otra réplica, {other}: RTT {rtt(node, other):.1f} ms")
    print("\nLa respuesta completa, en c1:")
    print(CDN.out("c1", "dig", f"@{NS}", NAME, "A", "+noall", "+answer", "+comments").strip())


def anycast():
    start_services()
    print("Rutas de r1 hacia el prefijo de anycast:")
    print(CDN.vtysh("r1", "show bgp ipv4 unicast 10.213.50.0/24").strip())
    print()
    for node in ("c1", "c2"):
        print(f"{node} -> {ANYCAST}: {who(node)}, RTT {rtt(node, ANYCAST):.1f} ms")


def withdraw(node):
    """La réplica deja de anunciar el prefijo, como si se cayera el sitio."""
    CDN.sh(node, "vtysh", "-c", "conf t", "-c", "router bgp 65310", "-c", "address-family ipv4 unicast",
           "-c", "no network 10.213.50.0/24")


def announce(node):
    CDN.sh(node, "vtysh", "-c", "conf t", "-c", "router bgp 65310", "-c", "address-family ipv4 unicast",
           "-c", "network 10.213.50.0/24")


def failover():
    start_services()
    print(f"antes:   c1 -> {who('c1')}, RTT {rtt('c1', ANYCAST):.1f} ms")
    withdraw("s1")
    began = time.monotonic()
    wait_until(lambda: "s2" in who("c1", wait="1"), seconds=30, step=0.2)
    print(f"s1 deja de anunciar; c1 llega a otra réplica tras {time.monotonic() - began:.1f} s")
    print(f"después: c1 -> {who('c1')}, RTT {rtt('c1', ANYCAST):.1f} ms")
    announce("s1")
    wait_until(lambda: "s1" in who("c1", wait="1"), seconds=30, step=0.2)
    print(f"s1 vuelve: c1 -> {who('c1')}")


def cut():
    """Una conexión TCP abierta no se muda: la otra réplica no la conoce."""
    start_services()
    download = subprocess.Popen(
        ["docker", "exec", CDN.prefix + "c1", "curl", "-sS", "-o", "/dev/null", "--limit-rate", "1M",
         "--max-time", "30", "-w", "%{size_download} bytes", f"http://{ANYCAST}/grande.bin"],
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    time.sleep(2.0)
    withdraw("s1")
    output = download.communicate(timeout=60)[0].strip()
    print("curl en c1, descargando mientras s1 deja de anunciar:")
    print(output)
    print("código de salida:", download.returncode)
    announce("s1")
    wait_until(lambda: "s1" in who("c1", wait="1"), seconds=30, step=0.2)


COMMANDS = {"dns": dns, "anycast": anycast, "failover": failover, "cut": cut}


def main():
    if len(sys.argv) != 2 or sys.argv[1] not in COMMANDS:
        print(__doc__)
        return 2
    if not wait_until(converged, seconds=60):
        print("la red no convergió; ¿corrieron make up-cdn?")
        return 1
    COMMANDS[sys.argv[1]]()
    return 0


if __name__ == "__main__":
    sys.exit(main())
