#!/usr/local/bin/python
import sys
import getopt
import isi.hw.hal as hal
import isi.app.lib.procs as procs
import isi.hw.check.lib.filenames as files

from isi.hw.hwmon.sel import THRESHOLDS

# Order matters
UPPER_THRESH = ['unc', 'ucr', 'unr']
LOWER_THRESH = ['lnc', 'lcr', 'lnr']

# refer to src/isilon/lib/isi_hw/cmcipmi/ipmi-sensor.h
RAW_THRESHOLDS = {
    'lnr' : 0x04,
    'lcr' : 0x02,
    'lnc' : 0x01,
    'unc' : 0x08,
    'ucr' : 0x10,
    'unr' : 0x20,
}

# Format of command:
#|NETFN|CMD  |CMC |CMC_SUB|SENSOR_NUM|VALUE|FLAG
# 0x2E  0xC0  0x81 0x02    0xXXXX     0xXX  0x00
SET_SENSOR_VAL_CMD = '2EC08102'

RESTORE_CMC_VAL_CMD = '2EC0810300'

def run_ipmicmc_cmd(cmd):
    ipmicmc = files.commands['isi_ipmicmc']
    error, output = procs.get_cmd_output("%s %s" % (ipmicmc, cmd))
    if error:
        print("isi_ipmicmc command failed: %s %s" % (ipmicmc, cmd))
    return error, output

sensors_default = {}

def init_sensor_defaults():
    global sensors_default
    sensors_default = hal.getRawDefaultSensors()

def get_sensors_default():
    global sensors_default
    if not sensors_default:
        init_sensor_defaults()
    return sensors_default

def dump_table():
    error, output = run_ipmicmc_cmd('-s -p t -a 20')
    if error:
        print('Unable to dump sensor table')
        return 1
    for line in output:
        print(line)
    return 0

# Running this will clear the last value overridden in the cmc
def clear_sensor_val(addr):
    cmd = "-V -a %x -R %s" % (addr, RESTORE_CMC_VAL_CMD)
    error, output = run_ipmicmc_cmd(cmd)
    if error:
        print("Failed to clear sensor override")
        return 1
    return 0

def clear_all_sensors():
    success = 0
    for sensor in get_sensors_default():
        if not clear_sensor(sensor):
            success += 1
    return success and 1 or 0

def clear_sensor(name, sev=None):
    rawsensor = get_sensors_default().get(name)
    if rawsensor is None:
        print('Could not find sensor %s' % name)
        return 1
    addr = rawsensor.get('owneraddr')
    sensornum = rawsensor.get('sensornum')
    result = 0
    sev_raw = RAW_THRESHOLDS.get(sev)
    # Check if sensor is of type 1. Type 1 is the only sensor
    # we change the values of.
    if rawsensor is None or (rawsensor.get('rectype', 0) != 1):
        result = 1
    # Make sure the severity is valid if specified
    elif sev is not None and sev_raw is None:
        result = 1
    else:
        print('Setting sensor %s to default' % name)
        cmcaddr = hal.getCMCIPMBAddr()
        if cmcaddr == addr:
            result = clear_sensor_val(addr)
        elif rawsensor.get('mask') != 0:
            validmask = rawsensor.get('mask')
            # want to set specific threshold back to default
            if sev is not None:
                validmask &= sev_raw
            result = set_raw_thresh(sensornum, addr, validmask, rawsensor)
        else:
            print("Sensor %s does not have threshold values" % name)
            result = 1
    return result


# XXX ERB If we move this code ('all' of it?) to isi.hw.hwmon.sel,
# then we can call it in this script .. but we can also write our
# own independent python code that uses it directly.  Why?  Consider
# test_hwmon, which system calls to isi_sel_inject a **LOT**, and
# every one  of these is hitting hal.getRawSensors() below, which
# takes ~ 5-10+ seconds, EACH.  If we were to just make this a
# (default=None) parameter, only call it when we needed -- optionally
# even cacheing it inside of isi.hw.hwmon.sel ?? -- then it would be
# a **LOT** faster !!!!!!!!!!!!!!!!
#
# Note: hal.getRawDefaultSensors() also takes several seconds.
#
# Note: I noticed the help for both getRawDefaultSensors and getRawSensors
# states it takes two args (given a sensornum and an address), but in
# fact neither takes any arguments.

