#!/usr/bin/env python
# SPDX-License-Identifier: ISC

#
# test_bgp_update_delay.py
# Part of NetDEF Topology Tests
#
# Copyright (c) 2019 by
# Don Slice <dslice@nvidia.com>
#

"""
Test the ability to define update-delay to delay bestpath, rib install
and advertisement to peers when frr is started, restarted or "clear ip
bgp *" is performed. Test both the vrf-specific and global configuration
and operation.

r1
|
r2----r3
| \
|  \
r5  r4


r2 is UUT and peers with r1, r3, and r4 in default bgp instance.
r2 peers with r5 in vrf vrf1.

Check r2 initial convergence in default table
Define update-delay with max-delay in the default bgp instance on r2
Shutdown peering on r1 toward r2 so that delay timers can be exercised
Clear bgp neighbors on r2 and then check for the 'in progress' indicator
Check that r2 only installs route learned from r4 after the max-delay timer expires
Define update-delay with max-delay and estabish-wait and check json output showing set
Clear neighbors on r2 and check that r3 installs route from r4 after establish-wait time
Remove update-delay timer on r2 to verify that it goes back to normal behavior
Clear neighbors on r2 and check that route install time on r2 does not delay
Define global bgp update-delay with max-delay and establish-wait on r2
Check that r2 default instance and vrf1 have the max-delay and establish set
Clear neighbors on r2 and check route-install time is after the establish-wait timer

Note that the keepalive/hold times were changed to 3/9 and the connect retry timer
to 10 to improve the odds the convergence timing in this test case is useful in the
event of packet loss.
"""

import os
import sys
import json
import pytest
import functools

CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(CWD, "../"))

# pylint: disable=C0413
from lib import topotest
from lib.topogen import Topogen, TopoRouter, get_topogen

pytestmark = [pytest.mark.bgpd]


CWD = os.path.dirname(os.path.realpath(__file__))


def build_topo(tgen):
    for routern in range(1, 6):
        tgen.add_router("r{}".format(routern))

    switch = tgen.add_switch("s1")
    switch.add_link(tgen.gears["r1"])
    switch.add_link(tgen.gears["r2"])

    switch = tgen.add_switch("s2")
    switch.add_link(tgen.gears["r2"])
    switch.add_link(tgen.gears["r3"])

    switch = tgen.add_switch("s3")
    switch.add_link(tgen.gears["r2"])
    switch.add_link(tgen.gears["r4"])

    switch = tgen.add_switch("s4")
    switch.add_link(tgen.gears["r2"])
    switch.add_link(tgen.gears["r5"])


def setup_module(mod):
    tgen = Topogen(build_topo, mod.__name__)
    tgen.start_topology()

    router_list = tgen.routers()

    for i, (rname, router) in enumerate(router_list.items(), 1):
        router.load_config(
            TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
        )
        router.load_config(
            TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format(rname))
        )

    tgen.start_router()


def teardown_module(mod):
    tgen = get_topogen()
    tgen.stop_topology()


