__main__.py 4.78 KB
Newer Older
1 2 3 4 5 6 7 8 9 10

"""
Picomon executable module.

This module can be executed from a command line with ``$python -m picomon`` or
from a python programme with ``picomon.__main__.run()``.

"""


Jonathan Michalon's avatar
Jonathan Michalon committed
11
import concurrent.futures
12 13
import signal
import argparse
14
import logging
15
import traceback
16 17
import sys
import os
18
from time import sleep
19
from datetime import datetime, timedelta
20 21
from . import config
from . import mails
22 23


24
def __create_report(only_old=False):
25 26 27
    has_error = False
    report = ''
    report += "\n    Checks in error:\n"
28 29
    now = datetime.now()
    delta = timedelta(seconds=config.emails.report.every)
30
    for check in config.checks:
31
        if not check.ok and (not only_old or now - check.failure_date > delta):
32 33
            has_error = True
            report += '-+' * 40 + '\n'
34 35 36 37
            report += "%s: %s\nSince %s\n\t%s\n" % (check.target_name, check,
                      check.failure_date, check.errmsg.strip())
    report += '-+' * 40 + "\n\n"
    report += "    Other checks (usually OK but may be in retry mode):\n"
38
    for check in config.checks:
39
        if check.ok:
40 41 42 43 44
            report += "Check %s is %s\n" % (check,
                      "OK" if check.retry_count == 0 else "retrying")

    return (report, has_error)

45

46 47
def __usr1_handler(signum, frame):
    (report, err) = __create_report()
48 49 50
    print ("Signal SIGUSR1 caught, printing state of checks.")
    print (report)

51

52 53
def __alarm_handler(signum, frame):
    (report, err) = __create_report(only_old=True)
Jonathan Michalon's avatar
Jonathan Michalon committed
54
    if err:
55 56
        report = "Following entries have failed for more than %ss:\n" % \
                 config.emails.report.every + report
Jonathan Michalon's avatar
Jonathan Michalon committed
57
        mails.send_email_report(report)
Jonathan Michalon's avatar
Jonathan Michalon committed
58 59


60
def parse_args():
61 62
    parser = argparse.ArgumentParser()
    parser.add_argument("-1", "--one",
Colomban Wendling's avatar
Colomban Wendling committed
63 64 65
                        help="single run with immediate output of " +
                             "check results (test/debug)",
                        action="store_true")
66 67 68
    parser.add_argument("-D", "--debug",
                        help="Set verbosity to DEBUG",
                        action="store_true")
69 70 71
    parser.add_argument("-c", "--config",
                        help="Set config file (defauts to config.py)",
                        default='config.py')
72 73
    return parser.parse_args()

74

75
def import_config(configfile):
76 77 78 79 80 81 82 83 84
    """ import config file module """
    # narrow importlib usage and avoid bytecode writing to be able to use
    # configfiles in RO directories
    from importlib import import_module
    sys.dont_write_bytecode = True

    sys.path.append(os.path.dirname(configfile))
    filename  = os.path.basename(configfile)
    base, ext = os.path.splitext(filename)
85
    try:
86
        import_module(base)
87 88 89 90 91
    except ImportError as e:
        logging.critical("Cannot load config from '%s': %s" % (
                         args.config, str(e)))
        sys.exit(1)

92 93 94 95 96 97 98 99

def run():
    # Parse command line
    args = parse_args()

    # import config file module
    import_config(args.config)

100 101 102 103 104 105
    # Configure logging
    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
                        level=config.verb_level)
    if args.debug:
        logging.getLogger().setLevel('DEBUG')

106
    # register signal handling
107 108
    signal.signal(signal.SIGUSR1, __usr1_handler)
    signal.signal(signal.SIGALRM, __alarm_handler)
109 110 111 112 113 114

    # register report signal interval
    if config.emails.report.every > 0:
        signal.setitimer(signal.ITIMER_REAL, config.emails.report.every,
                                             config.emails.report.every)

115
    # do the actual polling
Jonathan Michalon's avatar
Jonathan Michalon committed
116
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
117
        if args.one:
118 119 120
            def runner(check):
                return check.run(immediate=True), check

121
            futures = []
122
            for check in config.checks:
123 124 125 126 127 128 129 130
                futures.append(executor.submit(runner, check))

            for future in concurrent.futures.as_completed(futures):
                success, check = future.result()
                if success:
                    print("Check %s successful!" % (str(check)))
                else:
                    print("Check %s failed:\n%s" %
131
                          (str(check), check.errmsg.strip()))
132
        else:
133 134 135 136 137 138 139 140 141
            # Since we never reclaim finished tasks, exceptions raised during
            # run are never seen. Using a runner we can at least display them.
            def runner(check):
                try:
                    return check.run()
                except Exception as e:
                    traceback.print_exc()
                    raise e

142 143
            # This will drift slowly as it takes (base_tick + espilon) seconds
            while True:
144
                for check in config.checks:
145
                    executor.submit(runner, check)
146
                sleep(config.base_tick)
147
    mails.quit()
148 149 150 151


if __name__ == '__main__':
    run()