__main__.py 4.83 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
    print ("Signal SIGUSR1 caught, printing state of checks. (%s)" %
           datetime.now())
50
    print (report)
51
    sys.stdout.flush()
52

53

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


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

76

77
def import_config(configfile):
78 79 80 81 82 83 84 85 86
    """ 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)
87
    try:
88
        import_module(base)
89 90
    except ImportError as e:
        logging.critical("Cannot load config from '%s': %s" % (
91
                         configfile, str(e)))
92 93
        sys.exit(1)

94 95 96 97 98 99 100 101

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

    # import config file module
    import_config(args.config)

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

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

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

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

123
            futures = []
124
            for check in config.checks:
125 126 127 128 129 130 131 132
                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" %
133
                          (str(check), check.errmsg.strip()))
134
        else:
135 136 137 138 139 140 141 142 143
            # 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

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


if __name__ == '__main__':
    run()