#! /usr/bin/env python
import re
import os
import sys
import time

# make sure the mpt driver is loaded, bug 48501
# These statements MUST be first in the file so that if the mpt driver is
# unloaded it will get loaded prior to the hardware being scanned and disk 
# test availablity is determined.  
import isi.hw.check.lib.loadUnloadLsi as ldm
ldm.loadLsiDriver()

import isi.app.lib.procs as procs
import isi.hw.hal as hal
import isi.hw.version as hwver

import isi.hw.check.lib.args as argslib
import isi.hw.check.lib.cto as cto
import isi.hw.check.lib.filenames as files
import isi.hw.check.lib.log as loglib
import isi.hw.check.lib.menu as menulib
import isi.hw.check.lib.mfg as mfg
import isi.hw.check.lib.misc as misc
import isi.hw.check.lib.ramdisk as ramdisklib
import isi.hw.check.lib.stats as stats
import isi.hw.check.lib.update as update
import isi.hw.check.lib.version as hwcheck_ver

import isi.hw.check.mfg.config.config as configlib
import isi.hw.check.mfg.config.noop as noop_config
import isi.hw.check.mfg.human.text as text
import isi.hw.check.mfg.options as optionslib

def main():
    usage = optionslib.get_usage()
    options = optionslib.get_options(usage)
    return argslib.run(lambda :run(options), usage)

def early_exit(status):
    # Ensure we cleanup (remote logging, mounts, final fail count, etc)
    if loglib.is_init():
        import isi.hw.check.mfg.menu.exit as exitmod
        exitmod.run(status=status, final_stats=False)
    else:
        sys.exit(status)

