check_systemd_timer: parse cli output instead, more perfdta

This commit is contained in:
Cyberes 2024-03-07 14:09:42 -07:00
parent d32092c62f
commit efc3c010f9
3 changed files with 90 additions and 21 deletions

View File

@ -2,18 +2,61 @@
import argparse import argparse
import sys import sys
import time
import traceback import traceback
from datetime import datetime, timedelta
import humanize from dateutil import tz
from checker import nagios from checker import nagios
from checker.humanfriendly import parse_systemctl_time_delta
from checker.result import quit_check from checker.result import quit_check
sys.path.insert(0, "/usr/lib/python3/dist-packages") sys.path.insert(0, "/usr/lib/python3/dist-packages")
import dbus 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): def check_timer(timer_name):
if not timer_name.endswith('.timer'): 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') timer_properties = dbus.Interface(timer_unit, 'org.freedesktop.DBus.Properties')
active_state = timer_properties.Get('org.freedesktop.systemd1.Unit', 'ActiveState') active_state = timer_properties.Get('org.freedesktop.systemd1.Unit', 'ActiveState')
if active_state == 'active': 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: print(next_elapse)
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)
current_time = time.time() * 1e6 # convert current time to microseconds if next_elapse['left'] < 0 or next_elapse['passed'] < 0:
remaining_time_sec = int((next_elapse - current_time) / 1e6) # convert remaining time to seconds quit_check(f'Timer is negative? Left: {next_elapse["left"]}. Passed: {next_elapse["passed"]}', nagios.STATE_UNKNOWN)
remaining_time_human = str(humanize.naturaltime(datetime.now() + timedelta(seconds=remaining_time_sec))).strip(' from now')
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 = { perfdata_dict = {
'remaining_time': { 'remaining_time': {
'value': remaining_time_sec, 'value': int(next_elapse['left']),
'unit': 's',
'min': 0
},
'passed_time': {
'value': int(next_elapse['passed']),
'unit': 's', 'unit': 's',
'min': 0 '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: else:
quit_check(f'{timer_name} is not active.', nagios.STATE_CRIT) quit_check(f'{timer_name} is not active.', nagios.STATE_CRIT)
except dbus.exceptions.DBusException: except dbus.exceptions.DBusException:

24
checker/humanfriendly.py Normal file
View File

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

View File

@ -10,4 +10,5 @@ cf_speedtest==0.1.7
zfslib==0.11.0 zfslib==0.11.0
hurry.filesize==0.9 hurry.filesize==0.9
dateparser==1.2.0 dateparser==1.2.0
humanize==4.9.0 humanize==4.9.0
humanfriendly==10.0