#!/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 line[2] == '-' or int(line[2]) == 0:
                continue # exclude filesystems without size



            # 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 inventory_df(parsed):

    def is_mountpoint(inventory_entry):
        return "patterns" not in inventory_entry[1]

    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

    mplist = []
    for line in _filter_docker_filesystems(df_blocks):

        mountpoint = line[0]
        if mountpoint in inventory_df_exclude_mountpoints:
            continue # exclude this mount point (/tmp, /proc, whatever user wants)
        if volume_info[mountpoint]["fs_type"] in ignore_fs_types and mountpoint not in never_ignore_mountpoints:
            continue # ignore this filesystem type
        mplist.append(mountpoint)



    if include_volume_name:
        inventory = []
        for entry in df_inventory(mplist):
            if is_mountpoint(entry):
                mountpoint, params = entry
                item = "%s %s" % (volume_info[mountpoint]["volume_name"], mountpoint)
                inventory.append((item, params))
            else:
                inventory.append(entry)
    else:
        inventory = df_inventory(mplist)
    return inventory


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

    df_blocks = _filter_docker_filesystems(df_blocks)
    volume_and_mp_to_mp = { ("%s %s" % (volume_info[mp]["volume_name"], mp)) : mp for mp in volume_info }
    if "patterns" in params or item in volume_info:
        mountpoint = item
    elif item in volume_and_mp_to_mp:
        mountpoint = volume_and_mp_to_mp[item]
    else:
        mountpoint = item
    return df_check_filesystem_list(mountpoint, params, df_blocks, df_inodes)


# 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
def _filter_docker_filesystems(df_blocks):
    return [ e for e in df_blocks if not e[0].startswith("/var/lib/docker")
             and e[0] not in [ "/etc/resolv.conf", "/etc/hostname", "/etc/hosts" ] ]


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" ],
}