def inject_sel_entry(name, sev, addr=None):
    upper = False
    rawsensors = hal.getRawSensors()
    rawsensor = rawsensors.get(name)
    config_sensor = get_sensors_default().get(name)
    # Check if sensor is of type 1. Type 1 is the only sensor
    # we change the values of.
    if rawsensor is None or (rawsensor.get('rectype', 0) != 1):
        print("Sensor %s not recognized by tool" % name)
        return 1
    sensornum = rawsensor.get('sensornum')

    if addr is None:
        addr = int(rawsensor.get('owneraddr'))
    sen_val = get_raw_reading(addr, sensornum)
    if sen_val is None:
        print("Unable to get raw sensor reading")
        return 1

    if sev in UPPER_THRESH:
        upper = True
    elif sev in LOWER_THRESH:
        upper = False
    else:
        print("Invalid threshold severity specified: %s" % sev)
        return 1

    # The CMC case is handled differently.
    # Instead of moving the thresholds, the sensor reading is modified.
    cmcaddr = hal.getCMCIPMBAddr()
    bmcaddr = hal.getBMCIPMBAddr()
    if addr == cmcaddr:
        error, output = set_sensor_value(sev, rawsensor, upper, name)
        return error
    elif addr != bmcaddr:
        return 1

    if upper:
        sev_index = UPPER_THRESH.index(sev)
        thresh_list = UPPER_THRESH[:sev_index + 1]
        boundary_sev = get_boundary_sev(sen_val, rawsensor, LOWER_THRESH, upper)
    else:
        sev_index = LOWER_THRESH.index(sev)
        thresh_list = LOWER_THRESH[:sev_index + 1]
        boundary_sev = get_boundary_sev(sen_val, rawsensor, UPPER_THRESH, upper)

    # Check if the severity can be modified by looking at the mask 
    if (RAW_THRESHOLDS.get(sev) & rawsensor.get('mask')) == 0:
        print('%s threshold cannot be modified for sensor %s' %
              (THRESHOLDS.get(sev), name))
        return 1

    boundary = rawsensor.get(boundary_sev)
    # Check if the boundary severity is applicable
    if (RAW_THRESHOLDS.get(boundary_sev) & rawsensor.get('mask')) != 0:
        # Threshold boundaries that are set with hysteresis are
        # handled differently. We need to issue IPMI raw commands
        # to properly get the proper override value for a given
        # sensor
        boundary = boundary_hysteresis(rawsensor, upper, sen_val)
        if boundary is None:
            return 1
        return set_hyst_thresh(name, rawsensor, thresh_list, boundary, addr)

    result = 0
    for sev in (LOWER_THRESH + UPPER_THRESH):
        config_val = rawsensor.get(sev)
        if (rawsensor.get('mask') & RAW_THRESHOLDS.get(sev)) == 0:
            continue
        # If we are modifying an upper threshold and it's an
        # upper severity, set to boundary threshold if its config
        # threshold is larger than the value read else set to
        # config value
        if upper and sev in LOWER_THRESH:
            if config_val > sen_val: 
                thresh_val = boundary
            else:
                thresh_val = config_val
        # If we are modifying a lower threshold and it's a
        # lower severity, set to boundary threshold if its config
        # threshold is less than the value read else set to
        # config value
        elif not upper and sev in UPPER_THRESH:
            if config_val < sen_val:
                thresh_val = boundary
            else:
                thresh_val = config_val
        # If it is in the thresh_list to change, set to
        # boundary value else set to config value
        else:
            if sev in thresh_list:
                thresh_val = boundary
            else:
                thresh_val = config_val
        if set_thresh(rawsensor, name, sev, thresh_val, addr):
            result = 1
    return result

# Takes an address and sensor number and returns the
# raw sensor reading in base 10 format
def get_raw_reading(addr, sensornum):
    # construct command
    cmd = '-V -a %X -R 042D%02X' % (addr, sensornum)
    error, raw_reading = run_ipmicmc_cmd(cmd)
    if error:
        return None
    # Parse IPMI response
    # Should return a list of length 5 with raw data at
    # index 0
    if len(raw_reading) != 5:
        return None
    response = raw_reading[0].split(' ')
    # Check cc
    if int(response[1]) != 0:
        return None
    raw_reading = int(response[2], 16)
    return raw_reading

