So I just had to put together a tool, that would let me run an arbitrary command with arbitrary parameters, and pipe stdout/stderr/return status into an email. The tool handles locale/encoding correctly, tails a configurable amount of interspersed stdout/stderr output in the body of the email in a fixed size font, and lets you attach both stdout and stderr in full as separate attachments. It also provides run times for the run task. Uses SMTP via MTA/MSA, and supports TLS+auth. Unless requested, still prints to console all stdout/stderr.
Not the prettiest or best code ever, as I didn't have time, and I'm not really a Python person. It only depends on markup.py, because I was lazy and didn't want to deal with generating HTML.
The only thing I still want to add is coloring stderr output in the body of the email.
#!/usr/bin/python """Pipes the result and status of a started process into an email. This is useful for starting builds/syncs and getting an email notification. """ import smtplib import markup from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.header import Header from email import Encoders from StringIO import StringIO from optparse import OptionParser import subprocess import sys import os import select import fcntl import errno import locale import getpass import platform import time __license__ = "GPL" __version__ = "1.0.0" __maintainer__ = "Andrei Warkentin" __email__ = "andrey.warkentin@gmail.com" __status__ = "Production" # # This config should "just work" for gmail/google apps users. # SMTP server address is from MX record for domain. # mail_config = { 'user' : 'googleappsuser@domain.com', 'server' : 'gmail-smtp-in.l.google.com', 'tls' : False, 'auth' : False, 'pass' : None } # # If you have a dynamic IP or there are other reasons, # why the MTA rejects you, you need to use an MSA and auth. # Something like this. Server address is same as used to # configure your MUA. # # #mail_config = { # 'user' : 'googleappsuser@domain.com', # 'server' : 'smtp.gmail.com', # 'tls' : True, # 'auth' : True, # 'pass' : 'XXXXX' #} # def mail(config, encoding, subject, body, out_data, err_data): msg = MIMEMultipart() msg.set_charset(encoding) subject = Header(subject, encoding) msg['From'] = config['user'] msg['To'] = config['user'] msg['Subject'] = subject part = MIMEBase('text', 'html', _charset=encoding) part.set_payload(body) Encoders.encode_base64(part) msg.attach(part) if not out_data is None: part = MIMEText(out_data, _charset=encoding) part.add_header('Content-Disposition', 'attachment', filename='stdout.txt') msg.attach(part) if not err_data is None: part = MIMEText(err_data, _charset=encoding) part.add_header('Content-Disposition', 'attachment', filename='stderr.txt') msg.attach(part) port = 25 if config['auth']: port = 587 mailServer = smtplib.SMTP(config['server'], port) if config['tls']: mailServer.ehlo() mailServer.starttls() mailServer.ehlo() if config['auth']: mailServer.login(config['user'], config['pass']) mailServer.sendmail(config['user'], config['user'], msg.as_string()) mailServer.close() def process_more(fd, strio, suppress_con, is_err): errdone = False more = True data = "" while more: try: data = os.read(fd, 1024) if data == "": errdone = True more = False else: if not suppress_con: if is_err: sys.stderr.write(data) else: sys.stdout.write(data) strio.write(data) except OSError, err: if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK: more = False elif err.errno != errno.EINTR: errdone = True return errdone def get_output(args, separate, suppress_con): time_wall = time.time() p = subprocess.Popen(args, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) fdo = p.stdout.fileno() fde = p.stderr.fileno() fcntl.fcntl(fde, fcntl.F_SETFL, fcntl.fcntl(fde, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl(fdo, fcntl.F_SETFL, fcntl.fcntl(fdo, fcntl.F_GETFL) | os.O_NONBLOCK) rfd = [fde, fdo] outdone = False errdone = False outio = StringIO() errio = outio if separate: errio = StringIO() while True: if not errdone: errdone = process_more(fde, errio, suppress_con, True) if not outdone: outdone = process_more(fdo, outio, suppress_con, False) if errdone and outdone: break select.select(rfd, [], []) status = p.wait() time_wall = time.time() - time_wall if not separate: errio = None return (status, outio, errio, time_wall) def process_output(io, line_max): cut = False processed_io = StringIO() io.seek(0) lines = io.readlines() count = len(lines) # # If output too big, show last line_max lines. # -1 is a special value to show all lines. # if count > line_max > -1: cut = True lines = lines[count - line_max:] processed_io.write(''.join(lines)) return (processed_io, cut) def format_time(seconds): hours = int(seconds // 3600) seconds -= 3600 * hours minutes = int(seconds // 60) seconds -= 60 * minutes return "{0}h {1}m {2} seconds".format(hours, minutes, seconds) def main(): encoding = locale.getpreferredencoding() parser = OptionParser(usage="usage: %prog [options] cmd [cmd arguments]") parser.disable_interspersed_args() parser.add_option('-l', '--lines', dest='lines', type='int', help='send maximum last stdout+stderr lines in email (-1 for max)', metavar='LINES', default=50) parser.add_option('-c', '--no-console', dest='supress_con', action='store_true', help="don't output to stdout/stderr", default=False) parser.add_option('-s', '--separate', dest='separate', action='store_true', help="keep stderr separate from stdout and send both as attachments", default=False) (options, process_args) = parser.parse_args() if not len(process_args) >= 1: parser.print_usage() sys.exit(1) try: process_status, process_out, process_err, time_wall = get_output(process_args, options.separate, options.supress_con) except OSError, err: sys.stderr.write("Error while starting child process: {0}\n".format(err.strerror)) sys.exit(2) process_cmd = ' '.join(process_args) cut = False if not options.separate: process_out, cut = process_output(process_out, options.lines) process_out_string = None if process_out.tell() != 0: process_out_string = process_out.getvalue() process_err_string = None if (not process_err is None) and (process_err.tell() != 0): process_err_string = process_err.getvalue() page = markup.page() page.h3("Command: {0}".format(process_cmd)) page.h3("Return status: {0}".format(process_status)) page.h3("Time: {0}".format(format_time(time_wall))) if options.separate: if (process_out_string is None) and (process_err_string is None): page.i("... no stdout/stderr output to attach ...") else: page.i("... see attachments for logs ...") else: if process_out_string is None: page.i("... no stdout/stderr output ...") else: if cut: page.i("... showing last {0} lines...".format(options.lines)) page.hr() page.pre(markup.escape(process_out_string)) # # This ensures the output isn't attached as an attachment. # If we're ever in this code path, process_err_string is guaranteed # to be None, since process_err is None, since get_output was invoked # with options.separate = False. # process_out_string = None page.hr() subject = "{0}@{1}: {2}".format(getpass.getuser(), platform.node(),process_cmd) mail(mail_config, encoding, subject, page(), process_out_string, process_err_string) if __name__ == '__main__': main()
No comments:
Post a Comment