From 76d83a526628daf54cb88eb672c3096e4421d25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pavl=C3=AD=C4=8Dek?= <469355@mail.muni.cz> Date: Mon, 28 Aug 2023 09:49:45 +0200 Subject: [PATCH] feat: check_pgsql --- README.md | 16 ++ perun/proxy/utils/nagios/check_pgsql.py | 214 ++++++++++++++++++++++++ setup.py | 6 +- 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 perun/proxy/utils/nagios/check_pgsql.py diff --git a/README.md b/README.md index de59d49..43058c4 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,19 @@ python3 check_user_logins.py ```sh python3 check_privacyidea.py --help ``` + +### check_pgsql.py + +- check connection to PostgreSQL +- possible check with configurable query +- to use this check you must include the postgresql extra, which will install [psycopg2-binary](https://pypi.org/project/psycopg2-binary/): + + ```sh + pip install perun.proxy.utils[postgresql] + ``` + +- for usage run: + + ```sh + python3 check_pgpsql.py --help + ``` diff --git a/perun/proxy/utils/nagios/check_pgsql.py b/perun/proxy/utils/nagios/check_pgsql.py new file mode 100644 index 0000000..138221c --- /dev/null +++ b/perun/proxy/utils/nagios/check_pgsql.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +import argparse +import re +import sys +import time + +import psycopg2 + +# nagios return codes +OK = 0 +WARNING = 1 +CRITICAL = 2 +UNKNOWN = 3 + + +def get_args(): + parser = argparse.ArgumentParser(description="PostgreSQL connection check") + parser.add_argument( + "-H", + "--host", + type=str, + help="Host name or IP Address (default: 127.0.0.1)", + default="127.0.0.1", + ) + parser.add_argument( + "-P", + "--port", + type=str, + help="Port number (default: 5432)", + default="5432", + ) + parser.add_argument( + "-c", + "--critical", + type=int, + help="Response time to result in critical status (default: 8s)", + default=8, + ) + parser.add_argument( + "-w", + "--warning", + type=int, + help="Response time to result in warning status (default: 2s)", + default=2, + ) + parser.add_argument( + "-t", + "--timeout", + type=int, + help="Seconds before connection times out (default: 10s)", + default=10, + ) + parser.add_argument( + "-d", + "--database", + type=str, + help="Database to check (default: template1)", + default="template1", + ) + parser.add_argument( + "-l", + "--logname", + type=str, + help="Login name of user", + ) + parser.add_argument( + "-p", + "--password", + type=str, + help="Password (BIG SECURITY ISSUE)", + ) + parser.add_argument( + "-q", + "--query", + type=str, + help="SQL query to run. Only first column in first row will be read", + ) + parser.add_argument( + "-o", + "--option", + type=str, + help="Connection parameters (keyword = value)", + ) + parser.add_argument( + "-W", + "--query-warning", + type=threshold_type, + dest="query_warning", + help="SQL query value to result in warning status (float). " + "Single value or range, e.g. '20:50.", + ) + parser.add_argument( + "-C", + "--query-critical", + type=threshold_type, + dest="query_critical", + help="SQL query value to result in critical status (float). " + "Single value or range, e.g. '20:50", + ) + + return parser.parse_args() + + +def threshold_type(arg_value): + threshold_regex = re.compile(r"^\d+(\.\d+)?(:\d+(\.\d+)?)?$") + if not threshold_regex.match(arg_value): + raise argparse.ArgumentTypeError("Invalid threshold format.") + return arg_value + + +def val_in_threshold(threshold_str, value): + splitted = threshold_str.split(":") + if len(splitted) == 1: + return 0 < value < float(splitted[0]) + else: + return float(splitted[0]) < value < float(splitted[1]) + + +def do_query(connection, args): + try: + with connection.cursor() as cursor: + cursor.execute(args.query) + if len(cursor.description) < 1: + print("QUERY WARNING - No columns returned.") + return WARNING + rows = cursor.fetchone() + if len(rows) < 1: + print("QUERY WARNING - No rows returned.") + return WARNING + try: + numeric = float(rows[0]) + if args.query_warning and not val_in_threshold( + args.query_warning, numeric + ): + print("QUERY WARNING - ", end="") + state = WARNING + elif args.query_critical and not val_in_threshold( + args.query_critical, numeric + ): + print("QUERY CRITICAL - ", end="") + state = CRITICAL + else: + print("QUERY OK - ", end="") + state = OK + + print( + f"'{args.query}' returned {numeric}" + f"|{args.query_warning if args.query_warning else ''};" + f"{args.query_critical if args.query_critical else ''};;" + ) + + if len(cursor.description) > 1: + extra_info = rows[1] + if extra_info: + print(f"Extra info: {extra_info}") + return state + + except ValueError: + print(f"QUERY CRITICAL - Is not a numeric: {rows[0]}") + return CRITICAL + + except psycopg2.Error as e: + print(f"QUERY CRITICAL - Error with Query: {e}") + return CRITICAL + + +def main(): + args = get_args() + + query_status = UNKNOWN + connection_args = { + "database": args.database, + "host": args.host, + "port": args.port, + "connect_timeout": args.timeout, + } + if args.logname: + connection_args["user"] = args.logname + if args.password: + connection_args["password"] = args.password + if args.option: + connection_args["options"] = args.option + + try: + con_start_time = time.time() + with psycopg2.connect(**connection_args) as conn: + con_end_time = time.time() + + elapsed_time = con_end_time - con_start_time + if elapsed_time > args.critical: + status = CRITICAL + elif elapsed_time > args.warning: + status = WARNING + else: + status = OK + + print( + f"{status} check_pgsql - database {args.database} ({elapsed_time:.2f} " + f"sec.)|{args.warning};{args.critical};;" + ) + + if args.query: + query_status = do_query(conn, args) + + return query_status if args.query and query_status > status else status + + except psycopg2.Error as e: + print(f"{CRITICAL} check_pgsql - connection to {args.database} failed ({e}).") + return CRITICAL + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py index 76f96ca..d8716af 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,10 @@ setuptools.setup( extras_require={ "ldap": [ "ldap3~=2.9.1", - ] + ], + "postgresql": [ + "psycopg2-binary~=2.9", + ], }, entry_points={ "console_scripts": [ @@ -42,6 +45,7 @@ setuptools.setup( "check_webserver_availability=" "perun.proxy.utils.nagios.webserver_availability:main", "check_privacyidea=perun.proxy.utils.nagios.check_privacyidea:main", + "check_pgsql=perun.proxy.utils.nagios.check_pgsql:main" "metadata_expiration=perun.proxy.utils.metadata_expiration:main", "print_docker_versions=perun.proxy.utils.print_docker_versions:main", "run_version_script=perun.proxy.utils.run_version_script:main", -- GitLab