#! /usr/bin/python -u
# Copyright 2016 Cumulus Networks LLC, all rights reserved
#
# This is an implementation of the Entity MIB (RFC 4133)
# which is mostly the output of the decode-syseeprom command.
# To activate this script, snmpd must be running and this
# script should be installed /usr/share/snmp/entity_pp.py
# and include the following line in /etc/snmp/snmpd.conf
#
#   pass_persist .1.3.6.1.2.1.47  /usr/share/snmp/entity_pp.py
#

import subprocess
import sys
import json
import time
import os
import types
import syslog
import datetime
import platform
import lsb_release
import snmp_passpersist as snmp

syslog_already_logged = False

entityMIB = '.1.3.6.1.2.1.47'
entPhysicalEntry = '1.1.1.1'

# we only handle the PhysicalTable for now
entLogicalTable  = '1.2.1'
entAliasMappingIdentifier = '1.3.3.1'
# we will just set this to uptime since the physical table is static
entLastChangeTime = '1.4.1'

def get_json_output(commandList=None, useJson=True):
    '''
    This grabs the JSON output of a CLI command and returns an array.
    '''
    global syslog_already_logged
    output = {}
    if (not commandList) or (commandList == []):
        syslog.syslog('Error: called get_json_output with'
                      ' invalid command=%s' % \
                      commandList)
        syslog_already_logged = True
        return {}
    try:
        if useJson:
            output = json.loads(subprocess.check_output((commandList),
                                                        shell=False))
        else:
            output = subprocess.check_output((commandList),
                                             shell=False)
    except Exception as e:
        output = {}
        syslog.syslog('Error: command %s EXCEPTION=%s' % \
                      (' '.join(commandList), e))
    return output

def get_clversion():
    try:
        info = lsb_release.get_distro_information()
        distrib = "%s %s" % (info['ID'], info['RELEASE'])
    except:
        distrib = "Distribution Unknown"

    try:
        linuxversion = platform.version().split()
        versionname = "Linux Kernel %s" %  (linuxversion[-2])
    except:
        versionname = "Kernel Version Unknown"
    return ('%s (%s)' % (distrib, versionname))

def get_ports():
    try:
        # easy way to get a list of physical interfaces
        ports = [i.split()[0] for i in open('/var/lib/cumulus/porttab','r').\
                 readlines() if not i.startswith('#') and i.startswith('swp')]
    except:
        # if we have a problem, just create an empty list
        ports = []
    return ports

def get_uptime():
    # grab the uptime timetics
    try:
        fd = open('/proc/uptime', 'r')
        uptime = int(float(fd.read().split()[0]) * 100)
        fd.close()
    except:
        uptime = 0
    return uptime

def get_octetString(myval=''):
    try:
        if isinstance(myval, types.StringType) or \
           isinstance(myval, types.UnicodeType):
            strList = ['%02x' % ord(i) for i in myval]
        elif isinstance(myval, types.ListType):
            strList = ['%02x' % i if i != 0 else '00' for i in myval]
        else:
            strList = ['00', '00']
        strStr = ' '.join(strList)

    except:
        strStr = '00 00'

    return strStr

def get_Date(dateval=None):
    try:
        d = datetime.datetime.strptime(dateval, "%m/%d/%Y %H:%M:%S")
        mfgDate = [i for i in [(d.year & 0xFF00)>>8, d.year&0xFF, d.month,
                               d.day, d.hour, d.minute, d.second, 0]]
    except:
        mfgDate = ''
    return mfgDate

def get_value(jsonArray=None, attribute=None):
    val = jsonArray.get('tlv', {}).get(attribute, {}).get('value', '')
    val = val.strip()
    return val.replace('\x00','')

def get_sdkversion():
    # we handle either BCM or MLX chips
    output = 'Unknown'
    with open(os.devnull, 'w') as devnull:
        try:
            output = subprocess.check_output(['dpkg-query', '-W', '-f', "${Version}",
                                              'bcm-sdk', 'sx-sdk-eth'],
                                             stderr=devnull)
            output = output.split('-')[0]
            output = output.replace('1.mlnx.','')
        except subprocess.CalledProcessError as grepexc:
            #print "error code", grepexc.returncode, grepexc.output
            output = grepexc.output.split('-')[0]
            output = output.replace('1.mlnx.','')
    return output

uptime = get_uptime()

# isFru values
fru_true = 1
fru_false = 2

clVersion = get_clversion()
ports = get_ports()

# grab the decode-syseeprom for hardware rev, serial number
# we really should be using one interface and that is dmidecode.
decodeJson = get_json_output(['/usr/cumulus/bin/decode-syseeprom', '-j'])

hardwareRev = get_value(decodeJson, 'Device Version')
dateval = get_value(decodeJson, 'Manufacture Date')
if dateval == '':
    mfgDate = [0]*8
else:
    mfgDate = get_Date(dateval)

productName = get_value(decodeJson, 'Product Name')
platformName = get_value(decodeJson, 'Platform Name')
partNumber = get_value(decodeJson, 'Part Number')
modelName = get_value(decodeJson, 'Model Name')
vendorName =  get_value(decodeJson, 'Vendor Name')
mfgName = get_value(decodeJson, 'Manufacturer')
serviceTag = get_value(decodeJson, 'Service Tag')
serialNum = get_value(decodeJson, 'Serial Number')
if not modelName:
    modelName = partNumber
# Some companies put the manufacturer in the vendor name
if vendorName:
    mfgName = vendorName
description = '%s %s %s Chassis' % (mfgName, platformName, productName)

