#!/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 os
import pwd
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__ = '2026081401'

DESCRIPTION = """Displays system-wide Podman information including container counts, image count,
storage driver, logging driver, number of search registries, runtime version, available
CPUs, and total memory. Alerts on the warnings and errors Podman writes while answering.
Individual lines can be filtered out with --ignore (e.g. benign cgroup warnings on
rootless hosts). For Docker, use the docker-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 stderr lines matching this Python regular expression. '
        'Case-sensitive by default; use `(?i)` for case-insensitive matching. '
        'Can be specified multiple times. '
        'Example: `--ignore="cgroup v1"` to suppress a benign cgroup-version '
        'warning on hosts that have not yet migrated to cgroup v2. '
        'Example: `--ignore="(?i)rootless"` (case-insensitive) to suppress '
        'any rootless-related informational warning. '
        '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(
        '--user',
        help='Report on the rootless Podman of this user instead of the one visible to '
        'the executing user. '
        "Podman keeps each user's rootless containers and images in that user's own "
        'storage, so root (the monitoring user runs the check via sudo) does not see '
        'them. With --user, the check runs podman as that user. '
        'Requires the right to `sudo -u <user>` (root has this by default). '
        'Example: `--user=rocketchat`. '
        'Default: %(default)s',
        dest='USER',
        default=None,
    )

    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(
                ['podman', 'info', '--format', 'json'],
                run_as=args.USER,
            ),
        )
    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:
        lib.base.cu('Unable to parse podman info output as JSON.')

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

    # analyze data - extract values from podman info JSON output. Every field is read
    # defensively: what the engine reports depends on its version and on the host, and
    # a missing field is not worth ending the check over.
    host = result.get('host') or {}
    store = result.get('store') or {}
    container_store = store.get('containerStore') or {}
    image_store = store.get('imageStore') or {}

    containers = container_store.get('number')
    containers_paused = container_store.get('paused')
    containers_running = container_store.get('running')
    containers_stopped = container_store.get('stopped')
    cpus = host.get('cpus')
    # Every image record the storage holds, which includes the intermediate layers of a
    # locally built image. That is a larger number than the images `podman images` lists
    # and than podman-image reports: measured on Fedora 44 with podman 5.8, this store
    # held 4434 records where `podman images` listed 1204. Named as a store count in the
    # message so the two checks do not read as if they contradicted each other.
    images = image_store.get('number')
    # the driver containers log through, the counterpart of Docker's logging driver.
    # `eventLogger` is a different thing: where Podman writes its own event log.
    logging_driver = host.get('logDriver')
    memory = host.get('memTotal')
    # only reported when the host has unqualified search registries configured, which
    # a host that requires fully qualified image names deliberately has not
    num_registries = len((result.get('registries') or {}).get('search') or [])
    registry = (
        f'{num_registries} {lib.txt.pluralize("Registr", num_registries, "y,ies")}'
    )
    storage_driver = store.get('graphDriverName')
    ver = (result.get('version') or {}).get('Version')

    # Docker answers `podman info` with an error, but a docker-compatible endpoint
    # behind the socket may answer with a document that has none of these fields
    if not ver:
        lib.base.cu(
            'The engine did not report a version.'
            ' If you are using Docker, use the docker-info check instead.'
        )

    # check stderr for warnings and errors, skipping lines matched by
    # --ignore (#834: same treatment as docker-info so admins can
    # suppress boilerplate stderr warnings that cannot be silenced in
    # the daemon config).
    for row in stderr.strip().split('\n'):
        if any(pattern.search(row) for pattern in ignore_patterns):
            continue
        lcrow = row.lower()
        if 'warning: ' in lcrow:
            warn += f'{row}, '
            state = lib.base.get_worst(state, STATE_WARN)
        if 'error: ' in lcrow:
            crit += f'{row}, '
            state = lib.base.get_worst(state, STATE_CRIT)

    # 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)} in the store'
            ' (incl. intermediate layers)'
        )
    if storage_driver:
        msg += f', Storage Driver: {storage_driver}'
    if logging_driver:
        msg += f', Logging Driver: {logging_driver}'
    msg += f', Registry: {registry}'
    msg += f', Podman 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'
    # name the inspected user. Rootless Podman is per-user, so making the user
    # explicit removes any doubt about whose storage these counts came from.
    inspected_user = args.USER or pwd.getpwuid(os.geteuid()).pw_name
    msg += f' (user: `{inspected_user}`)'

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