#!/usr/bin/env python
# Copyright 2019-2020 Cumulus Networks, Inc
#
# cl-consistency-check --
#   Check consistency among various Cumulus Linux components

import argparse
from io import StringIO
import json
import subprocess
import sys

import cumulus.vxlan_check_kern
from cumulus.dataplane_check import *
from nlmanager.nlpacket import Route, Neighbor

platform_object = cumulus.platforms.probe()
chip = platform_object.switch.chip
if chip.sw_base == "bcm":
    import cumulus.vxlan_check_bcm as asicVxlanModule
elif chip.sw_base == "mlx":
    import cumulus.vxlan_check_mlx as asicVxlanModule

"""
How to add a new test for a link attribute:
-------------------------------------------
1) add the test name, e.g., "mtu", to the `tests` list below

2) implement check_<attr> in HardwareLinkChecker (or a subclass if the check needs to be asic-specific)
   e.g.,
   def check_mtu(link)
                   ^
                   |
              Link Object

   check_<attr> must perform the comparison with the kernel value and return the result as a TestResult object.
   kernel values can be obtained using the kernel global object of type Kernel, see check_mtu below to
   see how to get link attributes.


TestResult:
    a TestResult object contains
        - success: bool, True if the check succeeded, i.e., kernel value agrees with hardware value
        - log: string, describes the check results


How to add a new global test (i.e., not a per-link test)
--------------------------------------------------------
1) add your <global_test> to the tests_global list below

2) implement check_<global_test> in HardwareLinkChecker (or a subclass if the check needs to be asic-specific)
e.g,
def check_vxlans()
   check_<global> must perform the comparison with the need kernel values and return the result as a TestResult object.
   kernel values can be obtained using the kernel global object of type Kernel, see check_vxlans below to see
   examples to get routes, neighbors
"""

################################################################################
#                              TEST LIST                                       #
################################################################################

tests_per_link = []
tests_global = ["vxlans", "datapath-syntax-check"]

