#!/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 from agent
# <<<winperf_if>>>
# 1366721523.71 510
# 3 instances: Ethernetadapter_der_AMD-PCNET-Familie__2_-_Paketplaner-Miniport Ethernetadapter_der_AMD-PCNET-Familie_-_Paketplaner-Miniport MS_TCP_Loopback_interface
# -122 43364 1085829 41602 bulk_count
# -110 293 4174 932 counter
# -244 138 3560 466 counter
# -58 155 614 466 counter
# 10 100000000 100000000 10000000 rawcount
# -246 21219 780491 20801 counter
# 14 0 383 466 counter
# 16 138 3176 0 counter
# 18 0 0 0 rawcount
# 20 0 0 0 rawcount
# 22 0 1 0 rawcount
# -4 22145 305338 20801 counter
# 26 0 428 466 counter
# 28 155 186 0 counter
# 30 0 0 0 rawcount
# 32 0 0 0 rawcount
# 34 0 0 0 rawcount
# <<<winperf_if:sep(44)>>>
# Node,MACAddress,Name,NetConnectionID,NetConnectionStatus
# WINDOWSXP,08:00:27:8D:47:A4,Ethernetadapter der AMD-PCNET-Familie,LAN-Verbindung,2
# WINDOWSXP,,Asynchroner RAS-Adapter,,
# WINDOWSXP,08:00:27:8D:47:A4,Paketplaner-Miniport,,
# WINDOWSXP,,WAN-Miniport (L2TP),,
# WINDOWSXP,50:50:54:50:30:30,WAN-Miniport (PPTP),,
# WINDOWSXP,33:50:6F:45:30:30,WAN-Miniport (PPPOE),,
# WINDOWSXP,,Parallelanschluss (direkt),,
# WINDOWSXP,,WAN-Miniport (IP),,
# WINDOWSXP,00:E5:20:52:41:53,Paketplaner-Miniport,,
# WINDOWSXP,08:00:27:35:20:4D,Ethernetadapter der AMD-PCNET-Familie,LAN-Verbindung 2,2
# WINDOWSXP,08:00:27:35:20:4D,Paketplaner-Miniport,,
#
# Example output for the optional dhcp section. If this plugin is active, any interface for which
# dhcp is enabled will warn
# <<<dhcp:sep(44)>>>
# Node,Description,DHCPEnabled
# WINDOWS,Intel(R) PRO/1000 MT-Desktopadapter,TRUE
# WINDOWS,WAN Miniport (IP),FALSE
# WINDOWS,Microsoft-ISATAP-Adapter,FALSE
# WINDOWS,RAS Async Adapter,FALSE
# WINDOWS,Intel(R) PRO/1000 MT-Desktopadapter #2,TRUE


def winperf_if_canonize_nic_name(name):
    return name.replace("_", " ").replace("  ", " ").rstrip()

def winperf_if_normalize_nic_name(name, nic_names):
    # Intel[R] PRO 1000 MT-Desktopadapter__3   (perf counter)
    # Intel(R) PRO/1000 MT-Desktopadapter 3    (wmic name)
    # Intel(R) PRO/1000 MT-Desktopadapter #3   (wmic InterfaceDescription)
    mod_nic_name = name
    for from_token, to_token in [ ("/", " "), ("(", "["), (")", "]"), ("#", " ") ]:
        for n in nic_names:
            if from_token in n:
                # we do not modify it if this character is in any of the counter names
                break
        else:
            mod_nic_name = mod_nic_name.replace(from_token, to_token).replace("  ", " ")
    return mod_nic_name


