#!/usr/sfw/bin/python


"""
This program depends on the order of ipmitool's columns of output
specifically:
column 1: sensor text string
column 2: sensor reading
column 3: sensor's unit of measurement
column 4: sensor's status (ignored in this program)
column 5: sensor lower non-recoverable threshold
column 6: sensor lower critical threshold
column 7: sensor lower non-critical threshold
column 8: sensor upper non-critical threshold
column 9: sensor upper critical threshold
column 10: sensor upper non-recoverable threshold
"""

# Copyright 1999-2005 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.



#--------------------------------------------------------------------------------------------------
__version__ = "@(#)bmcenvironment  1.3  06/02/13  SMI."


#--------------------------------------------------------------------------------------------------
import os
import sys
import time # for sleep()

import vts


#--------------------------------------------------------------------------------------------------
def Enum(*names):
   """
   see discussion at http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/413486
   """

   #--------------------------------------------------------------------------------------------------
   class EnumClass(object):
        __slots__ = names
        def __iter__(self):        return iter(constants)
        def __len__(self):         return len(constants)
        def __getitem__(self, i):  return constants[i]
        def __repr__(self):        return 'Enum' + str(names)
        def __str__(self):         return 'enum ' + str(constants)

    #--------------------------------------------------------------------------------------------------
   class EnumValue(object):
        __slots__ = ('__value')
        def __init__(self, value): self.__value = value
        Value = property(lambda self: self.__value)
        EnumType = property(lambda self: EnumType)
        def __hash__(self):        return hash(self.__value)
        def __cmp__(self, other):
            assert self.EnumType is other.EnumType, "Only values from the same enum are comparable"
            return cmp(self.__value, other.__value)
        def __invert__(self):      return constants[maximum - self.__value]
        def __nonzero__(self):     return bool(self.__value)
        def __repr__(self):        return str(names[self.__value])

   maximum = len(names) - 1
   constants = [None] * len(names)
   for i, each in enumerate(names):
      val = EnumValue(i)
      setattr(EnumClass, each, val)
      constants[i] = val
   constants = tuple(constants)
   EnumType = EnumClass()
   return EnumType


#--------------------------------------------------------------------------------------------------
class NoComparison(Exception):
    pass

#--------------------------------------------------------------------------------------------------
class IpmiValue(object):
    """
    an IpmiValue is a float, including 0.0, but it can also be "n/a" when so reported by ipmitool
    """

    #--------------------------------------------------------------------------------------------------
    def __init__( self, f ):
        try:
            self.f  = float( f )
        except ValueError:
            self.f  = ()

    #--------------------------------------------------------------------------------------------------
    def __str__( self ):
        try:
            if self.f is ():
                s = "n/a"
            else:
                s = str( self.f )
        except TypeError:
            s = "n/a"
        return s

    #--------------------------------------------------------------------------------------------------
    def Valid( self ):
        return type(self.f) == type(0.0)

    #--------------------------------------------------------------------------------------------------
    def __le__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f <= i
            elif type(i.f) == type(0.0):
                return self.f <= i.f
        raise NoComparison

    #--------------------------------------------------------------------------------------------------
    def __lt__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f < i
            elif type(i.f) == type(0.0):
                return self.f < i.f
        raise NoComparison

    #--------------------------------------------------------------------------------------------------
    def __eq__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f == i
            elif type(i.f) == type(0.0):
                return self.f == i.f
        raise NoComparison

    #--------------------------------------------------------------------------------------------------
    def __ne__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f != i
            elif type(i.f) == type(0.0):
                return self.f != i.f
        raise NoComparison

    #--------------------------------------------------------------------------------------------------
    def __gt__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f > i
            elif type(i.f) == type(0.0):
                return self.f > i.f
        raise NoComparison

    #--------------------------------------------------------------------------------------------------
    def __ge__( self, i ):
        if type(self.f) == type(0.0):
            if type(i) == type(0.0):
                return self.f >= i
            elif type(i.f) == type(0.0):
                return self.f >= i.f
        raise NoComparison

