#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2016             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.

# <<<openhardwaremonitor:sep(44)>>>
# Index,Name,Parent,SensorType,Value
# 1,Available Memory,/ram,Data,3.009140
# 0,CPU Total,/intelcpu/0,Load,1.953125
# 0,Used Space,/hdd/0,Load,58.754211
# 4,CPU Core #4,/intelcpu/0,Load,0.000000
# 2,CPU Graphics,/intelcpu/0,Power,0.000000
# 2,CPU Core #2,/intelcpu/0,Load,6.250000
# 4,CPU Core #4,/intelcpu/0,Clock,3192.720947
# 0,Bus Speed,/intelcpu/0,Clock,99.772530
# 3,CPU DRAM,/intelcpu/0,Power,0.000000
# 3,CPU Core #3,/intelcpu/0,Load,0.000000
# 1,CPU Core #1,/intelcpu/0,Clock,3192.720947
# 3,CPU Core #3,/intelcpu/0,Clock,3192.720947
# 0,Memory,/ram,Load,24.763321
# 0,Used Memory,/ram,Data,0.990425
# 2,CPU Core #2,/intelcpu/0,Clock,3192.720947
# 0,CPU Package,/intelcpu/0,Power,0.000000
# 1,CPU Cores,/intelcpu/0,Power,0.000000
# 1,CPU Core #1,/intelcpu/0,Load,1.562500

# Newer agent data provide WMIStatus column
# Index,Name,Parent,SensorType,Value,WMIStatus
# 1,CPU Core #1,/intelcpu/0,Load,1.562500,OK/Timeout

# since the temperature sensors could be anything (cpu, gpu, hdd, psu) we need different
# default levels per item type
factory_settings['openhardwaremonitor_temperature_default_levels'] = {
    "cpu": {
        'levels': (60, 70)
    },
    "hdd": {
        'levels': (40, 50)
    },
    "_default": {
        'levels': (70, 80)
    }
}

OpenhardwaremonitorTraits = {
    'Clock': {
        'unit': " MHz",
        'factor': 1.0,
        'perf_var': 'clock'
    },
    'Temperature': {
        'unit': u"°C",
        'factor': 1.0
    },
    'Power': {
        'unit': " W",
        'factor': 1.0,
        'perf_var': 'w'
    },
    'Fan': {
        'unit': " RPM",
        'factor': 1.0
    },
    'Level': {
        'unit': "%",
        'factor': 1.0
    },
    # unused below here
    'Voltage': {
        'unit': " V",
        'factor': 1.0
    },
    'Load': {
        'unit': "%",
        'factor': 1.0
    },
    'Flow': {
        'unit': " L/h",
        'factor': 1.0
    },
    'Control': {
        'unit': "%",
        'factor': 1.0
    },
    'Factor': {
        'unit': "1",
        'factor': 1.0
    },
    'Data': {
        'unit': " B",
        'factor': 1073741824.0
    }
}
OpenhardwaremonitorSensor = collections.namedtuple("OpenhardwaremonitorSensor",
                                                   ("reading", "unit", "perf_var", "WMIstatus"))


def parse_openhardwaremonitor(info):
    parsed = {}
    for line in info:
        if line[0] == 'Index':
            # header line
            continue

        if len(line) == 5:
            # Old agent output has no WMIStatus column
            _index, name, parent, sensor_type, value = line
            wmistatus = 'OK'
        elif len(line) == 6:
            _index, name, parent, sensor_type, value, wmistatus = line
        else:
            continue

        full_name = _create_openhardwaremonitor_full_name(parent, name)
        traits = OpenhardwaremonitorTraits.get(sensor_type, {})
        parsed.setdefault(sensor_type, {}).setdefault(
            full_name,
            OpenhardwaremonitorSensor(
                float(value) * traits.get('factor', 1), traits.get('unit', ''),
                traits.get('perf_var'), wmistatus))
    return parsed


def _create_openhardwaremonitor_full_name(parent, name):
    def dict_replace(input_, replacements):
        pattern = regex(r'\b(' + '|'.join(replacements.keys()) + r')\b')
        return pattern.sub(lambda x: replacements[x.group()], input_)

    parent = dict_replace(parent, {"intelcpu": "cpu", "amdcpu": "cpu", "genericcpu": "cpu"})
    name = dict_replace(name, {"CPU ": "", "Temperature": ""})
    return (parent.replace("/", "") + " " + name).strip()


