#!/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
import json
import sys

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

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

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."""

DEFAULT_TIMEOUT = 8


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,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


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:
        started = lib.time.now(as_type='float')
        success, result = lib.container.run(
            ['docker', 'info', '--format', '{{json .}}'], args.TIMEOUT, started
        )
        if not success:
            # --always-ok suppresses what would alert somebody. An UNKNOWN (no
            # permission, client missing) says the check is deployed wrong and stays.
            lib.base.oao(
                *result, always_ok=args.ALWAYS_OK and result[1] != STATE_UNKNOWN
            )
        stdout, stderr, retc = result
    else:
        # do not call the command, put in test data
        stdout, stderr, retc = lib.lftest.test(args.TEST)

    if retc != 0:
        error = lib.container.get_engine_error(stderr, stdout)
        lib.base.oao(*error, always_ok=args.ALWAYS_OK and error[1] != STATE_UNKNOWN)

    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()