#--------------------------------------------------------------------------------------------------
class IpmiRange(object):

    #--------------------------------------------------------------------------------------------------
    def __init__( self, LoLimit, HiLimit ):
        self.LoLimit = LoLimit
        self.HiLimit = HiLimit

    #--------------------------------------------------------------------------------------------------
    def FailureCondition( self, reading ):
        if not reading.Valid():
            raise NoComparison
        f = False
        if not f:
            if self.LoLimit.Valid():
                f = reading <= self.LoLimit
        if not f:
            if self.HiLimit.Valid():
                f = reading >= self.HiLimit
        if f:
            if self.LoLimit.Valid() and self.HiLimit.Valid():
                # this is the problematic case when low == 0.0 and hi == 0.0
                # i.e. limits are not known for this sensor
                if self.LoLimit == self.HiLimit:
                    f = False
        return f

    #--------------------------------------------------------------------------------------------------
    def Valid( self ):
        return self.LoLimit.Valid() or self.HiLimit.Valid()


#--------------------------------------------------------------------------------------------------
WarningStates = Enum( "NotSet", "Warning", "NoWarning" )
FailingStates = Enum( "NotSet", "Failure", "NoFailure" )


#--------------------------------------------------------------------------------------------------
class IpmiSensor(object):

    #--------------------------------------------------------------------------------------------------
    def __init__( self, fields ):
        vts.message( vts.NO_EXIT, vts.DEBUG, "NULL", 0, str( fields ) )
        self.name              = fields[0]
        self.reading           = IpmiValue( fields[1] )
        self.unit              = fields[2]
        self.LoNonRecoverable  = IpmiValue( fields[4] )
        self.LoCritical        = IpmiValue( fields[5] )
        self.LoNonCritical     = IpmiValue( fields[6] )
        self.HiNonCritical     = IpmiValue( fields[7] )
        self.HiCritical        = IpmiValue( fields[8] )
        self.HiNonRecoverable  = IpmiValue( fields[9] )
        self.warningstate = WarningStates.NotSet
        self.failingstate = FailingStates.NotSet
        self.failurecondition = None
        self.warningcondition = None


    #--------------------------------------------------------------------------------------------------
    def vtsprint( self, statuscode=vts.NO_EXIT, vtsstreams=(vts.VERBOSE,) ):
        for j in vtsstreams:
            vts.message( statuscode, j, "NULL", 2004, "%s:" % (self.name)                   )
            vts.message( statuscode, j, "NULL", 2005, str( self.reading ) + " " + self.unit )
            vts.message( statuscode, j, "NULL", 2006, str( self.LoNonRecoverable )          )
            vts.message( statuscode, j, "NULL", 2007, str( self.LoCritical )                )
            vts.message( statuscode, j, "NULL", 2008, str( self.LoNonCritical )             )
            vts.message( statuscode, j, "NULL", 2009, str( self.HiNonCritical )             )
            vts.message( statuscode, j, "NULL", 2010, str( self.HiCritical )                )
            vts.message( statuscode, j, "NULL", 2011, str( self.HiNonRecoverable )          )

    #--------------------------------------------------------------------------------------------------
    def analyse( self, warning ):
        """
        - sensor reports a failing condition when the reading exceeds the critical range
        - if the critical range is not provided, then the non-recoverable range is used
        - a sensor reports a warning condition when the reading exceeds the non-critical range
        - if no ranges are set, then apply heuristics
        """
        self.NonCritical    = IpmiRange( self.LoNonCritical,    self.HiNonCritical    )
        self.Critical       = IpmiRange( self.LoCritical,       self.HiCritical       )
        self.NonRecoverable = IpmiRange( self.LoNonRecoverable, self.HiNonRecoverable )

        messages = []

        valid = self.NonCritical.Valid() or self.Critical.Valid() or self.NonRecoverable.Valid()
        if valid:
            if self.reading.Valid():
                if not self.failurecondition:
                    self.failurecondition = self.NonRecoverable.FailureCondition( self.reading )
                    if self.failurecondition:
                        messages = [ 2060 ]
                if not self.failurecondition:
                    self.failurecondition = self.Critical.FailureCondition( self.reading )
                    if self.failurecondition:
                        messages = [ 2062 ]
                if not self.failurecondition:
                    self.failingstate = FailingStates.NoFailure
                if not self.warningcondition:
                    self.warningcondition = self.NonCritical.FailureCondition( self.reading )
                if self.warningcondition:
                    self.warningstate = WarningStates.Warning
                    messages.append( 2030 )
                else:
                    self.warningstate = WarningStates.NoWarning
            else:
                self.warningstate = WarningStates.Warning
                messages.append( 2031 )
        else:
            self.failingstate = FailingStates.NoFailure
            self.warningstate = WarningStates.Warning
            messages.append( 2033 )
            if self.reading.Valid():
                if self.unit == "RPM":
                    if self.reading == 0.0:
                        self.warningstate = WarningStates.Warning
                        messages.append( 2031 )
                elif self.unit == "Volts":
                    if self.reading == 0.0:
                        self.warningstate = WarningStates.Warning
                        messages.append( 2031 )
            else:
                self.warningstate = WarningStates.Warning
                messages.append( 2034 )
        if self.failurecondition:
            self.failingstate = FailingStates.Failure
            messages.append( 2032 )
        else:
            self.failingstate = FailingStates.NoFailure
        if self.failingstate == FailingStates.Failure:
            vtsstream = vts.ERROR
        else:
            if self.warningstate == WarningStates.Warning and warning:
                vtsstream = vts.ERROR
            else:
                vtsstream = vts.VERBOSE
        self.vtsprint( vts.NO_EXIT, (vtsstream,) )
        for i in messages:
            vts.message( vts.NO_EXIT, vts.VERBOSE, "NULL", i, None )
        vts.message( vts.NO_EXIT, vts.VERBOSE,    "NULL", 2040, None ) # newline
        return self.failurecondition


