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

import lib.args
import lib.base
import lib.lftest
import lib.shell
import lib.version
from lib.globals import STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks the installed Redis version against the endoflife.date API and alerts if the
version is end-of-life or if newer major, minor, or patch releases are available. By
default, alerts 30 days before the official EOL date. The offset is configurable."""

DEFAULT_CHECK_MAJOR = False
DEFAULT_CHECK_MINOR = False
DEFAULT_CHECK_PATCH = False
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_OFFSET_EOL = -30  # days
DEFAULT_PATH = 'redis-server'
DEFAULT_TIMEOUT = 8
DEFAULT_UNREACHABLE_SEVERITY = 'ok'


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(
        '--check-major',
        help=lib.args.help('--check-major'),
        dest='CHECK_MAJOR',
        action='store_true',
        default=DEFAULT_CHECK_MAJOR,
    )

    parser.add_argument(
        '--check-minor',
        help=lib.args.help('--check-minor'),
        dest='CHECK_MINOR',
        action='store_true',
        default=DEFAULT_CHECK_MINOR,
    )

    parser.add_argument(
        '--check-patch',
        help=lib.args.help('--check-patch'),
        dest='CHECK_PATCH',
        action='store_true',
        default=DEFAULT_CHECK_PATCH,
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

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

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--offset-eol',
        help=lib.args.help('--offset-eol') + ' Default: %(default)s days',
        dest='OFFSET_EOL',
        type=int,
        default=DEFAULT_OFFSET_EOL,
    )

    parser.add_argument(
        '--path',
        help='Path to the Redis binary. '
        'Resolved via $PATH when given without a directory. '
        'Default: %(default)s',
        dest='PATH',
        default=DEFAULT_PATH,
    )

    parser.add_argument(
        '--proxy',
        help=lib.args.help('--proxy'),
        dest='PROXY',
        default=None,
    )

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

    parser.add_argument(
        '--unreachable-severity',
        help=lib.args.help('--unreachable-severity') + ' Default: %(default)s',
        dest='UNREACHABLE_SEVERITY',
        choices=['ok', 'warn', 'crit', 'unknown'],
        default=DEFAULT_UNREACHABLE_SEVERITY,
    )

    args, _ = parser.parse_known_args()
    return args


def get_installed_version(
    path, test_arg=None, timeout=DEFAULT_TIMEOUT, always_ok=False
):
    """Return the product ('Redis' or 'Valkey') and the version that the binary at
    `path` reports, or ('', '') if its output is neither.
    """
    if test_arg is None:
        cmd = [path, '--version']
        success, result = lib.shell.shell_exec(cmd, timeout=timeout)
        if not success:
            if result.startswith('Timeout after'):
                lib.base.oao(
                    f'Timeout after {timeout}s while running `{" ".join(cmd)}`.',
                    STATE_WARN,
                    always_ok=always_ok,
                )
            return '', ''
        stdout = result[0].strip()
    else:
        stdout, _, _ = lib.lftest.test(test_arg)
        stdout = stdout.strip()
    # where to find the version number in output?
    # `redis-server --version` prints the `REDIS_VERSION` define, `valkey-server
    # --version` the `VALKEY_VERSION` define, both always three numeric fields
    # (release candidates are encoded as e.g. "7.9.240", unstable as "255.255.255"):
    # * Redis >= 2.6: "Redis server v=7.2.4 sha=00000000:0 malloc=jemalloc-5.3.0
    #   bits=64 build=..." (src/server.c getVersion(), formerly src/redis.c version())
    # * Redis 2.2 - 2.4: "Redis server version 2.4.18 (00000000:0)"
    # * Redis 2.0: "Redis server version 2.0.0"
    # * Valkey >= 8.0: "Valkey server v=8.0.1 sha=00000000:0 malloc=jemalloc-5.3.0
    #   bits=64 build=..." (src/server.c getVersion())
    # * Valkey 7.2: "Server v=7.2.5 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64
    #   build=..."
    # Both products are told apart because Valkey installs `redis-server` as a symlink
    # to `valkey-server` (its own `make install` with USE_REDIS_SYMLINKS, Fedora's
    # valkey-compat-redis package).
    # Verified against redis 2.6.17, 2.8.23, 3.0.7, 5.0 - 8.6
    # (docker.io/library/redis), valkey 7.2.12, 8.0.7, 8.1.6, 9.0.3
    # (docker.io/valkey/valkey) and valkey-compat-redis 9.0.6 on Fedora 44.
    version_regexes = [
        ('Redis', r'^Redis server v(?:=|ersion )(\d+\.\d+\.\d+)(?: |$)'),
        ('Valkey', r'^(?:Valkey server|Server) v=(\d+\.\d+\.\d+) '),
    ]
    for product, version_regex in version_regexes:
        match = re.search(version_regex, stdout, re.MULTILINE)
        if match:
            return product, match.group(1)
    return '', ''


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)

    # fetch data
    product, installed_version = get_installed_version(
        args.PATH, args.TEST, timeout=args.TIMEOUT, always_ok=args.ALWAYS_OK
    )
    if product == 'Valkey':
        lib.base.cu(
            f'`{args.PATH}` is Valkey v{installed_version}, use the '
            'valkey-version check.'
        )
    if not installed_version:
        lib.base.cu(
            f'Redis not found at `{args.PATH}`. Use --path to point at the binary.'
        )

    # init some vars
    perfdata = ''
    # lib.version reads a suffix as version parts (4.5rc1 -> 4.51), use the core
    installed_core = re.match(r'\d+(?:\.\d+)*', installed_version).group()

    # analyze data
    state, eol = lib.version.check_eol(
        'https://endoflife.date/api/redis.json',
        installed_core,
        offset_eol=args.OFFSET_EOL,
        check_major=args.CHECK_MAJOR,
        check_minor=args.CHECK_MINOR,
        check_patch=args.CHECK_PATCH,
        insecure=args.INSECURE,
        no_proxy=args.NO_PROXY,
        proxy=args.PROXY,
        timeout=args.TIMEOUT,
        unreachable_severity=args.UNREACHABLE_SEVERITY,
        # where the distribution maintains the software, its life cycle counts
        package_path=args.PATH if args.TEST is None else None,
    )

    # build the message
    msg = f'Redis v{installed_version} ({eol})'

    perfdata += lib.base.get_perfdata(
        'redis-version',
        lib.version.version2float(installed_core),
        uom=None,
        _min=0,
    )

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