#! /usr/bin/python -u
#
# Copyright 2016 Cumulus Networks LLC, all rights reserved
#
# This program is a Net-SNMP pass persist script that services the
# Cumulus Networks custom POE MIB defined in file Cumulus-POE-MIB.txt
# This includes the system wide power information in poeSystemValues
# as well as per interface PoeObjectsEntry values for the poeObjectsTable.
#
# To activate this script, snmpd must be running and this
# script should be installed /usr/share/snmp/cl_poe_pp.py
# and include the following line in /etc/snmp/snmpd.conf
#
#   pass_persist .1.3.6.1.4.1.40310.3 /usr/share/snmp/cl_poe_pp.py
#
import subprocess
import syslog
import json
import traceback
import snmp_passpersist as snmp

# The Cumulus custom POE MIB starts with this object ID
oid_base = '.1.3.6.1.4.1.40310.3'

# change the debug to test without snmpd
debug = 0
powerMultiplier = 1000
already_logged = False

def pp_add_int(oid, val):
    if debug:
        print '%s : %s' %(oid, val)
    else:
        pp.add_int(oid, val)

def pp_add_tt(oid, val):
    if debug:
        print '%s : %s' %(oid, val)
    else:
        pp.add_tt(oid, val)

def pp_add_cnt(oid, val):
    if debug:
        print '%s : %s' %(oid, val)
    else:
        pp.add_cnt_32bit(oid, val)

def pp_add_str(oid, val):
    if debug:
        print '%s : %s' %(oid, val)
    else:
        pp.add_str(oid, val)

# these arrays do not change unless the MIB changes
trueFalse = {True:1, False:2}
priority = {'low':1, 'high':2, 'critical':3}
portType = {'none':1, 'IEEE802.3af':2, 'IEEE802.3at':3,
            'legacy':4, 'high-power':5, 'invalid':6,
            'IEEE802.3af/at':7}
status = {'unknown':1,
          'disabled':2,
          'searching':3,
          'connected':4,
          'power-denied':5,
          'fault':6}
# We are given a number string but this is left here for reference
portClass = {'0':0, '1':1, '2':2, '3':3, '4':4,
             'unimplemented':0,
             'none':0,
             'very-low-power':1,
             'low-power':2,
             'mid-power':3,
             'high-power':4}

# This is an ordered list of attributes we defined in the MIB and will loop over
poeObjectList = ['portName', 'portPriority', 'portType', 'portStatus',
                 'portClass', 'portFourPairModeEnabled', 'portVoltage',
                 'portCurrent', 'portPower', 'portMaxPower',
                 'portAllocatedPower', 'lldpRequestedPower', 'lldpAllocatedPower']

# The main array is defined here for each port. We need the following:
#    oid:       the numerical object identifier as defined in the MIB
#    type:      the encoding used to return values to snmpd
#    converter: conversion array to provide integers from strings or integer
#               multipiers
#    jsonName:  the name of the attribute provided by poectl in the json array
#    default:   the default used if poectl does not provide a value.
#               Note well: this must be converted.
#
poeObjectArray = {'portName':{'oid':1, 'type':pp_add_str, 'converter':None,
                              'jsonName':'swp', 'default':''},
                  'portPriority':{'oid':2, 'type':pp_add_int, 'converter':priority,
                                  'jsonName':'priority', 'default': 'low'},
                  'portType':{'oid':3, 'type':pp_add_int, 'converter':portType,
                              'jsonName':'pd_type', 'default':'none'},
                  'portStatus':{'oid':4, 'type':pp_add_int, 'converter':status,
                                'jsonName':'hw_status', 'default':'unknown'},
                  'portClass':{'oid':5, 'type':pp_add_int, 'converter':portClass,
                               'jsonName':'pd_class', 'default':'none'},
                  'portFourPairModeEnabled':{'oid':6, 'type':pp_add_int,
                                             'converter':trueFalse,
                                             'jsonName':'four_pair_mode_enabled',
                                             'default':False},
                  'portVoltage':{'oid':7, 'type':pp_add_int, 'converter':1000,
                                 'jsonName':'voltage', 'default':0},
                  'portCurrent':{'oid':8, 'type':pp_add_int, 'converter':1,
                                 'jsonName':'current', 'default':0},
                  'portPower':{'oid':9, 'type':pp_add_int, 'converter':1000,
                               'jsonName':'power', 'default':0},
                  'portMaxPower':{'oid':10, 'type':pp_add_int, 'converter':1,
                                  'jsonName':'max_port_power', 'default':0},
                  'portAllocatedPower':{'oid':11, 'type':pp_add_int, 'converter':1,
                                        'jsonName':'allocated_power', 'default':0},
                  'lldpRequestedPower':{'oid':12, 'type':pp_add_int, 'converter':1,
                                        'jsonName':'lldp_requested_power', 'default':0},
                  'lldpAllocatedPower':{'oid':13, 'type':pp_add_int, 'converter':1,
                                        'jsonName':'lldp_allocated_power', 'default':0}
                  }

