#!/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.shell
import lib.version
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks the state of a specific systemd unit (service, socket, device, mount, timer,
scope, etc.) via systemctl. Verifies the active state, sub-state, load state, and
unit file state against expected values.
Alerts when the unit is not in the expected state.
Requires root or sudo."""

DEFAULT_ACTIVESTATE = []
DEFAULT_LOADSTATE = 'loaded'
DEFAULT_SEVERITY = 'warn'
DEFAULT_SUBSTATE = []


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(
        '--activestate',
        help='Expected systemd ActiveState (high-level unit activation '
        'state, i.e. generalization of SUB). '
        'Can be specified multiple times. '
        'If omitted or set to "None", the unit\'s active state '
        'will not be checked.',
        dest='ACTIVESTATE',
        default=None,
        action='append',
        choices=[
            'None',
            'activating',
            'active',
            'deactivating',
            'failed',
            'inactive',
        ],
    )

    parser.add_argument(
        '--loadstate',
        help='Expected systemd LoadState, reflecting whether the unit '
        'definition was properly loaded. '
        'If omitted or set to "None", the unit\'s load state '
        'will not be checked. Default: %(default)s',
        dest='LOADSTATE',
        default=DEFAULT_LOADSTATE,
        choices=[
            'None',
            'activating',
            'active',
            'deactivating',
            'failed',
            'inactive',
            'loaded',
            'maintenance',
            'masked',
            'not-found',
            'reloading',
        ],
    )

    parser.add_argument(
        '--machine',
        help='Execute operation on a local container. '
        'Specify a container name to connect to, optionally prefixed by a user name '
        'and a separating "@" character. '
        'The special string ".host" connects to the local system '
        "(useful for reaching a specific user's bus: `--user --machine=lennart@.host`). "
        'Without "@" syntax, the connection is made as root. '
        'With "@" syntax, either side may be omitted (but not both), '
        'defaulting to the local user name and ".host".',
        dest='MACHINE',
    )

    parser.add_argument(
        '--severity',
        help=lib.args.help('--severity') + ' Default: %(default)s',
        dest='SEVERITY',
        default=DEFAULT_SEVERITY,
        choices=['warn', 'crit'],
    )

    parser.add_argument(
        '--substate',
        help='Expected systemd SubState (low-level unit activation '
        'state; values depend on unit type). '
        'Can be specified multiple times. '
        'If omitted or set to "None", the unit\'s substate '
        'will not be checked.',
        dest='SUBSTATE',
        default=None,
        action='append',
        choices=[
            'None',
            'abandoned',
            'activating',
            'activating-done',
            'active',
            'auto-restart',
            'cleaning',
            'condition',
            'deactivating',
            'deactivating-sigkill',
            'deactivating-sigterm',
            'dead',
            'elapsed',
            'exited',
            'failed',
            'final-sigkill',
            'final-sigterm',
            'final-watchdog',
            'listening',
            'mounted',
            'mounting',
            'mounting-done',
            'plugged',
            'reload',
            'remounting',
            'remounting-sigkill',
            'remounting-sigterm',
            'running',
            'start',
            'start-chown',
            'start-post',
            'start-pre',
            'stop',
            'stop-post',
            'stop-pre',
            'stop-pre-sigkill',
            'stop-pre-sigterm',
            'stop-sigkill',
            'stop-sigterm',
            'stop-watchdog',
            'tentative',
            'unmounting',
            'unmounting-sigkill',
            'unmounting-sigterm',
            'waiting',
        ],
    )

    parser.add_argument(
        '--unit',
        help='Systemd unit name to check (service, timer, mount, etc.). '
        'Required. '
        'Example: `--unit sshd.service`.',
        dest='UNIT',
        required=True,
    )

    parser.add_argument(
        '--unitfilestate',
        help='Expected systemd UnitFileState. '
        'If set to "empty", checks exactly for `UnitFileState=""`. '
        'If omitted or set to "None", the unit\'s unit-file state will not be checked.',
        dest='UNITFILESTATE',
        choices=[
            'None',
            'bad',
            'disabled',
            'empty',
            'enabled',
            'enabled-runtime',
            'generated',
            'indirect',
            'linked',
            'linked-runtime',
            'masked',
            'masked-runtime',
            'static',
            'transient',
        ],
    )

    parser.add_argument(
        '--user',
        help='Talk to the service manager of the calling user rather '
        'than the service manager of the system.',
        dest='USER',
        action='store_true',
        default=False,
    )

    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)

    # set default values for append parameters that were not specified
    if args.ACTIVESTATE is None:
        args.ACTIVESTATE = DEFAULT_ACTIVESTATE
    if args.SUBSTATE is None:
        args.SUBSTATE = DEFAULT_SUBSTATE

    # init some vars
    options = []
    if args.MACHINE:
        stdout, _, _ = lib.base.coe(lib.shell.shell_exec('systemctl --version'))

        # Systemd reports the version number in the second word/field:
        #   systemd 256 (256.12-1.fc41)
        systemd_version = (stdout.split()[1:2] or ['unknown'])[0]
        if lib.version.version(systemd_version) < lib.version.version('209'):
            lib.base.oao(
                (
                    f'Current systemd version "{systemd_version}" does not support '
                    'the `--machine` argument'
                ),
                STATE_UNKNOWN,
            )
        options.append(f'--machine {args.MACHINE}')

    if args.USER:
        options.append('--user')
    options = ' '.join(options)

    command = (
        f'systemctl {options} show '
        f'-p LoadState,ActiveState,SubState,UnitFileState '
        f'{args.UNIT}'
    )

    stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(command))
    if stderr or retc != 0:
        lib.base.oao(
            f'Bash command `{command}` failed.\nStdout: {stdout}\nStderr: {stderr}',
            STATE_UNKNOWN,
        )
    result = stdout.split('\n')

    loadstate = result[0].replace('LoadState=', '')
    activestate = result[1].replace('ActiveState=', '')
    substate = result[2].replace('SubState=', '')
    unitfilestate = result[3].replace('UnitFileState=', '')

    # build the message
    state = STATE_OK
    msg = f'{args.UNIT} is {loadstate}, {activestate}, {substate}'

    if args.LOADSTATE == 'None':
        args.LOADSTATE = None
    if 'None' in args.ACTIVESTATE:
        args.ACTIVESTATE = None
    if 'None' in args.SUBSTATE:
        args.SUBSTATE = None
    if args.UNITFILESTATE == 'None':
        args.UNITFILESTATE = None

    if args.UNITFILESTATE is not None:
        msg += f', UnitFileState is "{unitfilestate}".'

    # now do the checks
    problem = False
    problem_msg = ''
    if args.LOADSTATE is not None and loadstate == 'not-found':
        problem = True
        problem_msg += (
            f'Unit {args.UNIT} not found, so check with '
            f'`systemctl cat {args.UNIT}` if service file is '
            f'missing, and/or try re-installing the '
            f'{args.UNIT} application; '
        )
    else:
        if args.LOADSTATE is not None and loadstate != args.LOADSTATE:
            problem = True
            problem_msg += (
                f'LoadState is "{loadstate}", but supposed to be "{args.LOADSTATE}"; '
            )
        if args.ACTIVESTATE and activestate not in args.ACTIVESTATE:
            # ACTIVESTATE is not None and not empty
            problem = True
            problem_msg += (
                f'ActiveState is "{activestate}", but supposed '
                f'to be "{args.ACTIVESTATE}"; '
            )
        if args.SUBSTATE and substate not in args.SUBSTATE:
            # SUBSTATE is not None and not empty
            problem = True
            problem_msg += (
                f'SubState is "{substate}", but supposed to be "{args.SUBSTATE}"; '
            )
        if unitfilestate == '':
            unitfilestate = 'empty'
        if args.UNITFILESTATE is not None and unitfilestate != args.UNITFILESTATE:
            problem = True
            problem_msg += (
                f'UnitFileState is "{unitfilestate}", but supposed '
                f'to be "{args.UNITFILESTATE}"; '
            )

    if problem:
        msg = f'{args.UNIT} - {problem_msg[:-2]}'
        state = STATE_CRIT if args.SEVERITY == 'crit' else STATE_WARN

    # over and out
    lib.base.oao(msg, state)


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