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

# lookup tables for check implementation
brocade_fcport_phystates = [ '', 'noCard', 'noTransceiver', 'laserFault', 'noLight',
                              'noSync', 'inSync', 'portFault', 'diagFault', 'lockRef' ]
brocade_fcport_opstates  = [ 'unknown', 'online', 'offline', 'testing', 'faulty' ]
brocade_fcport_admstates = [ '', 'online', 'offline', 'testing', 'faulty' ]
brocade_fcport_speed     = [ 'unknown', '1Gbit', '2Gbit', 'auto-Neg', '4Gbit', '8Gbit', '10Gbit', ]


# settings for inventory: which ports should be inventorized
brocade_fcport_inventory_phystates = [ 3, 4, 5, 6, 7, 8, 9, ]
brocade_fcport_inventory_opstates  = [ 1, 2, 3, 4, ]
brocade_fcport_inventory_admstates = [ 1, 3, 4, ]
brocade_fcport_inventory_use_portname = True # use swFCPortName as part of service description
brocade_fcport_inventory_show_isl = True     # add "ISL" to service description for interswitch links



factory_settings["brocade_fcport_default_levels"] = {
    "rxcrcs":           (3.0, 20.0),   # allowed percentage of CRC errors
    "rxencoutframes":   (3.0, 20.0),   # allowed percentage of Enc-OUT Frames
    "notxcredits":      (3.0, 20.0),   # allowed percentage of No Tx Credits
    "c3discards":       (3.0, 20.0),   # allowed percentage of CRC errors
    "assumed_speed":    2.0,           # used, if speed not available in SNMP data
}


# Helper function for computing item from port number
def brocade_fcport_getitem(ports, index, portname, is_isl):
    int_len  = str(len(str(len(ports))))
    itemname = ("%0" + int_len + "d") % (index - 1)
    if is_isl and brocade_fcport_inventory_show_isl:
        itemname += " ISL"
    if portname.strip() and brocade_fcport_inventory_use_portname:
        itemname += " " + portname.strip()
    return itemname


def inventory_brocade_fcport(info):
    # info[0] is port table, info[1] is ISL table
    if len(info) != 2:
        return

    inventory = []
    isl_ports = {}
    if len(info) > 1:
        isl_ports = dict(info[1])

    for line in info[0]:
        if len(line) == 14:
            try:
                index    = int(line[0])
                phystate = int(line[1])
                opstate  = int(line[2])
                admstate = int(line[3])
            except: # missing vital data. Skipping this port
                continue
            portname = line[13]
            is_isl = line[0] in isl_ports

            if admstate in brocade_fcport_inventory_admstates and \
                opstate in brocade_fcport_inventory_opstates  and \
                phystate in brocade_fcport_inventory_phystates:

                inventory.append(( brocade_fcport_getitem(info[0], index, portname, is_isl),
                   '{ "phystate": %d, "opstate": %d, "admstate": %d }'
                    % (phystate, opstate, admstate) ))

    return inventory