def test_bgp_update_delay():
    tgen = get_topogen()

    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    router1 = tgen.gears["r1"]
    router2 = tgen.gears["r2"]
    router3 = tgen.gears["r3"]

    # initial convergence without update-delay defined
    def _bgp_converge(router):
        output = json.loads(router.vtysh_cmd("show ip bgp neighbor 192.168.255.2 json"))
        expected = {
            "192.168.255.2": {
                "bgpState": "Established",
                "addressFamilyInfo": {"ipv4Unicast": {"acceptedPrefixCounter": 2}},
            }
        }
        return topotest.json_cmp(output, expected)

    def _bgp_check_update_delay(router):
        output = json.loads(router.vtysh_cmd("show ip bgp sum json"))
        expected = {"ipv4Unicast": {"updateDelayLimit": 20}}

        return topotest.json_cmp(output, expected)

    def _bgp_check_update_delay_in_progress(router):
        output = json.loads(router.vtysh_cmd("show ip bgp sum json"))
        expected = {"ipv4Unicast": {"updateDelayInProgress": True}}

        return topotest.json_cmp(output, expected)

    def _bgp_check_route_install(router):
        output = json.loads(router.vtysh_cmd("show ip route 172.16.253.254/32 json"))
        expected = {"172.16.253.254/32": [{"protocol": "bgp"}]}

        return topotest.json_cmp(output, expected)

    def _bgp_check_update_delay_and_wait(router):
        output = json.loads(router.vtysh_cmd("show ip bgp sum json"))
        expected = {
            "ipv4Unicast": {"updateDelayLimit": 20, "updateDelayEstablishWait": 10}
        }

        return topotest.json_cmp(output, expected)

    def _bgp_check_update_delay(router):
        output = json.loads(router.vtysh_cmd("show ip bgp sum json"))
        expected = {"ipv4Unicast": {"updateDelayLimit": 20}}

        return topotest.json_cmp(output, expected)

    def _bgp_check_vrf_update_delay_and_wait(router):
        output = json.loads(router.vtysh_cmd("show ip bgp vrf vrf1 sum json"))
        expected = {
            "ipv4Unicast": {"updateDelayLimit": 20, "updateDelayEstablishWait": 10}
        }

        return topotest.json_cmp(output, expected)

    # Check r2 initial convergence in default table
    test_func = functools.partial(_bgp_converge, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed bgp convergence in "{}"'.format(router2)

    # Define update-delay with max-delay in the default bgp instance on r2
    router2.vtysh_cmd(
        """
          configure terminal
            router bgp 65002
              update-delay 20
        """
    )

    # Shutdown peering on r1 toward r2 so that delay timers can be exercised
    router1.vtysh_cmd(
        """
          configure terminal
            router bgp 65001
              neighbor 192.168.255.1 shut
        """
    )

    # Clear bgp neighbors on r2 and then check for the 'in progress' indicator
    router2.vtysh_cmd("""clear ip bgp *""")

    test_func = functools.partial(_bgp_check_update_delay_in_progress, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed to set update-delay max-delay timer "{}"'.format(
        router2
    )

    # Check that r2 only installs route learned from r4 after the max-delay timer expires
    test_func = functools.partial(_bgp_check_route_install, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed to install route after update-delay "{}"'.format(
        router2
    )

    # Define update-delay with max-delay and estabish-wait and check json output showing set
    router2.vtysh_cmd(
        """
          configure terminal
            router bgp 65002
              update-delay 20 10
        """
    )

    test_func = functools.partial(_bgp_check_update_delay_and_wait, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert (
        result is None
    ), 'Failed to set max-delay and establish-weight timers in "{}"'.format(router2)

    # Define update-delay with max-delay and estabish-wait and check json output showing set
    router2.vtysh_cmd("""clear ip bgp *""")

    test_func = functools.partial(_bgp_check_route_install, router3)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert (
        result is None
    ), 'Failed to installed advertised route after establish-wait timer espired "{}"'.format(
        router2
    )

    # Remove update-delay timer on r2 to verify that it goes back to normal behavior
    router2.vtysh_cmd(
        """
          configure terminal
            router bgp 65002
              no update-delay
        """
    )

    # Clear neighbors on r2 and check that route install time on r2 does not delay
    router2.vtysh_cmd("""clear ip bgp *""")

    test_func = functools.partial(_bgp_check_route_install, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed to remove update-delay delay timing "{}"'.format(
        router2
    )

    # Define global bgp update-delay with max-delay and establish-wait on r2
    router2.vtysh_cmd(
        """
          configure terminal
            bgp update-delay 20 10
        """
    )

    # Check that r2 default instance and vrf1 have the max-delay and establish set
    test_func = functools.partial(_bgp_check_update_delay_and_wait, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed to set update-delay in default instance "{}"'.format(
        router2
    )

    test_func = functools.partial(_bgp_check_vrf_update_delay_and_wait, router2)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert result is None, 'Failed to set update-delay in vrf1 "{}"'.format(router2)

    # Clear neighbors on r2 and check route-install time is after the establish-wait timer
    router2.vtysh_cmd("""clear ip bgp *""")

    test_func = functools.partial(_bgp_check_route_install, router3)
    success, result = topotest.run_and_expect(test_func, None, count=30, wait=1)

    assert (
        result is None
    ), 'Failed to installed advertised route after establish-wait timer espired "{}"'.format(
        router2
    )


if __name__ == "__main__":
    args = ["-s"] + sys.argv[1:]
    sys.exit(pytest.main(args))
