#!/usr/bin/env python3
# ***************************************************************************
#  IIIIII NNN  NNN  Copyright (C) 2022 Innovative Networks, Inc.
#    II    NNN  N   All Rights Reserved. Any redistribution or reproduction
#    II    N NN N   of part or all of the content of this program in any form
#    II    N  NNN   without expressed written consent of the copyright holder
#  IIIIII NNN  NNN  is strictly prohibited.  Please contact admins@in-kc.com
#   Be Innovative.  for additional information.
# ***************************************************************************
#  check_snmp_eatonups.py
#  Author - Ian Perry <iperry@indigex.com>
#  
#  Purpose:  Retrieves values and checks health of network-connected Eaton UPS devices
#  
#  Version History:
#       2023-09-29 - Initial creation
# ***************************************************************************

try:
    from inmon_utils import *
except:
    import sys
    print("Failed to import inmon_utils")
    sys.exit(3)

import argparse
from easysnmp import Session

parser = argparse.ArgumentParser(description="Checks health of network-connected Eaton UPS devices.")

parser.add_argument(
    "-H", "--hostname",
    dest="hostname",
    help="Specify hostname of device to check",
    required=True
)
parser.add_argument(
    "-T", "--test",
    dest="test",
    help="Specify test to run",
    choices=['all', 'input', 'output', 'battery', 'ambient', 'health', 'bypass'],
    type=str.lower,
    default='all'
)
parser.add_argument(
    "-p", "--percent",
    dest="battery_percent",
    help="Specify battery percentage remaining in format warn/crit",
    default='90/80'
)
parser.add_argument(
    "-r", "--runtime",
    dest="runtime",
    help="Specify battery runtime in seconds in format warn/crit",
    default='1800/600'
)
parser.add_argument(
    "-t", "--temperature",
    dest="temperature",
    help="Specify temperature thresholds in degrees Celsius in format warn:warn/crit:crit",
    default='4:30/2:35'
)
parser.add_argument(
    "-u", "--humidity",
    dest="humidity",
    help="Specify relative humidity thresholds in format warn:warn/crit:crit",
    default='30:60/15:70'
)

args = parser.parse_args()

# Specify global variables
oid_prefix = ".1.3.6.1.4.1.534.1"

return_value = 0
return_text = ""
return_data = ""

def input_test(session):
    global return_value
    global return_text
    global return_data
    # Get config values for input
    input_frequency  = int(session.get(f'{oid_prefix}.3.1.0').value)/10
    input_source     = int(session.get(f'{oid_prefix}.3.5.0').value)
    input_num_phases = int(session.get(f'{oid_prefix}.3.3.0').value)
    config_voltage   = int(session.get(f'{oid_prefix}.10.2.0').value)

    # Table for input sources. There is no 0 in the MIB
    input_source_table = [
        None,
        'other',
        'none',
        'primary utility',
        'bypass feed',
        'secondary utility',
        'generator',
        'flywheel',
        'fuel cell'
    ]

    # Input source check
    if input_source == 0:
        return_value = max(return_value, 3)
        return_text += "Unknown issue occurred when checking input source; "
    elif input_source != 3:
        return_value = max(return_value, 2)
        return_text += f"UPS input source critical, currently on {input_source_table[input_source]} input; "
    elif input_source == 3:
        pass # All good
    else:
        return_value = max(return_value, 3)
        return_text += "Unknown issue occurred when checking input source; "

    # Input phase voltage check
    for phase in range(1, int(input_num_phases) + 1):
        voltage = int(session.get(f'{oid_prefix}.3.4.1.2.{phase}').value)
        current = int(session.get(f'{oid_prefix}.3.4.1.3.{phase}').value)
        return_data += f"input_phase_{phase}_voltage={voltage}; "
        return_data += f"input_phase_{phase}_current={current}; "
        if voltage < (config_voltage * 0.917) or voltage > (config_voltage * 1.058):
            return_value = max(return_value, 2)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif voltage < (config_voltage * 0.95) or voltage > (config_voltage * 1.050):
            return_value = max(return_value, 1)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif (config_voltage * 0.95) < voltage < (config_voltage * 1.05):
            pass # Nothing to do, we're good.
        else:
            # something has gone wrong.
            return_value = max(return_value, 3)
            return_text += f"Error in voltage phase {phase} check; "
        
    # Input frequency check
    if not (59.9 <= input_frequency <= 60.1):
        return_value = max(return_value, 2)
        return_text += f"Input frequency outside acceptable range: {input_frequency:.1f}Hz; "
    return_data += f"input_freq={input_frequency:.1f}; "

