#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.


from cmk.base.plugins.agent_based.utils import multipath

# Configuration for using alias instead of UUID
inventory_multipath_rules = []


def parse_multipath(info) -> multipath.Section:
    # 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)
    # 3: matched regex-group id of dm-device (optional)
    reg_headers = [
        (regex(r"^[0-9a-z]{33}$"), 0, None, None),  # 1. (should be included in 3.)
        (regex(r"^([^\s]+)\s\(([-0-9A-Za-z_:]+)\)\s(dm.[0-9]+)"), 2, 1, 3),  # 2.
        (regex(r"^([^\s]+)\s\(([-0-9A-Za-z_]+)\)"), 2, 1, None),  # 2.
        (regex(r"^[a-zA-Z0-9_]+$"), 0, None, None),  # 3.
        (regex(r"^([0-9a-z]{33}|[0-9a-z]{49})\s?(dm.[0-9]+).*$"), 1, None, 2),  # 4.
        (regex(r"^[a-zA-Z0-9_]+(dm-[0-9]+).*$"), 0, None, 1),  # 5. Remove this line in 1.2.0
        (regex(r"^([-.a-zA-Z0-9_ :]+)\s?(dm-[0-9]+).*$"), 1, None, 2),  # 6. and 7.
    ]

    reg_prio = regex(r"[\[ ]prio=")
    reg_lun = regex("[0-9]+:[0-9]+:[0-9]+:[0-9]+")
    uuid = None
    alias = None
    groups = {}
    group = {}
    numpaths = None
    for line in info:
        # Ignore error messages due to invalid multipath.conf
        if line[0] == "multipath.conf":
            continue

        # 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("kernel driver not loaded")
            or l.endswith("does not exist, blacklisting all devices.")
            or l.endswith("A sample multipath.conf file is located at")
            or l.endswith("multipath.conf")
        ):
            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 header_regex, uuid_pos, alias_pos, dm_pos in reg_headers:
                matchobject = header_regex.search(l)
                if matchobject:
                    uuid = matchobject.group(uuid_pos).strip()

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

                    if dm_pos:
                        dm_device = matchobject.group(dm_pos)
                    else:
                        dm_device = None

                    break
            # No data row and no matching header row
            if not matchobject:
                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
            group["device"] = dm_device
            groups[uuid] = group

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

            # Proceed with next line after init
            continue
        elif uuid is not 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(parsed):
    settings = host_extra_conf_merged(host_name(), inventory_multipath_rules)

    inventory = []
    for uuid, info in parsed.items():
        # take current number of paths as target value
        if "alias" in info and settings.get("use_alias"):
            item = info["alias"]
        else:
            item = uuid
        inventory.append((item, info["numpaths"]))
    return inventory


# item is UUID (e.g. '360a9800043346937686f456f59386741') or alias (e.g. 'mpath0')
# def check_multipath(item, target_numpaths, parsed):
def check_multipath(item, params, parsed):
    # Keys in parsed are the UUIDs. First assume that we are
    # looking for a UUID. Then fall back to aliases
    if item in parsed:
        mmap = parsed[item]
    elif item.strip() in parsed:
        # support items discovered before 1.2.7
        mmap = parsed[item.strip()]
    else:
        for mmap in parsed.values():
            if mmap.get("alias") == item:
                break
        else:
            yield 3, "Multipath device not found in agent output"
            return

    # If the item is the alias, then show the UUID in the plugin output.
    # If the item is the UUID, then vice versa.
    alias = mmap.get("alias")
    uuid = mmap.get("uuid")

    if item == uuid and alias:
        aliasinfo = "(%s): " % alias
    elif item == alias and uuid:
        aliasinfo = "(%s): " % uuid
    else:
        aliasinfo = ""

    all_paths = mmap["paths"]
    broken_paths = mmap["broken_paths"]
    num_paths = len(all_paths)
    num_broken = len(broken_paths)
    num_active = num_paths - num_broken

    infotext = "%sPaths active: %s/%s" % (aliasinfo, num_active, num_paths)

    if isinstance(params, tuple):
        warn, crit = params
        warn_num = (warn / 100.0) * num_paths
        crit_num = (crit / 100.0) * num_paths
        if num_active < crit_num:
            state = 2
        elif num_active < warn_num:
            state = 1
        else:
            state = 0

        if state > 0:
            infotext += " (warn/crit below %d/%d)" % (warn_num, crit_num)
        yield state, infotext

    else:
        if isinstance(params, int):
            expected_paths = params
        else:
            expected_paths = 2

        infotext += ", Expected paths: %s" % expected_paths
        if num_active < expected_paths:
            state = 2
            infotext += " (crit below %d)" % expected_paths
        elif num_active > expected_paths:
            state = 1
            infotext += " (warn at %d)" % expected_paths
        else:
            state = 0
        yield state, infotext

    if num_broken > 0:
        yield 0, "Broken paths: %s" % ",".join(broken_paths)


check_info["multipath"] = {
    "check_function": check_multipath,
    "inventory_function": inventory_multipath,
    "parse_function": parse_multipath,
    "service_description": "Multipath %s",
    "group": "multipath",
}
