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

# <<<df>>>
# /dev/sda3     ext4     8123200   1207512   6496392      16% /
# /dev/sda6     ext3   117794932    192192 111522544       1% /data
# /dev/sda2     ext3     8123200    220388   7483516       3% /var
# /dev/sda1     reiserfs  256666     16052    227362       7% /boot
# /dev/mapper/mirrored-database ext3  20642428   1027112  19405604       6% /mirrored/database

# Another example from a Windows 7 system:
# <<<df>>>
# SYSTEM NTFS 312569172 180648472 131920700  58% C:\
# Data NTFS 976506816 528665344 447841472  55% D:\
# PS3 PlayStation(R)3 File System 0 0 0   0% P:\

# An example with btrfs (SLES 12). Here the same device is mounted
# several times at different mount point. But must only be monitored
# once. We use the device instead of the mount point in this case.
# <<<df>>>
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /
# devtmpfs       devtmpfs      497396       0    497396       0% /dev
# tmpfs          tmpfs         506312       0    506312       0% /dev/shm
# tmpfs          tmpfs         506312    6980    499332       2% /run
# tmpfs          tmpfs         506312       0    506312       0% /sys/fs/cgroup
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /.snapshots
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /var/tmp
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /var/spool
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /var/opt
# /dev/sda1      btrfs       20970496 4169036  16539348      21% /var/log

# <<<df>>>
# dev                     795652         0    795652   0% /dev
# run                     811756     11848    799908   1% /run
# /dev/sda2              1040280    716340    271512  73% /
# devtmpfs                795652         0    795652   0% /dev
# tmpfs                   811756         0    811756   0% /dev/shm
# tmpfs                   811756     11848    799908   1% /run
# tmpfs                   811756         0    811756   0% /sys/fs/cgroup
# none                    811756        12    811744   0% /var/tmp
# none                    811756         0    811756   0% /var/lock
# none                    409600     95460    314140  23% /var/log
# tmpfs                   811756     11848    799908   1% /var/run
# none                    811756        56    811700   0% /tmp
# /dev/sda1               126931     33759     86619  28% /boot
# /dev/sda5             12668904    360184  11670236   3% /persist
# <<<df>>>
# [df_inodes_start]
# dev                     198913       365    198548   0% /dev
# run                     202939       336    202603   0% /run
# /dev/sda2                65536     25533     40003  39% /
# devtmpfs                198913       365    198548   0% /dev
# tmpfs                   202939         1    202938   0% /dev/shm
# tmpfs                   202939       336    202603   0% /run
# tmpfs                   202939         7    202932   0% /sys/fs/cgroup
# none                    202939         4    202935   0% /var/tmp
# none                    202939         1    202938   0% /var/lock
# none                    202939        28    202911   0% /var/log
# tmpfs                   202939       336    202603   0% /var/run
# none                    202939        27    202912   0% /tmp
# /dev/sda1                32768        25     32743   0% /boot
# /dev/sda5               799680       118    799562   0% /persist
# [df_inodes_end]

# <<<df>>>
# C:\ NTFS 41838588 21776048 20062540 53% C:\
# C:\Program Files\Vision Solutions\Double-Take\Service\MountDir\usauhtest0010_c061b170-ad3f-473f-92ce-088c97fce98e_C\ NTFS 41835516 11895180 29940336 29% C:\Program Files\Vision Solutions\Double-Take\Service\MountDir\usauhtest0010_c061b170-ad3f-473f-92ce-088c97fce98e_C\

inventory_df_rules = []
inventory_df_exclude_fs = ['tmpfs', 'nfs', 'smbfs', 'cifs', 'iso9660']