def boundary_hysteresis(rawsensor, upper, value):
    poshys = int(rawsensor.get('poshys'))
    neghys = int(rawsensor.get('neghys'))
    value = value + (upper and -(poshys) or neghys)
    return value

def get_boundary_sev(value, config_sensor, thresh_list, upper):
    # Set to highest severity to start with
    boundary = thresh_list[-1]
    for thresh in thresh_list:
        thresh_val = config_sensor.get(thresh)
        # use mask to determine whether the threshold is na
        if (RAW_THRESHOLDS.get(thresh) & config_sensor.get('mask')) == 0:
            continue
        if (upper and thresh_val < value) or (not upper and thresh_val > value):
            boundary = thresh
            break
    return boundary

def set_raw_thresh(sensornum, addr, validmask, thresh):
    keys = ('lnr', 'lcr', 'lnc', 'unr', 'ucr', 'unc')
    lnr, lcr, lnc, unr, ucr, unc = [thresh.get(key) for key in keys]
    return hal.ipmiSetRawThresh(sensornum, addr, validmask,
                                lnr, lcr, lnc, unc, ucr, unr)

def set_thresh(rawsensor, name, sev, value, addr=None):
    sensornum = rawsensor.get('sensornum')
    if addr is None:
        addr = int(rawsensor.get('owneraddr'))
    sev_raw = RAW_THRESHOLDS.get(sev)
    sev_str = THRESHOLDS.get(sev)
    thresh = dict([(key, (sev == key) and value or 0)
                   for key in (LOWER_THRESH + UPPER_THRESH)])
    print('Setting %s %s threshold to %s' % (name, sev_str, value))
    return set_raw_thresh(sensornum, addr, sev_raw, thresh)

def set_hyst_thresh(name, rawsensor, thresh_list, value, addr=-1):
    sensornum = rawsensor.get('sensornum')
    if addr == -1:
        addr = rawsensor.get('owneraddr')
    result = 0
    for sev in thresh_list:
        # Setting threshold value per IPMI spec
        sev_raw = RAW_THRESHOLDS.get(sev)
        sev_str = THRESHOLDS.get(sev)
        thresh = dict([(key, (sev == key) and value or 0)
                       for key in (LOWER_THRESH + UPPER_THRESH)])
        print('Setting %s %s threshold' % (name, sev_str))
        if set_raw_thresh(sensornum, addr, sev_raw, thresh):
            result = 1
    return result

def set_sensor_value(sev, rawsensor, upper, name):
    sensornum = rawsensor.get('sensornum')
    addr = rawsensor.get('owneraddr')
    # Issue a raw ipmi command to the cmc that changes the value of the sensor
    cmd = "-V -a %x -R  %s" % (addr, SET_SENSOR_VAL_CMD)
    value = rawsensor.get(sev)
    mask = rawsensor.get('mask')
    sensormin = rawsensor.get('sensormin')
    sensormax = rawsensor.get('sensormax')
    poshys = rawsensor.get('poshys')
    neghys = rawsensor.get('neghys')
    if mask & RAW_THRESHOLDS.get(sev) != 0:
        if upper:
            value += poshys #(1 + poshys)
        else:
            value -= neghys #(1 + neghys)
    else:
        print("Unable to set %s for sensor %s" % (sev, name))
        return
    # Saturate
    if value > sensormax:
        value = sensormax
    elif value < sensormin:
        value = sensormin
    print("Setting sensor %s to %s" % (name, sev))
    cmd += "%02X00%02X00" % (sensornum, value)
    error, output = run_ipmicmc_cmd(cmd)
    if error:
        " not successful"
    return error, output

############################################################
# FAKE SEL
############################################################

def get_sel_entry(entry):
    import isi.hw.hwmon.fakesel as fakesel
    entry = str(entry).strip()
    isdict = entry.startswith('{') and entry.endswith('}')
    islist = entry.startswith('[') and entry.endswith(']')
    if isdict or islist:
        try:
            entry = eval(entry)
        except NameError as e:
            return None
    else:
        try:
            (index, entry) = fakesel.csv_string_to_entry(entry)
            # Ignore index. Expected to be None, normally, anyway.
        except:
            return None
    # Validate - XXX TODO
    return entry

