#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2015             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# NOTE: Devices of type 3850 with firmware versions 3.2.0SE, 3.2.1, 3.2.2
# have been observed to display a tenth of the actual temperature value.
# A firmware update on the device fixes this.

# OID: ifAdminStatus
cisco_temperature_admin_state_map = {
    '1': "up",
    '2': "down",
    '3': "testing",
}


def parse_cisco_temperature(info):
    # CISCO-ENTITY-SENSOR-MIB entSensorType
    cisco_sensor_types = {
        "1": "other",
        "2": "unknown",
        "3": "voltsAC",
        "4": "voltsDC",
        "5": "amperes",
        "6": "watts",
        "7": "hertz",
        "8": "celsius",
        "9": "parentRH",
        "10": "rpm",
        "11": "cmm",
        "12": "truthvalue",
        "13": "specialEnum",
        "14": "dBm",
    }

    # CISCO-ENTITY-SENSOR-MIB::entSensorScale
    cisco_entity_exponents = {
        "1": -24,  #     1:yocto
        "2": -21,  #     2:zepto
        "3": -18,  #     3:atto
        "4": -15,  #     4:femto
        "5": -12,  #     5:pico
        "6": -9,  #     6:nano
        "7": -6,  #     7:micro
        "8": -3,  #     8:milli
        "9": 0,  #     9:units
        "10": 3,  #     10:kilo
        "11": 6,  #     11:mega
        "12": 9,  #     12:giga
        "13": 12,  #     13:tera
        "14": 18,  #     14:exa
        "15": 15,  #     15:peta
        "16": 21,  #     16:zetta
        "17": 24,  #     17:yotta
    }

    # CISCO-ENTITY-SENSOR-MIB::entSensorStatus
    map_states = {
        "1": (0, "OK"),
        "2": (3, "unavailable"),
        "3": (3, "non-operational"),
    }

    # CISCO-ENVMON-MIB
    map_envmon_states = {
        '1': (0, "normal"),
        '2': (1, "warning"),
        '3': (2, "critical"),
        '4': (2, "shutdown"),
        '5': (3, "not present"),
        '6': (2, "not functioning"),
    }

    # description_info = [...,
    #                     [u'25955', u'Ethernet1/9(Rx-dBm)'],
    #                     [u'25956', u'Ethernet1/9(Tx-dBm)'],
    #                     ...]
    # state_info = [...,
    #               [u'25955', u'14', u'8', u'0', u'-3487', u'1'],
    #               [u'25956', u'14', u'8', u'0', u'-2525', u'1'],
    #               ...]
    # admin_states = [['Ethernet1/9', '1'], ...]
    #

    # IMPORTANT HINT: Temperature sensors uniquely identified via the indices in
    # description_info and linked to data in state_info are different sensors than
    # the ones contained in the perfstuff data structure. Sensors contained in the
    # perfstuff data structure contain only one threshold value for temperature.
    #
    # description_info = [...,
    #                     [u'1008', u'Switch 1 - WS-C2960X-24PD-L - Sensor 0'],
    #                     ...,
    #                     [u'2008', u'Switch 2 - WS-C2960X-24PD-L - Sensor 0'],
    #                     ...]
    # perfstuff = [...,
    #              [u'1008', u'SW#1, Sensor#1, GREEN', u'36', u'68', u'1'],
    #              [u'2008', u'SW#2, Sensor#1, GREEN', u'37', u'68', u'1'],
    #              ...]

    description_info, state_info, levels_info, perfstuff, admin_states = info

    # Create dict of sensor descriptions
    descriptions = dict(description_info)

    # Map admin state of Ethernet ports to sensor_ids of corresponding ethernet port sensors.
    # E.g. Ethernet1/9 -> Ethernet1/9(Rx-dBm), Ethernet1/9(Tx-dBm)
    # In case the description has been modified in the switch device this
    # mapping will not be successful. The description contains an ID instead of
    # a human readable string to identify the sensors then. The sensors cannot
    # be looked up in the description_info then.
    admin_states_dict = {}
    for if_name, admin_state in admin_states:
        for sensor_id, descr in descriptions.iteritems():
            if descr.startswith(if_name):
                admin_states_dict[sensor_id] = cisco_temperature_admin_state_map.get(admin_state)

    # Create dict with thresholds
    thresholds = {}
    for sensor_id, sensortype_id, scalecode, magnitude, value, sensorstate in state_info:
        thresholds.setdefault(sensor_id, [])

    for endoid, level in levels_info:
        # endoid is e.g. 21549.9 or 21459.10
        sensor_id, _subid = endoid.split('.')
        thresholds.setdefault(sensor_id, []).append(level)

    # Parse OIDs described by CISCO-ENTITY-SENSOR-MIB
    entity_parsed = {}
    for sensor_id, sensortype_id, scalecode, magnitude, value, sensorstate in state_info:
        sensortype = cisco_sensor_types.get(sensortype_id)
        if sensortype not in ("dBm", "celsius"):
            continue

        if sensor_id in descriptions:
            descr = descriptions[sensor_id]
        else:
            descr = sensor_id

        if not descr:
            continue

        entity_parsed.setdefault(sensortype_id, {})

        sensor_attrs = {
            'descr': descr,
            'raw_dev_state': sensorstate,  # used in discovery function
            'dev_state': map_states.get(sensorstate, (3, 'unknown[%s]' % sensorstate)),
            'admin_state': admin_states_dict.get(sensor_id),
        }

        if sensorstate == '1':
            factor = 10.0**(float(cisco_entity_exponents[scalecode]) - float(magnitude))
            sensor_attrs['reading'] = float(value) * factor
            # All sensors have 4 threshold values.
            # Map thresholds [crit_upper, warn_upper, crit_lower, warn_lower] to
            # dev_levels (warn_upper, crit_upper, warn_lower, crit_lower) conform
            # with check_levels() signature.
            # e.g. [u'75000', u'70000', u'-5000', u'0'] -> (70000, 75000, 0, -5000)
            # For temperature sensors only the upper levels are considered.
            # e.g. [u'75000', u'70000, u'-5000', u'0'] -> (70000, 75000)
            # In case devices do no validation when thresholds are set this could result
            # in threshold values in a wrong order. To keep the behaviour consistent
            # to temperature sensors the device levels are ordered accoringly.
            if sensortype == "dBm" and len(thresholds[sensor_id]) == 4:
                unsorted_thresholds = thresholds[sensor_id][0:4]
                converted_thresholds = [float(t) * factor for t in unsorted_thresholds]
                sorted_thresholds = sorted(converted_thresholds, key=float)
                opt_crit_upper, opt_warn_upper, opt_crit_lower, opt_warn_lower = sorted_thresholds[
                    3], sorted_thresholds[2], sorted_thresholds[0], sorted_thresholds[1]
                dev_levels = (opt_warn_upper, opt_crit_upper, opt_warn_lower, opt_crit_lower)
            elif sensortype == "celsius" and len(thresholds[sensor_id]) == 4:
                temp_crit_upper_raw, temp_warn_upper_raw = thresholds[sensor_id][0:2]
                # Some devices deliver these values in the wrong order. In case the devices
                # do no validation when thresholds are set this could result in values in a
                # wrong oder as well. Device levels are assigned accoring to their size.
                dev_levels = (
                    min(float(temp_warn_upper_raw) * factor,
                        float(temp_crit_upper_raw) * factor),
                    max(float(temp_warn_upper_raw) * factor,
                        float(temp_crit_upper_raw) * factor),
                )
            else:
                dev_levels = None
            sensor_attrs['dev_levels'] = dev_levels
            entity_parsed[sensortype_id].setdefault(sensor_id, sensor_attrs)

    found_temp_sensors = entity_parsed.get('8', {})
    parsed = {}
    temp_sensors = parsed.setdefault('8', {})
    for sensor_id, statustext, temp, max_temp, state in perfstuff:
        if sensor_id in descriptions and sensor_id in found_temp_sensors:
            # if this sensor is already in the dictionary, ensure we use the same name
            item = descriptions[sensor_id]
            prev_description = cisco_sensor_item(statustext, sensor_id)
            # also register the name we would have used up to 1.2.8b4, so we can give
            # the user a proper info message.
            # It's the little things that show you care
            temp_sensors[prev_description] = {"obsolete": True}
        else:
            item = cisco_sensor_item(statustext, sensor_id)

        temp_sensor_attrs = {
            'raw_dev_state': state,
            'dev_state': map_envmon_states.get(state, (3, 'unknown[%s]' % state)),
        }

        try:
            temp_sensor_attrs['reading'] = int(temp)
            if max_temp and int(max_temp):
                temp_sensor_attrs['dev_levels'] = (int(max_temp), int(max_temp))
            else:
                temp_sensor_attrs['dev_levels'] = None
        except:
            temp_sensor_attrs['dev_state'] = (3, 'sensor defect')

        temp_sensors.setdefault(item, temp_sensor_attrs)

    for sensor_type, sensors in entity_parsed.iteritems():
        for sensor_attrs in sensors.values():
            # Do not overwrite found sensors from perfstuff loop
            parsed.setdefault(sensor_type, {}).setdefault(sensor_attrs['descr'], sensor_attrs)

    return parsed


