#! /usr/bin/env python

import copy
import sys

import isi.hw.check.lib.args as argslib
import isi.hw.check.lib.barcode as barcode
import isi.hw.check.lib.mellanox as iblib
import isi.hw.check.lib.vpd as vpdlib
import isi.hw.dongle as hwdongle
import isi.hw.hal as hal
import isi.hw.version as hwver

_verbose = True

def verbose(msg):
    if _verbose:
        print msg

def print_dict(title, new, old=None, print_old=False):
    print title
    if new is None or len(new.keys()) == 0:
        print "  No entries"
    elif print_old and (old is None or len(old.keys()) == 0):
        print "  No entries"
    else:
        newkeys = new.keys()
        oldkeys = old and old.keys() or []
        keys = newkeys + oldkeys
        newkeys.sort(), oldkeys.sort(), keys.sort()
        mods = dict(zip(newkeys + oldkeys, [''] * (len(newkeys)+len(oldkeys))))
        if old:
            [ mods.update({
                k: ( k not in oldkeys and 'a'
                     or ( k not in newkeys and 'd'
                          or ( (old[k] != new[k]) and 'c'
                               or '' )))}) for k in keys ]
        output = []
        width = max(map(lambda x:len(x), keys))
        (d, keys) = print_old and (old, oldkeys) or (new, newkeys)
        [output.append("%1s %-*s : %s" % (mods[k],width,k,d[k])) for k in keys]
        for out in output:
            verbose(out)

def print_dict_diff(name, new, old):
    print_dict("Previous %s" % name, new, old, print_old=True)
    print_dict("New %s"      % name, new, old)

# These functions return error message on error, or None.
# If the error is fatal, sets args['fatal_error'].
# They update args['detected_hw'] dictionary.
#
# ===============================================================
def get_nvram_info(args):
    # What we need is tied to Isilon-specific NVRAM-IB card with
    # VSD info, and NOT to a particular HWGEN value.
    # Annapurna might not (won't) have a Isilon NVRAM-IB card.
    # Consider: Graham:
    # >>> (hwver.netiface_hasIB, hwver.infiniband, 
    #      hwver.infinibandName(hwver.infiniband))
    # (1, 5, 'ISI25208')
    # >>> (hwver.nvram, hwver.nvramName(hwver.nvram))
    # (3, 'MT25208')
    # Consider: Annapurna:
    # (1, 7, 'MT25208 DDR')
    # (0, 'Unknown')
    # Even though we store Isilon VPD info on an "NVRAM" card,
    # it's really stored on a (Belknap) NVRAM-IB card (so far).
    # This is not tracked or unique to a given hwver HWGEN,
    # but rather the IB/NVRAM card type.  We can't differentiate
    # hwver NVRAM card type by Isilon vs Vendor, only by the chip
    # type (e.g., MT25208 or MM5425), so we can't determine if we
    # have Isilon VPD on the card by the hwver.nvram value.
    # What we CAN do is check the hwver.infiniband card type -- 
    # even though this function is getting so-called "nvram_info".
    # We know which IB cards so far support Isilon VPD data...
    ib_card_supported_vpd = [ hwver.IB_ISI25208 ]
    if not hal.getInfinibandType() in ib_card_supported_vpd:
        verbose("No VPD data needed for NVRAM.")
        return None
    data = {}
    # Map desired entry to entry in Mellanox data.
    verbose("Reading VPD data for NVRAM...")
    flint = iblib.Flint()
    if flint is None:
        args['fatal_error'] = True
        return "error initializing NVRAM info"
    info = flint.info()
    if info is None:
        args['fatal_error'] = True
        return "error reading NVRAM info"
    translation = iblib.get_VSD_map()
    for key in translation.keys():
        data[translation[key].name()] = info[key]
    args['detected_hw'].update(data)
    verbose("Read VPD data for NVRAM.")
    return None

