#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- 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
import json
import sys

import lib.args
import lib.base
import lib.human
import lib.lftest
import lib.shell
import lib.txt
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Displays system-wide Docker information including container counts (running,
paused, stopped), image count, storage and logging driver, Docker version, available
CPUs, and total memory. Alerts when the daemon reports a warning about itself or its
host, and when the daemon answers with an error at all. Individual warnings can be
filtered out with --ignore (e.g. the "No swap limit support" message on hosts where the
kernel does not expose swap accounting). For Podman, use the podman-info check instead.
Requires root or sudo."""


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

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

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--ignore',
        help='Ignore daemon warnings and errors matching this Python regular expression. '
        'Case-sensitive by default; use `(?i)` for case-insensitive matching. '
        'Can be specified multiple times. '
        'Example: `--ignore="No swap limit support"` to suppress the Docker '
        'warning on kernels without swap accounting. '
        'Example: `--ignore="(?i)bridge-nf-call"` (case-insensitive) to '
        'suppress both `bridge-nf-call-iptables` and `bridge-nf-call-ip6tables` '
        'warnings on Debian hosts. '
        'Default: %(default)s',
        dest='IGNORE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    args, _ = parser.parse_known_args()
    return args


def get_engine_error(stderr, stdout=''):
    """Return `(message, state)` for a command that could not reach the container
    engine. A refused permission is a problem of how this check is deployed, not of
    the engine: the engine answers other callers just fine, this one is only not
    allowed to ask, so the check cannot say anything and reports UNKNOWN. Everything
    else, a socket that is not there or an engine that does not answer, is the
    outage this check exists to report.
    """
    text = f'{stderr}\n{stdout}'.strip()
    if 'permission denied' in text.lower():
        return (
            'No permission to talk to the container engine, so nothing can be said'
            ' about it. Run the check as root, or deploy the sudoers file that ships'
            f' with the plugins.\n{text}',
            STATE_UNKNOWN,
        )
    return (text, STATE_CRIT)


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    if args.IGNORE is None:
        args.IGNORE = []

    # compile ignore patterns (case-sensitive by default, matching the
    # lib.args convention for --match / --ignore-regex; the user can
    # opt into case-insensitive matching with the inline `(?i)` flag).
    ignore_patterns = [
        lib.base.coe(p) for p in lib.txt.compile_regex(args.IGNORE, '--ignore')
    ]

    # fetch data
    if args.TEST is None:
        stdout, stderr, retc = lib.base.coe(
            lib.shell.shell_exec(['docker', 'info', '--format', '{{json .}}']),
        )
    else:
        # do not call the command, put in test data
        stdout, stderr, retc = lib.lftest.test(args.TEST)

    if retc != 0:
        lib.base.oao(*get_engine_error(stderr, stdout))

    try:
        result = json.loads(stdout)
    except Exception:
        result = None
    if not isinstance(result, dict):
        lib.base.cu(
            'Unable to read the docker info output.'
            ' If you are using Podman, use the podman-info check instead.'
        )

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    warn, crit = '', ''

    # the client reached the daemon, but the daemon refused to answer. The CLI puts
    # these on stderr prefixed with "ERROR:", so use the wording an admin knows.
    for row in result.get('ServerErrors') or []:
        row = ' '.join(row.split())
        if any(pattern.search(f'ERROR: {row}') for pattern in ignore_patterns):
            continue
        crit += f'ERROR: {row}, '
        state = lib.base.get_worst(state, STATE_CRIT)

    # analyze data - extract values from the docker info JSON output
    containers = result.get('Containers')
    containers_paused = result.get('ContainersPaused')
    containers_running = result.get('ContainersRunning')
    containers_stopped = result.get('ContainersStopped')
    cpus = result.get('NCPU')
    images = result.get('Images')
    logging_driver = result.get('LoggingDriver')
    memory = result.get('MemTotal')
    storage_driver = result.get('Driver')
    ver = result.get('ServerVersion')

    # Podman answers `docker info` as well when podman-docker is installed, but its
    # information is shaped differently and carries no server version
    if not ver and not crit:
        lib.base.cu(
            'The daemon did not report a server version.'
            ' If you are using Podman, use the podman-info check instead.'
        )

    # what the daemon says about itself and its host, for example a kernel without
    # swap accounting or a socket reachable without encryption. Lines matched by
    # --ignore are skipped (#834: an admin cannot silence these in the daemon
    # config, so the check has to be able to).
    for row in result.get('Warnings') or []:
        row = ' '.join(row.split())
        if any(pattern.search(row) for pattern in ignore_patterns):
            continue
        warn += f'{row}, '
        state = lib.base.get_worst(state, STATE_WARN)

    # build perfdata
    perfdata += lib.base.get_perfdata(
        'containers',
        containers,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'containers_paused',
        containers_paused,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'containers_running',
        containers_running,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'containers_stopped',
        containers_stopped,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'cpu',
        cpus,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'images',
        images,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'ram',
        memory,
        uom='B',
        _min=0,
    )

    # create output
    if crit:
        # build the message
        msg += f'{crit}'
    if warn:
        msg += f'{warn}'
    if containers is not None:
        msg += f'{containers} {lib.txt.pluralize("Container", containers)}'
    if containers_running is not None:
        msg += (
            f' ({containers_running} running,'
            f' {containers_paused} paused,'
            f' {containers_stopped} stopped)'
        )
    if images is not None:
        msg += f', {images} {lib.txt.pluralize("Image", images)}'
    if storage_driver:
        msg += f', Storage Driver: {storage_driver}'
    if logging_driver:
        msg += f', Logging Driver: {logging_driver}'
    msg += f', Docker v{ver}'
    if cpus is not None:
        msg += f', {cpus} {lib.txt.pluralize("CPU", cpus)}'
    if memory is not None:
        msg += f', {lib.human.bytes2human(memory)} Memory'

    # over and out
    lib.base.oao(
        msg, state, perfdata, always_ok=args.ALWAYS_OK, no_perfdata=args.NO_PERFDATA
    )


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