def convert_winperf_if(info, nic_names, nic_index):

    lines = iter(info)
    lines.next() # skip line with timestamp and counter number
    lines.next() # skip interface line (already evaluated)
    nics = dict([(n, {}) for n in nic_names])

    # Scan lines with counters
    is_teaming_line = False
    teaming_headers = []
    teaming_info = {}

    dhcp_lines = []

    try:
        while True:
            line = lines.next()

            if line[0] == "[dhcp_start]":
                line = lines.next()  # skip start line
                while line[0] != "[dhcp_end]":
                    dhcp_lines.append(line)
                    line = lines.next()
                line = lines.next()  # skip end line

            # Process teaming information
            if line[0].startswith("[teaming_start]"):
                is_teaming_line = True
                teaming_headers = lines.next()
                continue
            elif line[0].startswith("[teaming_end]"):
                is_teaming_line = False
                continue
            if is_teaming_line:
                as_dict = dict(zip(teaming_headers, map(lambda x: x.rstrip(), line)))
                for guid in as_dict["GUID"].split(";"):
                    teaming_info[guid] = as_dict
                continue

            counter = saveint(line[0])
            if counter:
                for nr, value in enumerate(line[1:len(nic_names)+1]):
                    nics[nic_names[nr]][counter] = int(value)
            # Not an integer: then this must be the line with the additional
            # information from wmic (which is optional!)
            else:
                headers = line
                while True:
                    line = lines.next()
                    if line[0].startswith("[teaming_start]"):
                        is_teaming_line = True
                        teaming_headers = lines.next()
                        break

                    as_dict = dict(zip(headers, map(lambda x: x.rstrip(), line)))

                    # Unfortunately teamed interfaces lose their #x index and are no longer
                    # distinguishable with the data from the Win32_NetworkAdapter.
                    # Fortunately this index information is still available in the teaming section
                    # The GUID is used to combine the two datasets
                    guid = as_dict.get("GUID")
                    if guid in teaming_info:
                        guid_to_name = dict(zip(teaming_info[guid]["GUID"].split(";"),
                                                teaming_info[guid]["MemberDescriptions"].split(";")))
                        nic_name = winperf_if_canonize_nic_name(guid_to_name[guid])
                    elif "Name" in as_dict:
                        nic_name = winperf_if_canonize_nic_name(as_dict["Name"])
                    else:
                        # There seems a bug with some windows configurations
                        continue

                    found_match = False

                    # we need to ignore data on interfaces in the optional
                    # wmic section which are marked as non-existing, since
                    # it may happen that there are non-existing interfaces
                    # with the same nic_name as an active one (at least on HP
                    # hardware)
                    if as_dict.get("NetConnectionStatus") == "4":
                        continue

                    # Exact match
                    if nic_name in nic_names:
                        found_match = True

                    # In the perf counters the nics have strange suffixes, e.g.
                    # Ethernetadapter der AMD-PCNET-Familie 2 - Paketplaner-Miniport, while
                    # in wmic it's only named "Ethernetadapter der AMD-PCNET-Familie 2".
                    if not found_match:
                        mod_nic_name = winperf_if_normalize_nic_name(nic_name, nic_names)

                        if mod_nic_name not in nic_names:
                            for n in nic_names:
                                if n.startswith(mod_nic_name + " "):
                                    l = len(mod_nic_name)
                                    if not (n[l:].strip()[0]).isdigit():
                                        nic_name = n
                                        found_match = True
                                        break
                        else:
                            found_match = True
                            nic_name = mod_nic_name

                    if not found_match:
                        # Ignore interfaces that do not have counters
                        continue

                    nics[nic_name].update(as_dict)
    except StopIteration:
        pass


    # Now convert the dicts into the format that is needed by if.include
    converted = []

    # Sort NIC names
    nic_names.sort(reverse=True)

    for nic_name in nic_names:
        nic = nics[nic_name]
        mac_txt = nic.get('MACAddress')
        bandwidth = saveint(nic.get('Speed'))
        # Some interfaces report several exabyte as bandwidth when down..
        if bandwidth > 1024**5:
            # Greater than petabyte
            bandwidth = 0

        if mac_txt:
            mac = "".join(map(lambda x: chr(int(x, 16)), mac_txt.split(':')))
        else:
            mac = ''

        index_info = str(nic_index[nic_name])
        # Automatically group teamed interfaces
        if nic.get("GUID") in teaming_info:
            index_info = ( teaming_info[nic.get("GUID")]["TeamName"], index_info )

        # NetConnectionTable Stuff:
        #
        # if we have no status, but link information, we assume IF is connected
        connection_status = nic.get('NetConnectionStatus')
        if not connection_status:
            connection_status = '2'

        # Windows NetConnectionStatus Table
        connection_states = {
            '0':  ('2', 'Disconnected'),
            '1':  ('2', 'Connecting'),
            '2':  ('1', 'Connected'),
            '3':  ('2', 'Disconnecting'),
            '4':  ('2', 'Hardware not present'),
            '5':  ('2', 'Hardware disabled'),
            '6':  ('2', 'Hardware malfunction'),
            '7':  ('7', 'Media disconnected'),
            '8':  ('2', 'Authenticating'),
            '9':  ('2', 'Authentication succeeded'),
            '10': ('2', 'Authentication failed'),
            '11': ('2', 'Invalid address'),
            '12': ('2', 'Credentials required'),
        }

        # ifOperStatus Table
        # 1 up
        # 2 down
        # 3 testing
        # 4 unknown
        # 5 dormant
        # 6 notPresent
        # 7 lowerLayerDown

        ifoperstatus_annotated = connection_states[connection_status]

        converted.append((
           index_info,
           nic_name,
           "loopback" in nic_name.lower() and '24' or '6',
           bandwidth or nic[10],   # Bandwidth
           ifoperstatus_annotated, # ifOperStatus
           nic[-246],              # ifInOctets,
           nic[14],                # inucast
           0,                      # inmcast
           nic[16],                # non-unicast empfangen
           nic[18],                # ifInDiscards
           nic[20],                # ifInErrors
           nic[-4],                # ifOutOctets (Bytes gesendet)
           nic[26],                # outucast
           0,
           nic[28],                # outnonucast
           nic[30],                # ifOutDiscards
           nic[32],                # ifOutErrors
           nic[34],                # ifOutQLen
           nic.get('NetConnectionID', nic_name),
           mac,
        ))

    return converted, dhcp_lines