def get_fcb_info(args):
    # XXX - use a better method?  perhaps some hwver?
    supported_chassis = [
        hwver.HWV_CHASSIS_ISI12BAY,
        hwver.HWV_CHASSIS_ISI4BAY,
        hwver.HWV_CHASSIS_ISI36BAY,
        ]
    eeprom_vpd_keys = [
        barcode.BelknapFCBSerial().name(),
        barcode.BelknapFCBConfig().name(),
        ]
    error = None
    if not hal.getChassisType(0) in supported_chassis:
        verbose("No VPD data needed for FCB.")
        return None
    verbose("Reading VPD data for FCB.  This may take a moment...")
    data = {}
    vpd = args['vpd']
    # init_vpd() has already called: eeprom_info_init ~ eeprom_read_info
    eeprom_info = vpd.get_eeprom_info()
    eeprom_missing_keys = []
    for key in eeprom_vpd_keys:
        if key in eeprom_info:
            data[key] = vpd.get_eeprom_info_key_value(key)
        else:
            data[key] = None
            eeprom_missing_keys.append(key)
    if len(eeprom_missing_keys):
        error = ("EEPROM %s info not on EEPROM." %
                 str(eeprom_missing_keys)[1:-1])
        # Note this is NOT a 'fatal_error'; see main(). Fall-through,
        # with error set, and save what we DID find to return.
    args['detected_hw'].update(data)
    args['eeprom_missing_keys'] = eeprom_missing_keys
    verbose("Read VPD data for FCB.")
    return error

# ===============================================================
def init_vpd(args):
    """Returns None on success, error message otherwise. Any error is fatal."""
    error = None
    vpd = vpdlib.VPD(options={
        'verbose': args['verbose'],
        'debug': False,
        'unit_eeprom_test': False,
        })
    if vpd is None:
        return "Error initializing VPD object"
    verbose("Reading data from VPD file...")
    error = vpd.file_info_init()
    if error:
        error = "Error reading file VPD info: %s" % error
    else:
        file_vpd = vpd.get_file_info()
        if file_vpd is None:
            error = "Error reading file VPD info"
    if error:
        return error
    verbose("Reading data from EEPROM.  This may take a moment...")
    error = vpd.eeprom_info_init()
    if error:
        error = "Error reading EEPROM VPD info: %s" % error
    else:
        eeprom_vpd = vpd.get_eeprom_info()
        if eeprom_vpd is None:
            error = "Error reading EEPROM VPD info"
    if error:
        return error
    args.update({
        'vpd':            vpd,
        'old_file_vpd':   file_vpd.copy(),
        'old_eeprom_vpd': copy.deepcopy(eeprom_vpd),
        })
    return None

# ===============================================================

# Dictionary of tracked hardware and functions for retreiving their tracked
# information such as serial number.  The get_*_info functions return a dict
# of information to be merged into the vpd.
tracked_hw = {
    'nvram':
    {'get': get_nvram_info,
     },
    'fcb' :
    {'get': get_fcb_info,
     },
    }

opts_desc = [
    ('-f, --force',     "Force prompting (no confirms)"),
    ('-h, --help',      "Show this text"),
    ('-n, --neweeprom', "New FCB EEPROM installed"),
    ('-q, --quiet',     "Don't print verbose output"),
    ]

fulloptlist = map(lambda c:"%s" % c[0], opts_desc)
argwidth = 1 + max(map(lambda x:len(x), fulloptlist))
fmt_opt = lambda (o,d): " %-*s: %s" % (argwidth,o,d)
disp_opts = "\n".join(map(fmt_opt, opts_desc))

