#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details.
"""

import argparse  # pylint: disable=C0413
import os  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.base  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

try:
    import psutil  # pylint: disable=C0413
except ImportError:
    print('Python module "psutil" is not installed.')
    sys.exit(STATE_UNKNOWN)


__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2025020401'

DESCRIPTION = """Displays amount of free and used swap space in the system, checks against used swap
                 in percent."""

DEFAULT_CRIT  = 90  # %
DEFAULT_TOP = 5
DEFAULT_WARN  = 70  # %


def parse_args():
    """Parse command line arguments using argparse.
    """
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V', '--version',
        action='version',
        version='%(prog)s: v{} by {}'.format(__version__, __author__),
    )

    parser.add_argument(
        '--always-ok',
        help='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c', '--critical',
        default=DEFAULT_CRIT,
        dest='CRIT',
        help='Set the critical threshold for swap usage (in percent). '
             'Default: %(default)s',
        type=int,
    )

    parser.add_argument(
        '--top',
        help='List x "Top processes that use the most swap space" (except on Windows). '
             'Default: %(default)s',
        dest='TOP',
        type=int,
        default=DEFAULT_TOP,
    )

    parser.add_argument(
        '-w', '--warning',
        default=DEFAULT_WARN,
        dest='WARN',
        help='Set the warning threshold for swap usage (in percent). '
             'Default: %(default)s',
        type=int,
    )

    return parser.parse_args()


def get_swap_and_name(pid):
    """Scan for usage details in /proc-files.
    """
    try:
        with open('/proc/{}/status'.format(pid)) as file:
            content = file.read()
            name = lib.txt.extract_str(content, 'Name:', '\n')
            size = lib.txt.extract_str(content, 'VmSwap:', '\n')
            if not size:
                size = '0 kB'
            return name.strip(), int(size.replace(' kB', '').strip())
    except FileNotFoundError:
        return None, 0
    except ProcessLookupError:
        return None, 0


def top(count):
    """Get top X processes causing swap usage.
    """
    process_info = {}
    for pid in os.listdir('/proc'):
        if pid.isdigit():
            name, usage = get_swap_and_name(pid) # usage is in kB
            try:
                process_info[name] += usage * 1024
            except:
                process_info[name] = usage * 1024
    msg = '\nTop {} processes that use the most swap space:\n'.format(count)
    process_info = lib.base.sort(process_info)
    for i, p in enumerate(process_info[:count]):
        msg += '{}. {}: {}\n'.format(i + 1, p[0], lib.human.bytes2human(p[1]))
    return msg


def main():
    """The main function. Hier spielt die Musik.
    """

    # parse the command line, exit with UNKNOWN if it fails
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    try:
        swap = psutil.swap_memory()
    except RuntimeError as e:
        lib.base.oao(
            'Performance counters may be corrupt or disabled. If the counters are corrupt, attempt to [rebuild them](https://docs.microsoft.com/en-us/troubleshoot/windows-server/performance/rebuild-performance-counter-library-values). If the counters are disabled, check the registry `HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib` and make sure there is no key "Disable" (or set it to 0).', # pylint: disable=C0301
            STATE_UNKNOWN,
        )
    swap_usage_percent = swap.percent

    msg_header = '{}% - total: {}, used: {}, free: {}'.format(
        swap_usage_percent,
        lib.human.bytes2human(swap.total),
        lib.human.bytes2human(swap.used),
        lib.human.bytes2human(swap.free),
    )

    perfdata =  lib.base.get_perfdata('usage_percent', swap_usage_percent, '%', args.WARN, args.CRIT, 0, 100) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('total', swap.total, 'B', None, None, 0, swap.total)
    perfdata += lib.base.get_perfdata('used', swap.used, 'B', None, None, 0, swap.total)
    perfdata += lib.base.get_perfdata('free', swap.free, 'B', None, None, 0, swap.total)

    msg_body = ''
    if not lib.base.WINDOWS:
        msg_body = 'swapped in: {}, swapped out: {} (both cumulative)'.format(
            lib.human.bytes2human(getattr(swap, 'sin', 0)),
            lib.human.bytes2human(getattr(swap, 'sout', 0)),
        )
        perfdata += lib.base.get_perfdata('sin', getattr(swap, 'sin', 0), 'B', None, None, 0, None)
        perfdata += lib.base.get_perfdata('sout', getattr(swap, 'sout', 0), 'B', None, None, 0, None) # pylint: disable=C0301

    # On Linux only, get the top processes causing swap usage
    if not lib.base.WINDOWS and swap_usage_percent > 0:
        msg_body += '\n' + top(args.TOP)

    # calculating the final check state
    state = lib.base.get_state(swap_usage_percent, args.WARN, args.CRIT)

    # over and out
    lib.base.oao(msg_header + '\n' + msg_body, state, perfdata, always_ok=args.ALWAYS_OK)


if __name__ == '__main__':
    try:
        main()
    except Exception:   # pylint: disable=W0703
        lib.base.cu()
