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


# Output from multipath -l has the following format:

# orabase.lun50 (360a9800043346937686f456f59386741) dm-15 NETAPP,LUN
# [size=25G][features=1 queue_if_no_path][hwhandler=0]
# \_ round-robin 0 [prio=0][active]
#  \_ 1:0:3:50 sdy 65:128 [active][undef]
#  \_ 3:0:3:50 sdz 65:144 [active][undef]

# An alias might not be defined. The out is then:
# 360a980004334644d654a364469555a76
# [size=300 GB][features="0"][hwhandler="0"]
# \_ round-robin 0 [active]
#  \_ 1:0:2:13 sdc 8:32  [active][ready]
#  \_ 3:0:2:13 sdl 8:176 [active][ready]

# Might also be local disks:
# SFUJITSU_MAW3073NC_DBL2P62003VT
# [size=68 GB][features="0"][hwhandler="0"]
# \_ round-robin 0 [active]
#  \_ 0:0:3:0  sdb 8:16  [active][ready]

# Some very special header line
# (No space between uuid and dm-* - strange...)
# 360a980004334644d654a316e65306e51dm-4 NETAPP,LUN
# [size=30G][features=1 queue_if_no_path][hwhandler=0]
# \_ round-robin 0 [prio=0][active]
#  \_ 1:0:2:50 sdg 8:96  [active][undef]
# \_ round-robin 0 [prio=0][enabled]
#  \_ 4:0:1:50 sdl 8:176 [active][undef]

# And another one:
# 1494554000000000052303250303700000000000000000000 dm-0 IET,VIRTUAL-DISK
# [size=70G][features=0][hwhandler=0][rw]
# \_ round-robin 0 [prio=-1][active]
#  \_ 3:0:0:0 sdb 8:16  [active][undef]
# \_ round-robin 0 [prio=-1][enabled]
#  \_ 4:0:0:0 sdc 8:32  [active][undef]

# Other output from other host:
# anzvol2 (36005076306ffc648000000000000510a) dm-15 IBM,2107900
# [size=100G][features=0][hwhandler=0]
# \_ round-robin 0 [prio=-6][active]
#  \_ 4:0:5:1  sdbf 67:144 [active][undef]
#  \_ 4:0:4:1  sdau 66:224 [active][undef]
#  \_ 4:0:3:1  sdaj 66:48  [active][undef]
#  \_ 3:0:5:1  sdy  65:128 [active][undef]
#  \_ 3:0:4:1  sdn  8:208  [active][undef]
#  \_ 3:0:3:1  sdc  8:32   [active][undef]
# anzvol1 (36005076306ffc6480000000000005005) dm-16 IBM,2107900
# [size=165G][features=0][hwhandler=0]
# \_ round-robin 0 [prio=-6][active]
#  \_ 4:0:5:0  sdbe 67:128 [active][undef]
#  \_ 4:0:4:0  sdat 66:208 [active][undef]
#  \_ 4:0:3:0  sdai 66:32  [active][undef]
#  \_ 3:0:5:0  sdx  65:112 [active][undef]
#  \_ 3:0:4:0  sdm  8:192  [active][undef]
#  \_ 3:0:3:0  sdb  8:16   [active][undef]

# And one other output (ID has not 33 times A-Z0-9):
# mpath1 (SIBM_____SwapA__________DA02BF71)
# [size=41 GB][features="0"][hwhandler="0"]
# \_ round-robin 0 [active]
#  \_ 1:0:1:0 sdd 8:48  [active]

# Recently I've seen this output >:-P
# 360080e500017bd72000002eb4c1b1ae8 dm-1 IBM,1814      FAStT
# size=350G features='1 queue_if_no_path' hwhandler='1 rdac' wp=rw
# `-+- policy='round-robin 0' prio=-1 status=active
#   |- 7:0:2:81 sdd 8:48   active undef running
#   `- 8:0:2:81 sdp 8:240  active undef running

# And this has been seen on SLES 11 SP1 64 Bit:
# 3600508b40006d7da0001a00004740000 dm-0 HP,HSV210
# size=10G features='1 queue_if_no_path' hwhandler='0' wp=rw
# |-+- policy='round-robin 0' prio=-1 status=active
# | |- 2:0:0:1 sda        8:0    active undef running
# | `- 3:0:0:1 sdo        8:224  active undef running
# `-+- policy='round-robin 0' prio=-1 status=enabled
#   |- 3:0:1:1 sdv        65:80  active undef running
#   `- 2:0:1:1 sdh        8:112  active undef running