def _openhardwaremonitor_worst_status(*args):
    order = [0, 1, 3, 2]
    return sorted(args, key=lambda x: order[x], reverse=True)[0]


def _openhardwaremonitor_expect_order(*args):
    arglist = [x for x in args if x is not None]
    sorted_by_val = sorted(enumerate(arglist), key=lambda x: x[1])
    return max([abs(x[0] - x[1][0]) for x in enumerate(sorted_by_val)])


def inventory_openhardwaremonitor(sensor_type, parsed):
    return [(key, {}) for key in parsed.get(sensor_type, {}).keys()]


def check_openhardwaremonitor(sensor_type, item, params, parsed):
    if item in parsed.get(sensor_type, {}):
        data = parsed[sensor_type][item]
        _check_openhardwaremonitor_wmistatus(data)
        if 'lower' in params:
            status_lower = _openhardwaremonitor_expect_order(params['lower'][1], params['lower'][0],
                                                             data.reading)
        else:
            status_lower = 0
        if 'upper' in params:
            status_upper = _openhardwaremonitor_expect_order(data.reading, params['upper'][0],
                                                             params['upper'][1])
        else:
            status_upper = 0

        perfdata = []
        if data.perf_var:
            perfdata = [(data.perf_var, data.reading)]

        return (_openhardwaremonitor_worst_status(status_lower, status_upper),
                '%.1f%s' % (data.reading, data.unit), perfdata)


def _check_openhardwaremonitor_wmistatus(data):
    if data.WMIstatus.lower() == 'timeout':
        raise MKCounterWrapped('WMI query timed out')


#   .--clock---------------------------------------------------------------.
#   |                            _            _                            |
#   |                        ___| | ___   ___| | __                        |
#   |                       / __| |/ _ \ / __| |/ /                        |
#   |                      | (__| | (_) | (__|   <                         |
#   |                       \___|_|\___/ \___|_|\_\                        |
#   |                                                                      |
#   '----------------------------------------------------------------------'

check_info['openhardwaremonitor'] = {
    'inventory_function':
        lambda parsed: inventory_openhardwaremonitor('Clock', parsed),
    'check_function':
        lambda item, params, parsed: check_openhardwaremonitor('Clock', item, params, parsed),
    'parse_function':
        parse_openhardwaremonitor,
    'has_perfdata':
        True,
    'service_description':
        "Clock %s",
}

#.
#   .--temp----------------------------------------------------------------.
#   |                       _                                              |
#   |                      | |_ ___ _ __ ___  _ __                         |
#   |                      | __/ _ \ '_ ` _ \| '_ \                        |
#   |                      | ||  __/ | | | | | |_) |                       |
#   |                       \__\___|_| |_| |_| .__/                        |
#   |                                        |_|                           |
#   '----------------------------------------------------------------------'


def check_openhardwaremonitor_temperature(item, params, parsed):
    if not 'levels' in params:
        found = False
        for key, value in params.iteritems():
            if key in item:
                params = params[key]
                found = True
                break
        if not found:
            params = params["_default"]

    if item in parsed.get('Temperature', {}):
        data = parsed['Temperature'][item]
        _check_openhardwaremonitor_wmistatus(data)
        return check_temperature(data.reading, params, "openhardwaremonitor_%s" % item)


check_info['openhardwaremonitor.temperature'] = {
    'inventory_function': lambda parsed: inventory_openhardwaremonitor('Temperature', parsed),
    'check_function': check_openhardwaremonitor_temperature,
    'has_perfdata': True,
    'service_description': "Temperature %s",
    'group': 'temperature',
    'default_levels_variable': 'openhardwaremonitor_temperature_default_levels',
    'includes': ['temperature.include']
}

#.
#   .--power---------------------------------------------------------------.
#   |                                                                      |
#   |                    _ __   _____      _____ _ __                      |
#   |                   | '_ \ / _ \ \ /\ / / _ \ '__|                     |
#   |                   | |_) | (_) \ V  V /  __/ |                        |
#   |                   | .__/ \___/ \_/\_/ \___|_|                        |
#   |                   |_|                                                |
#   '----------------------------------------------------------------------'