def check_brocade_fcport(item, params, info):
    # Accept item, even if port name or ISL state has changed
    item_index = int(item.split()[0])
    portinfo = [ line for line in info[0] if int(line[0]) == item_index + 1]
    index, phystate, opstate, admstate, txwords, rxwords, txframes, rxframes, \
         notxcredits, rxcrcs, rxencoutframes, c3discards = map(int, portinfo[0][:-2])
    speed = saveint(portinfo[0][-2])

    summarystate = 0
    output = []
    perfdata = []

    # Lookup port speed in ISL table for ISL ports (older switches do not provide this
    # information in the normal table)
    isl_ports = dict(info[1])
    if str(index) in isl_ports:
        gbit = float(int(isl_ports.get(str(index))) / 16)
        speedmsg = ("ISL at %.0fGbit/s"  % gbit)
    else: # no ISL port
        if brocade_fcport_speed[speed] in [ "auto-Neg", "unknown" ]:
            # let user specify assumed speed via check parameter, default is 2.0
            gbit  = params.get("assumed_speed")
            speedmsg  = "assuming %gGbit/s" % gbit
        else:
            gbit = float(brocade_fcport_speed[speed].replace("Gbit", ""))
            speedmsg  = "%.0fGbit/s" % gbit

    output.append(speedmsg)

    # convert gbit netto link-rate to Byte/s (8/10 enc)
    wirespeed = gbit * 1000000000.0 * 0.8 / 8

    # Now check rates of various error counters
    this_time = time.time()
    try:
        timedif, rxwords_rate = get_counter("brocade_fcport.rxwords.%s" % index, this_time, rxwords)
        timedif, txwords_rate = get_counter("brocade_fcport.txwords.%s" % index, this_time, txwords)

        # compute traffic in B/s and MB/s
        in_bytes = rxwords_rate * 4
        out_bytes = txwords_rate * 4

        average = params.get("average") # range in minutes

        # B A N D W I D T H
        # convert thresholds in percentage into MB/s
        bw_thresh = params.get("bw")
        if bw_thresh == None: # no levels
            warn_bytes, crit_bytes = None, None
        else:
            warn, crit = bw_thresh
            if type(warn) == float:
                warn_bytes = wirespeed * warn / 100.0
            else: # in MB
                warn_bytes = warn * 1048576.0
            if type(crit) == float:
                crit_bytes = wirespeed * crit / 100.0
            else: # in MB
                crit_bytes = crit * 1048576.0

        for what, value in [("In", in_bytes), ("Out", out_bytes)]:
            output.append("%s: %s/s" % (what, get_bytes_human_readable(value)))
            perfdata.append((what.lower(), value, warn_bytes, crit_bytes, 0, wirespeed))

            # average turned on: use averaged traffic values instead of current ones
            if average:
                timedif, value = get_average("brocade_fcport.%s.%s.avg" % (what, item), this_time, value, average)
                output.append("Avg(%dmin): %s/s" % (average, get_bytes_human_readable(value)))
                perfdata.append( ("%s_avg" % what.lower(), value, warn_bytes, crit_bytes, 0, wirespeed))

            # handle levels for in/out
            if crit_bytes != None and value >= crit_bytes:
                summarystate = 2
                output.append(" >= %s/s(!!)" % (get_bytes_human_readable(crit_bytes)))
            elif warn_bytes != None and value >= warn_bytes:
                summarystate = max(1, summarystate)
                output.append(" >= %s/s(!!)" % (get_bytes_human_readable(warn_bytes)))

        # R X F R A M E S & T X F R A M E S
        # Put number of frames into performance data (honor averaging)
        timedif, rxframes_rate  = get_counter("brocade_fcport.rxframes.%s"  % index, this_time, rxframes)
        timedif, txframes_rate  = get_counter("brocade_fcport.txframes.%s"  % index, this_time, txframes)
        for what, value in [ ("rxframes", rxframes_rate), ("txframes", txframes_rate) ]:
            perfdata.append((what, value))
            if average:
                timedif, value = get_average("brocade_fcport.%s.%s.avg" % (what, item), this_time, value, average)
                perfdata.append( ("%s_avg" % what, value) )

        # E R R O R C O U N T E R S
        # handle levels on error counters

        for descr, counter, value, ref in [
               ("CRC errors",           "rxcrcs",              rxcrcs,          rxframes_rate, ),
               ("ENC-Out",              "rxencoutframes",      rxencoutframes,  rxframes_rate, ),
               ("C3 discards",          "c3discards",          c3discards,      txframes_rate, ),
               ("no TX buffer credits", "notxcredits",         notxcredits,     txframes_rate, ),]:
            timedif, per_sec = get_counter("brocade_fcport.%s.%s" % (counter, index), this_time, value)

            perfdata.append((counter, per_sec))

            # if averaging is on, compute average and apply levels to average
            if average:
                timedif, per_sec_avg = get_average("brocade_fcport.%s.%s.avg" % \
                        (counter, item), this_time, per_sec, average)
                perfdata.append( ("%s_avg" % counter, per_sec_avg ) )

            # compute error rate (errors in relation to number of frames) (from 0.0 to 1.0)
            if ref > 0 or per_sec > 0:
                rate = per_sec / (ref + per_sec)
            else:
                rate = 0
            text = "%s: %.2f%%" % (descr, rate * 100.0)

            # Honor averaging of error rate
            if average:
                timedif, rate = get_average("brocade_fcport.%s.%s.avgrate" %
                        (counter, item), this_time, rate, average)
                text += ", Avg: %.2f%%" % (rate * 100.0)

            error_percentage = rate * 100.0
            warn, crit = params[counter]
            if crit != None and error_percentage >= crit:
                summarystate = 2
                text += "(!!)"
                output.append(text)
            elif warn != None and error_percentage >= warn:
                summarystate = max(1, summarystate)
                text += "(!)"
                output.append(text)


    except MKCounterWrapped, e:
        # Assume that this is the first check of this port. Make sure, all counters
        # are initialized. If a counter is updated twice, get_counter will handle
        # that correctly.
        for counter, value in [ ( "rxwords", rxwords),
                                ( "txwords", txwords),
                                ( "txframes", txframes, ),
                                ( "notxcredits", notxcredits),
                                ( "rxcrcs", rxcrcs),
                                ( "rxencoutframes", rxencoutframes),
                                ( "c3discards", c3discards)]:
            try:
                get_counter("brocade_fcport.%s.%s" % (counter, index), this_time, value)
            except MKCounterWrapped, e:
                pass
        perfdata = [] # perfdata might not be valid


    # P O R T S T A T E
    # Port Status (physical layer)
    errorflag = ""
    if params.get("phystate") != None and phystate != params['phystate'] \
        and not (type(params['phystate']) == list and phystate in map(int, params['phystate'])):
        # noCard (1), noTransceiver (2), laserFault (3), noLight (4),
        # noSync (5), inSync (6), portFault (7), diagFault (8), lockRef (9)
        if phystate in [1, 6]:
            errorflag = "(!)"
            summarystate = max(summarystate, 1)
        else:
            errorflag = "(!!)"
            summarystate = 2
    output.append("Phy:%s(%d)%s" % (brocade_fcport_phystates[phystate], phystate, errorflag))

    errorflag = ""
    #unknown(0) online(1) offline(2) testing(3) faulty(4) ]
    if params.get("opstate") != None and opstate != params['opstate'] \
        and not (type(params['opstate']) == list and opstate in map(int, params['opstate'])):
        if opstate in [1, 3]:
            errorflag = "(!)"
            summarystate = max(summarystate, 1)
        else:
            errorflag = "(!!)"
            summarystate = 2
    output.append("Op:%s(%d)%s" % (brocade_fcport_opstates[opstate], opstate, errorflag))

    errorflag = ""
    # online(1) offline(2) testing(3) faulty(4)
    if params.get("admstate") and admstate != params['admstate'] \
        and not (type(params['admstate']) == list and admstate in map(int, params['admstate'])):
        if admstate not in [2, 4]:
            errorflag = "(!)"
            summarystate = max(summarystate, 1)
        else:
            errorflag = "(!!)"
            summarystate = 2
    output.append("Adm:%s(%d)%s" % (brocade_fcport_admstates[admstate], admstate, errorflag))

    return (summarystate, ', '.join(output), perfdata)