#--------------------------------------------------------------------------------------------------
def SensorsDataGather( warning ):
    sensordata = []
    ipmitoolpath = "/usr/sfw/bin/ipmitool"
    if os.access( ipmitoolpath, os.X_OK ):
        s = "%s -I bmc sensor list" % (ipmitoolpath)
        vts.message( vts.NO_EXIT, vts.VERBOSE, "NULL", 2000, "Launching " + s )
        f = os.popen( s, "r" )
        for line in f.readlines():
            fields = line.split( "|" )
            fields = [ field.strip() for field in fields ]
            if fields[2] in ("RPM","Volts","degrees C"):
                sensor = IpmiSensor( fields )
                sensordata.append( sensor )
                sensor.analyse( warning )
        f.close()    
        failurecount = 0
        warningcount = 0
        healthysensorcount = 0
        for i in sensordata:
            if i.failingstate == FailingStates.Failure:
                failurecount += 1
            if i.warningstate == WarningStates.Warning:
                warningcount += 1
            if (i.failingstate != FailingStates.Failure) and (i.warningstate == WarningStates.Warning):
                healthysensorcount += 1
        if failurecount > 0:
            vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6000, str(failurecount) )
        else:
            if not warning and healthysensorcount > 0:
                vts.message( vts.NO_EXIT, vts.INFO, "NULL", 2002, None )
        if warningcount > 0:
            vts.message( vts.NO_EXIT, vts.VERBOSE, "NULL", 2050, str(warningcount) )
            if warning:
                vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6004, None )

    else:
        vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6003, None )
    return sensordata


#--------------------------------------------------------------------------------------------------
def main( warning ):
    """
    return 0 == successfull test
    """
    status = 0
    if os.access( "/dev/bmc", os.F_OK ):
        try:
            sensordata = SensorsDataGather( warning )
            if not sensordata:
                vts.message( vts.NO_EXIT, vts.VERBOSE, "NULL", 2070, None )
                time.sleep( 60 )
                sensordata = SensorsDataGather( warning )
                if not sensordata:
                    vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6001, None )
            else:
                status = 1
        except IOError,e:
            vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 2000, str(e) )
            vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6001, None )
            status = 1
    else:
        vts.message( vts.NO_EXIT, vts.ERROR, "NULL", 6002, None )
        status = 1
    return status


#--------------------------------------------------------------------------------------------------
def commandline_parse( argv ):
    warning = False
    if argv:
        try:
            dasho = argv.index( "-o" )
            try:
                if "warning" in argv[dasho+1]:
                    warning = True
            except IndexError:
                vts.message( vts.NO_EXIT, vts.INFO, "NULL", 2001, None )
                pass
        except ValueError:
            pass
    return warning


#--------------------------------------------------------------------------------------------------
if __name__ == "__main__":
    vts.cvar.device_name = "/dev/bmc"
    warning = commandline_parse( sys.argv )
    # ipmitool doesn't allow me to specify a device, it always opens /dev/bmc
    vts.test_modes = vts.CONN_MODE | vts.OFFLINE_MODE | vts.EXCLUSIVE_MODE | vts.ONLINE_MODE 
    vts.vts_init( len(sys.argv), sys.argv, "" )
    status = main( warning )
    vts.test_end()
    sys.exit( status )