check_info['openhardwaremonitor.power'] = {
    'inventory_function':
        lambda parsed: inventory_openhardwaremonitor('Power', parsed),
    'check_function':
        lambda item, params, parsed: check_openhardwaremonitor('Power', item, params, parsed),
    'has_perfdata':
        True,
    'service_description':
        "Power %s",
}

#.
#   .--fan-----------------------------------------------------------------.
#   |                            __                                        |
#   |                           / _| __ _ _ __                             |
#   |                          | |_ / _` | '_ \                            |
#   |                          |  _| (_| | | | |                           |
#   |                          |_|  \__,_|_| |_|                           |
#   |                                                                      |
#   '----------------------------------------------------------------------'

factory_settings['openhardwaremonitor_fan_default_levels'] = {
    'lower': (None, None),
    'upper': (None, None),
}


def check_openhardwaremonitor_fan(item, params, parsed):
    if item in parsed.get("Fan", {}):
        data = parsed["Fan"][item]
        _check_openhardwaremonitor_wmistatus(data)
        return check_fan(data.reading, params)


check_info['openhardwaremonitor.fan'] = {
    'inventory_function': lambda parsed: inventory_openhardwaremonitor('Fan', parsed),
    'check_function': check_openhardwaremonitor_fan,
    'has_perfdata': True,
    'service_description': "Fan %s",
    'default_levels_variable': 'openhardwaremonitor_fan_default_levels',
    'group': 'hw_fans',
    'includes': ['fan.include'],
}

#.
#   .--smart---------------------------------------------------------------.
#   |                                             _                        |
#   |                    ___ _ __ ___   __ _ _ __| |_                      |
#   |                   / __| '_ ` _ \ / _` | '__| __|                     |
#   |                   \__ \ | | | | | (_| | |  | |_                      |
#   |                   |___/_| |_| |_|\__,_|_|   \__|                     |
#   |                                                                      |
#   '----------------------------------------------------------------------'

openhardwaremonitor_smart_readings = {
    'Level': [{
        'name': "Remaining Life",
        'key': 'remaining_life',
        'lower_bounds': True
    }],
}

factory_settings['openhardwaremonitor_smart_default_levels'] = {
    'remaining_life': (30, 10),  # wild guess
}


def inventory_openhardwaremonitor_smart(parsed):
    devices = set()
    # find all devices for which at least one known smart reading is available
    for sensor_type in openhardwaremonitor_smart_readings.keys():
        for key in parsed.get(sensor_type, {}):
            if "hdd" in key:
                devices.add(key.split(" ")[0])
    return [(dev, {}) for dev in devices]


def check_openhardwaremonitor_smart(item, params, parsed):
    for sensor_type, readings in openhardwaremonitor_smart_readings.iteritems():
        for reading in readings:
            reading_name = "%s %s" % (item, reading['name'])

            if not reading_name in parsed[sensor_type]:
                # what smart values ohm reports is device dependent
                continue

            warn, crit = params[reading['key']]
            data = parsed[sensor_type][reading_name]
            _check_openhardwaremonitor_wmistatus(data)

            if reading.get('lower_bounds', False):
                status = _openhardwaremonitor_expect_order(crit, warn, data.reading)
            else:
                status = _openhardwaremonitor_expect_order(data.reading, warn, crit)

            yield (
                status,
                "%s %.1f%s" % (reading['name'], data.reading, data.unit),
                [(reading['key'], data.reading)],
            )


# the smart check is different from the others as it has one item per device and
# combines different sensors per item (but not all, i.e. hdd temperature is still
# reported as a temperature item)
check_info['openhardwaremonitor.smart'] = {
    'inventory_function': inventory_openhardwaremonitor_smart,
    'check_function': check_openhardwaremonitor_smart,
    'has_perfdata': True,
    'service_description': "SMART %s Stats",
    'default_levels_variable': "openhardwaremonitor_smart_default_levels",
    'group': "openhardwaremonitor_smart"
}
