From efc3c010f9acb196c4569669c58292d2f1098b01 Mon Sep 17 00:00:00 2001 From: Cyberes Date: Thu, 7 Mar 2024 14:09:42 -0700 Subject: [PATCH] check_systemd_timer: parse cli output instead, more perfdta --- check_systemd_timer.py | 84 +++++++++++++++++++++++++++++--------- checker/humanfriendly.py | 24 +++++++++++ requirements-everybody.txt | 3 +- 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 checker/humanfriendly.py diff --git a/check_systemd_timer.py b/check_systemd_timer.py index d15779d..287a709 100755 --- a/check_systemd_timer.py +++ b/check_systemd_timer.py @@ -2,18 +2,61 @@ import argparse import sys -import time import traceback -from datetime import datetime, timedelta -import humanize +from dateutil import tz from checker import nagios +from checker.humanfriendly import parse_systemctl_time_delta from checker.result import quit_check sys.path.insert(0, "/usr/lib/python3/dist-packages") import dbus +import re +import subprocess +from datetime import datetime + +import humanfriendly + +SYSTEMCTL_TIMERS_RE = re.compile( + r'^([A-Za-z]*\s[0-9]{4}-[0-9]{2}-[0-9]{2}\s*[0-9]{2}:[0-9]{2}:[0-9]{2}\s[A-Z]*)\s*(([0-9]*[a-z]*\s)*left)\s*([A-Za-z]*\s[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[A-Z]*)\s*([0-9A-Za-z\s]*\sago)\s*([A-Za-z\-_]*.timer)\s*([A-Za-z\-_]*.service)') + + +def get_next_elapse(timer_name): + try: + output = subprocess.check_output(["systemctl", "list-timers", "--all"], universal_newlines=True) + lines = output.split('\n') + for line in lines: + if timer_name in line: + parts = re.search(SYSTEMCTL_TIMERS_RE, line) + + try: + datetime_object = datetime.strptime(parts.group(1), '%a %Y-%m-%d %H:%M:%S %Z') + except ValueError as e: + return None, e + + time_left = parse_systemctl_time_delta(parts.group(2)) + if isinstance(time_left, humanfriendly.InvalidTimespan): + return None, humanfriendly.InvalidTimespan + + time_passed = parse_systemctl_time_delta(parts.group(5)) + if isinstance(time_passed, humanfriendly.InvalidTimespan): + return None, humanfriendly.InvalidTimespan + + timer_info = { + 'next': datetime_object, + 'left': time_left, + 'last': parts.group(4), + 'passed': time_passed, + 'unit': parts.group(6), + 'activates': parts.group(7) + } + return timer_info, None + return None, ValueError('Timer not found') + except subprocess.CalledProcessError as e: + return None, e + def check_timer(timer_name): if not timer_name.endswith('.timer'): @@ -28,31 +71,32 @@ def check_timer(timer_name): timer_properties = dbus.Interface(timer_unit, 'org.freedesktop.DBus.Properties') active_state = timer_properties.Get('org.freedesktop.systemd1.Unit', 'ActiveState') if active_state == 'active': - next_elapse = timer_properties.Get('org.freedesktop.systemd1.Timer', 'NextElapseUSecRealtime') + next_elapse, err = get_next_elapse(timer_name) + if err: + quit_check(f'{err}', nagios.STATE_UNKNOWN) - try: - local_timezone_offset_seconds = time.localtime().tm_gmtoff - local_timezone_offset = timedelta(seconds=local_timezone_offset_seconds) - local_datetime = datetime.utcfromtimestamp(int(next_elapse) / 1e6) + local_timezone_offset - next_elapse_str = local_datetime.strftime("%m-%d-%Y %H:%M") - except ValueError as v_err: - if 'is out of range' in repr(v_err): - # This occurs whenever the timer resets. - # "year 586524 is out of range" - print(f'OK - {timer_name} has triggered.') - sys.exit(nagios.STATE_OK) + print(next_elapse) - current_time = time.time() * 1e6 # convert current time to microseconds - remaining_time_sec = int((next_elapse - current_time) / 1e6) # convert remaining time to seconds - remaining_time_human = str(humanize.naturaltime(datetime.now() + timedelta(seconds=remaining_time_sec))).strip(' from now') + if next_elapse['left'] < 0 or next_elapse['passed'] < 0: + quit_check(f'Timer is negative? Left: {next_elapse["left"]}. Passed: {next_elapse["passed"]}', nagios.STATE_UNKNOWN) + + local_tz = tz.tzlocal() + next_elapse_str = next_elapse['next'].replace(tzinfo=local_tz).strftime('%a %Y-%m-%d %H:%M %Z') + remaining_time_human = humanfriendly.format_timespan(next_elapse['left']) + passed_time_human = humanfriendly.format_timespan(next_elapse['passed']) perfdata_dict = { 'remaining_time': { - 'value': remaining_time_sec, + 'value': int(next_elapse['left']), + 'unit': 's', + 'min': 0 + }, + 'passed_time': { + 'value': int(next_elapse['passed']), 'unit': 's', 'min': 0 } } - quit_check(f'{timer_name} is active. Trigger time: {next_elapse_str}. Remaining time: {remaining_time_human}.', nagios.STATE_OK, perfdata_dict) + quit_check(f'{timer_name} is active. Trigger time: {next_elapse_str}. Remaining time: {remaining_time_human}. Time since last trigger: {passed_time_human}.', nagios.STATE_OK, perfdata_dict) else: quit_check(f'{timer_name} is not active.', nagios.STATE_CRIT) except dbus.exceptions.DBusException: diff --git a/checker/humanfriendly.py b/checker/humanfriendly.py new file mode 100644 index 0000000..e6a541f --- /dev/null +++ b/checker/humanfriendly.py @@ -0,0 +1,24 @@ +import re + +import humanfriendly + +SYSTEMCTL_TIMESPAN_RE = re.compile(r'([0-9]+((\s[a-z]*)|([a-z]+)))') + + +def parse_systemctl_time_delta(time_str: str): + """ + https://humanfriendly.readthedocs.io/en/latest/api.html?highlight=parse_timespan#humanfriendly.parse_timespan + """ + time_str = time_str.replace(' left', '').replace(' ago', '') + spans = [] + for part in re.findall(SYSTEMCTL_TIMESPAN_RE, time_str): + delta = part[0] + if 'months' in delta: + # humanfriendly does not support "months" so we convert it to weeks + num = int(delta.split(' ')[0]) + delta = f'{num * 4} weeks' + try: + spans.append(humanfriendly.parse_timespan(delta)) + except humanfriendly.InvalidTimespan as e: + return e + return sum(spans) diff --git a/requirements-everybody.txt b/requirements-everybody.txt index edb84ef..4c8aca3 100644 --- a/requirements-everybody.txt +++ b/requirements-everybody.txt @@ -10,4 +10,5 @@ cf_speedtest==0.1.7 zfslib==0.11.0 hurry.filesize==0.9 dateparser==1.2.0 -humanize==4.9.0 \ No newline at end of file +humanize==4.9.0 +humanfriendly==10.0 \ No newline at end of file