def parse_df(info):
    def parse_blocks_subsection(blocks_subsection):
        volume_info = {}
        df_blocks = []
        btrfs_devices = set()
        for line in blocks_subsection:
            try:
                int(line[1])
            except ValueError:
                pass
            else:
                line = [line[0], None] + line[1:]

            # Handle known cases, where the file system contains spaces
            for index, entry in enumerate(line):
                if entry == "NTFS":
                    line = [" ".join(line[:index])] + [line[index]] + line[index + 1:index + 5] + [
                        " ".join(line[index + 5:])
                    ]
                    break

            if line[2] == "File" and line[3] == "System":
                line = [line[0], " ".join(line[1:4])] + line[4:]

            fs_type = line[1]
            # This particular bit of magic originated in Werk #2671 and has the purpose of avoiding duplicate checks,
            # as btrfs filesystems are often mounted at multiple mountpoints. We keep it for compatibility.
            if fs_type == "btrfs":
                device = line[0]
                if device not in btrfs_devices:
                    btrfs_devices.add(device)
                    mountpoint = "btrfs " + device
                else:
                    continue

            else:
                mountpoint = " ".join(line[6:]).replace('\\', '/')  # Windows \ is replaced with /

            if mountpoint in ("/etc/resolv.conf", "/etc/hostname", "/etc/hosts"):
                continue

            # exclude filesystems without size
            try:
                if int(line[2]) == 0:
                    continue
            except ValueError:
                continue

            # Beware: the 6th column of df ("used perc") may includes 5% which are reserved
            # for the superuser, whereas the 4th colum ("used MB") does *not* include that.
            # Beware(2): the column used_mb does not account for the reserved space for
            # superusers. So we rather use the column 'avail' and subtract that from total
            # to compute the used space.
            size_mb = int(line[2]) / 1024.0
            avail_mb = int(line[4]) / 1024.0
            used_mb = int(line[3]) / 1024.0
            reserved_mb = size_mb - avail_mb - used_mb  # reserved for root
            df_blocks.append((mountpoint, size_mb, avail_mb, reserved_mb))

            volume_name = line[0]
            volume_info[mountpoint] = {
                "volume_name": volume_name,
                "fs_type": fs_type,
            }

        return df_blocks, volume_info

    def parse_inodes_subsection(inodes_subsection):
        df_inodes = []
        for line in inodes_subsection:
            try:
                int(line[1])
            except ValueError:
                pass
            else:
                line = [line[0], None] + line[1:]

            try:
                inodes_total = int(line[2])
                inodes_avail = int(line[4])
            except ValueError:
                continue

            mountpoint = line[-1]
            df_inodes.append((mountpoint, inodes_total, inodes_avail))
        return df_inodes

    blocks_subsection = []
    inodes_subsection = []

    is_inode = False
    for line in info:
        if line[-1] == '[df_inodes_start]':
            is_inode = True
            continue
        elif line[-1] == '[df_inodes_end]':
            is_inode = False
            continue

        if is_inode:
            inodes_subsection.append(line)
        else:
            blocks_subsection.append(line)

    return parse_blocks_subsection(blocks_subsection), parse_inodes_subsection(inodes_subsection)


def _filter_by_exclude_rule(inv_rule_excludes, iterable):
    """exclude these mount points (/tmp, /proc, whatever user wants)"""
    for df_block in iterable:
        if df_block[0] not in inv_rule_excludes:
            yield df_block


def _filter_by_type(volume_info, ignore_fs_types, never_ignore_mountpoints, iterable):
    for df_block in iterable:
        if (df_block[0] in never_ignore_mountpoints or
                volume_info[df_block[0]]["fs_type"] not in ignore_fs_types):
            yield df_block


def _format_with_description(description, mountpoint):
    """
    TODO: maybe do this only if description != mountpoint
    """
    return "%s %s" % (description, mountpoint)


def _get_item_list_from_blocks(df_blocks):
    """return the mountpoint"""  # TODO: fix this. Should include optional description
    return [df_block[0] for df_block in df_blocks]


def _is_grouped_item(params):
    return "patterns" in params


def inventory_df(parsed):
    inventory_options = host_extra_conf_merged(host_name(), inventory_df_rules)
    include_volume_name = inventory_options.get("include_volume_name", False)
    ignore_fs_types = inventory_options.get("ignore_fs_types", inventory_df_exclude_fs)
    never_ignore_mountpoints = inventory_options.get("never_ignore_mountpoints", [])

    (df_blocks, volume_info), _ = parsed

    filtered_blocks = _filter_by_exclude_rule(inventory_df_exclude_mountpoints, df_blocks)
    filtered_blocks = _filter_by_type(volume_info, ignore_fs_types, never_ignore_mountpoints,
                                      filtered_blocks)

    #Always exclude filesystems below dockers local storage area
    #and also exclude docker mounts in containers which are reported
    #by the agent when the agent is executed in the container context
    filtered_blocks = (mp for mp in filtered_blocks if not mp[0].startswith("/var/lib/docker/"))

    mplist = _get_item_list_from_blocks(filtered_blocks)

    inventory = df_inventory(mplist)
    if include_volume_name:
        for idx, (mountpoint, params) in enumerate(inventory):
            if _is_grouped_item(params):
                continue
            item = _format_with_description(volume_info[mountpoint]["volume_name"], mountpoint)
            inventory[idx] = (item, params)

    return inventory


def check_df(item, params, parsed):
    (df_blocks, volume_info), df_inodes = parsed

    if item in volume_info or _is_grouped_item(params):
        return df_check_filesystem_list(item, params, df_blocks, df_inodes)

    iter_name_mp = ((volume_info[mp]["volume_name"], mp) for mp in volume_info)
    for name, mountpoint in iter_name_mp:
        if item == _format_with_description(name, mountpoint):
            return df_check_filesystem_list(mountpoint, params, df_blocks, df_inodes)


check_info['df'] = {
    "parse_function": parse_df,
    "inventory_function": inventory_df,
    "check_function": check_df,
    "service_description": "Filesystem %s",
    "has_perfdata": True,
    "group": "filesystem",
    "default_levels_variable": "filesystem_default_levels",
    "includes": ["size_trend.include", "df.include"],
}