def parse_multipath_output(info, only_uuid = None):
    # only_uuid --> look only for data of this uuid or alias

    # New reported header lines need to be placed here
    # the matches need to be put in a list of tupples
    # while the structure of the tupple is:
    # 0: matching regex
    # 1: matched regex-group id of UUID
    # 2: matched regex-group id of alias (optional)
    reg_headers = [ (get_regex("^[0-9a-z]{33}$"), 0, None),
                    (get_regex("^([^\s]+)\s\(([0-9A-Za-z_-]+)\)"), 2, 1),
                    (get_regex("^[a-zA-Z0-9_]+$"), 0, None),
                    (get_regex("^([0-9a-z]{33}|[0-9a-z]{49})\s?dm.+$"), 1, None),
                    (get_regex("^[a-zA-Z0-9_]+dm-.+$"), 0, None) ]

    reg_prio = get_regex("[[ ]prio=")
    reg_lun  = get_regex("[0-9]+:[0-9]+:[0-9]+:[0-9]+")
    uuid = None
    alias = None
    groups = {}
    group = {}
    numpaths = None
    for line in info:
        # newer agent also output the device mapper table.
        # ignore those lines for now.
        if line[0] == "dm":
            # Reset current device and skip line
            uuid = None
            continue

        # restore original non-split line
        l = " ".join(line)

        # Skip output when multipath is not present
        if l.endswith('DM multipath kernel driver not loaded'):
            uuid = None
            continue

        # First simply separate between data row and header row
        if line[0][0] not in [ '[', '`', '|', '\\' ] and not line[0].startswith("size="):
            # Try to match header lines
            matchobject = None
            for regex, uuid_pos, alias_pos in reg_headers:
                matchobject = regex.search(l)

                if matchobject:
                    uuid = matchobject.group(uuid_pos)

                    if alias_pos:
                        alias = matchobject.group(alias_pos)
                    else:
                        alias = None

                    break

            # No data row and no matching header row
            if not matchobject:
                raise Exception("Invalid line in agent output: " + l)

            # Skip unwanted devices in one device mode
            if only_uuid and only_uuid not in [ uuid, alias ]:
                uuid = None # wrong uuid, ignore this one
                continue

            # initialize information about next device
            numpaths = 0
            lun_info = []
            paths_info = []
            broken_paths = []
            group = {}
            group['paths'] = paths_info
            group['broken_paths'] = broken_paths
            group['luns'] = lun_info
            group['uuid'] = uuid
            group['state'] = None
            group['numpaths'] = 0
            groups[uuid] = group

            # If the device has an alias, then extract it
            if alias:
                group['alias'] = alias

            # Proceed with next line after init
            continue
        else:
            if uuid != None:
                # Handle special syntax | |- 2:0:0:1 sda  ...
                if line[0] == '|':
                    line = line[1:]
                if reg_prio.search(l):
                    group['state'] = "".join(line[3:])
                elif len(line) >= 4 and reg_lun.match(line[1]):
                    luninfo = "%s(%s)" % (line[1], line[2])
                    lun_info.append(luninfo)
                    state = line[4]
                    if not "active" in state:
                        broken_paths.append(luninfo)
                    numpaths += 1
                    group['numpaths'] = numpaths
                    paths_info.append(line[2])
    return groups

# Get list of UUIDs of all multipath devices
# Length of UUID is 360a9800043346937686f456f59386741
def inventory_multipath(checkname, info):
    inventory = []
    parsed = parse_multipath_output(info)
    for uuid, info in parsed.items():
        # take current number of paths as target value
        inventory.append( (uuid, " ".join(info['luns']), info['numpaths']) )
    return inventory

# item is UUID (e.g. '360a9800043346937686f456f59386741') or alias (e.g. 'mpath0')
def check_multipath(item, target_numpaths, info):
    if target_numpaths == None:
        target_numpaths = 2 # default case: we need two paths

    parsed = parse_multipath_output(info, item) # we look for one specific uuid/alias
    if len(parsed) == 0:
        return (3, "UNKNOWN - no map with uuid/alias %s" % item)
    mmap = parsed.values()[0]

    alias = mmap.get('alias')
    if alias:
        aliasinfo = "(%s) " % alias
    else:
        aliasinfo = ""

    numpaths = mmap['numpaths']
    broken = mmap['broken_paths']
    numbroken = len(broken)
    if numbroken > 0:
        return (2, "CRIT - %sbroken paths: %s" % (aliasinfo, ",".join(broken)))

    info = "%spaths expected: %d, paths active: %d" % (aliasinfo, target_numpaths, numpaths)

    if numpaths < target_numpaths:
        return (2, "CRIT - " + info)
    elif numpaths > target_numpaths:
        return (1, "WARN - " + info)
    else:
        return (0, "OK - " + info)


check_info['multipath'] = (
    check_multipath,
    "Multipath %s",
    0,
    inventory_multipath)