def output_test(session):
    global return_value
    global return_text
    global return_data
    # Get config values for output
    output_frequency  = int(session.get(f'{oid_prefix}.4.2.0').value)/10
    output_source     = int(session.get(f'{oid_prefix}.4.5.0').value)
    output_num_phases = int(session.get(f'{oid_prefix}.4.3.0').value)
    config_voltage    = int(session.get(f'{oid_prefix}.10.2.0').value)

    # Table for output sources. There is no 0 in the MIB
    output_source_table = [
        None,
        'other',
        'none',
        'normal',
        'bypass',
        'battery',
        'booster',
        'reducer',
        'parallel capacity',
        'parallel redundant',
        'high efficiency mode'
    ]

    # Output source check
    if output_source == 0:
        return_value = max(return_value, 3)
        return_text += "Unknown issue occurred when checking output source; "
    elif output_source != 3:
        return_value = max(return_value, 2)
        return_text += f"UPS output source critical, currently on {output_source_table[output_source]} output; "
    elif output_source == 3:
        pass # All good
    else:
        return_value = max(return_value, 3)
        return_text += "Unknown issue occurred when checking output source; "

    # Output phase voltage check
    for phase in range(1, int(output_num_phases) + 1):
        voltage = int(session.get(f'{oid_prefix}.4.4.1.2.{phase}').value)
        current = int(session.get(f'{oid_prefix}.4.4.1.3.{phase}').value)
        return_data += f"output_phase_{phase}_voltage={voltage}; "
        return_data += f"output_phase_{phase}_current={current}; "
        if voltage < (config_voltage * 0.917) or voltage > (config_voltage * 1.058):
            return_value = max(return_value, 2)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif voltage < (config_voltage * 0.95) or voltage > (config_voltage * 1.050):
            return_value = max(return_value, 1)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif (config_voltage * 0.95) < voltage < (config_voltage * 1.05):
            pass # Nothing to do, we're good.
        else:
            # something has gone wrong.
            return_value = max(return_value, 3)
            return_text += f"Error in voltage phase {phase} check; "
        
    # Output frequency check
    if not (59.9 <= output_frequency <= 60.1):
        return_value = max(return_value, 2)
        return_text += f"Output frequency outside acceptable range: {output_frequency:.1f}Hz; "
    return_data += f"output_freq={output_frequency:.1f}; "

def bypass_test(session):
    global return_value
    global return_text
    global return_data
    # Get config values for bypass
    bypass_frequency  = int(session.get(f'{oid_prefix}.5.1.0').value)/10
    bypass_num_phases = int(session.get(f'{oid_prefix}.5.2.0').value)
    config_voltage    = int(session.get(f'{oid_prefix}.10.2.0').value)

    # Bypass phase voltage check
    for phase in range(1, int(bypass_num_phases) + 1):
        voltage = int(session.get(f'{oid_prefix}.5.3.1.2.{phase}').value)
        return_data += f"bypass_phase_{phase}_voltage={voltage}; "
        if voltage < (config_voltage * 0.917) or voltage > (config_voltage * 1.058):
            return_value = max(return_value, 2)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif voltage < (config_voltage * 0.95) or voltage > (config_voltage * 1.050):
            return_value = max(return_value, 1)
            return_text += f"Phase {phase} voltage outside acceptable range: {voltage}V; "
        elif (config_voltage * 0.95) < voltage < (config_voltage * 1.05):
            pass # Nothing to do, we're good.
        else:
            # something has gone wrong.
            return_value = max(return_value, 3)
            return_text += f"Error in voltage phase {phase} check; "
        
    # Bypass frequency check
    if not (59.9 <= bypass_frequency <= 60.1):
        return_value = max(return_value, 2)
        return_text += f"Bypass frequency outside acceptable range: {bypass_frequency:.1f}Hz; "
    return_data += f"bypass_freq={bypass_frequency:.1f}; "