def run(options):
    # Process command line args
    def restricted_error_handler(e, arg, value):
        log = loglib.get_logger()
        log.out('Invalid remote option: %s' % arg)
    args = argslib.RestrictedArgs(options, restricted_error_handler)
    args.process(sys.argv[1:])

    # Init lib.{filenames,log}
    if not files.init():
        early_exit(1)
    loglib.init(args['logdir'])
    log = loglib.get_logger()
    log.debug_level(args['debug_level'])

    # Start as mfg mode (may well change with menu choices; see mfg.config)
    misc.set_mode('mfg')

    # Run the test config menu to find out
    # which test the user wishes to perform
    config = get_test_config(args)

    # Init mac addr info
    mac_addr = mfg.init_mac_addr()
    if mac_addr == None:
        early_exit(0)

    # Log test start time
    log.out(time.asctime(time.localtime(log.start_time())))
    log.simple_out('')

    # Note log dir file
    log.out('Logging to local directory: %s' % log.orig_logdir())
    log.simple_out('')

    # Do any config-specific early initialization. This
    # typically includes scanning/verifying serial numbers,
    # and anything else that can/needs to be done early
    # on. This function also returns an identifier string
    # that's used to create a unique logging directory
    # for the piece of hardware we're testing on the
    # logserver if we're doing remote logging.
    id = config.early_init()

    # Setup remote logging, if desired
    # Note this will switch logging from prev local logdir to NFS mount
    if not args['no_remote_logging']:
        if log.config_remote_logging(id, async=args['async']) != 0:
            early_exit(0)

    # Do any config-specific post-remote logserver setup
    # initialization (i.e. anything that needs to be done
    # after remote logging has been setup, but before stats
    # have been gathered)
    if config.post_logserver_init():
        early_exit(1)

    # Handle text input request
    if args['do_text']:
        text.run()
        log.simple_out('')

    # Save away hidden nodeburn diskscrub override options to log
    for option in ['diskscrub_full', 'diskscrub_mult']:
        log.set_data_value(option, args[option])

    # Check for and install CTO CMDB files, and update node inventory
    # By default, will use logserver IP, user, and local dir
    # Save away skip_inventory option to log for safe.id.inventory, etc
    log.set_data_value('skip_inventory', args['skip_inventory'])
    if cto.supportsCto():
        cto_result = 0
        cto_options = cto.get_options()
        if args['debug_level']:
            cto_options['debug'] = args['debug_level']
        if args['debug_unhandled_exceptions']:
            if not cto_options['debug']:
                cto_options['debug'] = 1
        if hasattr(config, 'order_number'):
            cto_options['order'] = config.order_number()
        else:
            cto_options['order'] = None # Default None ==> No receipt udpate
        def cto_option_update(opt):
            arg = 'cmdb_%s' % opt
            args[arg] and cto_options.update({opt:args[arg]})
        map(cto_option_update, ['method','ip','user'])
        # It's possible to set CMDB options on the command line (args),
        # but it's also possible for mfg.config.cto to use lib.barcode,
        # as is done for the args['chassisconfig'] case.  Check both.
        if args['chassisconfig']:
            # Force any CMDB access WARNING errors to be FAILED
            cto_options['required'] = True
            import isi.hw.check.lib.barcode as barcodelib
            cmdb_barcodes = [
                ('method', barcodelib.CmdbMethod()),
                ('ip', barcodelib.CmdbServer()),
                ('user', barcodelib.CmdbUser()),
                ]
            for (option, barcode) in cmdb_barcodes:
                value = barcode.get()
                if barcode.check(value):
                    cto_options[option] = value
            server = cto.CMDB_SERVER
            server_ip = cto_options['ip']
            server_user = cto_options['user']
            if (not cto_options['method'] == 'local' and
                log.config_remote_ssh_keys(server, server_ip, server_user)):
                log.fail('Unable to setup ssh keys at %s at %s' %
                         (server, server_ip), 0)
                # Continue anyway; User will need to enter multiple passwords
        if args['cmdb_local']:
            method = args['cmdb_method']
            local = args['cmdb_local']
            cto_result = cto.use_local_cmdb_directory(cto_options, method, local)
        if args['skip_inventory']:
            # "--skip-inventory" is a hidden debug option to skip EVERYTHING,
            # use CMDB_ACCESS_NONE.  Do not use CMDB_ACCESS_SKIP, which is
            # the isi_hw_check/isi_ovt_check access method, which only skips
            # CMDB ACCESS but does do Inventory updates and config checks.
            cto_options['method'] = cto.CMDB_ACCESS_NONE
        if cto_result == 0:
            cto_result = cto.cto_update(cto_options)
        if cto_result:
            log.out('')
            if args['chassisconfig']:
                log.fail('CTO updates failed', 0)
                early_exit(1)
            else:
                prompt = 'CTO updates failed, continue with test?'
                if log.prompt(prompt, yesno='n'):
                    log.simple_out('')
                    log.fail('Stopping due to user abort', 0)
                    early_exit(1)
        log.simple_out('')

    # XXX_CTO Moved this down here from above, to AFTER CMDB udpates
    # If we have run with --newserial, we want to stop after scan,
    # update, and auto-copy of logs.
    # (If a full run is desired with the new serial numbers, re-run)
    if args['newserial']:
        log.out('Forcing new serial and config numbers complete')
        early_exit(0)
    # Ditto with --newconfig.
    if args['newconfig']:
        log.out('Forcing new system config number complete')
        early_exit(0)
    # And finally, similar with --chassiconfig (isi_update_cto).
    if args['chassisconfig']:
        log.out('CTO Node Configuration complete')
        early_exit(0)

    # Check for available hwcheck lib updates, and
    # prompt user to auto-update if applicable
    build = misc.get_build()
    update_result = update.prompt_for_auto_update(build)
    if update_result == update.RequiredReboot:
        log.out('Rebooting...')
        (error, output) = procs.get_cmd_output(files.commands['reboot'])
        if error:
            log.fail('Reboot failed:', 0)
            for line in output:
                log.out('- %s' % line)
            else:
                early_exit(0)
    log.simple_out('')

    # Gather and log stat info
    stats_skipped = False
    stats.fail_on_error = not (args['admin_mode'] or args['ignore_stats'])
    stats_to_skip = config.get_stats_to_skip()
    if stats_to_skip == 'all':
        stats_skipped = True
        stats.init_stats()
    else:
        stats_to_skip = [x['name'] for x in stats_to_skip]
        if not stats.fail_on_error:
            log.out('Treating stat failures as warnings', 'NOTE')
        stats_rc = stats.log_stats(stats_to_skip,
                                   fake_stats_dir=args['fake_stats_dir'])
        if stats_rc != 0 and stats.fail_on_error:
            log.fail('Stopping due to stat failures', 0)
            early_exit(1)
    log.simple_out('')

    # Process remotely specified options
    if log.remote_ip():
        if args['no_remote_opts']:
            log.out('Ignoring remote options due to --no-remote-opts', 'NOTE')
        elif stats_skipped:
            pass # No log message
        else:
            try:
                remote_opts = stats.get_remote_opts(build)
                remote_opts and args.process_restricted(remote_opts)
            except argslib.Error, e:
                if e.message:
                    log.fail(e.message, 0)
                    early_exit(e.exitCode)
        log.simple_out('')

    # Log if we're in admin mode
    if args['admin_mode']:
        # See bug 76603. Long term, we should change this to be a
        # FAILURE for CMs, and only internal usage will be a NOTE.
        # To make the transition a little less painful for Ops/CMs,
        # only implement this for 6.5.2 CHOPROCKS CTO case at first.
        if cto.supportsCto():
            # if Location code has been scanned, it will be available
            import isi.hw.check.lib.barcode as barcodelib
            import isi.hw.check.lib.sysconfig as sysconfig
            location = barcodelib.Location()
            internal = sysconfig.location_is_internal(location.get())
        else:
            internal = True # Not really, but for non-CTO case...
        if internal:
            log.out('Running in admin mode', 'NOTE')
        else:
            log.fail('Running in admin mode', 0)
        log.simple_out('')

    # Do any config-specific late initialization (i.e. anything
    # that needs to be done after remote logging has been setup
    # and stats have been gathered
    config.late_init()

    # Run id tests for the given config
    if not args['skip_identify']:
        # It's possible that earlier actions could have failed
        pre_id_fail_count = log.fail_count()
        run_id_tests(config.get_id_tests())
        
        # Note failures
        if (log.fail_count() > pre_id_fail_count and
            not args['ignore_id_failures']):
            log.fail('One or more identification tests failed', 0)
            log.out('Please correct the errors and run the test again')
            early_exit(1)

    # Make sure exit_on_fail is turned on for the rest of the tests
    log.exit_on_fail = 1

    # If given specific modules to run via the command line,
    # run them and exit. Otherwise, run the menu for the chosen
    # config.
    if args['modules_to_run']:
        run_modules_and_exit(args['modules_to_run'])
    else:
        # Run the menu
        menu = config.get_menu()
        menu.run()

    if log.fail_count() > 0:
        result = 1
    else:
        result = 0

    return result


