#! /usr/bin/python3
#-------------------------------------------------------------------------------
#
# Copyright (C) 2020-2021 NVIDIA Corporation. ALL RIGHTS RESERVED.
# Copyright 2013,2015,2016,2018, Cumulus Networks, Inc.  All rights reserved
#
#-------------------------------------------------------------------------------
#

#-------------------------------------------------------------------------------
#
# Imports
#

import syslog
import warnings
import re
import os
import sys
import time
import signal
import traceback
import argparse
import configparser
import cumulus.platforms
import cumulus.sdnotify
import shutil

ReloadDir = '/cumulus/smond/'
ReloadFile = ReloadDir + 'reload'

halt = 0

def setHaltSensorReads(h):
    global halt
    halt = h

def getHaltSensorReads():
    global halt
    return halt


# Classes
#
class ArgParseError(RuntimeError):
    pass

class SMONDRuntimeError(RuntimeError):
    pass

#-------------------------------------------------------------------------------
#
# Functions
#

#--------------------
#
# warning formats
#

def smondwarn(message, category, filename, lineno, line=None):
    return '%s:%s : %s : %s\n' % (filename, lineno, category.__name__, message)

#--------------------
#
# check to see if an instance is already running
#
def already_running(pidfile):
    myname=os.path.basename(sys.argv[0])
    try:
        if not os.path.isfile(pidfile):
            return False
        oldpid = re.findall('\D*(\d+).*', (open(pidfile, 'r').readline()))[0]
        if not os.path.exists('/proc/%s' % oldpid):
            return False
        if myname not in open('/proc/%s/cmdline' % oldpid, 'r').readline():
            return False
        sys.stderr.write("%s already running as process %s\n" % (myname, oldpid))
        return True
    except Exception as inst:
        raise SMONDRuntimeError("unable to validate pidfile %s: %s" %
                                (pidfile, str(inst)))

#--------------------
#
# normal exit
#
def exit_normally(signum=0, frame=None):
    setHaltSensorReads(1)
    syslog.syslog(syslog.LOG_INFO, "exiting normally")
    sys.stderr.write("%s : exiting normally\n" % str(sys.argv[0]))
    exit(0)