#   .--temperature---------------------------------------------------------.
#   |      _                                      _                        |
#   |     | |_ ___ _ __ ___  _ __   ___ _ __ __ _| |_ _   _ _ __ ___       |
#   |     | __/ _ \ '_ ` _ \| '_ \ / _ \ '__/ _` | __| | | | '__/ _ \      |
#   |     | ||  __/ | | | | | |_) |  __/ | | (_| | |_| |_| | | |  __/      |
#   |      \__\___|_| |_| |_| .__/ \___|_|  \__,_|\__|\__,_|_|  \___|      |
#   |                       |_|                                            |
#   +----------------------------------------------------------------------+
#   |                                                                      |
#   '----------------------------------------------------------------------'


def inventory_cisco_temperature(parsed):
    for item, value in parsed.get('8', {}).iteritems():
        if not value.get("obsolete", False):
            yield item, {}


def check_cisco_temperature(item, params, parsed):
    temp_parsed = parsed.get('8', {})
    if item in temp_parsed:
        data = temp_parsed[item]
        if data.get("obsolete", False):
            return 3, "This sensor is obsolete, please rediscover"

        state, state_readable = data['dev_state']
        reading = data.get('reading')
        if reading is None:
            return state, 'Status: %s' % state_readable
        return check_temperature(reading,
                                 params,
                                 "cisco_temperature_%s" % item,
                                 dev_levels=data['dev_levels'],
                                 dev_status=state,
                                 dev_status_name=state_readable)


