#!/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.

# Author: Lars Michelsen <lm@mathias-kettner.de>

# Relevant SNMP OIDs:
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.144 9
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.258 16
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.400 25
#
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.3974704 "6cos"
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.6208592 "ingress-map"
#
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.1593 "class-default"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.18785 "EF"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284945 "AF1"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284961 "AF2"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284977 "AF3"

# http://www.oidview.com/mibs/9/CISCO-CLASS-BASED-QOS-MIB.html

# TEST:
#
# search class table:
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284945 (cbQosCMName) "AF1"
# class_id = 284945 (cbQosConfigIndex)
#
# search config table for matching value
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.144.5256 284945
# key = 144.5256 (cbQosPolicyIndex: 144, cbQosObjectsIndex: 5256)
#
# search if table for matchin if_id: 144
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.144 (cbQosIfIndex) 9
# if_policy = 9 (ifIndex -> standard mib)
#
# get config_id from config table using if_id.if_id 144.144
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.144.144 (cbQosConfigIndex) 6208592
# config_index = 6208592
#
# get policy name using the policy_index
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.6208592 "ingress-map"
# policy_name = "ingress-map"
#
# get post bytes using the key
# .1.3.6.1.4.1.9.9.166.1.15.1.1.9.144.5256 0
# post_bytes = 0
#
# get dropped bytes using the key
# .1.3.6.1.4.1.9.9.166.1.15.1.1.16.144.5256 0
# dropped_bytes = 0
#
# get if_name using the if_policy: 9
# .1.3.6.1.2.1.31.1.1.1.1.9 Vl1
# if_name = Vl1
#
# get if_speed using the if_policy: 9
# .1.3.6.1.2.1.2.2.1.5.9 100000000
# if_speed = 100000000
#
###
# Test to find the badwidth of the classes. Not finished...
#
# 'cbQosObjectsType' => {
#         1 => 'policymap',
#         2 => 'classmap',
#         3 => 'matchStatement',
#         4 => 'queueing',
#         5 => 'randomDetect',
#         6 => 'trafficShaping',
#         7 => 'police',
#         8 => 'set' },
#
# Index:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.258.1244739 1608
#
# Type:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.3.258.1244739 4
#
# Parent ID:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.4.258.1244739 6184
#
# cbQosQueueingStatsEntry:
# .1.3.6.1.4.1.9.9.166.1.18.1.1.2.258.1244739 64
# ...

# Index:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.258.6184 18785
# Type:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.3.258.6184 2
# Parent ID:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.4.258.6184 258


# get cbQosQueueingCfgBandwidth
# .1.3.6.1.4.1.9.9.166.1.9.1.1.1.1608 3094

cisco_qos_default_levels = (None, None, 0.01, 0.01)
#factory_settings["cisco_qos_default_values"] = {
#    "drop" : (0.01, 0.01)
#}

def cisco_qos_get_config_entries_by_class_id(config, class_id):
    return [ if_index.split('.') for if_index, value in config.iteritems() if value == class_id ]

def inventory_cisco_qos(info):
    if len(info) == 12:
        ifs = dict(info[0])
        config = dict([ ('.'.join(oid.split('.')[-2:]), value) for oid, value in info[3] ])
        if_names = dict(info[6])

        # Find all interfaces for each class and create one service for each pair
        items = []
        for class_id, class_name in info[2]:
            # Get interface ids which use this qos class
            for policy_id, objects_id in cisco_qos_get_config_entries_by_class_id(config, class_id):
                if ifs.get(policy_id) in if_names:
                    if_name = if_names[ifs[policy_id]]
                    items += [ ('%s: %s' % (if_name, class_name), {}) ]

        return items

