make 'buildbot restart' work when buildbot is not running
[buildbot.git] / buildbot / scripts / runner.py
blob47c22b7e3841a808e0f3cd08e00d0ce33df8ade2
1 # -*- test-case-name: buildbot.test.test_runner -*-
3 # N.B.: don't import anything that might pull in a reactor yet. Some of our
4 # subcommands want to load modules that need the gtk reactor.
5 import os, sys, stat, re, time
6 import traceback
7 from twisted.python import usage, util, runtime
9 from buildbot.interfaces import BuildbotNotRunningError
11 # this is mostly just a front-end for mktap, twistd, and kill(1), but in the
12 # future it will also provide an interface to some developer tools that talk
13 # directly to a remote buildmaster (like 'try' and a status client)
15 # the create/start/stop commands should all be run as the same user,
16 # preferably a separate 'buildbot' account.
18 class MakerBase(usage.Options):
19 optFlags = [
20 ['help', 'h', "Display this message"],
21 ["quiet", "q", "Do not emit the commands being run"],
24 #["basedir", "d", None, "Base directory for the buildmaster"],
25 opt_h = usage.Options.opt_help
27 def parseArgs(self, *args):
28 if len(args) > 0:
29 self['basedir'] = args[0]
30 else:
31 self['basedir'] = None
32 if len(args) > 1:
33 raise usage.UsageError("I wasn't expecting so many arguments")
35 def postOptions(self):
36 if self['basedir'] is None:
37 raise usage.UsageError("<basedir> parameter is required")
38 self['basedir'] = os.path.abspath(self['basedir'])
40 makefile_sample = """# -*- makefile -*-
42 # This is a simple makefile which lives in a buildmaster/buildslave
43 # directory (next to the buildbot.tac file). It allows you to start/stop the
44 # master or slave by doing 'make start' or 'make stop'.
46 # The 'reconfig' target will tell a buildmaster to reload its config file.
48 start:
49 twistd --no_save -y buildbot.tac
51 stop:
52 kill `cat twistd.pid`
54 reconfig:
55 kill -HUP `cat twistd.pid`
57 log:
58 tail -f twistd.log
59 """
61 class Maker:
62 def __init__(self, config):
63 self.config = config
64 self.basedir = config['basedir']
65 self.force = config.get('force', False)
66 self.quiet = config['quiet']
68 def mkdir(self):
69 if os.path.exists(self.basedir):
70 if not self.quiet:
71 print "updating existing installation"
72 return
73 if not self.quiet: print "mkdir", self.basedir
74 os.mkdir(self.basedir)
76 def mkinfo(self):
77 path = os.path.join(self.basedir, "info")
78 if not os.path.exists(path):
79 if not self.quiet: print "mkdir", path
80 os.mkdir(path)
81 created = False
82 admin = os.path.join(path, "admin")
83 if not os.path.exists(admin):
84 if not self.quiet:
85 print "Creating info/admin, you need to edit it appropriately"
86 f = open(admin, "wt")
87 f.write("Your Name Here <admin@youraddress.invalid>\n")
88 f.close()
89 created = True
90 host = os.path.join(path, "host")
91 if not os.path.exists(host):
92 if not self.quiet:
93 print "Creating info/host, you need to edit it appropriately"
94 f = open(host, "wt")
95 f.write("Please put a description of this build host here\n")
96 f.close()
97 created = True
98 if created and not self.quiet:
99 print "Please edit the files in %s appropriately." % path
101 def chdir(self):
102 if not self.quiet: print "chdir", self.basedir
103 os.chdir(self.basedir)
105 def makeTAC(self, contents, secret=False):
106 tacfile = "buildbot.tac"
107 if os.path.exists(tacfile):
108 oldcontents = open(tacfile, "rt").read()
109 if oldcontents == contents:
110 if not self.quiet:
111 print "buildbot.tac already exists and is correct"
112 return
113 if not self.quiet:
114 print "not touching existing buildbot.tac"
115 print "creating buildbot.tac.new instead"
116 tacfile = "buildbot.tac.new"
117 f = open(tacfile, "wt")
118 f.write(contents)
119 f.close()
120 if secret:
121 os.chmod(tacfile, 0600)
123 def makefile(self):
124 target = "Makefile.sample"
125 if os.path.exists(target):
126 oldcontents = open(target, "rt").read()
127 if oldcontents == makefile_sample:
128 if not self.quiet:
129 print "Makefile.sample already exists and is correct"
130 return
131 if not self.quiet:
132 print "replacing Makefile.sample"
133 else:
134 if not self.quiet:
135 print "creating Makefile.sample"
136 f = open(target, "wt")
137 f.write(makefile_sample)
138 f.close()
140 def sampleconfig(self, source):
141 target = "master.cfg.sample"
142 config_sample = open(source, "rt").read()
143 if os.path.exists(target):
144 oldcontents = open(target, "rt").read()
145 if oldcontents == config_sample:
146 if not self.quiet:
147 print "master.cfg.sample already exists and is up-to-date"
148 return
149 if not self.quiet:
150 print "replacing master.cfg.sample"
151 else:
152 if not self.quiet:
153 print "creating master.cfg.sample"
154 f = open(target, "wt")
155 f.write(config_sample)
156 f.close()
157 os.chmod(target, 0600)
159 def public_html(self, index_html, buildbot_css, robots_txt):
160 webdir = os.path.join(self.basedir, "public_html")
161 if os.path.exists(webdir):
162 if not self.quiet:
163 print "public_html/ already exists: not replacing"
164 return
165 else:
166 os.mkdir(webdir)
167 if not self.quiet:
168 print "populating public_html/"
169 target = os.path.join(webdir, "index.html")
170 f = open(target, "wt")
171 f.write(open(index_html, "rt").read())
172 f.close()
174 target = os.path.join(webdir, "buildbot.css")
175 f = open(target, "wt")
176 f.write(open(buildbot_css, "rt").read())
177 f.close()
179 target = os.path.join(webdir, "robots.txt")
180 f = open(target, "wt")
181 f.write(open(robots_txt, "rt").read())
182 f.close()
184 def populate_if_missing(self, target, source, overwrite=False):
185 new_contents = open(source, "rt").read()
186 if os.path.exists(target):
187 old_contents = open(target, "rt").read()
188 if old_contents != new_contents:
189 if overwrite:
190 if not self.quiet:
191 print "%s has old/modified contents" % target
192 print " overwriting it with new contents"
193 open(target, "wt").write(new_contents)
194 else:
195 if not self.quiet:
196 print "%s has old/modified contents" % target
197 print " writing new contents to %s.new" % target
198 open(target + ".new", "wt").write(new_contents)
199 # otherwise, it's up to date
200 else:
201 if not self.quiet:
202 print "populating %s" % target
203 open(target, "wt").write(new_contents)
205 def upgrade_public_html(self, index_html, buildbot_css, robots_txt):
206 webdir = os.path.join(self.basedir, "public_html")
207 if not os.path.exists(webdir):
208 if not self.quiet:
209 print "populating public_html/"
210 os.mkdir(webdir)
211 self.populate_if_missing(os.path.join(webdir, "index.html"),
212 index_html)
213 self.populate_if_missing(os.path.join(webdir, "buildbot.css"),
214 buildbot_css)
215 self.populate_if_missing(os.path.join(webdir, "robots.txt"),
216 robots_txt)
218 def check_master_cfg(self):
219 from buildbot.master import BuildMaster
220 from twisted.python import log, failure
222 master_cfg = os.path.join(self.basedir, "master.cfg")
223 if not os.path.exists(master_cfg):
224 if not self.quiet:
225 print "No master.cfg found"
226 return 1
228 # side-effects of loading the config file:
230 # for each Builder defined in c['builders'], if the status directory
231 # didn't already exist, it will be created, and the
232 # $BUILDERNAME/builder pickle might be created (with a single
233 # "builder created" event).
235 # we put basedir in front of sys.path, because that's how the
236 # buildmaster itself will run, and it is quite common to have the
237 # buildmaster import helper classes from other .py files in its
238 # basedir.
240 if sys.path[0] != self.basedir:
241 sys.path.insert(0, self.basedir)
243 m = BuildMaster(self.basedir)
244 # we need to route log.msg to stdout, so any problems can be seen
245 # there. But if everything goes well, I'd rather not clutter stdout
246 # with log messages. So instead we add a logObserver which gathers
247 # messages and only displays them if something goes wrong.
248 messages = []
249 log.addObserver(messages.append)
250 try:
251 # this will raise an exception if there's something wrong with
252 # the config file. Note that this BuildMaster instance is never
253 # started, so it won't actually do anything with the
254 # configuration.
255 m.loadConfig(open(master_cfg, "r"))
256 except:
257 f = failure.Failure()
258 if not self.quiet:
259 print
260 for m in messages:
261 print "".join(m['message'])
262 print f
263 print
264 print "An error was detected in the master.cfg file."
265 print "Please correct the problem and run 'buildbot upgrade-master' again."
266 print
267 return 1
268 return 0
270 class UpgradeMasterOptions(MakerBase):
271 optFlags = [
272 ["replace", "r", "Replace any modified files without confirmation."],
275 def getSynopsis(self):
276 return "Usage: buildbot upgrade-master [options] <basedir>"
278 longdesc = """
279 This command takes an existing buildmaster working directory and
280 adds/modifies the files there to work with the current version of
281 buildbot. When this command is finished, the buildmaster directory should
282 look much like a brand-new one created by the 'create-master' command.
284 Use this after you've upgraded your buildbot installation and before you
285 restart the buildmaster to use the new version.
287 If you have modified the files in your working directory, this command
288 will leave them untouched, but will put the new recommended contents in a
289 .new file (for example, if index.html has been modified, this command
290 will create index.html.new). You can then look at the new version and
291 decide how to merge its contents into your modified file.
294 def upgradeMaster(config):
295 basedir = config['basedir']
296 m = Maker(config)
297 # TODO: check Makefile
298 # TODO: check TAC file
299 # check web files: index.html, classic.css, robots.txt
300 webdir = os.path.join(basedir, "public_html")
301 m.upgrade_public_html(util.sibpath(__file__, "../status/web/index.html"),
302 util.sibpath(__file__, "../status/web/classic.css"),
303 util.sibpath(__file__, "../status/web/robots.txt"),
305 m.populate_if_missing(os.path.join(basedir, "master.cfg.sample"),
306 util.sibpath(__file__, "sample.cfg"),
307 overwrite=True)
308 rc = m.check_master_cfg()
309 if rc:
310 return rc
311 if not config['quiet']:
312 print "upgrade complete"
315 class MasterOptions(MakerBase):
316 optFlags = [
317 ["force", "f",
318 "Re-use an existing directory (will not overwrite master.cfg file)"],
320 optParameters = [
321 ["config", "c", "master.cfg", "name of the buildmaster config file"],
323 def getSynopsis(self):
324 return "Usage: buildbot create-master [options] <basedir>"
326 longdesc = """
327 This command creates a buildmaster working directory and buildbot.tac
328 file. The master will live in <dir> and create various files there.
330 At runtime, the master will read a configuration file (named
331 'master.cfg' by default) in its basedir. This file should contain python
332 code which eventually defines a dictionary named 'BuildmasterConfig'.
333 The elements of this dictionary are used to configure the Buildmaster.
334 See doc/config.xhtml for details about what can be controlled through
335 this interface."""
337 masterTAC = """
338 from twisted.application import service
339 from buildbot.master import BuildMaster
341 basedir = r'%(basedir)s'
342 configfile = r'%(config)s'
344 application = service.Application('buildmaster')
345 BuildMaster(basedir, configfile).setServiceParent(application)
349 def createMaster(config):
350 m = Maker(config)
351 m.mkdir()
352 m.chdir()
353 contents = masterTAC % config
354 m.makeTAC(contents)
355 m.sampleconfig(util.sibpath(__file__, "sample.cfg"))
356 m.public_html(util.sibpath(__file__, "../status/web/index.html"),
357 util.sibpath(__file__, "../status/web/classic.css"),
358 util.sibpath(__file__, "../status/web/robots.txt"),
360 m.makefile()
362 if not m.quiet: print "buildmaster configured in %s" % m.basedir
364 class SlaveOptions(MakerBase):
365 optFlags = [
366 ["force", "f", "Re-use an existing directory"],
368 optParameters = [
369 # ["name", "n", None, "Name for this build slave"],
370 # ["passwd", "p", None, "Password for this build slave"],
371 # ["basedir", "d", ".", "Base directory to use"],
372 # ["master", "m", "localhost:8007",
373 # "Location of the buildmaster (host:port)"],
375 ["keepalive", "k", 600,
376 "Interval at which keepalives should be sent (in seconds)"],
377 ["usepty", None, 1,
378 "(1 or 0) child processes should be run in a pty"],
379 ["umask", None, "None",
380 "controls permissions of generated files. Use --umask=022 to be world-readable"],
383 longdesc = """
384 This command creates a buildslave working directory and buildbot.tac
385 file. The bot will use the <name> and <passwd> arguments to authenticate
386 itself when connecting to the master. All commands are run in a
387 build-specific subdirectory of <basedir>. <master> is a string of the
388 form 'hostname:port', and specifies where the buildmaster can be reached.
390 <name>, <passwd>, and <master> will be provided by the buildmaster
391 administrator for your bot. You must choose <basedir> yourself.
394 def getSynopsis(self):
395 return "Usage: buildbot create-slave [options] <basedir> <master> <name> <passwd>"
397 def parseArgs(self, *args):
398 if len(args) < 4:
399 raise usage.UsageError("command needs more arguments")
400 basedir, master, name, passwd = args
401 self['basedir'] = basedir
402 self['master'] = master
403 self['name'] = name
404 self['passwd'] = passwd
406 def postOptions(self):
407 MakerBase.postOptions(self)
408 self['usepty'] = int(self['usepty'])
409 self['keepalive'] = int(self['keepalive'])
410 if self['master'].find(":") == -1:
411 raise usage.UsageError("--master must be in the form host:portnum")
413 slaveTAC = """
414 from twisted.application import service
415 from buildbot.slave.bot import BuildSlave
417 basedir = r'%(basedir)s'
418 buildmaster_host = '%(host)s'
419 port = %(port)d
420 slavename = '%(name)s'
421 passwd = '%(passwd)s'
422 keepalive = %(keepalive)d
423 usepty = %(usepty)d
424 umask = %(umask)s
426 application = service.Application('buildslave')
427 s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir,
428 keepalive, usepty, umask=umask)
429 s.setServiceParent(application)
433 def createSlave(config):
434 m = Maker(config)
435 m.mkdir()
436 m.chdir()
437 try:
438 master = config['master']
439 host, port = re.search(r'(.+):(\d+)', master).groups()
440 config['host'] = host
441 config['port'] = int(port)
442 except:
443 print "unparseable master location '%s'" % master
444 print " expecting something more like localhost:8007"
445 raise
446 contents = slaveTAC % config
448 m.makeTAC(contents, secret=True)
450 m.makefile()
451 m.mkinfo()
453 if not m.quiet: print "buildslave configured in %s" % m.basedir
457 def stop(config, signame="TERM", wait=False):
458 import signal
459 basedir = config['basedir']
460 quiet = config['quiet']
461 os.chdir(basedir)
462 try:
463 f = open("twistd.pid", "rt")
464 except:
465 raise BuildbotNotRunningError
466 pid = int(f.read().strip())
467 signum = getattr(signal, "SIG"+signame)
468 timer = 0
469 os.kill(pid, signum)
470 if not wait:
471 if not quiet:
472 print "sent SIG%s to process" % signame
473 return
474 time.sleep(0.1)
475 while timer < 10:
476 # poll once per second until twistd.pid goes away, up to 10 seconds
477 try:
478 os.kill(pid, 0)
479 except OSError:
480 if not quiet:
481 print "buildbot process %d is dead" % pid
482 return
483 timer += 1
484 time.sleep(1)
485 if not quiet:
486 print "never saw process go away"
488 def restart(config):
489 quiet = config['quiet']
490 from buildbot.scripts.startup import start
491 try:
492 stop(config, wait=True)
493 except BuildbotNotRunningError:
494 pass
495 if not quiet:
496 print "now restarting buildbot process.."
497 start(config)
500 def loadOptions(filename="options", here=None, home=None):
501 """Find the .buildbot/FILENAME file. Crawl from the current directory up
502 towards the root, and also look in ~/.buildbot . The first directory
503 that's owned by the user and has the file we're looking for wins. Windows
504 skips the owned-by-user test.
506 @rtype: dict
507 @return: a dictionary of names defined in the options file. If no options
508 file was found, return an empty dict.
511 if here is None:
512 here = os.getcwd()
513 here = os.path.abspath(here)
515 if home is None:
516 if runtime.platformType == 'win32':
517 home = os.path.join(os.environ['APPDATA'], "buildbot")
518 else:
519 home = os.path.expanduser("~/.buildbot")
521 searchpath = []
522 toomany = 20
523 while True:
524 searchpath.append(os.path.join(here, ".buildbot"))
525 next = os.path.dirname(here)
526 if next == here:
527 break # we've hit the root
528 here = next
529 toomany -= 1 # just in case
530 if toomany == 0:
531 raise ValueError("Hey, I seem to have wandered up into the "
532 "infinite glories of the heavens. Oops.")
533 searchpath.append(home)
535 localDict = {}
537 for d in searchpath:
538 if os.path.isdir(d):
539 if runtime.platformType != 'win32':
540 if os.stat(d)[stat.ST_UID] != os.getuid():
541 print "skipping %s because you don't own it" % d
542 continue # security, skip other people's directories
543 optfile = os.path.join(d, filename)
544 if os.path.exists(optfile):
545 try:
546 f = open(optfile, "r")
547 options = f.read()
548 exec options in localDict
549 except:
550 print "error while reading %s" % optfile
551 raise
552 break
554 for k in localDict.keys():
555 if k.startswith("__"):
556 del localDict[k]
557 return localDict
559 class StartOptions(MakerBase):
560 optFlags = [
561 ['quiet', 'q', "Don't display startup log messages"],
563 def getSynopsis(self):
564 return "Usage: buildbot start <basedir>"
566 class StopOptions(MakerBase):
567 def getSynopsis(self):
568 return "Usage: buildbot stop <basedir>"
570 class ReconfigOptions(MakerBase):
571 optFlags = [
572 ['quiet', 'q', "Don't display log messages about reconfiguration"],
574 def getSynopsis(self):
575 return "Usage: buildbot reconfig <basedir>"
579 class RestartOptions(MakerBase):
580 optFlags = [
581 ['quiet', 'q', "Don't display startup log messages"],
583 def getSynopsis(self):
584 return "Usage: buildbot restart <basedir>"
586 class DebugClientOptions(usage.Options):
587 optFlags = [
588 ['help', 'h', "Display this message"],
590 optParameters = [
591 ["master", "m", None,
592 "Location of the buildmaster's slaveport (host:port)"],
593 ["passwd", "p", None, "Debug password to use"],
596 def parseArgs(self, *args):
597 if len(args) > 0:
598 self['master'] = args[0]
599 if len(args) > 1:
600 self['passwd'] = args[1]
601 if len(args) > 2:
602 raise usage.UsageError("I wasn't expecting so many arguments")
604 def debugclient(config):
605 from buildbot.clients import debug
606 opts = loadOptions()
608 master = config.get('master')
609 if not master:
610 master = opts.get('master')
611 if master is None:
612 raise usage.UsageError("master must be specified: on the command "
613 "line or in ~/.buildbot/options")
615 passwd = config.get('passwd')
616 if not passwd:
617 passwd = opts.get('debugPassword')
618 if passwd is None:
619 raise usage.UsageError("passwd must be specified: on the command "
620 "line or in ~/.buildbot/options")
622 d = debug.DebugWidget(master, passwd)
623 d.run()
625 class StatusClientOptions(usage.Options):
626 optFlags = [
627 ['help', 'h', "Display this message"],
629 optParameters = [
630 ["master", "m", None,
631 "Location of the buildmaster's status port (host:port)"],
634 def parseArgs(self, *args):
635 if len(args) > 0:
636 self['master'] = args[0]
637 if len(args) > 1:
638 raise usage.UsageError("I wasn't expecting so many arguments")
640 def statuslog(config):
641 from buildbot.clients import base
642 opts = loadOptions()
643 master = config.get('master')
644 if not master:
645 master = opts.get('masterstatus')
646 if master is None:
647 raise usage.UsageError("master must be specified: on the command "
648 "line or in ~/.buildbot/options")
649 c = base.TextClient(master)
650 c.run()
652 def statusgui(config):
653 from buildbot.clients import gtkPanes
654 opts = loadOptions()
655 master = config.get('master')
656 if not master:
657 master = opts.get('masterstatus')
658 if master is None:
659 raise usage.UsageError("master must be specified: on the command "
660 "line or in ~/.buildbot/options")
661 c = gtkPanes.GtkClient(master)
662 c.run()
664 class SendChangeOptions(usage.Options):
665 optParameters = [
666 ("master", "m", None,
667 "Location of the buildmaster's PBListener (host:port)"),
668 ("username", "u", None, "Username performing the commit"),
669 ("branch", "b", None, "Branch specifier"),
670 ("revision", "r", None, "Revision specifier (string)"),
671 ("revision_number", "n", None, "Revision specifier (integer)"),
672 ("revision_file", None, None, "Filename containing revision spec"),
673 ("comments", "m", None, "log message"),
674 ("logfile", "F", None,
675 "Read the log messages from this file (- for stdin)"),
677 def getSynopsis(self):
678 return "Usage: buildbot sendchange [options] filenames.."
679 def parseArgs(self, *args):
680 self['files'] = args
683 def sendchange(config, runReactor=False):
684 """Send a single change to the buildmaster's PBChangeSource. The
685 connection will be drpoped as soon as the Change has been sent."""
686 from buildbot.clients.sendchange import Sender
688 opts = loadOptions()
689 user = config.get('username', opts.get('username'))
690 master = config.get('master', opts.get('master'))
691 branch = config.get('branch', opts.get('branch'))
692 revision = config.get('revision')
693 # SVN and P4 use numeric revisions
694 if config.get("revision_number"):
695 revision = int(config['revision_number'])
696 if config.get("revision_file"):
697 revision = open(config["revision_file"],"r").read()
699 comments = config.get('comments')
700 if not comments and config.get('logfile'):
701 if config['logfile'] == "-":
702 f = sys.stdin
703 else:
704 f = open(config['logfile'], "rt")
705 comments = f.read()
706 if comments is None:
707 comments = ""
709 files = config.get('files', [])
711 assert user, "you must provide a username"
712 assert master, "you must provide the master location"
714 s = Sender(master, user)
715 d = s.send(branch, revision, comments, files)
716 if runReactor:
717 d.addCallbacks(s.printSuccess, s.printFailure)
718 d.addBoth(s.stop)
719 s.run()
720 return d
723 class ForceOptions(usage.Options):
724 optParameters = [
725 ["builder", None, None, "which Builder to start"],
726 ["branch", None, None, "which branch to build"],
727 ["revision", None, None, "which revision to build"],
728 ["reason", None, None, "the reason for starting the build"],
731 def parseArgs(self, *args):
732 args = list(args)
733 if len(args) > 0:
734 if self['builder'] is not None:
735 raise usage.UsageError("--builder provided in two ways")
736 self['builder'] = args.pop(0)
737 if len(args) > 0:
738 if self['reason'] is not None:
739 raise usage.UsageError("--reason provided in two ways")
740 self['reason'] = " ".join(args)
743 class TryOptions(usage.Options):
744 optParameters = [
745 ["connect", "c", None,
746 "how to reach the buildmaster, either 'ssh' or 'pb'"],
747 # for ssh, use --tryhost, --username, and --trydir
748 ["tryhost", None, None,
749 "the hostname (used by ssh) for the buildmaster"],
750 ["trydir", None, None,
751 "the directory (on the tryhost) where tryjobs are deposited"],
752 ["username", "u", None, "Username performing the trial build"],
753 # for PB, use --master, --username, and --passwd
754 ["master", "m", None,
755 "Location of the buildmaster's PBListener (host:port)"],
756 ["passwd", None, None, "password for PB authentication"],
758 ["diff", None, None,
759 "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."],
760 ["patchlevel", "p", 0,
761 "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"],
763 ["baserev", None, None,
764 "Base revision to use instead of scanning a local tree."],
766 ["vc", None, None,
767 "The VC system in use, one of: cvs,svn,tla,baz,darcs"],
768 ["branch", None, None,
769 "The branch in use, for VC systems that can't figure it out"
770 " themselves"],
772 ["builder", "b", None,
773 "Run the trial build on this Builder. Can be used multiple times."],
774 ["properties", None, None,
775 "A set of properties made available in the build environment, format:prop=value,propb=valueb..."],
778 optFlags = [
779 ["wait", None, "wait until the builds have finished"],
782 def __init__(self):
783 super(TryOptions, self).__init__()
784 self['builders'] = []
785 self['properties'] = {}
787 def opt_builder(self, option):
788 self['builders'].append(option)
790 def opt_properties(self, option):
791 # We need to split the value of this option into a dictionary of properties
792 properties = {}
793 propertylist = option.split(",")
794 for i in range(0,len(propertylist)):
795 print propertylist[i]
796 splitproperty = propertylist[i].split("=")
797 properties[splitproperty[0]] = splitproperty[1]
798 self['properties'] = properties
800 def opt_patchlevel(self, option):
801 self['patchlevel'] = int(option)
803 def getSynopsis(self):
804 return "Usage: buildbot try [options]"
806 def doTry(config):
807 from buildbot.scripts import tryclient
808 t = tryclient.Try(config)
809 t.run()
811 class TryServerOptions(usage.Options):
812 optParameters = [
813 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"],
816 def doTryServer(config):
817 import md5
818 jobdir = os.path.expanduser(config["jobdir"])
819 job = sys.stdin.read()
820 # now do a 'safecat'-style write to jobdir/tmp, then move atomically to
821 # jobdir/new . Rather than come up with a unique name randomly, I'm just
822 # going to MD5 the contents and prepend a timestamp.
823 timestring = "%d" % time.time()
824 jobhash = md5.new(job).hexdigest()
825 fn = "%s-%s" % (timestring, jobhash)
826 tmpfile = os.path.join(jobdir, "tmp", fn)
827 newfile = os.path.join(jobdir, "new", fn)
828 f = open(tmpfile, "w")
829 f.write(job)
830 f.close()
831 os.rename(tmpfile, newfile)
834 class CheckConfigOptions(usage.Options):
835 optFlags = [
836 ['quiet', 'q', "Don't display error messages or tracebacks"],
839 def getSynopsis(self):
840 return "Usage :buildbot checkconfig [configFile]\n" + \
841 " If not specified, 'master.cfg' will be used as 'configFile'"
843 def parseArgs(self, *args):
844 if len(args) >= 1:
845 self['configFile'] = args[0]
846 else:
847 self['configFile'] = 'master.cfg'
850 def doCheckConfig(config):
851 quiet = config.get('quiet')
852 configFile = config.get('configFile')
853 try:
854 from buildbot.scripts.checkconfig import ConfigLoader
855 ConfigLoader(configFile)
856 except:
857 if not quiet:
858 # Print out the traceback in a nice format
859 t, v, tb = sys.exc_info()
860 traceback.print_exception(t, v, tb)
861 sys.exit(1)
863 if not quiet:
864 print "Config file is good!"
867 class Options(usage.Options):
868 synopsis = "Usage: buildbot <command> [command options]"
870 subCommands = [
871 # the following are all admin commands
872 ['create-master', None, MasterOptions,
873 "Create and populate a directory for a new buildmaster"],
874 ['upgrade-master', None, UpgradeMasterOptions,
875 "Upgrade an existing buildmaster directory for the current version"],
876 ['create-slave', None, SlaveOptions,
877 "Create and populate a directory for a new buildslave"],
878 ['start', None, StartOptions, "Start a buildmaster or buildslave"],
879 ['stop', None, StopOptions, "Stop a buildmaster or buildslave"],
880 ['restart', None, RestartOptions,
881 "Restart a buildmaster or buildslave"],
883 ['reconfig', None, ReconfigOptions,
884 "SIGHUP a buildmaster to make it re-read the config file"],
885 ['sighup', None, ReconfigOptions,
886 "SIGHUP a buildmaster to make it re-read the config file"],
888 ['sendchange', None, SendChangeOptions,
889 "Send a change to the buildmaster"],
891 ['debugclient', None, DebugClientOptions,
892 "Launch a small debug panel GUI"],
894 ['statuslog', None, StatusClientOptions,
895 "Emit current builder status to stdout"],
896 ['statusgui', None, StatusClientOptions,
897 "Display a small window showing current builder status"],
899 #['force', None, ForceOptions, "Run a build"],
900 ['try', None, TryOptions, "Run a build with your local changes"],
902 ['tryserver', None, TryServerOptions,
903 "buildmaster-side 'try' support function, not for users"],
905 ['checkconfig', None, CheckConfigOptions,
906 "test the validity of a master.cfg config file"],
908 # TODO: 'watch'
911 def opt_version(self):
912 import buildbot
913 print "Buildbot version: %s" % buildbot.version
914 usage.Options.opt_version(self)
916 def opt_verbose(self):
917 from twisted.python import log
918 log.startLogging(sys.stderr)
920 def postOptions(self):
921 if not hasattr(self, 'subOptions'):
922 raise usage.UsageError("must specify a command")
925 def run():
926 config = Options()
927 try:
928 config.parseOptions()
929 except usage.error, e:
930 print "%s: %s" % (sys.argv[0], e)
931 print
932 c = getattr(config, 'subOptions', config)
933 print str(c)
934 sys.exit(1)
936 command = config.subCommand
937 so = config.subOptions
939 if command == "create-master":
940 createMaster(so)
941 elif command == "upgrade-master":
942 upgradeMaster(so)
943 elif command == "create-slave":
944 createSlave(so)
945 elif command == "start":
946 from buildbot.scripts.startup import start
947 start(so)
948 elif command == "stop":
949 stop(so, wait=True)
950 elif command == "restart":
951 restart(so)
952 elif command == "reconfig" or command == "sighup":
953 from buildbot.scripts.reconfig import Reconfigurator
954 Reconfigurator().run(so)
955 elif command == "sendchange":
956 sendchange(so, True)
957 elif command == "debugclient":
958 debugclient(so)
959 elif command == "statuslog":
960 statuslog(so)
961 elif command == "statusgui":
962 statusgui(so)
963 elif command == "try":
964 doTry(so)
965 elif command == "tryserver":
966 doTryServer(so)
967 elif command == "checkconfig":
968 doCheckConfig(so)