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

# Example output:
# <<<esx_vsphere_counters:sep(124)>>>
# net.broadcastRx|vmnic0|11|number
# net.broadcastRx||11|number
# net.broadcastTx|vmnic0|0|number
# net.broadcastTx||0|number
# net.bytesRx|vmnic0|3820|kiloBytesPerSecond
# net.bytesRx|vmnic1|0|kiloBytesPerSecond
# net.bytesRx|vmnic2|0|kiloBytesPerSecond
# net.bytesRx|vmnic3|0|kiloBytesPerSecond
# net.bytesRx||3820|kiloBytesPerSecond
# net.bytesTx|vmnic0|97|kiloBytesPerSecond
# net.bytesTx|vmnic1|0|kiloBytesPerSecond
# net.bytesTx|vmnic2|0|kiloBytesPerSecond
# net.bytesTx|vmnic3|0|kiloBytesPerSecond
# net.bytesTx||97|kiloBytesPerSecond
# net.droppedRx|vmnic0|0|number
# net.droppedRx|vmnic1|0|number
# net.droppedRx|vmnic2|0|number
# net.droppedRx|vmnic3|0|number
# net.droppedRx||0|number
# net.droppedTx|vmnic0|0|number
# net.droppedTx|vmnic1|0|number
# ...
# sys.uptime||630664|second



#   .--Disk IO-------------------------------------------------------------.
#   |                     ____  _     _      ___ ___                       |
#   |                    |  _ \(_)___| | __ |_ _/ _ \                      |
#   |                    | | | | / __| |/ /  | | | | |                     |
#   |                    | |_| | \__ \   <   | | |_| |                     |
#   |                    |____/|_|___/_|\_\ |___\___/                      |
#   |                                                                      |
#   '----------------------------------------------------------------------'
# Example output:
# disk.deviceLatency|naa.600605b002db9f7018d0a40c2a1444b0|0|millisecond
# disk.numberRead|naa.600605b002db9f7018d0a40c2a1444b0|8|number
# disk.numberWrite|naa.600605b002db9f7018d0a40c2a1444b0|47|number
# disk.read|naa.600605b002db9f7018d0a40c2a1444b0|12|kiloBytesPerSecond
# disk.read||12|kiloBytesPerSecond
# disk.write|naa.600605b002db9f7018d0a40c2a1444b0|51|kiloBytesPerSecond
# disk.write||51|kiloBytesPerSecond


def inventory_esx_vsphere_counters_diskio(parsed):
    if "disk.read" in parsed and "" in parsed["disk.read"]:
        return [("SUMMARY", {})]

def check_esx_vsphere_counters_diskio(item, params, parsed):
    if "disk.read" not in parsed:
        raise MKCounterWrapped("Counter data is missing")

    values = {}
    values['read_throughput']  = 0
    values['write_throughput'] = 0
    values['read_ios']         = 0
    values['write_ios']        = 0
    values['latency']          = 0


    for what in ["read", "write"]:
        if "disk.%s" % what in parsed and "" in parsed["disk.%s" % what]:
            values['%s_throughput' % what] = int(esx_vsphere_get_average(parsed["disk.%s" % what][""][0][0]) * 1024)

        for instance, info in parsed["disk.number%s" % what.title()].items():
            values["%s_ios" % what] += int(esx_vsphere_get_average(info[0][0]))

    if "disk.deviceLatency" in parsed:
        highest_latency = 0
        for entry in parsed["disk.deviceLatency"].values():
            highest_latency = max(highest_latency, max(map(int, entry[0][0])))
        values["latency"] = highest_latency / 1000.0

    return check_diskstat_dict(item, params, {"SUMMARY" : values})

check_info['esx_vsphere_counters.diskio'] = {
   'inventory_function'  : inventory_esx_vsphere_counters_diskio,
   'check_function'      : check_esx_vsphere_counters_diskio,
   'service_description' : 'Disk IO %s',
   'has_perfdata'        : True,
   'group'               : 'diskstat',
   'includes'            : [ "diskstat.include" ],
}

