#!/usr/bin/python
# Copyright (C) 2019 Cumulus Networks, Inc. all rights reserved
#
# This software is subject to the Cumulus Networks End User License Agreement available
# at the following locations:
#
# Internet: https://cumulusnetworks.com/downloads/eula/latest/view/
# Cumulus Linux systems: /usr/share/cumulus/EULA.txt
"""
Usage: cl-tdr [-h] [-d DELAY] [-j] [-y] [portlist]

Time Division Reflectometer(TDR) Tests

positional arguments:
  portlist              port names. e.g. swp1,swp5-7

optional arguments:
  -h, --help            show this help message and exit
  -d DELAY, --delay DELAY
                        Change delay between multiple ports, range 0-30,
                        default is 2
  -j, --json            Display JSON output
  -y, --yes             Proceed automatically without warning
"""

try:
    import shlex
    import argparse
    import json
    import os
    import re
    import sys
    import subprocess
    import datetime
    from time import sleep, time, gmtime, localtime, strftime

except ImportError as e:
    raise ImportError(str(e) + "- required module not found")

class ParseError(RuntimeError):
    pass

def run(command, ignore_return_code=False):
    # Split the command
    command = shlex.split(command)
    try:
        # Using devnull for stderr as some errors are expected
        output = subprocess.check_output(command, stderr=subprocess.STDOUT)
        return str(output)
    except subprocess.CalledProcessError as e:
        # Means the command ran failed, and will have some output, return that output if ignore_return_code is True
        if ignore_return_code:
            return str(e.output)
        else:
            raise e
    except Exception as e:
        raise e

def swp_sort_pattern(item):
    pattern = re.compile(r'''swp(\d+)s?(\d)?''')

    r = pattern.match(item)
    if r:
        if r.group(2):
            num = float(r.group(1) + '.' + r.group(2))
        else:
            num = float(r.group(1))
    return num if r else '&'


def parse_port_args(port_args):

    # Each port or port range should be seperated by commas
    ports = port_args.split(',')
    port_list = []

    for port in ports:
        try:   
            match = re.match('^swp?(\d{1,2})(-\d{1,2})?$', port)
            if not match:
                return []

            start =  match.group(1)
            end = match.group(2)
            if end:
                end = end.strip('-')
            else: 
                end = start

            if int(start[0]) == 0:
                return []

            if int(start) <= int(end):
                for i in range (int(start), int(end)+1):
                    port_list.append("swp"+str(i))
            else:
                return []
        except:
            return []

    # remove duplicates
    port_list = list(dict.fromkeys(port_list))
    return port_list

def localtime_from_epoch(epoch):
    try:
       time_str = strftime( "%Y-%m-%d %H:%M:%S %Z",  localtime(float(epoch)))
    except:
       time_str = ''
    return time_str

def fetch_info(path, fname):
    try:
        with open(path + fname, 'r') as f:
            info = str(f.readline()).strip('\n')
    except IOError:
        info = '' 
    return info

def trigger_tdr(path, fname):
    try:
       with open(path + fname, 'w') as f:
           f.write('run')
           return
    except:
        return

def run_tdr_tests(port_list, use_json, use_past, wait):
    delay = 0
    past_str = 'current' 
    
    if use_past:
        past_str = 'past' 

    ports_dict = {}
    for port in sorted(port_list, key=lambda l: swp_sort_pattern(l)):

        if not use_past:
            sleep(delay)
            delay = wait

        fqpath = '/cumulus/switchd/config/interface/' + port + '/'

        if not use_past:
            trigger_tdr(fqpath, 'tdr_status')

        status = fetch_info(fqpath, 'tdr_status')
        if status == 'valid':
            state       = fetch_info(fqpath, 'tdr_state')
            npairs      = fetch_info(fqpath, 'tdr_npairs')
            pairA_state = fetch_info(fqpath, 'tdr_pairA_state')
            pairB_state = fetch_info(fqpath, 'tdr_pairB_state')
            pairC_state = fetch_info(fqpath, 'tdr_pairC_state')
            pairD_state = fetch_info(fqpath, 'tdr_pairD_state')
            pairA_len   = fetch_info(fqpath, 'tdr_pairA_len')
            pairB_len   = fetch_info(fqpath, 'tdr_pairB_len')
            pairC_len   = fetch_info(fqpath, 'tdr_pairC_len')
            pairD_len   = fetch_info(fqpath, 'tdr_pairD_len')
            ts_epoch    = fetch_info(fqpath, 'tdr_timestamp')
            timestamp  = localtime_from_epoch(ts_epoch)

            pair_A_precision = '10' if pairA_state == "Ok" else '5'
            pair_B_precision = '10' if pairB_state == "Ok" else '5'
            pair_C_precision = '10' if pairC_state == "Ok" else '5'
            pair_D_precision = '10' if pairD_state == "Ok" else '5'

            pairA = { 'length'   : pairA_len + ' meters',
                      'precision' : pair_A_precision + ' meters',
                      'state'    : pairA_state,
                    }
            pairB = { 'length'   : pairB_len + ' meters',
                      'precision' : pair_B_precision + ' meters',
                      'state'    : pairB_state,
                    }
            pairC = { 'length'   : pairC_len + ' meters',
                      'precision' : pair_C_precision + ' meters',
                      'state'    : pairC_state,
                    }
            pairD = { 'length'   : pairD_len + ' meters',
                      'precision' : pair_D_precision + ' meters',
                      'state'    : pairD_state,
                    }

            info = {'status' : status,
                    'timestamp' : timestamp,
                    'npairs': npairs,
                    }

            if npairs > 0:
                info['pairA'] = pairA
            if npairs > 1: 
                info['pairB'] = pairB
            if npairs > 2: 
                info['pairC'] = pairC
            if npairs > 3: 
                info['pairD'] = pairD

        else:
            info = {'status' : (status)}

        ports_dict[port] =  info

        if not use_json:
            if status == 'valid':
                print '{0: <5} {1} results @ {2}'.format(port, past_str, timestamp)
                print '      cable ({0} pairs)'.format(str(npairs))

                if npairs > 0:
                    print '      pair A {0}, length {1} meters (+/- {2})'.format(pairA_state, pairA_len, pair_A_precision)
                if npairs > 1:
                    print '      pair B {0}, length {1} meters (+/- {2})'.format(pairB_state, pairB_len, pair_B_precision)
                if npairs > 2:
                    print '      pair C {0}, length {1} meters (+/- {2})'.format(pairC_state, pairC_len, pair_C_precision)
                if npairs > 3:
                    print '      pair D {0}, length {1} meters (+/- {2})'.format(pairD_state, pairD_len, pair_D_precision)
            else:
                print '{0: <5} status: {1}'.format(port, status)

    if use_json:
        print json.dumps(ports_dict, sort_keys=False, indent=3)

    return 0