def update():
    """
    Reads latest values for POE info from poectl and updates the cached values.
    """
    global already_logged
    # update the interface names with counters

    try:
        poectl_json = json.loads(subprocess.check_output((['sudo',
                                     '/usr/cumulus/bin/poectl','-j', '-a']),
                                  stderr=subprocess.STDOUT,
                                  shell=False))
        # grab the ports for later use
        poe_ports = poectl_json.get("ports", {})
    except BaseException as e:
        if not already_logged:
            syslog.syslog('cl_poe_pp: error running poectl EXCEPTION=%s' % e)
            already_logged = True
        poectl_json = {}
        poe_ports = {}

    # grab the uptime timetics
    try:
        fd = open('/proc/uptime', 'r')
        uptime = int(float(fd.read().split()[0]) * 100)
        fd.close()
    except BaseException as e:
        syslog.syslog('cl_poe_pp: problem getting uptime EXCEPTION=%s' % e)
        uptime = 0

    # create a new entry indexed by the ifIndex (an integer) since we loop over
    # this and not the poePort (a string) which is what we are given.
    poeifIndexArray = {}
    for poePort in poe_ports.keys():
        # since the json output is not indexed by ifIndex but rather poePort
        # we will save a copy indexed by ifIndex
        portName = poe_ports.get(poePort, {}).get('swp', '')
        ifIndex = portName2ifIndex.get(portName, None)
        if ifIndex:
            poeifIndexArray[ifIndex] = poe_ports[poePort]
    # the first table just grabs system wide values
    powerLimit = int(poectl_json.get('sys_power_limit',0) * powerMultiplier)
    powerUsed = int(poectl_json.get('sys_consumed_power',0) * powerMultiplier)
    powerAvail = powerLimit - powerUsed
    pp_add_int('1.1.0', powerLimit)
    pp_add_int('1.2.0', powerUsed)
    pp_add_int('1.3.0', powerAvail)
    pp_add_tt('1.4.0', uptime)
    # the poeObjectTable is indexed by ifIndex
    poeOid = '2.1'
    try:
        # loop over the attributes and show every port attribute
        for attr in poeObjectList:
            # now loop over all the ports
            for port in portifIndexList:
                # not all ports are poe ports
                poeEntry = poeifIndexArray.get(port, None)
                if poeEntry:
                    oid      = poeObjectArray[attr]['oid']
                    myfunc   = poeObjectArray[attr]['type']
                    jsonName = poeObjectArray[attr]['jsonName']
                    default  = poeObjectArray[attr]['default']
                    converter = poeObjectArray[attr]['converter']
                    value = poeEntry.get(jsonName, default)
                    #if attr=='portType':
                    #    import pdb;pdb.set_trace()
                    if isinstance(converter, int):
                        # need to convert strings to integers with converter hash
                        value = int(float(value) * converter)
                    elif isinstance(converter, dict):
                        # string names are usually converted to an integer with the
                        # dict.  if we do not know the index, do not send any value
                        # in case the value exists but cannot be converted, we need
                        # to convert it to a number.  The default must convert.
                        default = converter.get(default, None)
                        value = converter.get(value, default)
                    else:
                        # converter must be None so we just leave the value alone
                        pass
                    # call the function to return a result
                    myfunc('%s.%s.%s' % (poeOid, oid, port), value)
    except Exception, err:
        syslog.syslog('cl_poe_pp.py: leaving....Exception=%s' % err)
        syslog.syslog('cl_poe_pp.py: traceback=%s' % traceback.format_exc())
        already_logged = True

###############################################################################
portifIndexList = []
portName2ifIndex = dict()
# Build list of ports and ifIndices first
try:
    port_list = subprocess.check_output((['/sbin/ip','-o','link', 'show']),
                                        stderr=subprocess.STDOUT,
                                        shell=False).splitlines()
except BaseException as e:
    syslog.syslog('cl_poe_pp: problem getting port list EXCEPTION=%s' % e)
    port_list = []

for port in port_list:
    oline = port.split(':')
    indexCounter = int(oline[0].strip())
    portName = oline[1].strip()
    # Only handle hardware swp ports
    if "swp" not in portName or "@" in portName or "." in portName:
        continue
    portifIndexList.append(indexCounter)
    # also save x['swp1']=3 so we can go in reverse
    portName2ifIndex[portName] = indexCounter

# we need the ifIndex list in order because snmp likes increasing order
portifIndexList.sort()

if debug:
    update()
else:
    pp = snmp.PassPersist(oid_base)
    pp.start(update, 300)