class HardwareLinkChecker:

    def __init__(self):
        pass

    @staticmethod
    def print_verbose_vxlan(kernData, asicData):
        print("KERNEL VXLAN-INFO")
        print("-----------------")
        print("-----------------")
        for vxIf, vniInfo in list(kernData.items()):
            print(vxIf)
            print(("Vni - %d" % (kernData[vxIf]["vni"])))
            print(("IgmpSnoop - %d" % (kernData[vxIf]["igmpSnoop"])))
            print(("ArpSuppress - %d" % (kernData[vxIf]["arpSupress"])))
            master = kernData[vxIf]["master"]
            if master:
                print(("Master - %d" % (master)))
            else:
                print("Master - None")
            localPorts = kernData[vxIf]["localPorts"]
            print("LocalPorts -")
            if localPorts:
                for localPortTuple in localPorts:
                    print(("%s" % (str(localPortTuple))))
            localMacs = kernData[vxIf]["localMacs"]
            print("LocalMacs -")
            if localMacs:
                for localMacTuple in localMacs:
                    print(("%s" % (str(localMacTuple))))
            remoteMacs = kernData[vxIf]["remoteMacs"]
            print("RemoteMacs -")
            if remoteMacs:
                for remoteMacTuple in remoteMacs:
                    print(("%s:" % (str(remoteMacTuple))))
            print("------")
        print("\n")
        print("ASIC VXLAN-INFO")
        print("---------------")
        print("---------------")
        for vni, vniInfo in list(asicData.items()):
            print(vni)
            if asicData[vni].get("vpn"):
                print(("Vpn - %d" % (asicData[vni]["vpn"])))
            if asicData[vni].get("igmpSnoop"):
                print(("IgmpSnoop - %d" % (asicData[vni]["igmpSnoop"])))
            if asicData[vni].get("arpSupress"):
                print(("ArpReqSupress - %d" % (asicData[vni]["arpReqSup"])))
                print(("ArpReplySupress - %d" % (asicData[vni]["arpReplySupress"])))
            print("LocalPorts -")
            localPorts = asicData[vni]["localPorts"]
            if localPorts:
                for intfTuple in localPorts:
                    print(("%s" % (str(intfTuple))))
            print("LocalMacs -")
            localMacs = asicData[vni]["rawLocalMacs"]
            if localMacs:
                for localMacTuple in localMacs:
                    print(("%s" % (str(localMacTuple))))
            print("RemoteMacs -")
            remoteMacs = asicData[vni]["rawRemoteMacs"]
            if remoteMacs:
                for remoteMacTuple in remoteMacs:
                    print(("%s" % (str(remoteMacTuple))))
            print("-------")

    @staticmethod
    def check_vxlans(verbose):
        asic = asicVxlanModule.Asic()
        asicVxlanData = asic.dumpVxlans()
        kernVxlan = cumulus.vxlan_check_kern.KernelVxlan()
        kernVxlanData = kernVxlan.dumpVxlans()
        comp = Comparer()

        asicVxlans = set(asicVxlanData.keys())
        kernVxlans = set([kernVxlanData[k]["vni"]   \
                    for k,v in list(kernVxlanData.items())])

        check_tuples = (
            DiffTestParam(asicVxlans, kernVxlans, set(), "vni in hardware not \
                        and not in kernel", "vni_not_in_kernel"),
            DiffTestParam(kernVxlans, asicVxlans, set(), "vni in kernel and \
                        not in hardware", "vni_not_in_hardware")
        )
        comp.run_tests(check_tuples)

        for intf, intfInfo in list(kernVxlanData.items()):
            vni = kernVxlanData[intf]["vni"]
            if vni not in list(asicVxlanData.keys()):
                print("Continuing")
                continue
            asicLocalPorts = set(asicVxlanData[vni]["localPorts"])
            asicLocalMacs = set(asicVxlanData[vni]["localMacs"])
            asicRemoteMacs = [remoteTuple[:6] \
                        for remoteTuple in asicVxlanData[vni]["remoteMacs"]]
            asicRemoteMacs = set(asicRemoteMacs)

            kernLocalPorts = set(kernVxlanData[intf]["localPorts"])
            kernLocalMacs = set(kernVxlanData[intf]["localMacs"])
            kernRemoteMacs = [remoteTuple[:6] \
                            for remoteTuple in kernVxlanData[intf]["remoteMacs"]]
            kernRemoteMacs = set(kernRemoteMacs)

            check_tuples = (
                DiffTestParam(asicLocalPorts, kernLocalPorts, set(), \
                "localPorts in hardware and not in kernel", "localPorts_not_in_kernel"),
                DiffTestParam(kernLocalPorts, asicLocalPorts, set(), \
                "localPorts in kernel and not in hardware", "localPorts_not_in_hw"),

                DiffTestParam(asicLocalMacs, kernLocalMacs, set(), \
                "localMacs in hardware and not in kernel", "localMacs_not_in_kernel"),
                DiffTestParam(kernLocalMacs, asicLocalMacs, set(), \
                "localMacs in kernel and not in hardware", "localMacs_not_in_hardware"),

                DiffTestParam(asicRemoteMacs, kernRemoteMacs, set(),
                "remoteMacs in hardware and not in kernel", "remoteMacs_not_in_kernel"),
                DiffTestParam(kernRemoteMacs, asicRemoteMacs, set(),
                "remoteMacs in kernel and not in hardware", "remoteMacs_not_in_hardware")
            )
            comp.run_tests(check_tuples)
        if verbose:
            HardwareLinkChecker.print_verbose_vxlan(kernVxlanData, asicVxlanData)
        return comp

