├── README.markdown ├── scripts └── cronwrap └── setup.py /README.markdown: -------------------------------------------------------------------------------- 1 | cronwrap 2 | =========================================== 3 | 4 | A cron job wrapper that wraps jobs and enables better error reporting and command timeouts. 5 | 6 | 7 | Installing 8 | =========== 9 | 10 | To install cronwrap simply do:: 11 | 12 | $ sudo easy_install cronwrap 13 | $ cronwrap -h 14 | 15 | 16 | Example 17 | =========== 18 | 19 | Basic example of usage:: 20 | 21 | ##Will print out help 22 | $ cronwrap -h 23 | 24 | usage: cronwrap [-h] [-c CMD] [-e EMAILS] [-t TIME] [-v [VERBOSE]] 25 | 26 | A cron job wrapper that wraps jobs and enables better error reporting and command timeouts. 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | -c CMD, --cmd CMD Run a command. Could be `cronwrap -c "ls -la"`. 31 | -e EMAILS, --emails EMAILS 32 | Email following users if the command crashes or 33 | exceeds timeout. Could be `cronwrap -e 34 | "johndoe@mail.com, marcy@mail.com"`. Uses system's 35 | `mail` to send emails. If no command (cmd) is set a 36 | test email is sent. 37 | -t TIME, --time TIME Set the maxium running time. If this time is passed an 38 | alert email will be sent once the command ends. 39 | The command will keep running even if maximum running time 40 | is exceeded. Please note that this option doesn't 41 | prevent the job from stalling and does nothing to 42 | prevent running multiple cron jobs at the same time. 43 | The default is 1 hour `-t 1h`. Possible values include: 44 | `-t 2h`,`-t 2m`, `-t 30s`. 45 | -v [VERBOSE], --verbose [VERBOSE] 46 | Will send an email / print to stdout on successful run. 47 | 48 | 49 | ##Will send out a timeout alert to cron@my_domain.com: 50 | $ cronwrap -c "sleep 2" -t "1s" -e cron@my_domain.com 51 | 52 | ##Will send out an error alert to cron@my_domain.com: 53 | $ cronwrap -c "blah" -e cron@my_domain.com 54 | 55 | #Will not send any reports: 56 | $ cronwrap -c "ls" -e cron@my_domain.com 57 | 58 | #Will send a successful report to cron@my_domain.com: 59 | $ cronwrap -c "ls" -e cron@my_domain.com -v 60 | -------------------------------------------------------------------------------- /scripts/cronwrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | cronwrap 5 | ~~~~~~~~~~~~~~ 6 | 7 | A cron job wrapper that wraps jobs and enables better error reporting and command timeouts. 8 | 9 | Example of usage:: 10 | 11 | #Will print out help 12 | $ cronwrap -h 13 | 14 | #Will send out a timeout alert to cron@my_domain.com: 15 | $ cronwrap -c "sleep 2" -t "1s" -e cron@my_domain.com 16 | 17 | #Will send out an error alert to cron@my_domain.com: 18 | $ cronwrap -c "blah" -t "1s" -e cron@my_domain.com 19 | 20 | #Will not send any reports: 21 | $ cronwrap -c "ls" -e cron@my_domain.com 22 | 23 | #Will send a successful report to cron@my_domain.com: 24 | $ cronwrap -c "ls" -e cron@my_domain.com -v 25 | 26 | :copyright: 2010 by Plurk 27 | :license: BSD 28 | """ 29 | __VERSION__ = '1.3' 30 | 31 | import sys 32 | import re 33 | import os 34 | import argparse 35 | import tempfile 36 | import time 37 | import platform 38 | from datetime import datetime, timedelta 39 | 40 | 41 | #--- Handlers ---------------------------------------------- 42 | def handle_args(sys_args): 43 | """Handles comamnds that are parsed via argparse.""" 44 | if not sys_args.time: 45 | sys_args.time = '1h' 46 | 47 | if sys_args.verbose is not False: 48 | sys_args.verbose = True 49 | 50 | if sys_args.cmd: 51 | cmd = Command(sys_args.cmd) 52 | if cmd.returncode != 0: 53 | handle_error(sys_args, cmd) 54 | elif is_time_exceeded(sys_args, cmd): 55 | handle_timeout(sys_args, cmd) 56 | else: 57 | handle_success(sys_args, cmd) 58 | elif sys_args.emails: 59 | handle_test_email(sys_args) 60 | 61 | 62 | def handle_success(sys_args, cmd): 63 | """Called if a command did finish successfuly.""" 64 | out_str = render_email_template( 65 | 'CRONWRAP RAN COMMAND SUCCESSFULLY:', 66 | sys_args, 67 | cmd 68 | ) 69 | 70 | if sys_args.verbose: 71 | if sys_args.emails: 72 | send_email(sys_args, 73 | sys_args.emails, 74 | 'Host %s: cronwrap ran command successfully!' 75 | % platform.node().capitalize(), 76 | out_str) 77 | else: 78 | print out_str 79 | 80 | 81 | def handle_timeout(sys_args, cmd): 82 | """Called if a command exceeds its running time.""" 83 | err_str = render_email_template( 84 | "CRONWRAP DETECTED A TIMEOUT ON FOLLOWING COMMAND:", 85 | sys_args, 86 | cmd 87 | ) 88 | 89 | if sys_args.emails: 90 | send_email(sys_args, 91 | sys_args.emails, 92 | 'Host %s: cronwrap detected a timeout!' 93 | % platform.node().capitalize(), 94 | err_str) 95 | else: 96 | print err_str 97 | 98 | 99 | def handle_error(sys_args, cmd): 100 | """Called when a command did not finish successfully.""" 101 | err_str = render_email_template( 102 | "CRONWRAP DETECTED FAILURE OR ERROR OUTPUT FOR THE COMMAND:", 103 | sys_args, 104 | cmd 105 | ) 106 | 107 | if sys_args.emails: 108 | send_email(sys_args, 109 | sys_args.emails, 110 | 'Host %s: cronwrap detected a failure!' 111 | % platform.node().capitalize(), 112 | err_str) 113 | else: 114 | print err_str 115 | 116 | sys.exit(-1) 117 | 118 | 119 | def render_email_template(title, sys_args, cmd): 120 | result_str = [] 121 | 122 | result_str.append(title) 123 | result_str.append('%s\n' % sys_args.cmd) 124 | 125 | result_str.append('COMMAND STARTED:') 126 | result_str.append('%s UTC\n' % (datetime.utcnow() - 127 | timedelta(seconds=int(cmd.run_time)))) 128 | 129 | result_str.append('COMMAND FINISHED:') 130 | result_str.append('%s UTC\n' % datetime.utcnow()) 131 | 132 | result_str.append('COMMAND RAN FOR:') 133 | hours = cmd.run_time / 60 / 60 134 | if cmd.run_time < 60: 135 | hours = 0 136 | result_str.append('%d seconds (%.2f hours)\n' % (cmd.run_time, hours)) 137 | 138 | result_str.append("COMMAND'S TIMEOUT IS SET AT:") 139 | result_str.append('%s\n' % sys_args.time) 140 | 141 | result_str.append('RETURN CODE WAS:') 142 | result_str.append('%s\n' % cmd.returncode) 143 | 144 | result_str.append('ERROR OUTPUT:') 145 | result_str.append('%s\n' % trim_if_needed(cmd.stderr)) 146 | 147 | result_str.append('STANDARD OUTPUT:') 148 | result_str.append('%s' % trim_if_needed(cmd.stdout)) 149 | 150 | return '\n'.join(result_str) 151 | 152 | 153 | def handle_test_email(sys_args): 154 | send_email(sys_args, 155 | sys_args.emails, 156 | 'Host %s: cronwrap test mail' 157 | % platform.node().capitalize(), 158 | 'just a test mail, yo! :)') 159 | 160 | 161 | #--- Util ---------------------------------------------- 162 | class Command: 163 | 164 | """Runs a command, only works on Unix. 165 | 166 | Example of usage:: 167 | 168 | cmd = Command('ls') 169 | print cmd.stdout 170 | print cmd.stderr 171 | print cmd.returncode 172 | print cmd.run_time 173 | """ 174 | def __init__(self, command): 175 | outfile = tempfile.mktemp() 176 | errfile = tempfile.mktemp() 177 | 178 | t_start = time.time() 179 | 180 | self.returncode = os.system("( %s ) > %s 2> %s" % 181 | (command, outfile, errfile)) >> 8 182 | self.run_time = time.time() - t_start 183 | self.stdout = open(outfile, "r").read().strip() 184 | self.stderr = open(errfile, "r").read().strip() 185 | 186 | os.remove(outfile) 187 | os.remove(errfile) 188 | 189 | 190 | def send_email(sys_args, emails, subject, message): 191 | """Sends an email via `mail`.""" 192 | emails = emails.split(',') 193 | 194 | err_report = tempfile.mktemp() 195 | fp_err_report = open(err_report, "w") 196 | fp_err_report.write(message) 197 | fp_err_report.close() 198 | 199 | try: 200 | for email in emails: 201 | cmd = Command('mail -s "%s" %s < %s' % 202 | (subject.replace('"', "'"), 203 | email, 204 | err_report) 205 | ) 206 | 207 | if sys_args.verbose: 208 | if cmd.returncode == 0: 209 | print 'Sent an email to %s' % email 210 | else: 211 | print 'Could not send an email to %s' % email 212 | print trim_if_needed(cmd.stderr) 213 | finally: 214 | os.remove(err_report) 215 | 216 | 217 | def is_time_exceeded(sys_args, cmd): 218 | """Returns `True` if the command's run time has exceeded the maximum 219 | run time specified in command arguments. Else `False is returned.""" 220 | cmd_time = int(cmd.run_time) 221 | 222 | #Parse sys_args.time 223 | max_time = sys_args.time 224 | sp = re.match("(\d+)([hms])", max_time).groups() 225 | t_val, t_unit = int(sp[0]), sp[1] 226 | 227 | #Convert time to seconds 228 | if t_unit == 'h': 229 | t_val = t_val * 60 * 60 230 | elif t_unit == 'm': 231 | t_val = t_val * 60 232 | 233 | return cmd_time > t_val 234 | 235 | 236 | def trim_if_needed(txt, max_length=10000): 237 | if len(txt) > max_length: 238 | return '... START TRUNCATED...\n%s' % txt[-max_length:] 239 | else: 240 | return txt 241 | 242 | 243 | if __name__ == '__main__': 244 | parser = argparse.ArgumentParser(description="A cron job wrapper that wraps jobs and enables better error reporting and command timeouts. Version %s" % __VERSION__) 245 | 246 | parser.add_argument('-c', '--cmd', help='Run a command. Could be `cronwrap -c "ls -la"`.') 247 | 248 | parser.add_argument('-e', '--emails', 249 | help='Email following users if the command crashes or exceeds timeout. ' 250 | 'Could be `cronwrap -e "johndoe@mail.com, marcy@mail.com"`. ' 251 | "Uses system's `mail` to send emails. If no command (cmd) is set a test email is sent.") 252 | 253 | parser.add_argument('-t', '--time', 254 | help='Set the maximum running time.' 255 | 'If this time is passed an alert email will be sent.' 256 | "The command will keep running even if maximum running time is exceeded." 257 | "The default is 1 hour `-t 1h`. " 258 | "Possible values include: `-t 2h`,`-t 2m`, `-t 30s`." 259 | ) 260 | 261 | parser.add_argument('-v', '--verbose', 262 | nargs='?', default=False, 263 | help='Will send an email / print to stdout on successful run.') 264 | 265 | handle_args(parser.parse_args()) 266 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2007 Qtrac Ltd. All rights reserved. 3 | # This module is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or (at 6 | # your option) any later version. 7 | 8 | import sys 9 | from setuptools import setup 10 | 11 | install_requires = [] 12 | if sys.version_info < (2, 7): 13 | install_requires.append('argparse>=1.1') 14 | 15 | setup(name='cronwrap', 16 | version = '1.3', 17 | author="amix the lucky stiff", 18 | author_email="amix@amix.dk", 19 | url="http://www.amix.dk/", 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: BSD License", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | ], 28 | 29 | install_requires=install_requires, 30 | 31 | scripts=['scripts/cronwrap'], 32 | platforms=["Any"], 33 | license="BSD", 34 | keywords='cron wrapper crontab cronjob', 35 | description="A cron job wrapper that wraps jobs and enables better error reporting and command timeouts.", 36 | long_description="""\ 37 | Example 38 | ------- 39 | 40 | Basic example of usage:: 41 | 42 | ##Will print out help 43 | $ cronwrap -h 44 | 45 | usage: cronwrap [-h] [-c CMD] [-e EMAILS] [-t TIME] [-v [VERBOSE]] 46 | 47 | A cron job wrapper that wraps jobs and enables better error reporting and command timeouts. 48 | 49 | optional arguments: 50 | -h, --help show this help message and exit 51 | -c CMD, --cmd CMD Run a command. Could be `cronwrap -c "ls -la"`. 52 | -e EMAILS, --emails EMAILS 53 | Email following users if the command crashes or 54 | exceeds timeout. Could be `cronwrap -e 55 | "johndoe@mail.com, marcy@mail.com"`. Uses system's 56 | `mail` to send emails. If no command (cmd) is set a 57 | test email is sent. 58 | -t TIME, --time TIME Set the maxium running time. If this time is passed an 59 | alert email will be sent once the command ends. 60 | The command will keep running even if maximum running time 61 | is exceeded. Please note that this option doesn't 62 | prevent the job from stalling and does nothing to 63 | prevent running multiple cron jobs at the same time. 64 | The default is 1 hour `-t 1h`. Possible values include: 65 | `-t 2h`,`-t 2m`, `-t 30s`. 66 | -v [VERBOSE], --verbose [VERBOSE] 67 | Will send an email / print to stdout on successful run. 68 | 69 | 70 | ##Will send out a timeout alert to cron@my_domain.com: 71 | $ cronwrap -c "sleep 2" -t "1s" -e cron@my_domain.com 72 | 73 | ##Will send out an error alert to cron@my_domain.com: 74 | $ cronwrap -c "blah" -e cron@my_domain.com 75 | 76 | #Will not send any reports: 77 | $ cronwrap -c "ls" -e cron@my_domain.com 78 | 79 | #Will send a successful report to cron@my_domain.com: 80 | $ cronwrap -c "ls" -e cron@my_domain.com -v 81 | """) 82 | --------------------------------------------------------------------------------