#!/usr/bin/env python """ A script for reporting the results of an invirtibuild. Supports any combination of the following reporting mechanisms. Note that all strings are interpolated with a dictionary containing the following keys: build_id, commit, failed_stage, inserted_at, package, pocket, principal, result, short_commit, traceback, version == zephyr == To configure zephyr, add something like the following to your invirt config: build: hooks: post_build: zephyr: &post_build_zephyr class: myclass [required] instance: myinstance [optional] zsig: myzsig [optional] failed_build: zephyr: *post_build_zephyr ... == mail == To configure email notifications, add something like the following to your invirt config: build: hooks: post_build: mail: &post_build_mail to: myemail@example.com [required] from: myemail@example.com [required] subject: My Subject [optional] failed_build: mail: *post_build_mail ... The script chooses which configuration option to use based off the name it is called with. This name also determines which command-line arguments the script takes, as well as how they are formatted. When called as: post-build: uses post_build option failed-build: uses failed_build option post-submit: uses post_submit option failed-submit: uses failed_submit option post-add-repo: uses post_add_repo option """ import optparse import os import re import sys import textwrap from email.mime import text from invirt import common, database, builder from invirt.config import structs as config def build_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x, escape=lambda x: x): """Format a message reporting the results of a build""" values = dict(values) if not verbose and values['traceback'] is not None: split = values['traceback'].split('\n') # Here, have a hackish heuristic truncated = '(empty)' for i in xrange(2, len(split)): truncated = textwrap.fill('\n'.join(split[-i:])) if len(truncated) >= 10: break values['traceback'] = truncated for key in ['package', 'version', 'pocket', 'principal', 'inserted_at', 'short_commit']: values[key] = escape(values[key]) if succeeded: values['result'] = success(values['result']) msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s. Job submitted by %(principal)s at %(inserted_at)s. Branch %(pocket)s has been advanced to %(short_commit)s.""" % values else: values['result'] = failure(values['result']) msg = """Build of %(package)s version %(version)s in %(pocket)s %(result)s while %(failed_stage)s. Job submitted by %(principal)s at %(inserted_at)s. Error: %(traceback)s""" % values return msg def submit_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x, escape=lambda x: x): values = dict(values) if succeeded: values['result'] = success(values['result']) else: values['result'] = failure(values['result']) for key in ['commit', 'pocket', 'principal']: values[key] = escape(values[key]) msg = """Submission of %(commit)s to be built in %(pocket)s %(result)s. Build submitted by %(principal)s.""" % values return msg def repo_creation_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x, escape=lambda x: x): values = dict(values) assert succeeded for key in ['category', 'name', 'principal']: values[key] = escape(values[key]) msg = '%(principal)s just created a new repository, %(category)s/%(name)s.git' % values return msg def prebuild_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x, escape=lambda x: x): for key in ['build_id', 'pocket', 'package', 'commit', 'principal', 'version', 'inserted_at']: values[key] = escape(values[key]) msg = """Build started: %(package)s %(version)s in %(pocket)s. Base commit %(commit)s . Job submitted by %(principal)s at %(inserted_at)s.""" % values return msg # Names of hooks POST_BUILD = 'post-build' FAILED_BUILD = 'failed-build' POST_SUBMIT = 'post-submit' FAILED_SUBMIT = 'failed-submit' POST_ADD_REPO = 'post-add-repo' PRE_BUILD = 'pre-build' # Types of communication ZEPHYR = 'zephyr' MAIL = 'mail' message_generators = { ZEPHYR : { POST_BUILD : build_completion_msg, FAILED_BUILD : build_completion_msg, POST_SUBMIT : submit_completion_msg, FAILED_SUBMIT : submit_completion_msg, POST_ADD_REPO : repo_creation_msg, PRE_BUILD : prebuild_msg }, MAIL : { POST_BUILD : build_completion_msg, FAILED_BUILD : build_completion_msg, POST_SUBMIT : submit_completion_msg, FAILED_SUBMIT : submit_completion_msg, POST_ADD_REPO : repo_creation_msg, PRE_BUILD : prebuild_msg } } def zephyr_escape(m): m = str(m) m = re.sub('@', '@@', m) m = re.sub('}', '@(})', m) return m def zephyr_success(m): return '}@{@color(green)%s}@{' % zephyr_escape(m) def zephyr_failure(m): return '}@{@color(red)%s}@{' % zephyr_escape(m) def main(): parser = optparse.OptionParser('Usage: %prog [options] [arguments]') opts, args = parser.parse_args() prog = os.path.basename(sys.argv[0]) try: if prog == POST_BUILD: hook_config = config.build.hooks.post_build elif prog == FAILED_BUILD: hook_config = config.build.hooks.failed_build elif prog == POST_SUBMIT: hook_config = config.build.hooks.post_submit elif prog == FAILED_SUBMIT: hook_config = config.build.hooks.failed_submit elif prog == POST_ADD_REPO: hook_config = config.build.hooks.post_add_repo elif prog == PRE_BUILD: hook_config = config.build.hooks.pre_build else: parser.error('hook script invoked with unrecognized name %s' % prog) return 2 except common.InvirtConfigError: print >>sys.stderr, 'No hook configuration found for %s.' % prog return 1 if prog in [POST_BUILD, FAILED_BUILD, PRE_BUILD]: if len(args) != 1: parser.set_usage('Usage: %prog [options] build_id') parser.print_help() return 1 database.connect() build = database.Build.query().get(args[0]) short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True) values = { 'build_id' : build.build_id, 'commit' : build.commit, 'failed_stage' : build.failed_stage, 'inserted_at' : build.inserted_at, 'package' : build.package, 'pocket' : build.pocket, 'principal' : build.principal, 'short_commit' : short_commit, 'traceback' : build.traceback, 'version' : build.version, 'default_instance' : 'b%(build_id)s', 'default_subject' : 'Build %(build_id)d %(result)s: %(package)s %(version)s in %(pocket)s'} if prog == PRE_BUILD: succeeded = True elif build.succeeded: assert prog == POST_BUILD values['result'] = 'succeeded' succeeded = True else: assert prog == FAILED_BUILD values['result'] = 'failed' succeeded = False elif prog in [POST_SUBMIT, FAILED_SUBMIT]: if len(args) != 4: parser.set_usage('Usage: %prog [options] pocket package commit principal') parser.print_help() return 2 values = { 'pocket' : args[0], 'package' : args[1], 'commit' : args[2], 'principal' : args[3], 'default_instance' : 'submission', 'default_subject' : 'Submission %(result)s: %(package)s %(version)s in %(pocket)s'} if prog == POST_SUBMIT: values['result'] = 'succeeded' succeeded = True else: values['result'] = 'failed' succeeded = False elif prog in [POST_ADD_REPO]: if len(args) != 3: parser.set_usage('Usage: %prog [options] category name principal') parser.print_help() return 3 values = { 'category' : args[0], 'name' : args[1], 'principal' : args[2], 'default_instance' : 'new-repo', 'default_subject' : 'New repository %(category)s/%(name)s'} succeeded = True else: raise AssertionError('Impossible state') try: zephyr_config = hook_config.zephyr klass = zephyr_config['class'] % values except common.InvirtConfigError: print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog else: make_msg = message_generators[ZEPHYR][prog] msg = '@{%s}' % make_msg(succeeded, values, verbose=False, success=zephyr_success, failure=zephyr_failure, escape=zephyr_escape) instance = zephyr_config.get('instance', values['default_instance']) % values zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s', zsig, '-d', '-m', msg], stdout=None, stderr=None) try: mail_config = hook_config.mail to = mail_config.to % values sender = mail_config['from'] % values except common.InvirtConfigError: print >>sys.stderr, 'No email configuration specified for %s.' % prog else: make_msg = message_generators[MAIL][prog] msg = make_msg(succeeded, values) email = text.MIMEText(msg) email['To'] = to % values email['From'] = sender % values email['Subject'] = mail_config.get('subject', values['default_subject']) % values common.captureOutput(['sendmail', '-t'], email.as_string(), stdout=None, stderr=None) if __name__ == '__main__': sys.exit(main())