# get_test_config_modinfos_cto_config is required for isi_update_cto script,
# see 'chassisconfig' usage in get_test_config().
def get_test_config_modinfos_cto_config():
    if hal.isGenericNode():
        return
    if not cto.supportsCto():
        return
    modinfos = [
        {'name'      : 'mfg.config.cto',
         'args'      : {'config_mode' : 'CTO Node Configuration'},
         },
        ]
    return modinfos

def get_test_config_modinfos_cto():
    if hal.isGenericNode():
        return
    if not cto.supportsCto():
        return
    modinfos = [
        {'name'      : 'mfg.config.cto',
         'args'      : {'config_mode' : 'Base Chassis Burn-In'},
         },
        {'name'      : 'mfg.config.cto',
         'args'      : {'config_mode' : 'Assembly Verification'},
         },
        {'name'      : 'mfg.config.cto',
         'args'      : {'config_mode' : 'Nodeburn'},
         },
        ]
    return modinfos

def get_test_config_modinfos_noncto():
    if hal.isGenericNode():
        return
    if cto.supportsCto():
        return
    modinfos = [
        {'name'      : 'mfg.config.node',
         'args'      : {'config_mode' : 'Wake-Up',
                        'head_only'   : True},
         },
        {'name'      : 'mfg.config.node',
         'args'      : {'config_mode' : 'Wake-Up',
                        'head_only'   : False},
         'skip_func' : lambda :(hal.getDskctlFamily() != 
                                hwver.DISKCTL_FAMILY_LSI),
         },
        {'name'      : 'mfg.config.node',
         'args'      : {'config_mode' : 'Burn-In',
                        'head_only'   : True},
         },
        {'name'      : 'mfg.config.node',
         'args'      : {'config_mode' : 'Burn-In',
                        'head_only'   : False},
         'skip_func' : lambda :(hal.getDskctlFamily() !=
                                hwver.DISKCTL_FAMILY_LSI),
         },
        ]
    return modinfos

def get_test_config_modinfos_other():
    # We support FUT test menus in PCITH Generic nodes
    if hal.isGenericNode():
        modinfos = [
            {'name'      : 'mfg.config.fut',
             'submenu'   : True,
             },
            {'name'      : 'mfg.config.fru',
             'submenu'   : True,
             },
            ]
        return modinfos
    modinfos = [
        {'name'      : 'mfg.config.legacy',
         'submenu'   : True,
         'skip_func' : lambda : cto.supportsCto(),
        },
        {'name'      : 'mfg.config.fut',
         'submenu'   : True,
         },
        {'name'      : 'mfg.config.fru',
         'submenu'   : True,
         },
        {'name'      : 'mfg.config.utilities',
         'submenu'   : True,
         },
        ]
    return modinfos