#
# Main
#
def main():
    interval = 10
    path = '/etc/smond.conf'
    crit_action_path = None
    thres_action_path = None
    config = configparser.ConfigParser()
    if os.path.isfile(path):
        config.read(path)
        if config.has_option("DEFAULT", "interval"):
            interval = int(config.get("DEFAULT", "interval"))
        if config.has_option("DEFAULT", "crit_action_path"):
            crit_action_path = config.get("DEFAULT", "crit_action_path")
        if config.has_option("DEFAULT", "thres_action_path"):
            thres_action_path = config.get("DEFAULT", "thres_action_path")

    parser = argparse.ArgumentParser(description="Monitor sensors data in a loop")
    parser.add_argument('-i', '--interval', type = int,
                        help ="Update Interval in secs (default is %d secs)" %interval)
    args = parser.parse_args()
    if args.interval:
        interval = args.interval

    pidfile = "/var/run/smond.pid"
    if already_running(pidfile):
        sys.exit()
    else:
        open(pidfile, 'w').write(str(os.getpid()))

    # Create the smond reload file
    if not os.path.exists(ReloadDir):
        os.makedirs(ReloadDir)
    reload_file = open(ReloadFile, "w")
    reload_file.close()        

    # If we have to log a syslog message about a mismatch in fan directions,
    # log one message every 5 minutes. 
    interval_count = 0
    fan_dir_syslog_period = 300 / interval

    syslog.syslog(syslog.LOG_INFO, "Monitoring HW Sensors")
    while True:
        # Check if we need to do a reload of smond
        if os.path.exists(ReloadFile):
            os.remove(ReloadFile)            

            p = cumulus.platforms.probe()
            # Since p.sensors is created one time at init and never
            # reinstantiated, the sensors are not recreated without
            # an explicit function call
            if hasattr(p.sensors, 'reprobe'):
                p.sensors.reprobe()
                
            units = []

            if p.sensors is None:
                syslog.syslog(syslog.LOG_INFO, "smond doesnt manage sensors on this platform")
                # This dir is needs to satisfy ledmgrd and smonctl requirements.
                status_path = '/run/cache/cumulus/unit_state'
                if not os.path.exists(status_path):
                    os.makedirs(status_path)
                units = None
            else:
                for unit in p.sensors.units:
                    cp = unit
                    if os.path.exists(cp.status_path):
                        shutil.rmtree(cp.status_path, ignore_errors=True)
                    if p.cpld is not None:
                        cp.update_cpld_path(p.cpld.path)
                    if cp.attrs['type'] == 'temp':
                        cp.update_crit_action_path(crit_action_path)
                        cp.update_thres_action_path(thres_action_path)
                    units.append(cp)

            # This file is created when all sensors have been updated in
            # /run/cache/cumulus/unit_state. ledmgrd is a consumer of this
            # file.
            done_path = '/run/cache/cumulus/unit_state_init_done'
            if os.path.isfile(done_path):
                os.remove(done_path)

        if units:
            b2f = f2b = 0
            for unit in units:
                unit.run_state()
                if unit.attrs['state'] != unit.attrs['prev_state']:
                    msg = '%s' %unit.attrs['name']
                    if unit.attrs['name'] != unit.attrs['description']:
                        msg += '(%s)' %unit.attrs['description']
                    msg += ': state changed from %s to %s' \
                        %(unit.attrs['prev_state'], unit.attrs['state'])
                    syslog.syslog(syslog.LOG_WARNING, msg)
                if unit.attrs['msg'] is not None and \
                    unit.attrs['msg'] != unit.attrs['prev_msg']:
                    syslog.syslog(syslog.LOG_WARNING, unit.attrs['msg'])

                # System & PSU fan direction monitoring
                if unit.attrs['type'] == 'fan':
                    dir = unit.get_fan_dir()
                    if dir == 'B2F':
                        b2f += 1
                    elif dir == 'F2B':
                        f2b += 1

            # If we have a mix of B2F and F2B fans, generate a critical level syslog message
            if b2f > 0 and f2b > 0 and interval_count % fan_dir_syslog_period == 0:
                msg = 'Fan direction mismatch! (%d B2F fans, %d F2B fans)' % (b2f,f2b)
                syslog.syslog(syslog.LOG_CRIT, msg)
                
        if not os.path.isfile(done_path):
            open(done_path, 'a').close()

            # Tell systemd that we are initialized and ready
            # Done after first past, because this is what ledmgrd
            # is looking for, and the file is removed just prior to
            # the main loop being entered
            rc = cumulus.sdnotify.sd_notify(0, bytes('READY=1', 'ASCII'))

        interval_count += 1
        time.sleep(interval)
        if getHaltSensorReads():
            return 0 
    return 0


#--------------------
#
# execution check
#
if __name__ == "__main__":
    try:
        signal.signal(signal.SIGTERM, exit_normally)
        # Cause all warnings to always be triggered.
        warnings.simplefilter("always")
        warnings.formatwarning = smondwarn
        syslog.openlog(": %s : " % sys.argv[0])
        syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
        exit(main())
    except SMONDRuntimeError as errstr:
        syslog.syslog(syslog.LOG_ERR, "ERROR : %s" % str(errstr))
        sys.stderr.write("%s : ERROR : %s\n" % (sys.argv[0], str(errstr)))
        exit(1)
    except KeyboardInterrupt:
        exit_normally()
    except Exception:
        (exc_type, exc_value, exc_traceback) = sys.exc_info()
        err = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
        log = 'Unhandled Exception : %s' % err
        syslog.syslog(syslog.LOG_ERR, log)
        sys.stderr.write(log)