def query_yes_no(question):

    default = 'no'
    valid = {'yes': True,
             'no': False, 'n': False}

    while True:
        sys.stdout.write(question + ' [yes/NO]')
        choice = raw_input().lower()
        if choice == '':
            return valid[default]
        elif choice in valid:
            return valid[choice]
        else:
            sys.stdout.write("Please respond with 'yes' or 'no'\n")

def main(argv):
    """ main function """
    descr = 'Time Division Reflectometer(TDR) Tests'

    if os.getuid():
        sys.stderr.write('root permission required\n')
        sys.exit(1)

    switchd_up = run('systemctl is-active switchd', ignore_return_code=True)
    if not switchd_up.startswith('active'):
        sys.stderr.write("switchd must be running\n")
        sys.exit(1)

    arg_parser = argparse.ArgumentParser(description=descr)
    arg_parser.add_argument('portlist', nargs='?', default='',
                        help='port names. e.g. swp1,swp5-7')
    arg_parser.add_argument('-d', '--delay', dest='delay',
                             type=int, default=2,
                        help='Change delay between multiple ports, range 0-30, default is 2')
    arg_parser.add_argument('-j', '--json', dest='json',
                        action='store_true',
                        default=False,
                        help='Display JSON output')
    arg_parser.add_argument('-p', '--past', dest='past',
                        action='store_true',
                        default=False,
                        help=argparse.SUPPRESS)
    arg_parser.add_argument('-y', '--yes', dest='yes',
                        action='store_true',
                        default=False,
                        help='Proceed automatically without warning')

    try:
        args = arg_parser.parse_args()
    except ParseError, e:
        arg_parser.error(str(e))

    if not args.portlist:
        sys.stderr.write('missing port name. e.g. swp1\n')
        sys.exit(1)

    if args.delay < 0 or args.delay> 30:
        sys.stderr.write('delay must be between 0 and 30 inclusive\n')
        sys.exit(1)

    # check the syntax of port list input
    port_list = parse_port_args(args.portlist);

    if not len(port_list):
        sys.stderr.write('no ports\n')
        sys.exit(1)

    # check that the port exists on the switch
    allports = os.listdir('/sys/class/net')
    for port in sorted(port_list, key=lambda l: swp_sort_pattern(l)):
       if port not in allports:
           sys.stderr.write('non-existent port {}\n'.format(port))
           sys.exit(1)

    # running tdr test could bring links down.
    # issue warning
    if not args.past:
        if not args.yes:
            sys.stdout.write('Time Domain Reflectometer (TDR) diagnostics tests are disruptive.\n')
            sys.stdout.write('When TDR is run on an interface, it will cause the interface to\n')
            sys.stdout.write('go down momentarily during the test. The interface will be restarted\n')
            sys.stdout.write('at the conclusion of the test.\n')
            sys.stdout.write('\nThe following interfaces may be affected:\n')
            newline = 0 
            for port in sorted(port_list, key=lambda l: swp_sort_pattern(l)):
                newline += 1
                sys.stdout.write('{0: <6}'.format(port))
                if newline >= 12:
                    newline = 0
                    sys.stdout.write('\n')

            if newline != 0:
                sys.stdout.write('\n') 

            if not query_yes_no('\nAre you sure you want to continue?'):
                sys.exit(1)  

    run_tdr_tests(port_list, args.json, args.past, args.delay)

    sys.exit(0)

if __name__ == "__main__":
    main(sys.argv[1:])
