#!/usr/bin/python
# Copyright 2012, 2013, 2014, 2015, 2016, 2017, 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)

    # 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/defaults_policy.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()

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

    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)

    exit = update_config(platform, args)
    sys.exit(exit)