classes = {'chassis':3, 'container':5, 'powerSupply':6,
           'fan':7, 'sensor':8, 'port':10}
hostname = get_json_output(['/bin/hostname'], useJson=False).strip()

firmwareRev = get_sdkversion()

# Grab the sensor data first
# this is a list of dictionaries, one for each sensor
# Since we only need the description here, we only need to
# call this once at the beginning.
smonctlJsonList = get_json_output(['/usr/sbin/smonctl', '-j'])

# physical classes
# since there is no seperate voltage class we count it as a power supply
classes = {'chassis':'3',
           'container':'5',
           'power':'6',
           'volt':'6',
           'fan':'7',
           'temp':'8',
           'port':'10'}

entityIDStart = {'chassis'  :        0,
                 'container':       10,
                 'temp'     :100000000,
                 'fan'      :100011000,
                 'power'    :110000000,
                 'volt'     :110001000,
                 'port'     :111000000,
                 }

entityNames = entityIDStart.keys()

physicalTableRows = ['index', 'description', 'vendorType', 'containedIn',
                     'class', 'parentRelPos', 'name', 'hardwareRev',
                     'firmwareRev', 'softwareRev', 'serialNum', 'mfgName',
                     'modelName', 'alias', 'assetID', 'isFRU', 'mfgDate',
                     'uris']
# these are the elements of the table we will loop over
# most vendors seem to skip the first item called index so we skip it too
entPhysicalTable = list(enumerate(physicalTableRows, start=1))

oidType = {'index':'add_int',
           'description':'add_oct',
           'vendorType':'add_oid',
           'containedIn':'add_int',
           'class':'add_int',
           'parentRelPos':'add_int',
           'name':'add_oct',
           'hardwareRev':'add_oct',
           'firmwareRev':'add_oct',
           'softwareRev':'add_oct',
           'serialNum':'add_oct',
           'mfgName':'add_oct',
           'modelName':'add_oct',
           'alias':'add_oct',
           'assetID':'add_oct',
           'isFRU':'add_int',
           'mfgDate':'add_oct',
           'uris':'add_oct'
           }

rowDefault = {'index':'',
              'description':'',
              'vendorType':'0.0',
              'containedIn':1, # everything is in the chassis
              'class':0,
              'parentRelPos':1,
              'name':'',
              'hardwareRev':'',
              'firmwareRev':'',
              'softwareRev':'',
              'serialNum':'',
              'mfgName':'',
              'modelName':'',
              'alias':'',
              'assetID':'',
              'isFRU':fru_false,
              'mfgDate':[0]*8,
              'uris':[0,0]
              }

# there is only one chassis, just define it, leave defaults blank
# FIXME we will need to fix these when we get chassis info
chassisList = [{'description':description,
                'class':classes['chassis'],
                'name':hostname,
                'alias':hostname,
                'hardwareRev':hardwareRev,
                'containedIn':0,
                'firmwareRev':firmwareRev,
                'softwareRev':clVersion,
                'serialNum':serialNum,
                'modelName':modelName,
                'mfgName':mfgName,
                'mfgDate':mfgDate,
                'type':'chassis'
                }]

# create the port list of arrays, initializing the type and description
#  FIXME we will need to fix these when we get more port info
portList = [{'description':p,
             'name':p,
             'class':classes['port'],
             'mfgName':'', # FIXME LATER
             'modelName':'', # FIXME LATER
             'type':'port'}
            for p in ports]

# very nice that all sensors come with a "type" key
# we split them up into types
temperatureList = [s for s in smonctlJsonList if s['type'] == 'temp']
fanList = [s for s in smonctlJsonList if s['type'] == 'fan']
powerList = [s for s in smonctlJsonList if s['type'] == 'power']
voltList = [s for s in smonctlJsonList if s['type'] == 'volt']

# One list to bind them all
# regroup the types into one large list of lists
entityGroupList = [chassisList, temperatureList, fanList, powerList, voltList, portList]
# now just flatten it so we have one list of entities
entityList = [val for sublist in entityGroupList for val in sublist]

# add the ID and class for each device
for group in entityGroupList:
    IDCounter = 0
    for entity in group:
        IDCounter += 1
        # each entity type will start at a new number range
        entity['ID'] = entityIDStart[entity['type']] + IDCounter
        entity['class'] = classes[entity['type']]

def update():
    """
    Nothing really gets updated since the physical entities do not
    change.  Until we start supporting hot swapping of chassis, or
    networking card, we are static.
    """
    # we start with description rows, which is second
    for rowval,rowname in entPhysicalTable[1:]:
        # loop over the entity groups like
        # chassis, ports, temp sensors, fan sensors, power sensors
        for entity in entityList:
            try:
                myfunc = getattr(pp, oidType[rowname])
                myval = entity.get(rowname,rowDefault[rowname])
                if oidType[rowname] == "add_oct":
                    # octet strings are special values
                    myval = get_octetString(myval)
                myfunc('%s.%s.%s' % (entPhysicalEntry, rowval,
                                     entity['ID']), myval)
            except Exception as e:
                syslog.syslog(" ERROR: %s" % e)

    # handle the entLastChange
    pp.add_tt('%s.%s' % (entLastChangeTime, 0), uptime)

################################################################################
pp = snmp.PassPersist(entityMIB)
pp.debug = False
if pp.debug:
    # just run the update script once and print debugs to stdout
    update()
else:
    syslog.syslog("entity_pp.py: initialized...")
    pp.start(update, 31536000) # update once a year