def get_test_config_menu(args):

    # Config modules
    modinfos_list = [
        # We are NOT providing get_test_config_modinfos_cto_config
        # menu choices by intent. This is only available via isi_update_cto
        # script (args['chassisconfig']).
        get_test_config_modinfos_cto(),
        get_test_config_modinfos_noncto(),
        get_test_config_modinfos_other(),
        ]

    info = [
        'Isilon Manufacturing Check Tool',
        menulib.divider(),
        ]

    # XXX_CTO : Read the descriptions below. Ops request to break up
    # current FUT/FRU tests into submenus is going to have an impact
    # on how this "id tests for -m modules" behavior works...
    if args['modules_to_run']:
        info += [
            'Please choose the type of id tests you would like to run',
            'before running the modules specified on the command line:',
            ]
    else:
        info += [
            'Please choose the type of hardware',
            'you intend to test:'
            ]

    return menulib.get_test_config_menu(args, info, modinfos_list)

def get_test_config(args):
    log = loglib.get_logger()

    # Set any global test options in internal logger dictionary
    test_options_None_for_default = [
        'bootflash_drives', 'bootflash_reads',
        'bootflash_wipes', 'bootflash_writes',
        ]
    test_options_always_set = [
        ]
    for option in test_options_None_for_default:
        try:
            if args[option] is not None:
                log.set_test_option(option, args[option])
        except KeyError:
            pass
    for option in test_options_always_set:
        try:
            log.set_test_option(option, args[option])
        except KeyError:
            pass

    if args['chassisconfig']:
        info = [
            'Isilon CTO Node Configuration Tool',
            ]
        [ log.out(line) for line in info ]
        log.out('')
        log.out('Hit Return (blank entry) for options at any prompt', 'NOTE')
        log.out('Refer to the --help output for list of options and values')
        log.out('Ctrl-C to abort at any time')
        log.out('')
        log.out(menulib.divider())
        modinfos = get_test_config_modinfos_cto_config()
        if not modinfos:
            log.fail('This node is not a CTO node.', 0)
            early_exit(1)
        modinfos_list = [ modinfos ] # Need list of modinfos for code
        menu = menulib.get_test_config_menu(args, info, modinfos_list)
        config = menu.run_given_item(1)
        if not isinstance(config, configlib.Config):
            log.fail('Invalid Menu Choice', 0)
            early_exit(1)
        return config

    elif args['modules_to_run'] and args['skip_identify']:
        # We have modules to run and we're skipping the id tests,
        # so we have no need for a real test config. Thus we just
        # return one that just no-ops everything of interest.
        # Note this means "id = config.early_init()" is now no-op'ed,
        # and we won't be able to lib.log.config_remote_logging();
        # Simplest solution to this failure is to disable remote
        # logging for this case:
        args['no_remote_logging'] = True
        class NoopMenu:
            def run(self):
                return noop_config.Config(args)

        menu = NoopMenu()
    else:
        menu = get_test_config_menu(args)

    # Run the menu until we get a choice, or the user exits.
    while 1:
        config = menu.run()
        if config == None:
            # ctrl+d. Run the menu again.
            pass
        elif isinstance(config, configlib.Config):
            break
        elif isinstance(config, menulib.Menu): # or RestrictedMenu or etc?
            menu = config
        else:
            log.out('Invalid Choice - Re-Enter')
        # Run the menu again.
        log.simple_out('')
        log.simple_out('')

    return config
    
# cf. misc.run_modules()
def run_modules_and_exit(modinfos):
    log = loglib.get_logger()
    error = 0
    for modinfo in modinfos:
        modname = modinfo['name']
        try:
            modargs = modinfo['args']
        except KeyError:
            modargs = {}
        try:
            module = misc.dynamic_import(misc.full_module_name(modname))
            result = misc.run_module(module, modargs)
            if result != 0:
                error = 1
        except ImportError:
            print "ERROR: no such module '%s'" % modname
            error = 1
    log.close_remote_logging()
    sys.exit(error)

def run_id_tests(modinfos):
    log = loglib.get_logger()
    
    # Don't exit on failure for id tests
    log.exit_on_fail = 0

    # XXX: NOTE: look at misc.run_modules() -- slight re-
    # ordering (load/run/load/run/... rather than two loops)
    # but otherwise the same, PLUS better error checking
    # (log.fail with specific error importing module msg)

    # Load appropriate modules
    infos = []
    for modinfo in modinfos:
        modname = modinfo['name']
        try:
            modargs = modinfo['args']
        except KeyError:
            modargs = {}
        mod = misc.dynamic_import(misc.full_module_name(modname))
        infos.append((mod, modargs))

    # Run them
    for (mod, modargs) in infos:
        misc.run_module(mod, modargs)

#
# Start running here
#
if __name__ == '__main__':
    sys.exit(main())