def check_cisco_qos(item, params, info):
    # Convert old params definitions
    # Note: the float values of the post levels are converted to int
    had_legacy_params = False
    if type(params) == tuple:
        params = {
            "post": tuple(map(lambda x: x and int(x) or None, params[0:2])),
            "drop": params[2:4]
        }
        had_legacy_params = True

    unit = params.get("unit", "bit")
    average = params.get("average")
    post_warn, post_crit = params.get("post",(None, None))
    drop_warn, drop_crit = params.get("drop",(None, None))


    # Load values and format them
    ifs = dict(info[0])
    policies= dict(info[1])
    classes = dict(info[2])
    config = dict([ ('.'.join(oid.split('.')[-2:]), value) for oid, value in info[3] ])
    post_bytes = dict([ ('.'.join(oid.split('.')[-2:]), value) for oid, value in info[4] ])
    drop_bytes = dict([ ('.'.join(oid.split('.')[-2:]), value) for oid, value in info[5] ])
    if_names = dict(info[6])
    if_speeds = dict(info[7])
    parents = dict(info[8])
    if_qos_bandwidth = dict(info[9])
    if_qos_bandwidth_units = dict(info[10])
    object_types = dict(info[11])

    if_name, class_name = item.split(': ')

    # Gather the class id by class_name
    class_id = None
    for cid, cname in classes.iteritems():
        if class_name == cname:
            class_id = cid
            break

    # Gather the interface id by class_name
    if_id = None
    for iid, iid2 in ifs.iteritems():
        if if_name == if_names[iid2]:
            if_id = iid2
            break

    if not if_id or not class_id:
        return (3, "QoS class not found for that interface")

    policy_id, objects_id, policy_map_id, policy_name = None, None, None, None
    for this_policy_id, this_objects_id in cisco_qos_get_config_entries_by_class_id(config, class_id):
        if if_id != ifs[this_policy_id]:
            continue # skip the ones of other interfaces

        # Get the policy_map_id. To retrieve this get one of the config entries
        # of type "policy map" from the config table. All of this type should have
        # the same value, which is then the policy_map_id.
        for key in object_types.keys():
            if key.startswith(this_policy_id+'.') and object_types[key] == '1':
                policy_map_id = config[key]
                break

        if policy_map_id is None:
            return 3, 'Invalid policy map id'

        policy_name = policies.get(policy_map_id)
        policy_id   = this_policy_id
        objects_id  = this_objects_id

    if policy_id is None or objects_id is None:
        return 3, 'Could not find policy_id or objects_id'

    post_b      = post_bytes.get(policy_id+'.'+objects_id, 0)
    drop_b      = drop_bytes.get(policy_id+'.'+objects_id, 0)
    speed       = saveint(if_speeds[if_id])

    parent_value_cache = {}
    for a_key, a_value in config.items():
        parent_value_cache.update({ a_value :  a_key.split(".")[1] })

       # if a_value == class_id:
       #     parent_value = a_key.split(".")[1]
    for b_key, b_value in parents.items():
        if parent_value_cache[class_id] == b_value:
            if object_types[b_key] == "4":
                try:
                    # 1 kbps
                    # 2 percentage
                    # 3 percentageRemaining
                    # 4 ratioRemaining
                    # 5 perThousand
                    # 6 perMillion
                    qos_unit      = int(if_qos_bandwidth_units[config[b_key]])
                    qos_bandwidth = saveint(if_qos_bandwidth[config[b_key]])
                    if qos_unit == 1:
                        speed = qos_bandwidth * 1000

                    elif qos_unit == 2:
                        speed = speed * (qos_bandwidth / 100.0)

                    elif qos_unit == 3:
                        remaining = (speed / 100) * qos_bandwidth
                        speed = speed - remaining
                    break
                except KeyError:
                    pass

    # Bandwidth needs to be in bytes for later calculations
    bw = speed / 8.0

    # Determine post warn/crit levels
    if type(post_warn) == float:
        post_warn = bw / 100.0 * post_warn
        post_crit = bw / 100.0 * post_crit
    elif type(post_warn) == int:
        if unit == 'bit':
            post_warn = post_warn / 8
            post_crit = post_crit / 8

    # Determine drop warn/crit levels
    if type(drop_warn) == float and not had_legacy_params:
        drop_warn = bw / 100.0 * drop_warn
        drop_crit = bw / 100.0 * drop_crit
    # Convert the drop levels to byte
    elif unit == "bit":
        # But only if our params where already provided in the new format
        if not had_legacy_params:
            if type(drop_warn) == int:
                drop_warn = drop_warn / 8.0
            if type(drop_crit) == int:
                drop_crit = drop_crit / 8.0


    # Handle counter values
    state = 0
    infotext = ''
    this_time = time.time()
    rates = []
    perfdata = []
    perfdata_avg = []

    min_value = ("0", "0.0")[unit == 'bit']
    for name, counter, warn, crit, min_val, max_val in [
        ( "post", post_b, post_warn, post_crit, min_value, bw),
        ( "drop", drop_b, drop_warn, drop_crit, min_value, bw),
    ]:
        rate = get_rate("cisco_qos.%s.%s" % (name, item), this_time, saveint(counter))
        rates.append(rate)
        perfdata.append( (name, rate, warn, crit, min_val, max_val) )

        if average:
            avg_value = get_average("cisco_qos.%s.%s.avg" % (name, item), this_time, rate, average)
            rates.append(avg_value)
            perfdata_avg.append( ("%s_avg_%d" % (name, average), avg_value, warn, crit, min_val, max_val) )

    perfdata.extend(perfdata_avg)
    def format_value(value):
        if unit == "bit":
            value = value * 8
            return get_nic_speed_human_readable(value)
        else:
            return "%s/s" % get_bytes_human_readable(value)

    if average:
        post_rate = rates[1]
        drop_rate = rates[3]
    else:
        post_rate = rates[0]
        drop_rate = rates[1]

    for what, rate, warn, crit in [ ("post",  post_rate, post_warn, post_crit),
                                    ("drop",  drop_rate, drop_warn, drop_crit) ]:
        infotext += ', %s: %s' % (what, format_value(rate))
        if crit is not None and rate >= crit:
            state = max(2, state)
            infotext += '(!!)'
        elif warn is not None and rate >= warn:
            state = max(1, state)
            infotext += '(!)'

    if policy_name:
        infotext += ', Policy-Name: %s, Int-Bandwidth: %s' % (policy_name, format_value(bw))
    else:
        infotext += ', Policy-Map-ID: %s, Int-Bandwidth: %s' % (policy_map_id, format_value(bw))
    return (state, infotext.lstrip(', '), perfdata)


