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

import lib.args
import lib.base
import lib.disk
import lib.distro
import lib.lftest
import lib.shell
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks for processes that were started before they or one of their dependencies were
updated. Returns WARN if a full system reboot is required or if individual services
need a restart. Useful for detecting servers that have been patched but not yet
rebooted.
Requires root or sudo."""


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

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

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

    # hidden test hook: force the OS family detection instead of reading
    # /etc/os-release, so a single host can exercise both the RedHat and
    # Debian code paths via fixtures
    parser.add_argument(
        '--test-os-family',
        help=argparse.SUPPRESS,
        dest='TEST_OS_FAMILY',
    )

    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)

    # Pad args.TEST to 3 elements so the per-command fixture
    # loads below that splice `args.TEST[1]` and `args.TEST[2]`
    # into custom lib.lftest.test() calls do not go out of range
    # when the user passes `--test=path` without trailing commas.
    if args.TEST is not None:
        while len(args.TEST) < 3:
            args.TEST.append('')

    # init some vars
    if args.TEST_OS_FAMILY:
        distro = {'os_family': args.TEST_OS_FAMILY}
    else:
        distro = lib.distro.get_distribution_facts()

    # build the message
    if distro['os_family'] == 'RedHat':
        # 1. full reboot necessary? check the return code (<> 0: needs reboot):
        # needs-restarting --reboothint
        if args.TEST is None:
            stdout, stderr, retc = lib.base.coe(
                lib.shell.shell_exec('needs-restarting --reboothint')
            )
        else:
            stdout, stderr, retc = lib.lftest.test(
                [args.TEST[0] + '-reboothint', args.TEST[1], args.TEST[2]],
            )
        if int(retc) == 1:
            lib.base.oao('A system reboot may be required.', STATE_WARN)

        # 2. long list of all updated services (process list): needs-restarting
        if args.TEST is None:
            stdout, _, retc = lib.base.coe(lib.shell.shell_exec('needs-restarting'))
        else:
            stdout, _, retc = lib.lftest.test(
                [args.TEST[0] + '-services', args.TEST[1], args.TEST[2]],
            )
        service_restart_needed = stdout.strip()
        if service_restart_needed:
            #  support for backslash characters within the brace substitution is still disallowed
            lib.base.oao(
                (
                    f'Found {stdout.count(chr(10))} running '
                    f'{lib.txt.pluralize("process", stdout.count(chr(10)), "es")} that '
                    f'{lib.txt.pluralize("", stdout.count(chr(10)), "has,have")} been updated '
                    f'and may need a restart:\n{service_restart_needed}'
                ),
                STATE_WARN,
            )
        if stderr:
            lib.base.oao(f'No system or service restart needed, but {stderr}', STATE_OK)
        lib.base.oao('No system or service restart needed.', STATE_OK)

    if distro['os_family'] == 'Debian':
        # 1. check if needrestart is available - if yes, use it
        if args.TEST is None:
            success, result = lib.shell.shell_exec('needrestart -b')
        else:
            success = True
            result = lib.lftest.test(
                [args.TEST[0] + '-needrestart', args.TEST[1], args.TEST[2]],
            )
        if success:
            stdout, stderr, retc = result
            # NEEDRESTART-VER: 3.5
            # NEEDRESTART-KCUR: 4.19.0-20-amd64
            # NEEDRESTART-KEXP: 5.10.0-13-amd64
            # NEEDRESTART-KSTA: 3
            # NEEDRESTART-SVC: dbus.service
            # NEEDRESTART-SVC: getty@tty1.service
            # NEEDRESTART-SVC: ifup@enp1s0.service
            # NEEDRESTART-SVC: systemd-logind.service
            # The kernel status (NEEDRESTART-KSTA) value has the following meaning:
            #     0: unknown or failed to detect
            #     1: no pending upgrade
            #     2: ABI compatible upgrade pending
            #     3: version upgrade pending
            msg = ''
            svc = 0
            svcs = ''
            kcur, kexp = '', ''
            for line in stdout.split('\n'):
                if line.startswith('NEEDRESTART-KCUR: '):
                    kcur = line.replace('NEEDRESTART-KCUR: ', '')
                if line.startswith('NEEDRESTART-KEXP: '):
                    kexp = line.replace('NEEDRESTART-KEXP: ', '')
                if line.startswith('NEEDRESTART-KSTA: '):
                    if line.endswith('0'):
                        msg += '(Unknown or failed to detect)'
                    if line.endswith('2'):
                        msg += '(ABI compatible upgrade pending)'
                    if line.endswith('3'):
                        msg += '(Version upgrade pending)'
                if line.startswith('NEEDRESTART-SVC: '):
                    svc += 1
                    svcs += f'* {line.replace("NEEDRESTART-SVC: ", "")}\n'
            if kcur != kexp:
                msg = f'Running Kernel {kcur} != Installed Kernel {kexp} {msg}. '
            if svc:
                msg += (
                    f'Found {svc} running '
                    f'{lib.txt.pluralize("process", svc, "es")} that '
                    f'{lib.txt.pluralize("", svc, "has,have")} been updated '
                    f'and may need a restart:\n{svcs}, '
                )
            if msg:
                lib.base.oao(f'A system reboot may be required. {msg[:-2]}', STATE_WARN)
            lib.base.oao('No system or service restart needed.', STATE_OK)

        # 2. if not, check if reboot is required
        if lib.disk.file_exists('/var/run/reboot-required', allow_empty=True):
            lib.base.oao('A system reboot may be required.', STATE_WARN)
        # over and out
        lib.base.oao('No system or service restart needed.', STATE_OK)

    lib.base.cu('OS not supported.')


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