check_info['cisco_temperature'] = {
    "parse_function"     : parse_cisco_temperature,
    "inventory_function" : inventory_cisco_temperature,
    "check_function"     : check_cisco_temperature,
    "service_description": "Temperature %s",
    "group"              : "temperature",
    "has_perfdata"       : True,
    "snmp_scan_function" : lambda oid: "cisco" in oid(".1.3.6.1.2.1.1.1.0").lower() and \
                                    ( oid(".1.3.6.1.4.1.9.9.91.1.1.1.1.*") is not None or
                                      oid(".1.3.6.1.4.1.9.9.13.1.3.1.3.*") is not None ),
    "snmp_info"          : [
                               # cisco_temp_sensor data
                               ( ".1.3.6.1.2.1.47.1.1.1.1", [
                                 OID_END,
                                 CACHED_OID(2), # Description of the sensor
                               ]),

                               # Type and current state
                               ( ".1.3.6.1.4.1.9.9.91.1.1.1.1", [
                                 OID_END,
                                 1, # CISCO-ENTITY-SENSOR-MIB::entSensorType
                                 2, # CISCO-ENTITY-SENSOR-MIB::entSensorScale
                                 3, # CISCO-ENTITY-SENSOR-MIB::entSensorPrecision
                                 4, # CISCO-ENTITY-SENSOR-MIB::entSensorValue
                                 5, # CISCO-ENTITY-SENSOR-MIB::entSensorStatus
                               ]),

                               # Threshold
                               ( ".1.3.6.1.4.1.9.9.91.1.2.1.1", [
                                 OID_END,
                                 4, # Thresholds
                               ]),

                               # cisco_temp_perf data
                               ( ".1.3.6.1.4.1.9.9.13.1.3.1", [ # CISCO-SMI
                                 OID_END,
                                 2, # ciscoEnvMonTemperatureStatusDescr
                                 3, # ciscoEnvMonTemperatureStatusValue
                                 4, # ciscoEnvMonTemperatureThreshold
                                 6, # ciscoEnvMonTemperatureState
                               ]),
                               ( ".1.3.6.1.2.1.2.2.1", [
                                 CACHED_OID(2),  # Description of the sensor
                                 CACHED_OID(7),  # ifAdminStatus
                               ]),
                            ],
    "includes"          : [ "temperature.include", 'cisco_sensor_item.include' ],
}

