diff options
Diffstat (limited to 'buildbot/buildbot/scripts/runner.py')
-rw-r--r-- | buildbot/buildbot/scripts/runner.py | 1023 |
1 files changed, 1023 insertions, 0 deletions
diff --git a/buildbot/buildbot/scripts/runner.py b/buildbot/buildbot/scripts/runner.py new file mode 100644 index 0000000..4e22dbc --- /dev/null +++ b/buildbot/buildbot/scripts/runner.py @@ -0,0 +1,1023 @@ +# -*- test-case-name: buildbot.test.test_runner -*- + +# N.B.: don't import anything that might pull in a reactor yet. Some of our +# subcommands want to load modules that need the gtk reactor. +import os, sys, stat, re, time +import traceback +from twisted.python import usage, util, runtime + +from buildbot.interfaces import BuildbotNotRunningError + +# this is mostly just a front-end for mktap, twistd, and kill(1), but in the +# future it will also provide an interface to some developer tools that talk +# directly to a remote buildmaster (like 'try' and a status client) + +# the create/start/stop commands should all be run as the same user, +# preferably a separate 'buildbot' account. + +class MakerBase(usage.Options): + optFlags = [ + ['help', 'h', "Display this message"], + ["quiet", "q", "Do not emit the commands being run"], + ] + + #["basedir", "d", None, "Base directory for the buildmaster"], + opt_h = usage.Options.opt_help + + def parseArgs(self, *args): + if len(args) > 0: + self['basedir'] = args[0] + else: + self['basedir'] = None + if len(args) > 1: + raise usage.UsageError("I wasn't expecting so many arguments") + + def postOptions(self): + if self['basedir'] is None: + raise usage.UsageError("<basedir> parameter is required") + self['basedir'] = os.path.abspath(self['basedir']) + +makefile_sample = """# -*- makefile -*- + +# This is a simple makefile which lives in a buildmaster/buildslave +# directory (next to the buildbot.tac file). It allows you to start/stop the +# master or slave by doing 'make start' or 'make stop'. + +# The 'reconfig' target will tell a buildmaster to reload its config file. + +start: + twistd --no_save -y buildbot.tac + +stop: + kill `cat twistd.pid` + +reconfig: + kill -HUP `cat twistd.pid` + +log: + tail -f twistd.log +""" + +class Maker: + def __init__(self, config): + self.config = config + self.basedir = config['basedir'] + self.force = config.get('force', False) + self.quiet = config['quiet'] + + def mkdir(self): + if os.path.exists(self.basedir): + if not self.quiet: + print "updating existing installation" + return + if not self.quiet: print "mkdir", self.basedir + os.mkdir(self.basedir) + + def mkinfo(self): + path = os.path.join(self.basedir, "info") + if not os.path.exists(path): + if not self.quiet: print "mkdir", path + os.mkdir(path) + created = False + admin = os.path.join(path, "admin") + if not os.path.exists(admin): + if not self.quiet: + print "Creating info/admin, you need to edit it appropriately" + f = open(admin, "wt") + f.write("Your Name Here <admin@youraddress.invalid>\n") + f.close() + created = True + host = os.path.join(path, "host") + if not os.path.exists(host): + if not self.quiet: + print "Creating info/host, you need to edit it appropriately" + f = open(host, "wt") + f.write("Please put a description of this build host here\n") + f.close() + created = True + if created and not self.quiet: + print "Please edit the files in %s appropriately." % path + + def chdir(self): + if not self.quiet: print "chdir", self.basedir + os.chdir(self.basedir) + + def makeTAC(self, contents, secret=False): + tacfile = "buildbot.tac" + if os.path.exists(tacfile): + oldcontents = open(tacfile, "rt").read() + if oldcontents == contents: + if not self.quiet: + print "buildbot.tac already exists and is correct" + return + if not self.quiet: + print "not touching existing buildbot.tac" + print "creating buildbot.tac.new instead" + tacfile = "buildbot.tac.new" + f = open(tacfile, "wt") + f.write(contents) + f.close() + if secret: + os.chmod(tacfile, 0600) + + def makefile(self): + target = "Makefile.sample" + if os.path.exists(target): + oldcontents = open(target, "rt").read() + if oldcontents == makefile_sample: + if not self.quiet: + print "Makefile.sample already exists and is correct" + return + if not self.quiet: + print "replacing Makefile.sample" + else: + if not self.quiet: + print "creating Makefile.sample" + f = open(target, "wt") + f.write(makefile_sample) + f.close() + + def sampleconfig(self, source): + target = "master.cfg.sample" + config_sample = open(source, "rt").read() + if os.path.exists(target): + oldcontents = open(target, "rt").read() + if oldcontents == config_sample: + if not self.quiet: + print "master.cfg.sample already exists and is up-to-date" + return + if not self.quiet: + print "replacing master.cfg.sample" + else: + if not self.quiet: + print "creating master.cfg.sample" + f = open(target, "wt") + f.write(config_sample) + f.close() + os.chmod(target, 0600) + + def public_html(self, index_html, buildbot_css, robots_txt): + webdir = os.path.join(self.basedir, "public_html") + if os.path.exists(webdir): + if not self.quiet: + print "public_html/ already exists: not replacing" + return + else: + os.mkdir(webdir) + if not self.quiet: + print "populating public_html/" + target = os.path.join(webdir, "index.html") + f = open(target, "wt") + f.write(open(index_html, "rt").read()) + f.close() + + target = os.path.join(webdir, "buildbot.css") + f = open(target, "wt") + f.write(open(buildbot_css, "rt").read()) + f.close() + + target = os.path.join(webdir, "robots.txt") + f = open(target, "wt") + f.write(open(robots_txt, "rt").read()) + f.close() + + def populate_if_missing(self, target, source, overwrite=False): + new_contents = open(source, "rt").read() + if os.path.exists(target): + old_contents = open(target, "rt").read() + if old_contents != new_contents: + if overwrite: + if not self.quiet: + print "%s has old/modified contents" % target + print " overwriting it with new contents" + open(target, "wt").write(new_contents) + else: + if not self.quiet: + print "%s has old/modified contents" % target + print " writing new contents to %s.new" % target + open(target + ".new", "wt").write(new_contents) + # otherwise, it's up to date + else: + if not self.quiet: + print "populating %s" % target + open(target, "wt").write(new_contents) + + def upgrade_public_html(self, index_html, buildbot_css, robots_txt): + webdir = os.path.join(self.basedir, "public_html") + if not os.path.exists(webdir): + if not self.quiet: + print "populating public_html/" + os.mkdir(webdir) + self.populate_if_missing(os.path.join(webdir, "index.html"), + index_html) + self.populate_if_missing(os.path.join(webdir, "buildbot.css"), + buildbot_css) + self.populate_if_missing(os.path.join(webdir, "robots.txt"), + robots_txt) + + def check_master_cfg(self): + from buildbot.master import BuildMaster + from twisted.python import log, failure + + master_cfg = os.path.join(self.basedir, "master.cfg") + if not os.path.exists(master_cfg): + if not self.quiet: + print "No master.cfg found" + return 1 + + # side-effects of loading the config file: + + # for each Builder defined in c['builders'], if the status directory + # didn't already exist, it will be created, and the + # $BUILDERNAME/builder pickle might be created (with a single + # "builder created" event). + + # we put basedir in front of sys.path, because that's how the + # buildmaster itself will run, and it is quite common to have the + # buildmaster import helper classes from other .py files in its + # basedir. + + if sys.path[0] != self.basedir: + sys.path.insert(0, self.basedir) + + m = BuildMaster(self.basedir) + # we need to route log.msg to stdout, so any problems can be seen + # there. But if everything goes well, I'd rather not clutter stdout + # with log messages. So instead we add a logObserver which gathers + # messages and only displays them if something goes wrong. + messages = [] + log.addObserver(messages.append) + try: + # this will raise an exception if there's something wrong with + # the config file. Note that this BuildMaster instance is never + # started, so it won't actually do anything with the + # configuration. + m.loadConfig(open(master_cfg, "r")) + except: + f = failure.Failure() + if not self.quiet: + print + for m in messages: + print "".join(m['message']) + print f + print + print "An error was detected in the master.cfg file." + print "Please correct the problem and run 'buildbot upgrade-master' again." + print + return 1 + return 0 + +class UpgradeMasterOptions(MakerBase): + optFlags = [ + ["replace", "r", "Replace any modified files without confirmation."], + ] + + def getSynopsis(self): + return "Usage: buildbot upgrade-master [options] <basedir>" + + longdesc = """ + This command takes an existing buildmaster working directory and + adds/modifies the files there to work with the current version of + buildbot. When this command is finished, the buildmaster directory should + look much like a brand-new one created by the 'create-master' command. + + Use this after you've upgraded your buildbot installation and before you + restart the buildmaster to use the new version. + + If you have modified the files in your working directory, this command + will leave them untouched, but will put the new recommended contents in a + .new file (for example, if index.html has been modified, this command + will create index.html.new). You can then look at the new version and + decide how to merge its contents into your modified file. + """ + +def upgradeMaster(config): + basedir = config['basedir'] + m = Maker(config) + # TODO: check Makefile + # TODO: check TAC file + # check web files: index.html, classic.css, robots.txt + webdir = os.path.join(basedir, "public_html") + m.upgrade_public_html(util.sibpath(__file__, "../status/web/index.html"), + util.sibpath(__file__, "../status/web/classic.css"), + util.sibpath(__file__, "../status/web/robots.txt"), + ) + m.populate_if_missing(os.path.join(basedir, "master.cfg.sample"), + util.sibpath(__file__, "sample.cfg"), + overwrite=True) + rc = m.check_master_cfg() + if rc: + return rc + if not config['quiet']: + print "upgrade complete" + + +class MasterOptions(MakerBase): + optFlags = [ + ["force", "f", + "Re-use an existing directory (will not overwrite master.cfg file)"], + ] + optParameters = [ + ["config", "c", "master.cfg", "name of the buildmaster config file"], + ["log-size", "s", "1000000", + "size at which to rotate twisted log files"], + ["log-count", "l", "None", + "limit the number of kept old twisted log files"], + ] + def getSynopsis(self): + return "Usage: buildbot create-master [options] <basedir>" + + longdesc = """ + This command creates a buildmaster working directory and buildbot.tac + file. The master will live in <dir> and create various files there. + + At runtime, the master will read a configuration file (named + 'master.cfg' by default) in its basedir. This file should contain python + code which eventually defines a dictionary named 'BuildmasterConfig'. + The elements of this dictionary are used to configure the Buildmaster. + See doc/config.xhtml for details about what can be controlled through + this interface.""" + + def postOptions(self): + MakerBase.postOptions(self) + if not re.match('^\d+$', self['log-size']): + raise usage.UsageError("log-size parameter needs to be an int") + if not re.match('^\d+$', self['log-count']) and \ + self['log-count'] != 'None': + raise usage.UsageError("log-count parameter needs to be an int "+ + " or None") + + +masterTAC = """ +from twisted.application import service +from buildbot.master import BuildMaster + +basedir = r'%(basedir)s' +configfile = r'%(config)s' +rotateLength = %(log-size)s +maxRotatedFiles = %(log-count)s + +application = service.Application('buildmaster') +try: + from twisted.python.logfile import LogFile + from twisted.python.log import ILogObserver, FileLogObserver + logfile = LogFile.fromFullPath("twistd.log", rotateLength=rotateLength, + maxRotatedFiles=maxRotatedFiles) + application.setComponent(ILogObserver, FileLogObserver(logfile).emit) +except ImportError: + # probably not yet twisted 8.2.0 and beyond, can't set log yet + pass +BuildMaster(basedir, configfile).setServiceParent(application) + +""" + +def createMaster(config): + m = Maker(config) + m.mkdir() + m.chdir() + contents = masterTAC % config + m.makeTAC(contents) + m.sampleconfig(util.sibpath(__file__, "sample.cfg")) + m.public_html(util.sibpath(__file__, "../status/web/index.html"), + util.sibpath(__file__, "../status/web/classic.css"), + util.sibpath(__file__, "../status/web/robots.txt"), + ) + m.makefile() + + if not m.quiet: print "buildmaster configured in %s" % m.basedir + +class SlaveOptions(MakerBase): + optFlags = [ + ["force", "f", "Re-use an existing directory"], + ] + optParameters = [ +# ["name", "n", None, "Name for this build slave"], +# ["passwd", "p", None, "Password for this build slave"], +# ["basedir", "d", ".", "Base directory to use"], +# ["master", "m", "localhost:8007", +# "Location of the buildmaster (host:port)"], + + ["keepalive", "k", 600, + "Interval at which keepalives should be sent (in seconds)"], + ["usepty", None, 0, + "(1 or 0) child processes should be run in a pty (default 0)"], + ["umask", None, "None", + "controls permissions of generated files. Use --umask=022 to be world-readable"], + ["maxdelay", None, 300, + "Maximum time between connection attempts"], + ["log-size", "s", "1000000", + "size at which to rotate twisted log files"], + ["log-count", "l", "None", + "limit the number of kept old twisted log files"], + ] + + longdesc = """ + This command creates a buildslave working directory and buildbot.tac + file. The bot will use the <name> and <passwd> arguments to authenticate + itself when connecting to the master. All commands are run in a + build-specific subdirectory of <basedir>. <master> is a string of the + form 'hostname:port', and specifies where the buildmaster can be reached. + + <name>, <passwd>, and <master> will be provided by the buildmaster + administrator for your bot. You must choose <basedir> yourself. + """ + + def getSynopsis(self): + return "Usage: buildbot create-slave [options] <basedir> <master> <name> <passwd>" + + def parseArgs(self, *args): + if len(args) < 4: + raise usage.UsageError("command needs more arguments") + basedir, master, name, passwd = args + self['basedir'] = basedir + self['master'] = master + self['name'] = name + self['passwd'] = passwd + + def postOptions(self): + MakerBase.postOptions(self) + self['usepty'] = int(self['usepty']) + self['keepalive'] = int(self['keepalive']) + self['maxdelay'] = int(self['maxdelay']) + if self['master'].find(":") == -1: + raise usage.UsageError("--master must be in the form host:portnum") + if not re.match('^\d+$', self['log-size']): + raise usage.UsageError("log-size parameter needs to be an int") + if not re.match('^\d+$', self['log-count']) and \ + self['log-count'] != 'None': + raise usage.UsageError("log-count parameter needs to be an int "+ + " or None") + +slaveTAC = """ +from twisted.application import service +from buildbot.slave.bot import BuildSlave + +basedir = r'%(basedir)s' +buildmaster_host = '%(host)s' +port = %(port)d +slavename = '%(name)s' +passwd = '%(passwd)s' +keepalive = %(keepalive)d +usepty = %(usepty)d +umask = %(umask)s +maxdelay = %(maxdelay)d +rotateLength = %(log-size)s +maxRotatedFiles = %(log-count)s + +application = service.Application('buildslave') +try: + from twisted.python.logfile import LogFile + from twisted.python.log import ILogObserver, FileLogObserver + logfile = LogFile.fromFullPath("twistd.log", rotateLength=rotateLength, + maxRotatedFiles=maxRotatedFiles) + application.setComponent(ILogObserver, FileLogObserver(logfile).emit) +except ImportError: + # probably not yet twisted 8.2.0 and beyond, can't set log yet + pass +s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir, + keepalive, usepty, umask=umask, maxdelay=maxdelay) +s.setServiceParent(application) + +""" + +def createSlave(config): + m = Maker(config) + m.mkdir() + m.chdir() + try: + master = config['master'] + host, port = re.search(r'(.+):(\d+)', master).groups() + config['host'] = host + config['port'] = int(port) + except: + print "unparseable master location '%s'" % master + print " expecting something more like localhost:8007" + raise + contents = slaveTAC % config + + m.makeTAC(contents, secret=True) + + m.makefile() + m.mkinfo() + + if not m.quiet: print "buildslave configured in %s" % m.basedir + + + +def stop(config, signame="TERM", wait=False): + import signal + basedir = config['basedir'] + quiet = config['quiet'] + os.chdir(basedir) + try: + f = open("twistd.pid", "rt") + except: + raise BuildbotNotRunningError + pid = int(f.read().strip()) + signum = getattr(signal, "SIG"+signame) + timer = 0 + os.kill(pid, signum) + if not wait: + if not quiet: + print "sent SIG%s to process" % signame + return + time.sleep(0.1) + while timer < 10: + # poll once per second until twistd.pid goes away, up to 10 seconds + try: + os.kill(pid, 0) + except OSError: + if not quiet: + print "buildbot process %d is dead" % pid + return + timer += 1 + time.sleep(1) + if not quiet: + print "never saw process go away" + +def restart(config): + quiet = config['quiet'] + from buildbot.scripts.startup import start + try: + stop(config, wait=True) + except BuildbotNotRunningError: + pass + if not quiet: + print "now restarting buildbot process.." + start(config) + + +def loadOptions(filename="options", here=None, home=None): + """Find the .buildbot/FILENAME file. Crawl from the current directory up + towards the root, and also look in ~/.buildbot . The first directory + that's owned by the user and has the file we're looking for wins. Windows + skips the owned-by-user test. + + @rtype: dict + @return: a dictionary of names defined in the options file. If no options + file was found, return an empty dict. + """ + + if here is None: + here = os.getcwd() + here = os.path.abspath(here) + + if home is None: + if runtime.platformType == 'win32': + home = os.path.join(os.environ['APPDATA'], "buildbot") + else: + home = os.path.expanduser("~/.buildbot") + + searchpath = [] + toomany = 20 + while True: + searchpath.append(os.path.join(here, ".buildbot")) + next = os.path.dirname(here) + if next == here: + break # we've hit the root + here = next + toomany -= 1 # just in case + if toomany == 0: + raise ValueError("Hey, I seem to have wandered up into the " + "infinite glories of the heavens. Oops.") + searchpath.append(home) + + localDict = {} + + for d in searchpath: + if os.path.isdir(d): + if runtime.platformType != 'win32': + if os.stat(d)[stat.ST_UID] != os.getuid(): + print "skipping %s because you don't own it" % d + continue # security, skip other people's directories + optfile = os.path.join(d, filename) + if os.path.exists(optfile): + try: + f = open(optfile, "r") + options = f.read() + exec options in localDict + except: + print "error while reading %s" % optfile + raise + break + + for k in localDict.keys(): + if k.startswith("__"): + del localDict[k] + return localDict + +class StartOptions(MakerBase): + optFlags = [ + ['quiet', 'q', "Don't display startup log messages"], + ] + def getSynopsis(self): + return "Usage: buildbot start <basedir>" + +class StopOptions(MakerBase): + def getSynopsis(self): + return "Usage: buildbot stop <basedir>" + +class ReconfigOptions(MakerBase): + optFlags = [ + ['quiet', 'q', "Don't display log messages about reconfiguration"], + ] + def getSynopsis(self): + return "Usage: buildbot reconfig <basedir>" + + + +class RestartOptions(MakerBase): + optFlags = [ + ['quiet', 'q', "Don't display startup log messages"], + ] + def getSynopsis(self): + return "Usage: buildbot restart <basedir>" + +class DebugClientOptions(usage.Options): + optFlags = [ + ['help', 'h', "Display this message"], + ] + optParameters = [ + ["master", "m", None, + "Location of the buildmaster's slaveport (host:port)"], + ["passwd", "p", None, "Debug password to use"], + ] + + def parseArgs(self, *args): + if len(args) > 0: + self['master'] = args[0] + if len(args) > 1: + self['passwd'] = args[1] + if len(args) > 2: + raise usage.UsageError("I wasn't expecting so many arguments") + +def debugclient(config): + from buildbot.clients import debug + opts = loadOptions() + + master = config.get('master') + if not master: + master = opts.get('master') + if master is None: + raise usage.UsageError("master must be specified: on the command " + "line or in ~/.buildbot/options") + + passwd = config.get('passwd') + if not passwd: + passwd = opts.get('debugPassword') + if passwd is None: + raise usage.UsageError("passwd must be specified: on the command " + "line or in ~/.buildbot/options") + + d = debug.DebugWidget(master, passwd) + d.run() + +class StatusClientOptions(usage.Options): + optFlags = [ + ['help', 'h', "Display this message"], + ] + optParameters = [ + ["master", "m", None, + "Location of the buildmaster's status port (host:port)"], + ] + + def parseArgs(self, *args): + if len(args) > 0: + self['master'] = args[0] + if len(args) > 1: + raise usage.UsageError("I wasn't expecting so many arguments") + +def statuslog(config): + from buildbot.clients import base + opts = loadOptions() + master = config.get('master') + if not master: + master = opts.get('masterstatus') + if master is None: + raise usage.UsageError("master must be specified: on the command " + "line or in ~/.buildbot/options") + c = base.TextClient(master) + c.run() + +def statusgui(config): + from buildbot.clients import gtkPanes + opts = loadOptions() + master = config.get('master') + if not master: + master = opts.get('masterstatus') + if master is None: + raise usage.UsageError("master must be specified: on the command " + "line or in ~/.buildbot/options") + c = gtkPanes.GtkClient(master) + c.run() + +class SendChangeOptions(usage.Options): + optParameters = [ + ("master", "m", None, + "Location of the buildmaster's PBListener (host:port)"), + ("username", "u", None, "Username performing the commit"), + ("branch", "b", None, "Branch specifier"), + ("category", "c", None, "Category of repository"), + ("revision", "r", None, "Revision specifier (string)"), + ("revision_number", "n", None, "Revision specifier (integer)"), + ("revision_file", None, None, "Filename containing revision spec"), + ("comments", "m", None, "log message"), + ("logfile", "F", None, + "Read the log messages from this file (- for stdin)"), + ] + def getSynopsis(self): + return "Usage: buildbot sendchange [options] filenames.." + def parseArgs(self, *args): + self['files'] = args + + +def sendchange(config, runReactor=False): + """Send a single change to the buildmaster's PBChangeSource. The + connection will be drpoped as soon as the Change has been sent.""" + from buildbot.clients.sendchange import Sender + + opts = loadOptions() + user = config.get('username', opts.get('username')) + master = config.get('master', opts.get('master')) + branch = config.get('branch', opts.get('branch')) + category = config.get('category', opts.get('category')) + revision = config.get('revision') + # SVN and P4 use numeric revisions + if config.get("revision_number"): + revision = int(config['revision_number']) + if config.get("revision_file"): + revision = open(config["revision_file"],"r").read() + + comments = config.get('comments') + if not comments and config.get('logfile'): + if config['logfile'] == "-": + f = sys.stdin + else: + f = open(config['logfile'], "rt") + comments = f.read() + if comments is None: + comments = "" + + files = config.get('files', []) + + assert user, "you must provide a username" + assert master, "you must provide the master location" + + s = Sender(master, user) + d = s.send(branch, revision, comments, files, category=category) + if runReactor: + d.addCallbacks(s.printSuccess, s.printFailure) + d.addBoth(s.stop) + s.run() + return d + + +class ForceOptions(usage.Options): + optParameters = [ + ["builder", None, None, "which Builder to start"], + ["branch", None, None, "which branch to build"], + ["revision", None, None, "which revision to build"], + ["reason", None, None, "the reason for starting the build"], + ] + + def parseArgs(self, *args): + args = list(args) + if len(args) > 0: + if self['builder'] is not None: + raise usage.UsageError("--builder provided in two ways") + self['builder'] = args.pop(0) + if len(args) > 0: + if self['reason'] is not None: + raise usage.UsageError("--reason provided in two ways") + self['reason'] = " ".join(args) + + +class TryOptions(usage.Options): + optParameters = [ + ["connect", "c", None, + "how to reach the buildmaster, either 'ssh' or 'pb'"], + # for ssh, use --tryhost, --username, and --trydir + ["tryhost", None, None, + "the hostname (used by ssh) for the buildmaster"], + ["trydir", None, None, + "the directory (on the tryhost) where tryjobs are deposited"], + ["username", "u", None, "Username performing the trial build"], + # for PB, use --master, --username, and --passwd + ["master", "m", None, + "Location of the buildmaster's PBListener (host:port)"], + ["passwd", None, None, "password for PB authentication"], + + ["diff", None, None, + "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."], + ["patchlevel", "p", 0, + "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"], + + ["baserev", None, None, + "Base revision to use instead of scanning a local tree."], + + ["vc", None, None, + "The VC system in use, one of: cvs,svn,tla,baz,darcs"], + ["branch", None, None, + "The branch in use, for VC systems that can't figure it out" + " themselves"], + + ["builder", "b", None, + "Run the trial build on this Builder. Can be used multiple times."], + ["properties", None, None, + "A set of properties made available in the build environment, format:prop=value,propb=valueb..."], + ] + + optFlags = [ + ["wait", None, "wait until the builds have finished"], + ["dryrun", 'n', "Gather info, but don't actually submit."], + ] + + def __init__(self): + super(TryOptions, self).__init__() + self['builders'] = [] + self['properties'] = {} + + def opt_builder(self, option): + self['builders'].append(option) + + def opt_properties(self, option): + # We need to split the value of this option into a dictionary of properties + properties = {} + propertylist = option.split(",") + for i in range(0,len(propertylist)): + print propertylist[i] + splitproperty = propertylist[i].split("=") + properties[splitproperty[0]] = splitproperty[1] + self['properties'] = properties + + def opt_patchlevel(self, option): + self['patchlevel'] = int(option) + + def getSynopsis(self): + return "Usage: buildbot try [options]" + +def doTry(config): + from buildbot.scripts import tryclient + t = tryclient.Try(config) + t.run() + +class TryServerOptions(usage.Options): + optParameters = [ + ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"], + ] + +def doTryServer(config): + import md5 + jobdir = os.path.expanduser(config["jobdir"]) + job = sys.stdin.read() + # now do a 'safecat'-style write to jobdir/tmp, then move atomically to + # jobdir/new . Rather than come up with a unique name randomly, I'm just + # going to MD5 the contents and prepend a timestamp. + timestring = "%d" % time.time() + jobhash = md5.new(job).hexdigest() + fn = "%s-%s" % (timestring, jobhash) + tmpfile = os.path.join(jobdir, "tmp", fn) + newfile = os.path.join(jobdir, "new", fn) + f = open(tmpfile, "w") + f.write(job) + f.close() + os.rename(tmpfile, newfile) + + +class CheckConfigOptions(usage.Options): + optFlags = [ + ['quiet', 'q', "Don't display error messages or tracebacks"], + ] + + def getSynopsis(self): + return "Usage :buildbot checkconfig [configFile]\n" + \ + " If not specified, 'master.cfg' will be used as 'configFile'" + + def parseArgs(self, *args): + if len(args) >= 1: + self['configFile'] = args[0] + else: + self['configFile'] = 'master.cfg' + + +def doCheckConfig(config): + quiet = config.get('quiet') + configFile = config.get('configFile') + try: + from buildbot.scripts.checkconfig import ConfigLoader + ConfigLoader(configFile) + except: + if not quiet: + # Print out the traceback in a nice format + t, v, tb = sys.exc_info() + traceback.print_exception(t, v, tb) + sys.exit(1) + + if not quiet: + print "Config file is good!" + + +class Options(usage.Options): + synopsis = "Usage: buildbot <command> [command options]" + + subCommands = [ + # the following are all admin commands + ['create-master', None, MasterOptions, + "Create and populate a directory for a new buildmaster"], + ['upgrade-master', None, UpgradeMasterOptions, + "Upgrade an existing buildmaster directory for the current version"], + ['create-slave', None, SlaveOptions, + "Create and populate a directory for a new buildslave"], + ['start', None, StartOptions, "Start a buildmaster or buildslave"], + ['stop', None, StopOptions, "Stop a buildmaster or buildslave"], + ['restart', None, RestartOptions, + "Restart a buildmaster or buildslave"], + + ['reconfig', None, ReconfigOptions, + "SIGHUP a buildmaster to make it re-read the config file"], + ['sighup', None, ReconfigOptions, + "SIGHUP a buildmaster to make it re-read the config file"], + + ['sendchange', None, SendChangeOptions, + "Send a change to the buildmaster"], + + ['debugclient', None, DebugClientOptions, + "Launch a small debug panel GUI"], + + ['statuslog', None, StatusClientOptions, + "Emit current builder status to stdout"], + ['statusgui', None, StatusClientOptions, + "Display a small window showing current builder status"], + + #['force', None, ForceOptions, "Run a build"], + ['try', None, TryOptions, "Run a build with your local changes"], + + ['tryserver', None, TryServerOptions, + "buildmaster-side 'try' support function, not for users"], + + ['checkconfig', None, CheckConfigOptions, + "test the validity of a master.cfg config file"], + + # TODO: 'watch' + ] + + def opt_version(self): + import buildbot + print "Buildbot version: %s" % buildbot.version + usage.Options.opt_version(self) + + def opt_verbose(self): + from twisted.python import log + log.startLogging(sys.stderr) + + def postOptions(self): + if not hasattr(self, 'subOptions'): + raise usage.UsageError("must specify a command") + + +def run(): + config = Options() + try: + config.parseOptions() + except usage.error, e: + print "%s: %s" % (sys.argv[0], e) + print + c = getattr(config, 'subOptions', config) + print str(c) + sys.exit(1) + + command = config.subCommand + so = config.subOptions + + if command == "create-master": + createMaster(so) + elif command == "upgrade-master": + upgradeMaster(so) + elif command == "create-slave": + createSlave(so) + elif command == "start": + from buildbot.scripts.startup import start + start(so) + elif command == "stop": + stop(so, wait=True) + elif command == "restart": + restart(so) + elif command == "reconfig" or command == "sighup": + from buildbot.scripts.reconfig import Reconfigurator + Reconfigurator().run(so) + elif command == "sendchange": + sendchange(so, True) + elif command == "debugclient": + debugclient(so) + elif command == "statuslog": + statuslog(so) + elif command == "statusgui": + statusgui(so) + elif command == "try": + doTry(so) + elif command == "tryserver": + doTryServer(so) + elif command == "checkconfig": + doCheckConfig(so) + + |