def datapath_syntax_check(verbose, quiet, traffic_file, datapath_file = None):
    # default directories
    script_dir      = '/usr/lib/cumulus/'
    usr_config_dir  = '/etc/cumulus/'
    chip_config_dir = '/etc/bcm.d/datapath/'

    # check the chip type
    platform_object = cumulus.platforms.probe()
    chip = platform_object.switch.chip
    if isinstance(chip, cumulus.platform.SpectrumChip):
        chip_config_dir = '/etc/mlx/datapath/'

    # default config values
    forwarding_file = chip_config_dir + 'rc.forwarding'
    hw_desc_file    = chip_config_dir + 'hw_desc'

    if datapath_file == None:
        datapath_file    = chip_config_dir + 'datapath.conf'

    if traffic_file == None:
        traffic_file    = usr_config_dir  + 'datapath/traffic.conf'

    command_str = [ script_dir + 'datapath-update',
                    '--dryrun', '-c', hw_desc_file, '-t', traffic_file,
                    '-d', datapath_file, '-f', forwarding_file]
    if quiet:
        command_str.append('-q')

    try:
        ret = subprocess.call(command_str)
    except BaseException as e:
        if not quiet:
            print 'Exception: ' + str(e)
            print 'Errors detected while checking traffic config file %s.' % traffic_file
        sys.exit(1)

    if ret and not quiet:
        print 'Errors detected while checking traffic config file %s.' % traffic_file
    elif verbose and not quiet:
        print 'No errors detected in traffic config file %s.' % traffic_file
    sys.exit(ret)



################################################################################
#                              MAIN                                            #
################################################################################
def parse_cmd_line_arguments():

    def usage(name=None):
        return """cl-consistency-check [-h] [-v] [-j]
            [--vxlan]
            [--datapath-syntax-check [-t TRAFFIC_CONF_FILE] [-d DATAPATH_CONF_FILE] [-q]]"""

    parser = argparse.ArgumentParser(
        description="""Beta version of Cumulus Linux Consistency Checker.
                       Checks consistency among various Cumulus Linux components.
                       Returns 0 if equivalent. Always prints failures. Only the
                       --vxlan and --datapath-syntax-check checkers are useable in
                       this version. The goal for this tool is to consolidate various
                       checkers available today in Cumulus Linux. Will be extended to
                       include more options in the future (e.g., equivalent functionality
                       of cl-route-check will be available here in the future, stay tuned)""",
        usage=usage()
    )

    parser.add_argument(
        "-v", "--verbose", action='store_true',
        help="prints tests passing as well as failures"
    )

    parser.add_argument(
        "--json", "-j", action='store_true',
        help="output result as JSON (to be implemented)"
    )

    parser.add_argument(
        "--vxlan", action='store_true',
        help="Check equivalence of hardware and kernel VXLANs (beta)"
    )

    parser.add_argument(
        "--datapath-syntax-check", action='store_true',
        help="check syntax of traffic.conf",
    )

    parser.add_argument(
        "-t", "--traffic-conf-file", type=str,
        help="specify alternate location of traffic.conf",
    )

    parser.add_argument(
        "-d", "--datapath-conf-file", type=str,
        help="specify alternate location of datapath.conf",
    )

    parser.add_argument(
        "-q", "--quiet", action='store_true',
        help="no output to stdout for datapath-syntax-check",
    )

    return parser.parse_args()


def main():
    hw_checker = HardwareLinkChecker()
    global chip

    args = parse_cmd_line_arguments()

    if args.datapath_conf_file and not args.datapath_syntax_check:
        sys.stderr.write("-d DATAPATH_CONF_FILE requires --datapath-syntax-check\n")
        sys.exit(1)

    if args.traffic_conf_file and not args.datapath_syntax_check:
        sys.stderr.write("-t TRAFFIC_CONF_FILE requires --datapath-syntax-check\n")
        sys.exit(1)

    if args.quiet and not args.datapath_syntax_check:
        sys.stderr.write("-q/--quiet requires --datapath-syntax-check\n")
        sys.exit(1)

    for test in tests_global:
        if test == "vxlans" and args.vxlan:
            comp = check(test, hw_checker, args.verbose)
            if args.json:
                sys.stdout.write(comp.get_result_json() + '\n')
            else:
                sys.stdout.write(comp.get_result_str() + '\n')

        if test == "datapath-syntax-check" and args.datapath_syntax_check:
            datapath_syntax_check(args.verbose, args.quiet, args.traffic_conf_file, args.datapath_conf_file)

if __name__ == '__main__':
    main()