check_info["brocade_fcport"] = {
    'check_function':          check_brocade_fcport,
    'inventory_function':      inventory_brocade_fcport,
    'service_description':     'Port %s',
    'has_perfdata':            True,
    'snmp_info':               [
        ( ".1.3.6.1.4.1.1588.2.1.1.1.6.2.1",[
            1,  # swFCPortIndex
            3,  # swFCPortPhyState
            4,  # swFCPortOpStatus
            5,  # swFCPortAdmStatus
            11, # swFCPortTxWords
            12, # swFCPortRxWords
            13, # swFCPortTxFrames
            14, # swFCPortRxFrames
            20, # swFCPortNoTxCredits
            22, # swFCPortRxCrcs
            26, # swFCPortRxEncOutFrs
            28, # swFCPortC3Discards
            35, # swFCPortSpeed
            36, # swFCPortName  (not supported by all devices)
        ]),

        # Information about Inter-Switch-Links (contains baud rate of port)
        ( ".1.3.6.1.4.1.1588.2.1.1.1.2.9.1", [
            2, # swNbMyPort
            5, # swNbBaudRate
        ]),
    ],
    'snmp_scan_function':      \
     lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.1588.2.1.1") or \
                 oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.24.1.1588.2.1.1"),
    'group':                   'brocade_fcport',
    'default_levels_variable': 'brocade_fcport_default_levels',
}