def fake_sel_insert_entries(entries, filename):
    import isi.hw.hwmon.fakesel as fakesel
    return fakesel.save_fake_sel_updates(entries, filename)

def fake_sel_inject_sel_entry(name, sev, filename):
    print('%s TBD' % 'fake_sel_inject_sel_entry')
    return 1

def fake_sel_clear_sensor(name, sev, filename):
    print('%s TBD' % 'fake_sel_clear_sensor')
    return 1

def fake_sel_clear_all_sensors(filename):
    print('%s TBD' % 'fake_sel_clear_all_sensors')
    return 1

############################################################
# MAIN
############################################################

def usage():
    sevs = LOWER_THRESH[::-1] + UPPER_THRESH
    print('Usage:')
    print('    Show Help')
    print('        isi_sel_inject -h')
    print('')
    print('    Dump Sensor Table   : isi_sel_inject -a dump')
    print('    Assert              : isi_sel_inject -a assert  -n <name> -s <sev>')
    print('    Default Sensor All  : isi_sel_inject -a default -n <name>')
    print('    Default Sensor Sev  : isi_sel_inject -a default -n <name> -s <sev>')
    print('    Default All Sensors : isi_sel_inject -a default-all')
    print('')
    print('    name: sensor name')
    print('    severity: %s' % ', '.join(sevs))
    for sev in sevs:
        print('        %s: %s' % (sev, THRESHOLDS.get(sev)))
    print('    action: assert, default, default-all, dump')
    print('        assert      : Set specific sensor thresholds')
    print('        default     : Set specific sensor thresholds back to default')
    print('        default-all : Set all sensor thresholds back to default')
    print('        dump        : Dump sensor table')
    print('')
    return

def main(argv):
    name = sev = action = fake_sel = fake_file = None 
    fake_entries = []
    # This utiliy is only used with motherboards that support a BMC IPMI device
    if not hal.supportsBmcSensorMonitoring():
        print('Hardware does not support BMC sensor monitoring')
        return 1
    longopts = ['fakesel', 'fakefile=', 'entry=']
    try:
        opts, args = getopt.getopt(argv, "hn:s:a:", longopts)
    except getopt.GetoptError:
        usage()
        return 2
    for opt, arg in opts:
        if opt == '-h':
            usage()
            return 0
        elif opt == '-n':
            name = arg
        elif opt == '-s':
            sev = arg
        elif opt == '-a':
            action = arg
        elif opt in ['--fakesel']:
            fake_sel = True
        elif opt in ['--fakefile']:
            fake_file = arg
        elif opt in ['--entry']:
            entry = get_sel_entry(arg)
            if not entry:
                print('Invalid SEL entry: %s' % arg)
                return 2
            if isinstance(entry, list):
                fake_entries.extend(entry)
            elif isinstance(entry, dict):
                fake_entries.append(entry)
            else:
                print('Invalid SEL entry: %s' % entry)
                return 1
        else:
            usage()
            return 2

    if action is None:
        print('No action specified, exiting.')
        return 2
    elif action not in ['assert', 'dump', 'default', 'default-all', 'insert']:
        print('Incorrect action specified: %s' % action)
        return 2
    elif name is None and not action in ['dump', 'default-all', 'insert']:
        print('No sensor name specified, exiting.')
        return 2
    elif sev is None and not action in ['dump', 'default', 'default-all', 'insert']:
        print('No severity specified, exiting.')
        return 2

    if action == 'dump':
        result = dump_table()

    elif fake_sel:
        # If unspecified fake_file, uses default (See isi.hw.hwmon.fakesel)
        if action == 'insert':
            result = fake_sel_insert_entries(fake_entries, fake_file)
        elif action == 'assert':
            result = fake_sel_inject_sel_entry(name, sev, fake_file)
        elif action == 'default':
            result = fake_sel_clear_sensor(name, sev, fake_file) 
        elif action == 'default-all':
            result = fake_sel_clear_all_sensors(fake_file)
        else:
            print('Incorrect action specified: %s' % action)
            result = 2
    else:
        if action == 'assert':
            result = inject_sel_entry(name, sev)
        elif action == 'default':
            result = clear_sensor(name, sev) 
        elif action == 'default-all':
            result = clear_all_sensors()
        else:
            print('Incorrect action specified: %s' % action)
            result = 2

    return result

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