#.
#   .--dom-----------------------------------------------------------------.
#   |                            _                                         |
#   |                         __| | ___  _ __ ___                          |
#   |                        / _` |/ _ \| '_ ` _ \                         |
#   |                       | (_| | (_) | | | | | |                        |
#   |                        \__,_|\___/|_| |_| |_|                        |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | digital optical monitoring                                           |
#   '----------------------------------------------------------------------'

discovery_cisco_dom_rules = []


def inventory_cisco_temperature_dom(parsed):
    discovery_options = host_extra_conf_merged(host_name(), discovery_cisco_dom_rules)
    parsed_dom = parsed.get('14', {})
    admin_states_to_discover = {
        cisco_temperature_admin_state_map[admin_state]
        for admin_state in discovery_options.get('admin_states', ['1'])
    } | {None}
    for item, attrs in parsed_dom.iteritems():
        dev_state = attrs.get('raw_dev_state')
        adm_state = attrs.get('admin_state')
        if dev_state == '1' and adm_state in admin_states_to_discover:
            yield item, {}


def _determine_levels(user_levels, device_levels):
    if isinstance(user_levels, tuple):
        return user_levels
    elif user_levels:
        return device_levels
    return (None, None)


def check_cisco_temperature_dom(item, params, parsed):
    # TODO perf, precision, severity, etc.

    data = parsed.get('14', {}).get(item, {})
    reading = data.get('reading')
    if reading is None:
        return

    # TODO: care about check status which is always OK
    state, state_readable = data['dev_state']
    yield state, 'Status: %s' % state_readable

    # get won't save you, because 'dev_levels' may be present, but None.
    device_levels = data.get('dev_levels') or (None, None, None, None)

    # Map WATO configuration of levels to check_levels() compatible tuple.
    # Default value in case of missing WATO config is use device levels.
    levels = (_determine_levels(params.get("power_levels_upper", True), device_levels[0:2]) +
              _determine_levels(params.get("power_levels_lower", True), device_levels[2:4]))

    if "Transmit" in data["descr"]:
        dsname = "output_signal_power_dbm"
    elif "Receive" in data["descr"]:
        dsname = "input_signal_power_dbm"
    else:
        # in rare case of sensor id instead of sensor description no destinction
        # between transmit/receive possible
        dsname = "signal_power_dbm"
    yield check_levels(reading, dsname, levels, unit='dBm', infoname="Signal power")


check_info['cisco_temperature.dom'] = {
    "inventory_function": inventory_cisco_temperature_dom,
    "check_function": check_cisco_temperature_dom,
    "service_description": "DOM %s",
    "group": "cisco_dom",
    "has_perfdata": True,
    "includes": ['cisco_sensor_item.include'],
}
