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


# targetstate is 1 (used) or 0 (unused)
# info columns: INDEX PHYSTATE OPSTATE TXWORDS RXWORDS
#                          0     1             2             3            4      
brocade_phystate_names = [ '', 'noCard', 'noTransceiver', 'laserFault', 'noLight', 
#                            5         6          7            8          9         10
                          'noSync', 'inSync', 'portFault', 'diagFault', 'lockRef', 'noSystemControlAccessToSlot' ]
#                            0          1         2          3           4
brocade_opstate_names = [ 'unknown', 'online', 'offline', 'testing', 'faulty' ]

fc_brocade_port_detailed_default_levels = ( (1, 0.1), (1, 0.1), (1, 0.1), (2, 160, None, []) )

def inventory_brocade_port(checkname, info):
    porttable, isltable = info
    inventory = []
    for index, phystate, opstate, txwords, rxwords, txframes, rxframes, crcerrors, encout, c3discards in porttable:
        state = (int(phystate), int(opstate))
        if state == (4,2):
            used = 0
            used_txt = "unused"
        elif phystate == "1" or phystate == "2":
            used = 0
            used_txt = "no card or no transceiver"
        else:
            used = 1
            used_txt = "used"
        # index  is e.g. '3'. But port number printed on switch
        # and in management software of switch counts from 0, so subtract
        # one. Also make it 2-digits => '03'
        if used:
            index = "%02d" % (int(index) - 1)
            inventory.append( (index, used_txt, "fc_brocade_port_detailed_default_levels") )
    return inventory


