#!/usr/bin/python
# Copyright 2012,2013,2014,2015,2016,2017,2018,2019,2020 Cumulus Networks, Inc.  All rights reserved.
#
# cl-update-ports --
#
#     Read /etc/cumulus/ports.conf and update /etc/bcm.d/*  Intended to be run
#     at boot, or when /etc/cumulus/ports.conf changes.  Idempotent.
#
#     Also generates /var/lib/cumulus/porttab, a machine readable file mapping
#     SDK interface names to Linux interface names.
#

import os
import time
import sys
import argparse
import subprocess

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(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)
    pc = cumulus.portconfig.SDKConfig(platform, verbose=args.verbose,
                                      exploded=args.exploded)

    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 = {}
        for line in cardsconf.readlines():
            line = line.strip()

            if '#' in line:
                continue
            slotinfo = line.split()
            lc_map[slotinfo[0]] = slotinfo[3]

        cardsconf.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):
            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)
    if args.verbose:
        sys.stdout.write('write /var/lib/cumulus/porttab\n')
    porttab = open('/var/lib/cumulus/porttab', 'w')
    porttab.write(autogen)
    porttab.write(pc.output('porttab') + '\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(pc.output('phytab') + '\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(pc.output('sfptab') + '\n')
    sfptab.close()

    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 == 'bcm':
        for unit in pc.port_units:
            if args.verbose:
                sys.stdout.write('write /etc/bcm.d/rc.ports_0\n')
            rcports = open('/etc/bcm.d/rc.ports_%d' % (unit,), 'w')
            rcports.write(autogen)
            rcports.write(pc.output('rcports_%d' % (unit)) + '\n')

        config_dir = '/etc/bcm.d/config.d'
        if not os.path.exists(config_dir) :
            os.makedirs(config_dir)

        fname = '/etc/bcm.d/config.d/02sdk.bcm'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('sdk')
        if file_string != None :
            sdk = open(fname, 'w')
            sdk.write(autogen)
            sdk.write(file_string + '\n')
            sdk.close()

        if args.verbose:
            sys.stdout.write('write /etc/bcm.d/config.d/10phy-ucode.bcm\n')
        ucodebcm = open('/etc/bcm.d/config.d/10phy-ucode.bcm', 'w')
        ucodebcm.write(autogen)
        ucodebcm.write(pc.output('ucodebcm') + '\n')
        ucodebcm.close()

        if args.verbose:
            sys.stdout.write('write /etc/bcm.d/config.d/11ports.bcm\n')
        portsbcm = open('/etc/bcm.d/config.d/11ports.bcm', 'w')
        portsbcm.write(autogen)
        portsbcm.write(pc.output('portsbcm') + '\n')
        portsbcm.close()

        chip = platform.switch.chip
        if not (isinstance(chip, cumulus.platform.Qumran_88375_Chip) or \
            isinstance(chip, cumulus.platform.Qumran_88370_Chip)):
            portwd = '/usr/sbin/portwd'
            if os.path.isfile(portwd) and os.access(portwd, os.X_OK):
                fname = '/etc/bcm.d/config.d/12portwd.bcm'
                portwdbcm = open(fname, 'w')
                if args.verbose:
                     sys.stdout.write('write %s\n' % fname)
                try:
                    subprocess.call((portwd, '--config'), stdout=portwdbcm)
                except subprocess.CalledProcessError, e:
                    sys.stderr.write('error: failed to launch portwd: %s' % str(e))
                    sys.exit(-1)

        if args.verbose:
            sys.stdout.write('write /etc/bcm.d/rc.led\n')
        if not platform.portleds:
            rcled = open('/etc/bcm.d/rc.led', 'w')
            rcled.write(autogen)
            rcled.write(pc.output('rcled') + '\n')

        if args.verbose:
            sys.stdout.write('write /etc/bcm.d/rc.phy\n')
        rcphy = open('/etc/bcm.d/rc.phy', 'w')
        rcphy.write(autogen)
        rcphy.write(pc.output('rcphy') + '\n')

        datapath_dir = '/etc/bcm.d/datapath'
        if not os.path.exists(datapath_dir) :
            os.makedirs(datapath_dir)

        fname = datapath_dir + '/rc.forwarding'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('rcforwarding')
        if file_string != None :
            rcforwarding = open(fname, 'w')
            rcforwarding.write(autogen)
            rcforwarding.write(file_string + '\n')

        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')

        fname = datapath_dir + '/hw_desc'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('hwdesc')
        if file_string != None :
            hwdesc = open(fname, 'w')
            hwdesc.write(autogen)
            hwdesc.write(file_string + '\n')

        fname = datapath_dir + '/uft.json'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('uft')
        if file_string != None :
            uft = open(fname, 'w')
            uft.write(file_string + '\n')

        fname = datapath_dir + '/riot.json'
        if args.verbose:
            sys.stdout.write('write %s\n' % fname)
        file_string = pc.output('riot')
        if file_string != None :
            riot = open(fname, 'w')
            riot.write(file_string + '\n')

        for chain in pc.led_chains:
            asm_filename = '/etc/bcm.d/led%d.asm' % chain
            hex_filename = '/etc/bcm.d/led%d.hex' % chain
            if args.verbose:
                sys.stdout.write('write %s\n' % asm_filename)
                sys.stdout.write('write %s\n' % hex_filename)
            asm_file = open(asm_filename, 'w')
            asm_file.write(autogen.replace('#', ';'))
            asm_file.write(pc.led_asm[chain])
            hex_file = open(hex_filename, 'w')
            hex_file.write(pc.led_hex[chain])

        be_file = open(backend_conf_fname, 'w')
        be_file.write(autogen)

        if platform.switch.chip.dnx:
            be_file.write('backend_lib = libhalbcmdnx.so, enum_fn = bcm_dnx_enum_backends')
        else:
            be_file.write('backend_lib = libhalbcm.so, enum_fn = bcm_enum_backends')

        be_file.close()

    elif 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()

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')

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

    try:
        platform = cumulus.platforms.probe(use_acpi=args.acpi)
    except cumulus.platforms.NoSuchPlatform, 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:
        sys.stdout.write('portconfig RUNNING\n')
        sys.stdout.flush()
        with open('/cumulus/switchd/run/portconfig/state', 'w') as f:
            f.write("1")

    exit = update_config(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 30 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
        timeout = 30
        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)