def winperf_if_get_nic_index_map(nic_names):
    return dict(map(lambda x: (x[1], x[0] + 1), enumerate(nic_names)))


def check_if_dhcp(item, _no_params, info, nic_names, nic_index):
    lines = iter(info)
    header = lines.next()

    for line in lines:
        # wmic is bugged on some windows versions such that we can't use proper csv output, only
        # visual tables. Those aren't properly split up by the check_mk parser.
        # Try to fix that mess

        # assumption 1: header fields contain no spaces
        num_fields = len(header)
        # assumption 2: only the leftmost field contains spaces
        lm_field = " ".join(line[:(num_fields - 1) * -1])

        line = [lm_field] + line[(len(line) - num_fields + 1):]

        as_dict = dict(zip(header, map(lambda x: x.rstrip(), line)))
        name = winperf_if_normalize_nic_name(as_dict["Description"], nic_names)
        idx = nic_index.get(name)

        try:
            match = idx == int(item)
        except ValueError:
            match = name == item

        if match:
            if as_dict["DHCPEnabled"]:
                return 1, "dhcp enabled"
            else:
                return 0, "dhcp %s" % as_dict["DHCPEnabled"]
    return 0, ""


def inventory_winperf_if(info):
    nic_names = map(winperf_if_canonize_nic_name, info[1][2:])
    nic_index = winperf_if_get_nic_index_map(nic_names)

    perf, dhcp = convert_winperf_if(info, nic_names, nic_index)

    return inventory_if_common(perf)
    # dhcp info irrelevant to inventory


def check_winperf_if(item, params, info):
    nic_names = map(winperf_if_canonize_nic_name, info[1][2:])
    nic_index = winperf_if_get_nic_index_map(nic_names)

    perf, dhcp = convert_winperf_if(info, nic_names, nic_index)

    yield check_if_common(item, params, perf, group_name = "Teaming")
    yield check_if_dhcp(item, params, dhcp, nic_names, nic_index)


check_info["winperf_if"] = {
    'check_function':          check_winperf_if,
    'inventory_function':      inventory_winperf_if,
    'service_description':     'Interface %s',
    'has_perfdata':            True,
    'includes':                [ 'if.include' ],
    'group':                   'if',
    'default_levels_variable': 'if_default_levels'
}