def battery_test(session):
    global return_value
    global return_text
    global return_data
    # Obtain battery information
    battery_time_remaining = int(session.get(f'{oid_prefix}.2.1.0').value)
    battery_voltage        = int(session.get(f'{oid_prefix}.2.2.0').value)
    battery_current        = int(session.get(f'{oid_prefix}.2.3.0').value)
    battery_percentage     = int(session.get(f'{oid_prefix}.2.4.0').value)
    battery_abm_status     = int(session.get(f'{oid_prefix}.2.5.0').value)

    abm_status_table = [
        None, # 0 not in table
        "charging",
        "discharging",
        "floating",
        "resting",
        "unknown"
    ]

    # Parse warn/crit values
    battery_time_thresh = args.runtime.split('/')
    battery_time_warn = int(battery_time_thresh[0])
    battery_time_crit = int(battery_time_thresh[1])

    battery_percent_thresh = args.battery_percent.split('/')
    battery_percent_warn = int(battery_percent_thresh[0])
    battery_percent_crit = int(battery_percent_thresh[1])

    # Get minutes and seconds
    battery_time_tuple = divmod(battery_time_remaining, 60)

    # Process battery time remaining
    if battery_time_remaining < battery_time_crit:
        return_value = max(return_value, 2)
        return_text += f"Battery time critical at {battery_time_tuple[0]}m {battery_time_tuple[1]}s; "
    elif battery_time_crit < battery_time_remaining < battery_time_warn:
        return_value = max(return_value, 1)
        return_text += f"Battery time warning at {battery_time_tuple[0]}m {battery_time_tuple[1]}s; "
    elif battery_time_warn < battery_time_remaining:
        pass # All good
    else:
        return_value = max(return_value, 3)
        return_text += f"Unexpected value encountered in battery time check: {battery_time_remaining}s; "
    return_data += f"battery_time={battery_time_remaining}s; "

    # Process battery percent charge remaining
    if battery_percentage < battery_percent_crit:
        return_value = max(return_value, 2)
        return_text += f"Battery percentage critical at {battery_percentage}%; "
    elif battery_percent_crit < battery_percentage < battery_percent_warn:
        return_value = max(return_value, 1)
        return_text += f"Battery percentage warning at {battery_percentage}%; "
    elif battery_percent_warn < battery_percentage:
        pass # All good
    else:
        return_value = max(return_value, 3)
        return_text += f"Unexpected value encountered in battery percentage check: {battery_percentage}%; "
    return_data += f"battery_percentage={battery_percentage}%; "

    # These two give us nothing to check against but provide valuable data.
    return_data += f"battery_voltage={battery_voltage}V; "
    return_data += f"battery_current={battery_current}A; "
    return_data += f"battery_abm_status={battery_abm_status}; "

def ambient_test(session):
    global return_value
    global return_text
    global return_data
    temperature = int(session.get(f'{oid_prefix}.6.1.0').value)

    temp_ranges = args.temperature.split('/')
    temp_warn = temp_ranges[0].split(':')
    temp_warn = [int(i) for i in temp_warn]
    temp_crit = temp_ranges[1].split(':')
    temp_crit = [int(i) for i in temp_crit]

    if temperature < temp_crit[0] or temperature > temp_crit[1]:
        return_value = max(return_value, 2)
        return_text += f"Ambient temperature critical: {temperature}C; "
    elif temperature < temp_warn[0] or temperature > temp_warn[1]:
        return_value = max(return_value, 1)
        return_text += f"Ambient temperature warning: {temperature}C; "
    elif temp_warn[0] < temperature < temp_warn[1]:
        pass # All good
    else:
        return_value = max(return_value, 3)
        return_text += f"Unexpected value encountered in ambient test: {temperature}C; "
    return_data += f"temperature={temperature}C; "
    
def health_test(session):
    global return_value
    global return_text
    global return_data
    alarms      = int(session.get(f'{oid_prefix}.7.1.0').value)
    test_status = int(session.get(f'{oid_prefix}.8.2.0').value)

    test_result_table = [
        None, # 0 not in table
        "unknown",
        "passed",
        "failed",
        "in progress",
        "not supported",
        "inhibited",
        "scheduled"
    ]

    if test_status == 3 or test_status == 6:
        return_value = max(return_value, 2)
        return_text += f"Self test status critical: {test_result_table[test_status]}; "
    
    if alarms > 0:
        return_value = max(return_value, 2)
        return_text += f"Number of alarms critical: {alarms}; "

# Main
# Create SNMP session
session = Session(hostname=args.hostname, community='public', version=1)

if args.test == 'all' or args.test == 'input':
    input_test(session)
if args.test == 'all' or args.test == 'output':
    output_test(session)
if args.test == 'all' or args.test == 'bypass':
    bypass_test(session)
if args.test == 'all' or args.test == 'battery':
    battery_test(session)
if args.test == 'all' or args.test == 'ambient':
    ambient_test(session)
if args.test == 'all' or args.test == 'health':
    health_test(session)

if return_value == 0:
    return_text = f"[OKAY] - All checks have returned values within range."
if return_value == 1:
    return_text = f"[WARN] - {return_text}"
if return_value == 2:
    return_text = f"[CRIT] - {return_text}"
if return_value == 3:
    return_text = f"[UNKN] - {return_text}"

print(f"{return_text} | {return_data}")
sys.exit(return_value)