#!/usr/bin/env python3
r"""Ling Aggregation Basic

Verify communication over a link aggregate in static and LACP operating
modes during basic failure scenarios.

.Internal network setup, PC verifies connectivity with dut2 via dut1
image::lag-basic.svg[align=center, scaledwidth=75%]

The host verifies connectivity with dut2 via dut1 over the aggregate for
each test step using the `mon` interface.

"""
from time import sleep, time
from datetime import datetime
import infamy
import infamy.lag
from infamy.util import parallel, until

class DumbLinkBreaker:
    """Encapsulates basic, dumb link-breaking ops over SSH."""

    def __init__(self, sys, dut, netns):
        self.env = sys
        self.dut = dut
        self.net = netns
        self.tgt = {}
        for i, (name, _) in dut.items():
            self.tgt[i] = env.attach(name, "mgmt", "ssh")

    def set_link(self, link, updown):
        """Set link up or down, verify before returning."""
        def set_and_verify(i):
            name, dut = self.dut[i]

            cmd = self.tgt[i].runsh(f"sudo ip link set {dut[link]} {updown}")
            if cmd.returncode:
                for out in [cmd.stdout, cmd.stderr]:
                    if out:
                        print(f"{name}: {out.rstrip()}")
                raise RuntimeError(f"{name}: failed setting {link} {updown}")

            for _ in range(10):
                sleep(0.1)
                check = self.tgt[i].runsh(f"ip link show {dut[link]}")
                if f"state {updown.upper()}" in check.stdout:
                    break
            else:
                raise RuntimeError(f"{name}: {dut[link]} did not go {updown}")

        parallel(*[lambda i=i: set_and_verify(i) for i in self.dut])

    def fail_check(self, peer):
        """Verify connectivity with peer during link failure."""
        sequence = [
            [("link1", "up"),   ("link2", "up")],
            [("link1", "down"), ("link2", "up")],
            [("link1", "up"),   ("link2", "down")],
            [("link1", "up"),   ("link2", "up")]
        ]

        total_start = time()
        for state in sequence:
            state_start = time()
            print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} {state}")

            for link, updown in state:
                self.set_link(link, updown)
            self.net.must_reach(peer, timeout=10)

            print(f"Completed in {time() - state_start:.2f}s")

        print(f"Total time: {time() - total_start:.2f}s")


def lag_init(mode):
    """Set up mode specific attributes for the LAG"""
    if mode == "lacp":
        lag = [{
            "name": "lag0",
            "lag": {"lacp": {"rate": "fast"}}
        }]
    else:
        lag = []
    return lag


def net_init(host, addr):
    """Set up DUT network, dut1 bridges host port with lag0"""
    if host:
        net = [{
            "name": "br0",
            "type": "infix-if-type:bridge",
        }, {
            "name": host,
            "bridge-port": {"bridge": "br0"}
        }, {
            "name": "lag0",
            "bridge-port": {"bridge": "br0"}
        }]
    else:
        net = [{
            "name": "lag0",
            "ipv4": {
                "address": [{"ip": addr, "prefix-length": 24}]
            }
        }]
    return net


def dut_init(dut, mode, addr):
    """Set up link aggregate on dut"""
    net = net_init(dut["mon"], addr)
    lag = lag_init(mode)

    dut.put_config_dict("ietf-interfaces", {
        "interfaces": {
            "interface": [{
                "name": "lag0",
                "type": "infix-if-type:lag",
                "lag": {
                    "mode": mode,
                    "link-monitor": {"interval": 100}
                }
            }, {
                "name": dut["link1"],
                "lag-port": {"lag": "lag0"}
            }, {
                "name": dut["link2"],
                "lag-port": {"lag": "lag0"}
            }] + net + lag
        }
    })


with infamy.Test() as test:
    with test.step("Set up topology and attach to target DUTs"):
        env = infamy.Env(edge_mappings=infamy.lag.edge_mappings)
        dut1 = env.attach("dut1", "mgmt")
        dut2 = env.attach("dut2", "mgmt")

    _, mon = env.ltop.xlate("host", "mon")
    with infamy.IsolatedMacVlan(mon) as ns:
        dm = {
            '1': ("dut1", dut1),
            '2': ("dut2", dut2)
        }
        lb = DumbLinkBreaker(env, dm, ns)
        ns.addip("192.168.2.1")

        with test.step("Set up LACP link aggregate, lag0, on dut1 and dut2"):
            parallel(lambda: dut_init(dut1, "lacp", None),
                     lambda: dut_init(dut2, "lacp", "192.168.2.42"))

        with test.step("Verify failure modes for lacp mode"):
            lb.fail_check("192.168.2.42")

        with test.step("Set up static link aggregate, lag0, on dut1 and dut2"):
            parallel(lambda: dut_init(dut1, "static", None),
                     lambda: dut_init(dut2, "static", "192.168.2.42"))

        with test.step("Verify failure modes for static mode"):
            lb.fail_check("192.168.2.42")

    test.succeed()
