#! /usr/bin/python -u
#
# Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# This software product is a proprietary product of Nvidia Corporation
# and its affiliates (the "Company") and all right, title, and interest
# in and to the software product, including all associated intellectual
# property rights, are and shall remain exclusively with the Company.
#
# This software product is governed by the End User License Agreement
# provided with the software product.
# All Rights reserved.

# To activate this script, snmpd must be running and this
# script should be installed /usr/share/snmp/entity_sensor_pp.py
# and include the following line in /etc/snmp/snmpd.conf
#
#   pass_persist .1.3.6.1.2.1.99  /usr/share/snmp/entity_sensor_pp.py
#

import subprocess
import sys
import json
import time
import types
import os
import syslog
from collections import OrderedDict

import snmp_passpersist as snmp

# this is the Entity Sensor MIB (RFC 3433) which is mostly the output
# of the smonctl command
entitySensorMIB = '.1.3.6.1.2.1.99'

sensorIDStart = {'temp' :100000000,
                 'fan'  :100011000,
                 'power':110000000,
                 'volt' :110001000}

# some typical types
celsius = 8
watts = 6
rpm = 10
voltsAC = 3
voltsDC = 4

# just the value, no multipliers
units = 9

# precision, meaning the number of decimal places in the
# fractional part of an associated EntitySensorValue fixed-
# point number
precision_zero = 0
precision_one = 1
precision_two = 2

# status is either ok, etc...
status_ok = 1
status_unavailable = 2
status_nonoperational = 3

status = {'OK':status_ok,
          'ABSENT':status_unavailable,
          'BAD':status_nonoperational}

valueArray = {'datatype':{'temp':celsius,
                          'fan':rpm,
                          'power':watts,
                          'volt':voltsDC,
                          'oid_type':'add_int'},
              'scale':{'temp':units,
                       'fan':units,
                       'power':units,
                       'volt':units,
                       'oid_type':'add_int'},
              'precision':{'temp':precision_one,
                           'fan':precision_zero,
                           'power':precision_two,
                           'volt':precision_two,
                           'oid_type':'add_int'},
              'value':{'temp':'input',
                       'fan':'input',
                       'power':'input',
                       'volt':'input',
                       'oid_type':'add_int'},
              'operational_status':{'temp':'state',
                                    'fan':'state',
                                    'power':'state',
                                    'volt':'state',
                                    'oid_type':'add_int'},
              'units_display':{'temp':'Celsius',
                               'fan':'RPM',
                               'power':'Watts',
                               'volt':'Volts',
                               'oid_type':'add_str'},
              'value_timestamp':{'temp':None,
                                 'fan':None,
                                 'power':None,
                                 'volt':None,
                                 'oid_type':'add_tt'},
              'update_rate':{'temp':'15000',
                             'fan':'15000',
                             'power':'15000',
                             'volt':'15000',
                             'oid_type':'add_gau'},
              }

def getValue(rowname, sensorType, sensor, uptime):
    if rowname == 'value':
        # this is the only case where we return an actual number, or 0
        # we need to make sure this is an integer value and scaled by
        # the precision
        multiplier = 10 ** int(valueArray['precision'][sensorType])
        inputval = sensor.get(valueArray[rowname].get(sensorType, None), 0)
        if inputval and (isinstance(inputval, types.IntType) or \
                         isinstance(inputval, types.FloatType)):
            val = int(inputval * multiplier)
        else:
            val = 0
    elif rowname == 'operational_status':
        # here, we return the numerical value, we map (via status) to OK=1, etc.
        # if status does not exist, we see it as unavailable
        val = status.get(sensor.get(valueArray[rowname].get(sensorType, None), 0),
                         status_unavailable)
    elif rowname == 'value_timestamp':
        val = uptime
    else:
        # these are mostly statics
        # we could get a new sensor type so we need to default without an error
        val = valueArray[rowname].get(sensorType,0)
    return(val)

def update():
    """
    Simply grab the output of smonctl -j and stick it in our hashed array
    """
    oid = '1.1.1'
    # these are the elements of the table we will loop over
    entPhySensorTable = enumerate(['datatype', 'scale', 'precision', 'value',
                                   'operational_status', 'units_display',
                                   'value_timestamp', 'update_rate'],
                                  start=1)

    # this is just a list of dictionaries, one for each sensor
    try:
        smonctlJsonList = json.loads(subprocess.check_output((['/usr/sbin/smonctl', '-j']),
                                                         stderr=subprocess.STDOUT,
                                                         shell=False))
    except Exception as e:
        syslog.syslog("entity_sensor_pp.py: Exception=%s" % e)
        smonctlJsonList = []

    hwTypes = OrderedDict([('temp', []), ('fan', []), ('power', []), ('volt', [])])
    # reorganize the data by sensor type in hwTypes dict
    # and give each sensor an ID
    allTypes = hwTypes.keys()
    for sensor in smonctlJsonList:
        # create the index if needed
        # for now, we only keep the types we know about
        if sensor['type'] not in allTypes:
            # we do not know about this type, so skip it.
            syslog.syslog("entity_sensor_pp.py: Missing support for type=%s" % sensor['type'])
            continue
        hwTypes.setdefault(sensor['type'],[]).append(sensor)

    # grab the uptime timetics since smonctl -j does not actually set it
    try:
        fd = open('/proc/uptime', 'r')
        uptime = int(float(fd.read().split()[0]) * 100)
        fd.close()
    except Exception as e:
        syslog.syslog("entity_sensor_pp.py: Exception=%s" % e)
        uptime = 0

    for rowval,rowname in entPhySensorTable:  # 1, units
        for sensorType in allTypes:     # temp, fan, power, volt
            # set the sensor ID, this is order dependent based on hwTypes
            sensorID = sensorIDStart[sensorType]
            for sensor in hwTypes[sensorType]:# actual sensor arrays
                # make sure to update the sensorID
                sensorID += 1
                myval = getValue(rowname, sensorType, sensor, uptime)
                # call the method defined in oid_type
                myfunc = getattr(pp, valueArray[rowname]['oid_type'])
                # here, we will call add_int, add_str, add_tt, or add_gau
                myfunc('%s.%s.%s' % (oid, rowval, sensorID), myval)


syslog.syslog("entity_sensor_pp.py: starting...")
pp = snmp.PassPersist(entitySensorMIB)
pp.debug = False

if pp.debug:
    update()
else:
    syslog.syslog("entity_sensor_pp.py: initialized...")
    pp.start(update, 22)
    syslog.syslog("entity_sensor_pp.py: after first update, we should not see this...")