check_info["cisco_qos"] = {
    "service_description"     : "QoS %s",
    "check_function"          : check_cisco_qos,
    "inventory_function"      : inventory_cisco_qos,
    "has_perfdata"            : True,
    "group"                   : "cisco_qos",
    "default_levels_variable" : "cisco_qos_default_"
}

snmp_info['cisco_qos']  = [ ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '1.1.1.4' ] ),   # qosIfIndex
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '6.1.1.1' ] ),   # qosPolicies
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '7.1.1.1' ] ),   # qosClasses
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_STRING, '5.1.1.2' ] ),   # qosConfig
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_STRING, '15.1.1.9' ] ),  # qosPostBytes
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_STRING, '15.1.1.16' ] ), # qosDropBytes
                            ( '.1.3.6.1.2.1.2.2.1',     [ OID_END,    '2' ]),          # ifNames
                            ( '.1.3.6.1.2.1.2.2.1',     [ OID_END,    '5' ]),          # ifSpeeds
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '5.1.1.4' ]),    # cbQosParentObjectsIndex
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '9.1.1.1' ]),    # qosQueueingConfigBandwidth
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '9.1.1.2' ]),    # cbQosQueueingCfgBandwidthUnits
                            ( '.1.3.6.1.4.1.9.9.166.1', [ OID_END,    '5.1.1.3' ]),    # cbQosObjectsType
                          ]
snmp_scan_functions['cisco_qos'] = 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.166.1.1.1.1.4.*")