#.
#   .--Datastore IO--------------------------------------------------------.
#   |        ____        _            _                   ___ ___          |
#   |       |  _ \  __ _| |_ __ _ ___| |_ ___  _ __ ___  |_ _/ _ \         |
#   |       | | | |/ _` | __/ _` / __| __/ _ \| '__/ _ \  | | | | |        |
#   |       | |_| | (_| | || (_| \__ \ || (_) | | |  __/  | | |_| |        |
#   |       |____/ \__,_|\__\__,_|___/\__\___/|_|  \___| |___\___/         |
#   |                                                                      |
#   +----------------------------------------------------------------------+

# Example output:
# datastore.read|4c4ece34-3d60f64f-1584-0022194fe902|0#1#2|kiloBytesPerSecond
# datastore.read|4c4ece5b-f1461510-2932-0022194fe902|0#4#5|kiloBytesPerSecond
# datastore.numberReadAveraged|511e4e86-1c009d48-19d2-bc305bf54b07|0#0#0|number
# datastore.numberWriteAveraged|4c4ece34-3d60f64f-1584-0022194fe902|0#0#1|number
# datastore.totalReadLatency|511e4e86-1c009d48-19d2-bc305bf54b07|0#5#5|millisecond
# datastore.totalWriteLatency|4c4ece34-3d60f64f-1584-0022194fe902|0#2#7|millisecond

def parse_esx_vsphere_counters(info):
    parsed = {}
    # The data reported by the ESX system is split into multiple real time samples with
    # a fixed duration of 20 seconds. A check interval of one minute reports 3 samples
    # The esx_vsphere_counters checks need to figure out by themselves how to handle this data
    for counter, instance, multivalues, unit in info:
        values = multivalues.split("#")
        parsed.setdefault(counter, {})
        parsed[counter].setdefault(instance, [])
        parsed[counter][instance].append((values, unit))
    return parsed

def esx_vsphere_get_average(values):
    if not values:
        return 0
    int_values = map(int, values)
    return sum(int_values) / float(len(int_values))


# esx datastores are either shown by human readable name (if available) or by the uid
def esx_vsphere_counters_get_item_mapping(parsed):
    map_instance_to_item = {}
    for counter in [ "read", "write", "datastoreReadIops", "datastoreWriteIops", "sizeNormalizedDatastoreLatency" ]:
        for instance in parsed.get("datastore." + counter, {}).keys():
            map_instance_to_item[instance] = instance

    for instance, values in parsed.get("datastore.name", {}).items():
        if instance in map_instance_to_item and values[0][0] != "":
            map_instance_to_item[instance] = values[0][0][-1].replace(" ", "_")
    return map_instance_to_item

def inventory_esx_vsphere_counters_datastoreio(parsed):
    return inventory_diskstat_generic(map(lambda x: [None, x],
                             esx_vsphere_counters_get_item_mapping(parsed).values()))

def check_esx_vsphere_counters_datastoreio(item, params, parsed):
    if "datastore.read" not in parsed:
        raise MKCounterWrapped("Counter data is missing")

    datastores = {}
    item_mapping = esx_vsphere_counters_get_item_mapping(parsed)

    for new_name, eval_function, name, scaling in [
                ("read_throughput",  lambda x: int(esx_vsphere_get_average(x)), "datastore.read",                                  1024),
                ("write_throughput", lambda x: int(esx_vsphere_get_average(x)), "datastore.write",                                 1024),
                ("read_ios",         lambda x: int(esx_vsphere_get_average(x)), "datastore.datastoreReadIops",                        1),
                ("write_ios",        lambda x: int(esx_vsphere_get_average(x)), "datastore.datastoreWriteIops",                       1),
                ("latency",          lambda x: max(map(int, x)),                "datastore.sizeNormalizedDatastoreLatency", 1/1000000.0) ]:
        field_data = parsed.get(name, {})

        for instance, values in field_data.items():
            item_name = item_mapping[instance]
            datastores.setdefault(item_name, {})
            value = eval_function(values[0][0])
            datastores[item_name][new_name] = value * scaling

    return check_diskstat_dict(item, params, datastores)

check_info['esx_vsphere_counters'] = {
   'parse_function'          : parse_esx_vsphere_counters,
   'inventory_function'      : inventory_esx_vsphere_counters_datastoreio,
   'check_function'          : check_esx_vsphere_counters_datastoreio,
   'service_description'     : 'Datastore IO %s',
   'has_perfdata'            : True,
   'includes'                :  [ 'diskstat.include' ],
   'group'                   : 'diskstat'
}


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