def check_brocade_port(portno, params, info):
    porttable, isltable = info

    # SNMP counts ports from 1, but management console and hardware from 0
    portinfo = [ line[1:] for line in porttable if int(line[0]) == int(portno) + 1 ]
    if len(portinfo) < 1:
        return (3, "UNKNOWN - No port number %d present" % int(portno))

    phystate, opstate, txwords, rxwords, txframes, rxframes, crcerrors, encout, c3discards = map(int, portinfo[0])
    baudrate = None
    baudinfo = ""
    io_warn = None
    io_crit = None

    # Lookup baud rate in inter switch table
    for portnr, brmult in isltable:
        if int(portno) + 1 == int(portnr):
            # brmult: 16 = 1GBit/s, 32 = 2GBit/s, 64 = 4GBit/s, ...
            baudrate = 1.0 * (float(brmult) / 16)
            baudinfo = ", ISL with baudrate: %gGBit/s" % (baudrate, )

    special_states = []
    if len(params) >= 4:
        assumed_baudrate, io_warn, io_crit = params[3][0:3]
        if len(params[3]) >= 4:
            special_states = params[3][3]
        if not baudinfo:
            baudinfo = ", assumed baudrate: %gGBit/s" % assumed_baudrate

        # if we known the baudrate, scale level accordingly
        if baudrate and int(baudrate) != int(assumed_baudrate):
            if io_warn: io_warn *= baudrate / float(assumed_baudrate)
            if io_crit: io_crit *= baudrate / float(assumed_baudrate)
        elif not baudrate:
            baudrate = assumed_baudrate

    if baudrate:
        speedmax = baudrate * 1024.0 / 8.0
    else:
        speedmax = None

    perfdata = [
        ( "txwords",    "%dc" % txwords, io_warn, io_crit, 0, speedmax),
        ( "rxwords",    "%dc" % rxwords, io_warn, io_crit, 0, speedmax),
        ( "crcerrors",  "%dc" % crcerrors ),  # while receiving
        ( "encout",     "%dc" % encout ),     # while transmitting
        ( "c3discards", "%dc" % c3discards ), #
    ]

    # First check if port has physical and logical link
    state = phystate, opstate
    if state != (6,1):
        # If port is in one of the states that are especially configured in the parameters,
        # the exit could is set by the admin
        nagioscode = 2
        for ncode, phy, op in special_states:
            if (phy,op) == (phystate, opstate):
                nagioscode = ncode
                break
        return (nagioscode, "%s - physical state %s(%d), opstate %s(%d)" % (
                nagios_state_names[nagioscode], 
                (len(brocade_phystate_names) > phystate and brocade_phystate_names[phystate] or 'UNHANDLED'),
                phystate, brocade_opstate_names[opstate], opstate), perfdata)

    # Now check rates of various error counters
    this_time = time.time()
    worst = 0
    texts = []
    try:
        timedif, rxframes_rate = get_counter("fc_brocade_port_detailed.rxframes.%s" % portno, this_time, rxframes)
        timedif, txframes_rate = get_counter("fc_brocade_port_detailed.txframes.%s" % portno, this_time, txframes)
        timedif, rxwords_rate = get_counter("fc_brocade_port_detailed.rxwords.%s" % portno, this_time, rxwords)
        timedif, txwords_rate = get_counter("fc_brocade_port_detailed.txwords.%s" % portno, this_time, txwords)
        in_mb  = rxwords_rate / 262144.0  # words -> megabytes
        out_mb = txwords_rate / 262144.0
        texts.append("In: %.1fMB/sec, Out: %.1fMB/sec%s" % (in_mb, out_mb, baudinfo))

        # handle levels for in/out
        if len(params) >= 4: # New in 1.1.3: Levels on in/out
            for text, value in [("In", in_mb), ("Out", out_mb)]:
                if io_crit and value >= io_crit:
                    worst = 2
                    texts.append("%s >= %dMB/s!!" % (text, io_crit))
                elif io_warn and value >= io_warn:
                    worst = max(worst, 1)
                    texts.append("%s >= %dMB/s!" % (text, io_warn))

        # handle levels on error counters
        def has_reached_level(level, timedif, per_sec, rate):
            if type(level) == int: # absolute number
                absnumber = timedif * per_sec
                return absnumber >= level
            else:
                return rate * 100.0 >= level

        for descr, counter, value, ref, (warn, crit) in [
               ("CRC errors",  "crcerrors",   crcerrors,  rxframes_rate, params[0] ),
               ("ENC-Out",     "encout",      encout,     txframes_rate, params[1] ),
               ("C3 discards", "c3discards",  c3discards, txframes_rate, params[2] )]:
            timedif, per_sec = get_counter("fc_brocade_port_detailed.%s.%s" % (counter, portno), this_time, value)

            if ref > 0 or per_sec > 0:
                rate = per_sec / (ref + per_sec)
            else:
                rate = 0

            text = "%s: %d (%.1f%%)" % (descr, timedif * per_sec, rate * 100.0)
            if has_reached_level(crit, timedif, per_sec, rate):
                worst = 2
                text += "!!"
                texts.append(text)
            elif has_reached_level(warn, timedif, per_sec, rate):
                worst = max(worst, 1)
                text += "!"
                texts.append(text)

    except MKCounterWrapped:
        # 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, ),
                                ("crcerrors", crcerrors), ( "encout", encout), ("c3discards", c3discards)]:
            try:
                get_counter("fc_brocade_port_detailed.%s.%s" % (counter, portno), this_time, value)
            except:
                pass
        perfdata = [] # perfdata might not be valid

    if texts == []:
        texts = [ "link status is online/inSync" ]
    infotext  = {0:"OK", 1:"WARN", 2:"CRIT"}[worst] + " - " + ", ".join(texts)
    return (worst, infotext, perfdata)


check_info['fc_brocade_port_detailed'] = (check_brocade_port, "PORT %s", 1,  inventory_brocade_port)

# This checks make use of a new feature in 1.1.3: An SNMP check can now
# fetch more than one table. The check and inventory function get then
# a list of tables with one table for each entry, rather then one table.
# Please note, that the merging of check outputs in cluster checks does
# not merge the subtables but simply appends the main lists. But does anyone
# out there use cluster checks for SNMP devices?
snmp_info['fc_brocade_port_detailed'] = [
   ( ".1.3.6.1.4.1.1588.2.1.1.1.6.2.1", [
    1, # swFCPortIndex
    3, # swFCPortOpStatus
    4, # swFCPortAdmStatus
   11, # swFCPortTxWords
   12, # swFCPortRxWords
   13, # swFCPortTxFrames
   14, # swFCPortRxFrames
   22, # swFCPortRxCrcs
   26, # swFCPortRxEncOutFrs
   28, # swFCPortC3Discards
   ]),

   # 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_functions['fc_brocade_port_detailed'] = \
       lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.1588.2.1.1.")

