#!/usr/bin/python3
# Copyright (C) 2020-2023 NVIDIA Corporation. ALL RIGHTS RESERVED.
# Copyright 2012,2013,2014,2015,2016,2017,2018,2019,2020 Cumulus Networks, Inc.  All rights reserved.
#
# cl-update-ports --
#
#     Read /etc/cumulus/ports.conf. Intended to be run
#     at boot, or when /etc/cumulus/ports.conf changes.  Idempotent.
#

import os
import time
import sys
import argparse
import subprocess
import os.path

import cumulus.platforms
import cumulus.portconfig
import cumulus.switchconfiggen

class ArgParseError(RuntimeError):
    pass

def switchd_status():
    null = open('/dev/null')

    status = subprocess.call(('/bin/systemctl', 'is-active', 'switchd'),
                             stdin=null, stdout=null, stderr=null)
    if status == 0:
        return 'running'
    elif status == 3:
        return 'stopped'
    else:
        return 'unknown(%d)' % status

def update_config(pc, platform, args):
    status = switchd_status()

    if status == 'running' and not args.force:
        sys.stderr.write('error:'
                         ' refusing to update configuration while switchd is running. '
                         '(override with --force)\n')
        return -1

    if status not in ('running', 'stopped'):
        sys.stderr.write('warning: switchd in unknown state (status=%s)\n' % status)

    if args.verbose:
        sys.stdout.write('configuring ports for %s\n' % platform.name)

    ports_conf_fname = '/etc/cumulus/ports.conf'
    backend_conf_fname = '/var/lib/cumulus/backend.conf'
    if args.init and os.path.exists(ports_conf_fname) and not args.force:
        sys.stderr.write('error: %s already exists, override with --force\n' % ports_conf_fname)
        return -1

    if args.verbose:
        sys.stdout.write('sw_base is %s\n' % platform.switch.chip.sw_base)
        sys.stdout.write('read %s\n' % ports_conf_fname)

    # Generate the ports.conf for modular platforms
    if platform.modular:
        cardsconf = open('/etc/cumulus/cards.conf', 'r')
        lc_map = {}
        lc_dirty = {}
        for line in cardsconf.readlines():
            line = line.strip()
            if '#' in line or len(line) == 0:
                continue
            slotinfo = line.split()
            lc_map[slotinfo[0]] = slotinfo[3]
            lc_dirty[slotinfo[0]] = slotinfo[4]

        cardsconf.close()

        # Prepare port cache to retain the configs for non-dirty slots
        port_cache = []
        if os.path.exists(ports_conf_fname):
            portsconf = open(ports_conf_fname, 'r')
            for line in portsconf.readlines():
                line = line.strip()
                if '#' in line or len(line) == 0:
                    continue
                port_cache.append((line[1], line))

            portsconf.close()

        lc_ports_pre = '/etc/cumulus/ports.d/ports_'
        lc_ports_ext = '.conf'
        portsconf = open('/etc/cumulus/ports.conf', 'w')
        slots = platform.slots
        for x in range(1, slots+1):
            if lc_map.get(str(x)) is None:
                continue
            if int(lc_dirty.get(str(x))) == 0:
                for (slot, port) in port_cache:
                    if slot == str(x):
                        portsconf.write('%s\n' % port)
            else:
                try:
                    tempfile = open(''.join([lc_ports_pre, lc_map.get(str(x)), lc_ports_ext]), 'r')
                    lines = [line.rstrip('\r\n') for line in tempfile]
                    for line in lines:
                        #modular platforms will have port naming as mXpY
                        portsconf.write('m%sp%s\n' % (str(x),line.strip()))
                    tempfile.close()
                except:
                    continue
        portsconf.close()

    # the input mac mode must be set before we call config,
    # since config may override the mode
    pc.set_macmode(args.mac_mode)

    if args.init:
        if args.verbose:
            sys.stdout.write('init %s\n' % ports_conf_fname)
        pc.init_config()
        pc.write_config(ports_conf_fname)

    if args.verbose:
        sys.stdout.write('read %s\n' % ports_conf_fname)
    pc.read_config(ports_conf_fname)

    autogen = ('# Automatically generated by %s.\n# %s\n# Do not edit.\n#\n' %
               (sys.argv[0], time.asctime()) + \
               '# Warning:\n' + \
               '# Some platforms dynamically change the port mapping' + \
               ' based on speed\n' + \
               '# of the link. To see the current mapping of logical' + \
               ' to physical ports,\n' + \
               '# use the /usr/lib/cumulus/portmap tool instead.\n' +
               '# The output of this file may not match the current mapping' + \
               '\n#\n')

    # Perform any platform dependent switch config generation
    #
    cumulus.switchconfiggen.switchconfiggen(pc, ports_conf_fname, autogen,
                                            args.verbose)

    varlib_dir = '/var/lib/cumulus'
    if not os.path.exists(varlib_dir) :
        os.makedirs(varlib_dir)
    # Create dummy config files for VX as switchd doesnt generate them
    # on VX.
    if platform.name == 'cumulus,vx':
        if args.verbose:
            sys.stdout.write('write /var/lib/cumulus/porttab\n')
        porttab = open('/var/lib/cumulus/porttab', 'w')
        porttab.write(autogen)
        porttab.write('\n')
        porttab.close()

        if args.verbose:
            sys.stdout.write('write /var/lib/cumulus/phytab\n')
        phytab = open('/var/lib/cumulus/phytab', 'w')
        phytab.write(autogen)
        phytab.write('\n')
        phytab.close()

        if args.verbose:
            sys.stdout.write('write /var/lib/cumulus/sfptab\n')
        sfptab = open('/var/lib/cumulus/sfptab', 'w')
        sfptab.write(autogen)
        sfptab.write('\n')
        sfptab.close()
    # End - dummy files on VX

    if args.verbose:
        sys.stdout.write('Update netdevices\n')
    pc.output('netdevs_mng')

    try:
        ch = subprocess.Popen(['switchd', '-lic'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        cmd_returncode = ch.wait()
        if cmd_returncode != 99 and cmd_returncode != 0:
            raise Exception('switch not licensed')

        linkdefaults = '/var/lib/ifupdown2/policy.d/ethtool.json'
        if args.verbose:
            sys.stdout.write('write %s\n' % linkdefaults)
        portspolicy = open(linkdefaults,'w')
        portspolicy.write(pc.output('linkdefaults') + '\n')
        portspolicy.close()
    except Exception as e:
        sys.stderr.write('warning: could not write ifupdown2 ethtool policy (defaults_policy.json): %s\n' % e)

    if platform.switch.chip.sw_base == 'mlx':
        datapath_dir = '/etc/mlx/datapath'
        if not os.path.exists(datapath_dir) :
                        os.makedirs(datapath_dir)
        fname = datapath_dir + '/datapath.conf'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('datapath')
        if file_string != None :
            datapath = open(fname, 'w')
            datapath.write(autogen)
            datapath.write(file_string + '\n')
        else :
            if args.verbose:
                sys.stdout.write('file string is None\n')

        be_file = open(backend_conf_fname, 'w')
        be_file.write(autogen)
        be_file.write('backend_lib = libhalmlx.so, enum_fn = hal_mlx_enum_backends')
        be_file.close()

#
#       VALIDATE_PORTSCONF_IDLE = 0,    
#       VALIDATE_PORTSCONF_RUNNING,    
#       VALIDATE_PORTSCONF_FAILURE,    
#       VALIDATE_PORTSCONF_SUCCESS,    
#       VALIDATE_PORTSCONF_AVAILABLE
#

def validate_portsconf():
        PORTSCONF_V='/cumulus/switchd/run/validate_portsconf/state'
        sys.stdout.flush()
        if os.path.exists(PORTSCONF_V):
            with open(PORTSCONF_V, 'r') as f:
                curr_state = int(f.read())
                if (curr_state != 0):
                    sys.stdout.write('switchd ports.conf node status not ready ')
                    status = 2
                    f.close()
                    return status
                f.close()

            with open(PORTSCONF_V, 'w') as f:
                sys.stdout.write('Triggered switchd for ports.conf validation\n')
                f.write("4") # 4 is trigger to start validation
                f.close()

        else:
            sys.stdout.write('ports validation node file is not accessible')
            status = 2
            return status

        sys.stdout.flush()
        # now, wait for switchd to finish processing the port config
        timeout = 150
        count = 0
        while count < timeout:
            # checking switchd port breakout state machine
            with open(PORTSCONF_V, 'r') as f:
                state = int(f.read())
            sys.stdout.flush()

            # switchd has finished port config, safe to return
            if state == 2: # 2 is VALIDATION FAILURE
                status = 0
                sys.stdout.write('switchd ports.conf validation failed\n')
                break
            elif state == 3: # 3 is VALIDATION SUCCESS
                sys.stdout.write('switchd ports.conf validation successful\n')
                status = 1
                break

            count = count + 1
            time.sleep(1)

        if count >= timeout:
            sys.stdout.write('validate_portsconf taking more than %d secs\n' % count)
            status = 0;

        with open('/cumulus/switchd/run/validate_portsconf/state', 'w') as f:
            f.write("0")
            f.close()

        sys.stdout.flush()
        return status



if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Update Cumulus port configuration based on /etc/cumulus/ports.conf')
    parser.add_argument('-f', '--force',
                        required=False,
                        action='store_true',
                        help='Force update, even if switchd is running.')
    parser.add_argument('-i', '--init',
                        required=False,
                        action='store_true',
                        help='Initialize config files')
    parser.add_argument('-v', '--verbose',
                        required=False,
                        action='store_true',
                        help='Verbose output')
    parser.add_argument('-e', '--exploded',
                        required=False,
                        action='store_true',
                        help='Created exploded port map')
    parser.add_argument('-m', '--mac_mode',
                        required=False,
                        default='standard',
                        choices=['l3-only','standard'],
                        help='Default mac assignment mode,'
                        'l3-only will replicate the same mac on all ports'
                        'standard will use incremental macs for each port')
    parser.add_argument('-a', '--acpi',
                        required=False,
                        action='store_true',
                        help='Use ACPI for platform information')
    parser.add_argument('-w', '--warm',
                        required=False,
                        action='store_true',
                        help='Hitless port breakout configuration')
    parser.add_argument('-s', '--sanitiseportsconf',
                        required=False,
                        action='store_true',
                        help='Sanitise ports.conf breakout config')

    try:
        args = parser.parse_args()
    except ArgParseError as e:
        parser.error(str(e))

    try:
        platform = cumulus.platforms.probe(use_acpi=args.acpi)
    except cumulus.platforms.NoSuchPlatform as e:
        sys.stderr.write('WARNING: unknown platform: %s\n' % str(e))
        sys.exit(0)

    if platform.switch is None:
        sys.stderr.write('WARNING: no switching ASIC\n')
        sys.exit(0)

    if args.verbose:
        sys.stdout.write('platform: %s\n' % platform)

    if args.warm and platform.switch.chip.sw_base != 'mlx':
        sys.stderr.write('WARNING: platform: %s hitless port breakout is not supported\n' % platform)
        sys.exit(0)

    # switchd port config is managed using a portconfig state machine,
    # with following states
    #
    #  PORTCONFIG_IDLE      = 0 ==> state machine is idle i.e. it has
    #                               processed successfully previous
    #                               port config
    #  PORTCONFIG_RUNING    = 1 ==> update-ports started creating config
    #  PORTCONFIG_AVAILABLE = 2 ==> update-ports finished creating config,
    #                               switchd ready to process config
    # notify switchd we are starting port breakout config
    if args.warm or args.sanitiseportsconf:
        if platform.name == 'cumulus,vx':
            sys.exit(0)
        valid_status = validate_portsconf()
        
        if valid_status == 1:
                sys.stdout.write('ports validation successful \n')
                if args.sanitiseportsconf:
                    sys.exit(0)
        elif valid_status == 2:
                sys.stdout.write('switchd validate_node is absent/not ready \n')
                # if validate node is absent, switchd is dead/not started
                if args.sanitiseportsconf: 
                    sys.exit(-2)
                sys.exit(-1)
        else:
                sys.stderr.write('WARNING: ports validation failed, check switchd.log for failure reason')
                sys.exit(-1)

    if args.warm:
        sys.stdout.write('portconfig RUNNING\n')
        sys.stdout.flush()
        with open('/cumulus/switchd/run/portconfig/state', 'w') as f:
            f.write("1")

    pc = cumulus.portconfig.SDKConfig(platform, verbose=args.verbose,
                                      exploded=args.exploded)
    exit = update_config(pc, platform, args)

    # notify switchd that new config is ready for consume
    if args.warm:
        sys.stdout.write('portconfig AVAILABLE\n')
        sys.stdout.flush()
        with open('/cumulus/switchd/run/portconfig/state', 'w') as f:
            f.write("2")
        # now, wait for switchd to finish processing the port config
        #
        # we are having a max of 150 secs to let switchd to complete processing
        # the port breakout config. however, profile/debug data shows it takes
        # about a sec to complete the config
        # We have chosen 150 seconds since the periodic cb timer for idle_cb
        # trigger is 120 second. The processing of below file happens in idle_cb.
        # 120second + 30 second is the buffer for switchd to complete processing
        # of other event handler in the boundary condition.
        timeout = 150
        count = 0
        while count < timeout:
            # checking switchd port breakout state machine
            with open('/cumulus/switchd/run/portconfig/state', 'r') as f:
                state = int(f.read())
            sys.stdout.write('portconfig count %d state %d\n' % (count, state))
            sys.stdout.flush()
            # switchd has finished port config, safe to return
            if state == 0:
                sys.stdout.write('portconfig IDLE\n')
                break
            count = count + 1
            time.sleep(1)

        if count < timeout:
            sys.stdout.write('portconfig took %d secs to complete\n' % count)
        else:
            sys.stdout.write('portconfig taking more than %d secs\n' % count)
        sys.stdout.flush()

    sys.exit(exit)