def main(options):
    global _verbose
    result = 0
    optargs = argslib.Args(options)
    optargs.process(sys.argv[1:])
    args = {}
    [ args.update({key:optargs[key]}) for key in optargs.keys() ]

    if args['quiet']:
        _verbose = False
    _force = args['force']
    _neweeprom = args['neweeprom']

    args.update({
        'verbose': _verbose,
        'error': None, # reported error messages
        'fatal_error': False,
        'detected_hw': {},
        'eeprom_missing_keys': [],
        })

    verbose("Rescanning hardware...")
    if _neweeprom:
        verbose("Scanning with new FCB EEPROM...")

    # Get the VPD object setup adequately
    # Includes reading EEPROM and file, creating old vpd copies
    error = init_vpd(args)
    if error:
        print error
        sys.exit(-1)
    vpd = args['vpd']
    _vpdfile = vpd.file

    # One special requirement here.  The tracked hw function for the FCB
    # must return its info directly from the FCB EEPROM before anything
    # else is done to it.
    for hw in tracked_hw:
        error = tracked_hw[hw]['get'](args)
        if error:
            print "Error getting VPD data for %s" % hw
            print "Error: %s" % error
            if args['fatal_error']:
                sys.exit(-1)
            else:
                result = 1
    detected_hw = args['detected_hw']

    def print_vpd_update_message():
        print "  To add the correct key/value information for missing data,"
        print "  you can edit %s, followed by:" % _vpdfile
        print "  isi_dongle_sync -p"

    # Handle an FCB EEPROM which doesn't have it's VPD stored on it.
    # (bug 34660)
    eeprom_missing_keys = args['eeprom_missing_keys']
    if len(eeprom_missing_keys):
        prompt = ""
        print "\n**ATTENTION**"
        print "  Necessary VPD data is missing from EEPROM.  You may "
        print "  either write blank information to the EEPROM or quit "
        print "  and enter the correct information by hand.\n"
        while prompt.lower() not in ['yes', 'no']:
            prompt = raw_input("Write blank information to EEPROM(yes, no)? ")
        if prompt.lower() == 'yes':
            for key in eeprom_missing_keys:
                detected_hw[key] = ''
            print "Using blank values for missing EEPROM data."
            print_vpd_update_message()
        else:
            print_vpd_update_message()
            sys.exit(-1)

    # Do our sync: new data is from the scan (detected_hw).
    # priority is either file, or eeprom, depending on new_eeprom:
    # if new_eeprom:
    #   priority = file  : file > EEPROM: EEPROM lowest priority
    # else:
    #   priority = eeprom: file < EEPROM:   file lowest priority
    # In any case, we retain and use our original FCB-specific EEPROM
    # data ('old_eeprom_vpd'), which is identically in the detected_hw
    # scanned data, for our FCB update.
    error,info = vpd.sync_vpd(['eeprom','file'][_neweeprom],
                              new_vpd_data=detected_hw)
    if error:
        print error
        sys.exit(-1)

    (  file_changed,   file_vpd,   old_file_vpd) = (vpd._file_needs_update,
                                                    vpd.get_file_info(),
                                                    args['old_file_vpd'])
    (eeprom_changed, eeprom_vpd, old_eeprom_vpd) = (vpd._eeprom_needs_update,
                                                    vpd.get_eeprom_info(),
                                                    args['old_eeprom_vpd'])

    save = _force
    if (file_changed or eeprom_changed) and not _force :
        verbose("Please make sure everything looks correct"
                " (c=change, d=delete, a=add)")
        print_dict("Current Hardware VPD data", detected_hw)
        if file_changed:
            print_dict_diff("%s VPD data" % _vpdfile, file_vpd, old_file_vpd)
        else:
            verbose("%s VPD data unchanged" % _vpdfile)
        if eeprom_changed:
            print_dict_diff("EEPROM data", eeprom_vpd, old_eeprom_vpd)
        else:
            verbose("EEPROM VPD data unchanged")
        prompt = ""
        while prompt.lower() not in ['yes','no']:
            prompt = raw_input("Save these changes(yes, no)? ")
        if prompt.lower() != 'yes':
            print "Aborting hardware rescan"
            sys.exit(1)
        else:
            save = True

    if save:
        # Write updates if needed
        if eeprom_changed:
            err = vpd.eeprom_update(force=True)
            if err:
                result = 1
                print err
        if file_changed:
            err = vpd.file_update(force=True)
            if err:
                result = 1
                print err
        verbose("Syncing local files...")
        errors = hwdongle.dongle_user_init()
        if errors:
            print "Failed dongle sync"
            for error in errors:
                print error
            result = 1
        else:
            error = hwdongle.dongle_push()
            if error:
                print "Failed dongle sync"
                result = 1
    else:
        print "No changes to hardware detected."

    return result

if __name__ == '__main__':
    usage = \
"""Usage: %s [OPTIONS]

OPTIONS:
%s

Isilon Hardware Rescan Tool.  Used to update Vital Product Data (VPD) when
Field Replaceable Units (FRUs) are changed.
""" % (argslib.appname(), disp_opts)

    options = {
    'force':
        {'opts'    : ['f', 'force'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        
    'help':
        {'opts'    : ['h', 'help'],
         'func'    : argslib.help_func(usage),
         },
        
    # Normally EEPROM VPD wins.  Overturn this on neweeproms
    'neweeprom':
        {'opts'    : ['n', 'neweeprom'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        
    'quiet':
        {'opts'    : ['q', 'quiet'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        }

    sys.exit(argslib.run(lambda :main(options), usage))
