#!/usr/bin/env python # -*- mode: python; coding: utf-8 -*- # vim:smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class:ts=4:sts=4:sta:et:ai:shiftwidth=4 # # arch-tag: Simple patch queue manager for tla # Copyright © 2003,2004 Colin Walters # Copyright © 2004 Canonical Ltd. # Author: Robert Collins # Copyright © 2003, 2005 Walter Landry # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Some junk to try finding Python 2.3, if "python" on this system # is too old. import os,sys if sys.hexversion >= 0x2030000: pass else: if os.getenv('PYTHON'): try: os.execvp(os.getenv('PYTHON'), [os.getenv('PYTHON')] + sys.argv) except: 1 try: os.execvp('python2.3', ['python2.3'] + sys.argv) except: 1 sys.stderr.write("This program requires Python 2.3\n") sys.exit(1) import string, stat, re, glob, getopt, time, traceback, gzip, getpass, popen2 import smtplib, email import logging, logging.handlers import pqm from pqm import * def usage(ecode, ver_only=None): print "pqm 0" if ver_only: sys.exit(ecode) print "Usage: pqm [OPTIONS...] [DIRECTORY]" print "Options:" print " -v, --verbose\t\tDisplay extra information" print " -q, --quiet\t\tDisplay less information" print " -c, --config=FILE\tParse configuration info from FILE" print " -d, --debug\t\tOutput information to stdout as well as log" print " --no-log\t\tDon't write information to log file" print " -n, --no-act\t\tDon't actually perform changes" print " -r, --read\t\tRead a request from stdin" print " --run\t\tProcess queue" print " --report\t\tPrint patch report (used with --run)" print " --no-verify\t\tDon't verify signatures" print " --queuedir=DIR\t\tPerform first-time configuration" print " --keyring=FILE\t\tUse the specified GPG keyring" print " --help\t\tWhat you're looking at" print " --version\t\tPrint the software version and exit" sys.exit(ecode) def do_mkdir(name): if os.access(name, os.X_OK): return try: logger.info('Creating directory "%s"' % (name)) except: pass if not no_act: os.mkdir(name) def do_rename(source, target): try: logger.debug('Renaming "%s" to "%s"' % (source, target)) except: pass if not no_act: os.rename(source, target) def do_chmod(name, mode): try: logger.info('Changing mode of "%s" to %o' % (name, mode)) except: pass if not no_act: os.chmod(name, mode) def dir_from_option(configp, option, default): """calculate a working dir path""" return os.path.abspath(os.path.expanduser(configp.get_option('DEFAULT',option, os.path.join(queuedir, default)))) class RevisionOptionHandler: def __init__(self, revisions, configp): self._configp = configp self._revisions = revisions self._optionmap = {} self._optionmap['precommit_hook'] = ['str', None] self._optionmap['published_at'] = ['str', None] self._optionmap['build_config'] = ['str', None] self._optionmap['build_dir'] = ['str', None] self._optionmap['commiters'] = ['str', None] self._optionmap['commit_re'] = ['str', None] def get_option_map(self, dist): ret = self._revisions[dist] for key in self._optionmap.keys(): type = self._optionmap[key][0] ret[key] = self._optionmap[key][1] if self._configp.has_option ('DEFAULT', key): ret[key] = self.get_option (type, 'DEFAULT', key) if self._configp.has_option (dist, key): ret[key] = self.get_option (type, dist, key) return ret def get_option (self, type, dist, key): if type == 'int': return self._configp.getint(dist, key) elif type == 'str': return self._configp.get(dist, key) elif type == 'bool': return self._configp.getboolean(dist, key) assert(None) def runtla_internal(sender, cmd, *args): return apply(popen_noshell, [arch_path, cmd] + list(args)) def runtla(sender, cmd, *args): (status, msg, output) = apply(runtla_internal, [sender, cmd] + list(args)) if not ((status is None) or (status == 0)): raise PQMTlaFailure(sender, ["VCS command %s %s failed (%s): %s" % (cmd, args, status, msg)] + output) return output class LockFile(object): """I represent a lock that is made on the file system, to prevent concurrent execution of this code""" def __init__(self, filename): self.filename=filename self.locked=False def acquire(self): if no_act: return logger.info('creating lockfile') try: os.open(self.filename, os.O_CREAT | os.O_EXCL) self.locked=True except OSError, e: if cron_mode: logger.info("lockfile %s already exists, exiting", self.filename) sys.exit(0) else: logger.error("Couldn't create lockfile: %s", self.filename) sys.exit(1) def release(self): if not self.locked: return if no_act: return logger.debug('Removing lock file: %s', self.filename) os.unlink(self.filename) self.locked=False def do_run_mode(queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report): scripts = find_patches(queuedir, logger, verify_sigs) (goodscripts, badscripts) = ([], []) for script in scripts: run_one_script(logger, script, logdir, goodscripts, badscripts, mail_reply, mail_server, from_address, fromaddr) if print_report: for (patchname, logname) in goodscripts: print "Patch: " + patchname print "Status: success" print "Log: " + logname print for (patchname, logname) in badscripts: print "Patch: " + patchname print "Status: failure" print "Log: " + logname print def run_one_script(logger, script, logdir, goodscripts, badscripts, mail_reply, mail_server, from_address, fromaddr): try: logger.info('trying script ' + script.filename) logname = os.path.join(logdir, os.path.basename(script.filename) + '.log') (sender, subject, msg, sig) = read_email(logger, open(script.filename)) if verify_sigs: sigid,siguid = verify_sig(script.getSender(), msg, sig, 0, logger) success = False output = [] failedcmd=None # ugly transitional code pqm.allowed_revisions = allowed_revisions pqm.logger = logger pqm.workdir = workdir pqm.runtla = runtla pqm.precommit_hook = precommit_hook cmd=CommandRunner() cmd.arch_impl = arch_impl cmd.arch_path = arch_path (successes, unrecognized, output) = cmd.run(script, siguid) logger.info('successes: %s' % (successes,)) logger.info('unrecognized: %s' % (unrecognized,)) success = True goodscripts.append((script.filename, logname)) except pqm.PQMCmdFailure, e: badscripts.append((script.filename, logname)) successes = e.goodcmds failedcmd = e.badcmd output = e.output unrecognized=[] except PQMException, e: badscripts.append((script.filename, logname)) successes = [] failedcmd = [] output = [str(e)] unrecognized=[] log_list(logname, output) os.unlink(script.filename) if mail_reply: send_mail_reply(success, successes, unrecognized, mail_server, from_address, script.getSender(), fromaddr, failedcmd, output, cmd) else: logger.info('not sending mail reply') def gather_output(retmesg, output): result='' for line in output: result += '\n%s' % line return result def send_mail_reply(success, successes, unrecognized, mail_server, from_address, sender, fromaddr, failedcmd, output, cmd): if success: retmesg = mail_format_successes(successes, "Command was successful.", unrecognized, line) if len(successes) > 0: statusmsg='success' else: statusmsg='no valid commands given' else: retmesg = mail_format_successes(successes, "Command passed checks, but was not committed.", unrecognized, line) retmesg+= "\n%s" % failedcmd retmesg+= '\nCommand failed!' if not cmd.debug: retmesg+= '\nLast 20 lines of log output:' retmesg += gather_output (retmesg, output[-20:]) else: retmesg+= '\nAll lines of log output:' retmesg += gather_output (retmesg, output) statusmsg='failure' server = smtplib.SMTP(mail_server) server.sendmail(from_address, [sender], 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\n' % (fromaddr, sender, statusmsg, retmesg)) server.quit() def mail_format_successes(successes, command_msg, unrecognized, line): retmesg = [] for success in successes: retmesg.append('> ' + success) retmesg.append(command_msg) for line in unrecognized: retmesg.append('> ' + line) retmesg.append('Unrecognized command.') return string.join(retmesg, '\n') def log_list(logname, list): f = open(logname, 'w') for l in list: f.write(l) f.close() def run(pqm_subdir, run_mode, queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report): lockfile=LockFile(os.path.join(pqm_subdir, 'pqm.lock')) lockfile.acquire() try: if run_mode: do_run_mode(queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report) finally: lockfile.release() def do_read_mode(logger): sender = None try: (sender, subject, msg, sig) = read_email(logger) if verify_sigs: sigid,siguid = verify_sig(sender, msg, sig, 1, logger) open(transaction_file, 'a').write(sigid + '\n') fname = 'patch.%d' % (time.time()) logger.info('new patch ' + fname) f = open('tmp.' + fname, 'w') f.write('From: ' + sender + '\n') f.write('Subject: ' + subject + '\n') f.write(string.join(re.split('\r?\n', msg), '\n')) # canonicalize line endings f.close() os.rename('tmp.' + fname, fname) except: if sender and mail_reply: server = smtplib.SMTP(mail_server) tb=string.join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback), '') server.sendmail(from_address, [sender], 'From: %s\r\nTo: %s\r\nSubject: error processing requests\r\n\r\n' % (fromaddr, sender) + 'An error was encountered:\n' + tb) server.quit() logger.exception("Caught exception") sys.exit(1) sys.exit(0) arch_path = 'arx' #arch_path = 'baz' arch_impl = None logfile_name = 'pqm.log' default_mail_log_level = logging.ERROR mail_server = 'localhost' queuedir = None workdir = None logdir = None mail_reply = 1 verify_sigs = 1 from_address = None allowed_revisions = {} precommit_hook = [] try: opts, args = getopt.getopt(sys.argv[1:], 'vqc:dnrk', ['verbose', 'quiet', 'config=', 'debug', 'no-log', 'no-act', 'read', 'run', 'report', 'cron', 'no-verify', 'queuedir=', 'keyring=', 'help', 'version', ]) except getopt.GetoptError, e: sys.stderr.write("Error reading arguments: %s\n" % e) usage(1) for (key, val) in opts: if key == '--help': usage(0) elif key == '--version': usage(0, ver_only=1) if len(args) > 1: sys.stderr.write("Unknown arguments: %s\n" % args[1:]) usage(1) logger = logging.getLogger("pqm") loglevel = logging.WARN no_act = 0 debug_mode = 0 run_mode = 0 read_mode = 0 cron_mode = 0 print_report = 0 no_log = 0 batch_mode = 0 custom_config_files = 0 for key, val in opts: if key in ('-v', '--verbose'): if loglevel == logging.INFO: loglevel = logging.DEBUG elif loglevel == logging.WARN: loglevel = logging.INFO elif key in ('-q', '--quiet'): if loglevel == logging.WARN: loglevel = logging.ERROR elif loglevel == logging.WARN: loglevel = logging.CRITICAL elif key in ('-c', '--config'): if not custom_config_files: custom_config_files = 1 configfile_names = [] configfile_names.append(os.path.abspath(os.path.expanduser(val))) elif key in ('--keyring'): pqm.keyring = val elif key in ('-n', '--no-act'): no_act = 1 elif key in ('-d', '--debug'): debug_mode = 1 elif key in ('--queuedir',): queuedir = val elif key in ('--keyring',): pqm.keyring = val elif key in ('--no-log',): no_log = 1 elif key in ('--no-verify',): verify_sigs = 0 elif key in ('-r', '--read'): read_mode = 1 elif key in ('--run',): run_mode = 1 elif key in ('--cron',): cron_mode = 1 elif key in ('--report',): print_report = 1 logger.setLevel(logging.DEBUG) stderr_handler = logging.StreamHandler(strm=sys.stderr) stderr_handler.setLevel(loglevel) logger.addHandler(stderr_handler) stderr_handler.setLevel(loglevel) stderr_handler.setFormatter(logging.Formatter(fmt="%(name)s [%(thread)d] %(levelname)s: %(message)s")) if not (read_mode or run_mode): logger.error("Either --read or --run must be specified") sys.exit(1) configp = ConfigParser() configfile_names = map(lambda x: os.path.abspath(os.path.expanduser(x)), configfile_names) logger.debug("Reading config files: %s" % (configfile_names,)) configp.read(configfile_names) if configp.has_option('DEFAULT', 'arch_path'): arch_path = configp.get('DEFAULT', 'arch_path') elif configp.has_option('DEFAULT', 'tlapath'): logger.warn("Option 'tlapath' is deprecated") arch_path = configp.get('DEFAULT', 'tlapath') if configp.has_option('DEFAULT', 'groups'): for group in configp.get('DEFAULT', 'groups').split(','): groups[group.strip()]=[] logger.info('found groups %s', groups) for group in groups.keys(): for member in configp.get(group, 'members').split(','): groups[group].append(member.strip()) logger.info('group %s has members %s', group, groups[group]) if os.access(arch_path, os.X_OK): logger.error("Can't execute \"%s\", please fix arch_path" % (arch_path,)) sys.exit(1) # ugly transitional code pqm.logger = logger if configp.has_option('DEFAULT', 'arch_impl'): impl = configp.get('DEFAULT', 'arch_impl') if impl == 'tla': arch_impl = TlaHandler() elif impl == 'arx': arch_impl = ArXHandler() elif impl == 'baz': arch_impl = Baz1_1Handler() elif impl == 'baz1.0': arch_impl = Baz1_0Handler() else: logger.error("Unknown arch_impl \"%s\"" % (impl,)) sys.exit(1) pqm.gpgv_path = configp.get_option('DEFAULT', 'gpgv_path', 'gpgv') myname = configp.get_option('DEFAULT', 'myname', 'Arch Patch Queue Manager') if configp.has_option('DEFAULT', 'from_address'): from_address = configp.get('DEFAULT', 'from_address') else: logger.error("No from_address specified") sys.exit(1) fromaddr = '%s <%s>' % (myname, from_address) mail_reply=configp.get_boolean_option('DEFAULT', 'mail_reply',1) verify_sigs=configp.get_boolean_option('DEFAULT', 'verify_sigs', verify_sigs) if not queuedir: queuedir = get_queuedir (configp, logger, args) queuedir=os.path.abspath(queuedir) if not configp.has_option('DEFAULT', 'dont_set_home'): os.environ['HOME'] = queuedir workdir=dir_from_option(configp, 'workdir', 'workdir') logdir=dir_from_option(configp, 'logdir', 'logs') if not pqm.keyring: if configp.has_option('DEFAULT', 'keyring'): pqm.keyring = configp.get('DEFAULT', 'keyring') else: logger.error("No keyring specified on command line or in config files.") sys.exit(1) if not os.access(pqm.keyring, os.R_OK): logger.error("Couldn't access keyring %s" % (pqm.keyring,)) sys.exit(1) sects = configp.sections() if len(sects) > 0: for sect in sects: if str(sect) in groups.keys(): continue logger.info("managing revision: " + sect) allowed_revisions[sect] = {} else: logger.error("No revisions to manage!") sys.exit(1) rev_optionhandler = RevisionOptionHandler(allowed_revisions, configp) for rev in allowed_revisions.keys(): allowed_revisions[rev] = rev_optionhandler.get_option_map(rev) do_mkdir(queuedir) os.chdir(queuedir) do_mkdir(workdir) do_mkdir(logdir) pqm_subdir = os.path.join(queuedir, 'arch-pqm') pqm.pqm_subdir = pqm_subdir do_mkdir(pqm_subdir) if configp.has_option('DEFAULT', 'logfile'): logfile_name = configp.get('DEFAULT', 'logfile') if not no_log: if not os.path.isabs(logfile_name): logfile_name = os.path.join(pqm_subdir, logfile_name) logger.debug("Adding log file: %s" % (logfile_name,)) filehandler = logging.FileHandler(logfile_name) if loglevel == logging.WARN: filehandler.setLevel(logging.INFO) else: filehandler.setLevel(logging.DEBUG) logger.addHandler(filehandler) filehandler.setFormatter(logging.Formatter(fmt="%(asctime)s %(name)s [%(thread)d] %(levelname)s: %(message)s", datefmt="%b %d %H:%M:%S")) if not (debug_mode or batch_mode): # Don't log to stderr past this point logger.removeHandler(stderr_handler) transaction_file = os.path.join(queuedir, 'transactions-completed') if os.access(transaction_file, os.R_OK): lines = open(transaction_file).readlines() for line in lines: pqm.used_transactions[line[0:-1]] = 1 if read_mode: do_read_mode(logger) assert(run_mode) run(pqm_subdir, run_mode, queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report) logger.info("main thread exiting...") sys.exit(0)