dachte icon

compumancer

dachte | PRO | 02/11/21 06:56:26 PM UTC | 0 ⭐ | 985 👁️ | Never ⏰ | []
Python |

6.96 KB

|

None

|

0 👍

/

0 👎

#!/usr/bin/env python
 
import argparse
import code # for code.interact(local=dict(globals, **locals()) ) debugging
import datetime
import json
import os
import requests
import psutil
import sys
import time
from typing import Dict, List, Tuple
 
######################
# compumancer
#
# A very small tool meant to be run under cron, so it will email somebody
# if something is up. This will return a nonzero response code if any defined
# tests are not entirely okay. Meant to be run with chronic or some other wrapper.
#
# This is meant to be useful if you need cron-based monitoring because you're working somewhere where
# nagios is impractical. This is not meant as a replacement for good monitoring, just a stopgap.
#
# This uses conda, because Ubuntu's python doesn't come with psutil.
 
def main():
    cfg, jconf = handle_args()
    anger = False
    all_msgs = []
 
    fail, msgs = trial_of_space(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
 
    fail, msgs = trial_of_attention(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
 
    fail, msgs = trial_of_whispers(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
 
    fail, msgs = trial_of_calligraphy(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
 
    fail, msgs = trial_of_the_burro(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
 
    fail, msgs = trial_of_loneliness(jconf, cfg.fatal_only)
    if fail:
        anger = True
        all_msgs += msgs
    if anger:
        print("Compumancer is angry. Reasons:")
        print("\n".join(map(lambda x: "\t" + x, all_msgs)))
        sys.exit(1)
    else:
        print("Compumancer is happy")
 
def handle_args():
    # Unlike my normal handle_args(), this loads a json configuration from a file.
    parser = argparse.ArgumentParser(description="Mini monitoring")
    parser.add_argument("--fatal_only", action="store_true", help="Be noisier")
    ret = parser.parse_args()
    config_fn = 'compumancer.cfg'
    if not os.path.exists(config_fn):
        raise Exception('JSON Config file {config_fn} does not exist!')
    with open(config_fn, 'r') as jconf:
        jret = json.load(jconf)
    return ret, jret
 
###########
# trials
#
# Trials should take a dict and a boolean (even if they ignore either/both).
# Trials should not take extra optional parameters, and must not require additional parameters.
# They should not refer to globals for machine-specific configuration
# Trials must return a tuple of a boolean (whether the trial failed, where grounds for failure *may* be loosened by the parameter)
#   and a list of strings providing useful diagnostic information.
# Trials *should not* write to stdout.
# Trials *must* leave their list return value empty if their boolean is False.
# Trials *should* be written with a sense of humour in their variable names.
 
def trial_of_space(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # Is there enough free space on filesystems?
    # Threshold for warnings is 80%
    # Threshold for errors is 90%
    # psutil.disk_usage('path') returns (total,used,free,percentage)
    anger = False
    if fatal_only:
        thresh = 90
    else:
        thresh = 80
 
    ret = []
    for fs in jconf['filesystems']:
        if not os.path.exists(fs):
            anger = True
            ret.append(f"Filesystem {fs} does not exist")
            continue
        total, used, free, percent = psutil.disk_usage(fs)
        if percent > thresh:
            anger = True
            ret.append(f"Filesystem {fs} is {percent} full, over {thresh} threshold")
    return anger, ret
 
def trial_of_attention(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # Is the system load acceptable?
    # Threshold for warnings is 10
    # Threshold for errors is 30
    # use os.getloadavg()
    anger = False
    ret = []
    if fatal_only:
        thresh = 30
    else:
        thresh = 10
 
    loadavg = os.getloadavg()[1] # returns 3 figures. Picking the five-minute average just because
    if loadavg > thresh:
        anger = True
        ret.append(f"Load average {loadavg} is over threshold {thresh}")
    return anger, ret
 
def trial_of_whispers(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # Visit certain URLs. Meant to verify that
    # some web service is running (and, if SSL, that its certs work)
    # use the requests library
    anger = False
    ret = []
    for url in jconf['urls']:
        req = requests.get(url)
        status = req.status_code
        if status != 200:
            anger = True
            r.append(f'Got a {status} when visiting {url}')
    return anger, ret
 
def trial_of_calligraphy(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # See if there's any mention of the oom killer in dmesg. Try to just look at recent events
    # so rebooting isn't needed to recover. Note that we're a little bit lazy and only read one file
    # to look for this.
    anger = False
    ret = []
    if not os.path.isfile(jconf['logfile']):
        anger = True
        ret.append(f"Could not open logfile {jconf['logfile']}")
        return anger, ret
    with open(jconf['logfile']) as syslog:
        for sline in syslog:
            if 'killed process' in sline:
                mon,day,time = sline.split(' ', 3)
                # Next we make a good-faith effort to get the time the oom killer ran.
                # Note that because the file we're consulting lacks a year string, we assume that the logs are from the
                # current year, and to compensate for when it is not we filter out times in the future. This will produce
                # weirdness around the turning of the year, but hopefully survivable weirdness. There's nothing better that
                # we can do that immediately comes to mind.
                logzeit = int(time.mktime(time.strptime(f'{datetime.now().year} {mon} {day} {time}', '%Y %b %d %H:%M:%S')))
                if logzeit > time.time():
                    continue # We could try to parse these by decrementing the year and giving it another go
                if logzeit - int(time.time()) > 60*60*3: # OOM killer in the last 3 hours
                    continue
                anger = True
                ret.append(f'OOM killer at {time}')
    return anger, ret
 
def trial_of_the_burro(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # See if the system has an acceptable amount of free RAM.
    # Use psutil.virtual_memory() for this.
    anger = False
    ret = []
    if fatal_only:
        thresh = 90
    else:
        thresh = 70
    mem_used_percent = psutil.virtual_memory().percent
    if mem_used_percent > thresh:
        anger = True
        ret.append(f'Memory usage at {mem_used_percent}, over threshold of {thresh}')
    return anger, ret
 
def trial_of_loneliness(jconf:Dict, fatal_only:bool=False) -> Tuple[bool, List[str]]:
    # See if processes necessary for this box are running.
    # use psutil.process_iter() for this (in a loop)
    anger = False
    ret = []
    seen = {}
    for proc in psutil.process_iter(): # This iter, we just note all the process names active on the system
        try:
            info = proc.as_dict(attrs=['pid', 'name'])
        except psutil.NoSuchProcess: # handle race conditions
            pass
        else:
            seen[info['name']] = 1
    for sought in jconf['processes']:
        if sought not in seen:
            anger = True
            ret.append(f"Could not find process {sought}")
    return anger, ret
 
 
#####
main()

Comments