# The bad thing here: ESX does not send *counters* but *rates*. This might
# seem user friendly on the first look, but is really bad at the second. The
# sampling rate defaults to 20s and is not aligned with our check rate. Also
# a reschedule of the check does not create new data. And: our if.include really
# requires counters. In order to use if.include we therefore simulate counters.

def convert_esx_counters_if(parsed):
    this_time = time.time()
    by_item = {}

    for name, instances in parsed.items():
        if name.startswith("net."):
            for instance, values in instances.items():
                by_item.setdefault(instance, {})
                if name == "net.macaddress":
                    by_item[instance][name[4:]] = values[0][0][-1]
                else:
                    by_item[instance][name[4:]] = int(esx_vsphere_get_average(values[0][0]))

    # Example of by_item:
    # {
    #   'vmnic0': {
    #         'broadcastRx': 31,
    #         'broadcastTx': 0,
    #         'bytesRx': 3905,  # is in Kilobytes!
    #         'bytesTx': 134,
    #         'droppedRx': 0,
    #         'droppedTx': 0,
    #         'errorsRx': 0,
    #         'errorsTx': 0,
    #         'multicastRx': 5,
    #         'multicastTx': 1,
    #         'packetsRx': 53040,
    #         'packetsTx': 30822,
    #         'received': 3905,
    #         'transmitted': 134,
    #         'unknownProtos': 0,
    #         'usage': 4040,
    #         'macaddress': 'AA:BB:CC:DD:EE:FF",
    #         'state': 2,
    #         'bandwidth': 10000000,
    #     },
    # }
    nics = by_item.keys()
    nics.sort()

    converted = [
        [], #  0 ifIndex                   0
        [], #  1 ifDescr                   1
        [], #  2 ifType                    2
        [], #  3 ifHighSpeed               .. 1000 means 1Gbit
        [], #  4 ifOperStatus              4
        [], #  5 ifHCInOctets              5
        [], #  6 ifHCInUcastPkts           6
        [], #  7 ifHCInMulticastPkts       7
        [], #  8 ifHCInBroadcastPkts       8
        [], #  9 ifInDiscards              9
        [], # 10 ifInErrors               10
        [], # 11 ifHCOutOctets            11
        [], # 12 ifHCOutUcastPkts         12
        [], # 13 ifHCOutMulticastPkts     13
        [], # 14 ifHCOutBroadcastPkts     14
        [], # 15 ifOutDiscards            15
        [], # 16 ifOutErrors              16
        [], # 17 ifOutQLen                17
        [], # 18 ifAlias                  18
        [], # 19 ifPhysAddress            19
    ]

    tableindex = {
        'bytesRx':      5,  # is in Kilobytes!
        'packetsRx':    6,
        'multicastRx':  7,
        'broadcastRx':  8,
        'droppedRx':    9,
        'errorsRx':    10,
        'bytesTx':     11,
        'packetsTx':   12,
        'multicastTx': 13,
        'broadcastTx': 14,
        'droppedTx':   15,
        'errorsTx':    16,
        # 'received': 3905,
        # 'transmitted': 134,
        # 'unknownProtos': 0,
        # 'usage': 4040,
    }

    converted = []
    for index, name in enumerate(nics):
        entry = ['0'] * 20
        converted.append(entry)
        if name: # Skip summary entry without interface name
            entry[0] = str(index)
            entry[1] = name
            entry[2] = '6'   # Ethernet
            entry[3] = str(by_item[name].get("bandwidth", "")) # Speed not known
            entry[4] = str(by_item[name].get("state", "1"))
            entry[18] = name # ifAlias
            if by_item[name].get("macaddress"):
                mac = "".join(map(lambda x: chr(int(x, 16)), by_item[name]["macaddress"].split(':')))
                entry[19] = mac
            else:
                entry[19] = ''   # MAC address not known here
            for ctr_name, ti in tableindex.items():
                ctr_value = by_item[name].get(ctr_name, 0)
                if ctr_name.startswith("bytes"):
                    ctr_value *= 1024
                countername = "vmnic." + name + "." + ctr_name
                last_time, last_value = get_item_state(countername, (None, None))
                if last_time or last_value:
                    new_value = last_value + ((this_time - last_time) * ctr_value)
                else:
                    last_time = this_time - 60
                    last_value = 0
                    new_value = ctr_value * 60
                set_item_state(countername, (this_time, new_value))
                entry[ti] = str(int(new_value))

    return converted


