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

import lib.args
import lib.base
import lib.disk
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 PostgreSQL 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 = 'psql'
DEFAULT_TIMEOUT = 8
DEFAULT_UNREACHABLE_SEVERITY = 'ok'
DEFAULT_USERNAME = 'postgres'


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 psql 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,
    )
    parser.add_argument(
        '--username',
        help='PostgreSQL username for running `psql`. Default: %(default)s',
        dest='USERNAME',
        default=DEFAULT_USERNAME,
    )

    args, _ = parser.parse_known_args()
    return args


def find_psql(path):
    """Return the `psql` to run.

    The check is meant to run as the database account through sudo, so `--path` names a
    program that runs with that account's rights. Only a binary that nobody but root can
    change is run, and the resolved path is what runs, so a symlink swapped after the
    check changes nothing.
    """
    candidate = path if os.sep in path else lib.shell.which(path)
    if not candidate or not os.path.isfile(candidate):
        return (
            False,
            f'PostgreSQL not found at `{path}`. Use --path to point at the binary.',
        )
    return lib.disk.resolve_trusted_path(candidate)


def get_installed_version(args, psql):
    """Ask the running server for its version.

    Returns `(True, version)`, or `(False, message)` if `psql` cannot reach or log in to
    the server, or answers without a version.
    """
    if args.TEST is None:
        cmd = [
            psql,
            f'--username={args.USERNAME}',
            '--command=SELECT version();',
        ]
        success, result = lib.shell.shell_exec(cmd, timeout=args.TIMEOUT)
        if not success:
            if result.startswith('Timeout after'):
                lib.base.oao(
                    f'Timeout after {args.TIMEOUT}s while running `{" ".join(cmd)}`.',
                    STATE_WARN,
                    always_ok=args.ALWAYS_OK,
                )
            return (False, result)
        stdout, stderr, retc = result
    else:
        stdout, stderr, retc = lib.lftest.test(args.TEST)
    if retc:
        # psql is there, but the server is down or refuses the login (for example peer
        # authentication for an account other than the database user)
        return (
            False,
            f'`{args.PATH}` could not ask PostgreSQL for its version: {stderr.strip()}',
        )
    # where to find the version number in output?
    match = re.search(r'L (\d+\.\d+)', stdout)
    if not match:
        return (
            False,
            f'PostgreSQL not found at `{args.PATH}`. Use --path to point at the binary.',
        )
    return (True, match.group(1).strip())


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
    psql = lib.base.coe(find_psql(args.PATH)) if args.TEST is None else None
    installed_version = lib.base.coe(get_installed_version(args, psql))

    # init some vars
    perfdata = ''

    # analyze data
    state, eol = lib.version.check_eol(
        'https://endoflife.date/api/postgresql.json',
        installed_version,
        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=psql,
    )

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

    perfdata += lib.base.get_perfdata(
        'postgresql-version',
        lib.version.version2float(installed_version),
        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()