def inventory_esx_vsphere_counters_if(parsed):
    converted = convert_esx_counters_if(parsed)
    return inventory_if_common(converted)

def check_esx_vsphere_counters_if(item, params, parsed):
    if "net.bytesRx" not in parsed:
        raise MKCounterWrapped("Counter data is missing")

    converted = convert_esx_counters_if(parsed)
    return check_if_common(item, params, converted)

check_info['esx_vsphere_counters.if'] = {
   'inventory_function'      : inventory_esx_vsphere_counters_if,
   'check_function'          : check_esx_vsphere_counters_if,
   'service_description'     : 'Interface %s',
   'has_perfdata'            : True,
   'group'                   : 'if',
   'default_levels_variable' : 'if_default_levels',
   'includes'                : [ 'if.include' ],
}

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

def inventory_esx_vsphere_counters_uptime(parsed):
    if "sys.uptime" in parsed:
        return [ (None, {}) ]

def check_esx_vsphere_counters_uptime(_no_item, params, parsed):
    if "sys.uptime" not in parsed:
        raise MKCounterWrapped("Counter data is missing")
    uptime = int(parsed["sys.uptime"][""][0][0][-1])
    if uptime < 0:
        raise MKCounterWrapped("Counter data is corrupt")
    return check_uptime_seconds(params, uptime)


check_info['esx_vsphere_counters.uptime'] = {
   'inventory_function'      : inventory_esx_vsphere_counters_uptime,
   'check_function'          : check_esx_vsphere_counters_uptime,
   'service_description':     'Uptime',
   'has_perfdata':            True,
   'includes':               ['uptime.include'],
   'group':                   'uptime',
}

#.
#   .--Ramdisk-------------------------------------------------------------.
#   |                ____                     _ _     _                    |
#   |               |  _ \ __ _ _ __ ___   __| (_)___| | __                |
#   |               | |_) / _` | '_ ` _ \ / _` | / __| |/ /                |
#   |               |  _ < (_| | | | | | | (_| | \__ \   <                 |
#   |               |_| \_\__,_|_| |_| |_|\__,_|_|___/_|\_\                |
#   |                                                                      |
#   +----------------------------------------------------------------------+

# We assume that all ramdisks have the same size (in mb) on all hosts
# -> To get size infos about unknown ramdisks, connect to the ESX host via
#    SSH and check the size of the disk via "du" command
esx_vsphere_counters_ramdisk_sizes = {
    'root':           32,
    'etc':            28,
    'tmp':            192,
    'hostdstats':     319,
    'snmptraps':      1,
    'upgradescratch': 300,
    'ibmscratch':     300,
    'sfcbtickets':    1,
}

def inventory_esx_vsphere_counters_ramdisk(parsed):
    ramdisks = []

    for instance in parsed.get("sys.resourceMemConsumed", {}):
        if instance.startswith('host/system/kernel/kmanaged/visorfs/'):
            ramdisks.append(instance.split('/')[-1])

    return df_inventory(ramdisks)

def check_esx_vsphere_counters_ramdisk(item, params, parsed):
    if "sys.resourceMemConsumed" not in parsed:
        raise MKCounterWrapped("Counter data is missing")

    ramdisks = []
    for instance, values in parsed.get("sys.resourceMemConsumed").items():
        if instance.startswith('host/system/kernel/kmanaged/visorfs/'):
            name = instance.split('/')[-1]
            try:
                size_mb  = esx_vsphere_counters_ramdisk_sizes[name]
            except KeyError:
                if item == name:
                    return 3, 'Unhandled ramdisk found (%s)' % name
                else:
                    continue
            used_mb  = float(parsed["sys.resourceMemConsumed"][instance][0][0][-1]) / 1000
            avail_mb = size_mb - used_mb
            ramdisks.append((name, size_mb, avail_mb, 0))

    return df_check_filesystem_list(item, params, ramdisks)

check_info['esx_vsphere_counters.ramdisk'] = {
    'inventory_function':      inventory_esx_vsphere_counters_ramdisk,
    'check_function':          check_esx_vsphere_counters_ramdisk,
    'service_description':     'Ramdisk %s',
    'has_perfdata':            True,
    'includes':                [ 'df.include' ],
    'group':                   'filesystem',
    'default_levels_variable': 'filesystem_default_levels',
}
