remove spurious 'print'
[buildbot.git] / buildbot / scripts / runner.py
blob10e4c7fc25c668de05c6e0e38d4c66fafc341d86
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"],
322 ["log-size", "s", "1000000",
323 "size at which to rotate twisted log files"],
324 ["log-count", "l", "None",
325 "limit the number of kept old twisted log files"],
327 def getSynopsis(self):
328 return "Usage: buildbot create-master [options] <basedir>"
330 longdesc = """
331 This command creates a buildmaster working directory and buildbot.tac
332 file. The master will live in <dir> and create various files there.
334 At runtime, the master will read a configuration file (named
335 'master.cfg' by default) in its basedir. This file should contain python
336 code which eventually defines a dictionary named 'BuildmasterConfig'.
337 The elements of this dictionary are used to configure the Buildmaster.
338 See doc/config.xhtml for details about what can be controlled through
339 this interface."""
341 def postOptions(self):
342 MakerBase.postOptions(self)
343 if not re.match('^\d+$', self['log-size']):
344 raise usage.UsageError("log-size parameter needs to be an int")
345 if not re.match('^\d+$', self['log-count']) and \
346 self['log-count'] != 'None':
347 raise usage.UsageError("log-count parameter needs to be an int "+
348 " or None")
351 masterTAC = """
352 from twisted.application import service
353 from buildbot.master import BuildMaster
355 basedir = r'%(basedir)s'
356 configfile = r'%(config)s'
357 rotateLength = %(log-size)s
358 maxRotatedFiles = %(log-count)s
360 application = service.Application('buildmaster')
361 try:
362 from twisted.python.logfile import LogFile
363 from twisted.python.log import ILogObserver, FileLogObserver
364 logfile = LogFile.fromFullPath("twistd.log", rotateLength=rotateLength,
365 maxRotatedFiles=maxRotatedFiles)
366 application.setComponent(ILogObserver, FileLogObserver(logfile).emit)
367 except ImportError:
368 # probably not yet twisted 8.2.0 and beyond, can't set log yet
369 pass
370 BuildMaster(basedir, configfile).setServiceParent(application)
374 def createMaster(config):
375 m = Maker(config)
376 m.mkdir()
377 m.chdir()
378 contents = masterTAC % config
379 m.makeTAC(contents)
380 m.sampleconfig(util.sibpath(__file__, "sample.cfg"))
381 m.public_html(util.sibpath(__file__, "../status/web/index.html"),
382 util.sibpath(__file__, "../status/web/classic.css"),
383 util.sibpath(__file__, "../status/web/robots.txt"),
385 m.makefile()
387 if not m.quiet: print "buildmaster configured in %s" % m.basedir
389 class SlaveOptions(MakerBase):
390 optFlags = [
391 ["force", "f", "Re-use an existing directory"],
393 optParameters = [
394 # ["name", "n", None, "Name for this build slave"],
395 # ["passwd", "p", None, "Password for this build slave"],
396 # ["basedir", "d", ".", "Base directory to use"],
397 # ["master", "m", "localhost:8007",
398 # "Location of the buildmaster (host:port)"],
400 ["keepalive", "k", 600,
401 "Interval at which keepalives should be sent (in seconds)"],
402 ["usepty", None, 0,
403 "(1 or 0) child processes should be run in a pty (default 0)"],
404 ["umask", None, "None",
405 "controls permissions of generated files. Use --umask=022 to be world-readable"],
406 ["maxdelay", None, 300,
407 "Maximum time between connection attempts"],
408 ["log-size", "s", "1000000",
409 "size at which to rotate twisted log files"],
410 ["log-count", "l", "None",
411 "limit the number of kept old twisted log files"],
414 longdesc = """
415 This command creates a buildslave working directory and buildbot.tac
416 file. The bot will use the <name> and <passwd> arguments to authenticate
417 itself when connecting to the master. All commands are run in a
418 build-specific subdirectory of <basedir>. <master> is a string of the
419 form 'hostname:port', and specifies where the buildmaster can be reached.
421 <name>, <passwd>, and <master> will be provided by the buildmaster
422 administrator for your bot. You must choose <basedir> yourself.
425 def getSynopsis(self):
426 return "Usage: buildbot create-slave [options] <basedir> <master> <name> <passwd>"
428 def parseArgs(self, *args):
429 if len(args) < 4:
430 raise usage.UsageError("command needs more arguments")
431 basedir, master, name, passwd = args
432 self['basedir'] = basedir
433 self['master'] = master
434 self['name'] = name
435 self['passwd'] = passwd
437 def postOptions(self):
438 MakerBase.postOptions(self)
439 self['usepty'] = int(self['usepty'])
440 self['keepalive'] = int(self['keepalive'])
441 self['maxdelay'] = int(self['maxdelay'])
442 if self['master'].find(":") == -1:
443 raise usage.UsageError("--master must be in the form host:portnum")
444 if not re.match('^\d+$', self['log-size']):
445 raise usage.UsageError("log-size parameter needs to be an int")
446 if not re.match('^\d+$', self['log-count']) and \
447 self['log-count'] != 'None':
448 raise usage.UsageError("log-count parameter needs to be an int "+
449 " or None")
451 slaveTAC = """
452 from twisted.application import service
453 from buildbot.slave.bot import BuildSlave
455 basedir = r'%(basedir)s'
456 buildmaster_host = '%(host)s'
457 port = %(port)d
458 slavename = '%(name)s'
459 passwd = '%(passwd)s'
460 keepalive = %(keepalive)d
461 usepty = %(usepty)d
462 umask = %(umask)s
463 maxdelay = %(maxdelay)d
464 rotateLength = %(log-size)s
465 maxRotatedFiles = %(log-count)s
467 application = service.Application('buildslave')
468 try:
469 from twisted.python.logfile import LogFile
470 from twisted.python.log import ILogObserver, FileLogObserver
471 logfile = LogFile.fromFullPath("twistd.log", rotateLength=rotateLength,
472 maxRotatedFiles=maxRotatedFiles)
473 application.setComponent(ILogObserver, FileLogObserver(logfile).emit)
474 except ImportError:
475 # probably not yet twisted 8.2.0 and beyond, can't set log yet
476 pass
477 s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir,
478 keepalive, usepty, umask=umask, maxdelay=maxdelay)
479 s.setServiceParent(application)
483 def createSlave(config):
484 m = Maker(config)
485 m.mkdir()
486 m.chdir()
487 try:
488 master = config['master']
489 host, port = re.search(r'(.+):(\d+)', master).groups()
490 config['host'] = host
491 config['port'] = int(port)
492 except:
493 print "unparseable master location '%s'" % master
494 print " expecting something more like localhost:8007"
495 raise
496 contents = slaveTAC % config
498 m.makeTAC(contents, secret=True)
500 m.makefile()
501 m.mkinfo()
503 if not m.quiet: print "buildslave configured in %s" % m.basedir
507 def stop(config, signame="TERM", wait=False):
508 import signal
509 basedir = config['basedir']
510 quiet = config['quiet']
511 os.chdir(basedir)
512 try:
513 f = open("twistd.pid", "rt")
514 except:
515 raise BuildbotNotRunningError
516 pid = int(f.read().strip())
517 signum = getattr(signal, "SIG"+signame)
518 timer = 0
519 os.kill(pid, signum)
520 if not wait:
521 if not quiet:
522 print "sent SIG%s to process" % signame
523 return
524 time.sleep(0.1)
525 while timer < 10:
526 # poll once per second until twistd.pid goes away, up to 10 seconds
527 try:
528 os.kill(pid, 0)
529 except OSError:
530 if not quiet:
531 print "buildbot process %d is dead" % pid
532 return
533 timer += 1
534 time.sleep(1)
535 if not quiet:
536 print "never saw process go away"
538 def restart(config):
539 quiet = config['quiet']
540 from buildbot.scripts.startup import start
541 try:
542 stop(config, wait=True)
543 except BuildbotNotRunningError:
544 pass
545 if not quiet:
546 print "now restarting buildbot process.."
547 start(config)
550 def loadOptions(filename="options", here=None, home=None):
551 """Find the .buildbot/FILENAME file. Crawl from the current directory up
552 towards the root, and also look in ~/.buildbot . The first directory
553 that's owned by the user and has the file we're looking for wins. Windows
554 skips the owned-by-user test.
556 @rtype: dict
557 @return: a dictionary of names defined in the options file. If no options
558 file was found, return an empty dict.
561 if here is None:
562 here = os.getcwd()
563 here = os.path.abspath(here)
565 if home is None:
566 if runtime.platformType == 'win32':
567 home = os.path.join(os.environ['APPDATA'], "buildbot")
568 else:
569 home = os.path.expanduser("~/.buildbot")
571 searchpath = []
572 toomany = 20
573 while True:
574 searchpath.append(os.path.join(here, ".buildbot"))
575 next = os.path.dirname(here)
576 if next == here:
577 break # we've hit the root
578 here = next
579 toomany -= 1 # just in case
580 if toomany == 0:
581 raise ValueError("Hey, I seem to have wandered up into the "
582 "infinite glories of the heavens. Oops.")
583 searchpath.append(home)
585 localDict = {}
587 for d in searchpath:
588 if os.path.isdir(d):
589 if runtime.platformType != 'win32':
590 if os.stat(d)[stat.ST_UID] != os.getuid():
591 print "skipping %s because you don't own it" % d
592 continue # security, skip other people's directories
593 optfile = os.path.join(d, filename)
594 if os.path.exists(optfile):
595 try:
596 f = open(optfile, "r")
597 options = f.read()
598 exec options in localDict
599 except:
600 print "error while reading %s" % optfile
601 raise
602 break
604 for k in localDict.keys():
605 if k.startswith("__"):
606 del localDict[k]
607 return localDict
609 class StartOptions(MakerBase):
610 optFlags = [
611 ['quiet', 'q', "Don't display startup log messages"],
613 def getSynopsis(self):
614 return "Usage: buildbot start <basedir>"
616 class StopOptions(MakerBase):
617 def getSynopsis(self):
618 return "Usage: buildbot stop <basedir>"
620 class ReconfigOptions(MakerBase):
621 optFlags = [
622 ['quiet', 'q', "Don't display log messages about reconfiguration"],
624 def getSynopsis(self):
625 return "Usage: buildbot reconfig <basedir>"
629 class RestartOptions(MakerBase):
630 optFlags = [
631 ['quiet', 'q', "Don't display startup log messages"],
633 def getSynopsis(self):
634 return "Usage: buildbot restart <basedir>"
636 class DebugClientOptions(usage.Options):
637 optFlags = [
638 ['help', 'h', "Display this message"],
640 optParameters = [
641 ["master", "m", None,
642 "Location of the buildmaster's slaveport (host:port)"],
643 ["passwd", "p", None, "Debug password to use"],
646 def parseArgs(self, *args):
647 if len(args) > 0:
648 self['master'] = args[0]
649 if len(args) > 1:
650 self['passwd'] = args[1]
651 if len(args) > 2:
652 raise usage.UsageError("I wasn't expecting so many arguments")
654 def debugclient(config):
655 from buildbot.clients import debug
656 opts = loadOptions()
658 master = config.get('master')
659 if not master:
660 master = opts.get('master')
661 if master is None:
662 raise usage.UsageError("master must be specified: on the command "
663 "line or in ~/.buildbot/options")
665 passwd = config.get('passwd')
666 if not passwd:
667 passwd = opts.get('debugPassword')
668 if passwd is None:
669 raise usage.UsageError("passwd must be specified: on the command "
670 "line or in ~/.buildbot/options")
672 d = debug.DebugWidget(master, passwd)
673 d.run()
675 class StatusClientOptions(usage.Options):
676 optFlags = [
677 ['help', 'h', "Display this message"],
679 optParameters = [
680 ["master", "m", None,
681 "Location of the buildmaster's status port (host:port)"],
684 def parseArgs(self, *args):
685 if len(args) > 0:
686 self['master'] = args[0]
687 if len(args) > 1:
688 raise usage.UsageError("I wasn't expecting so many arguments")
690 def statuslog(config):
691 from buildbot.clients import base
692 opts = loadOptions()
693 master = config.get('master')
694 if not master:
695 master = opts.get('masterstatus')
696 if master is None:
697 raise usage.UsageError("master must be specified: on the command "
698 "line or in ~/.buildbot/options")
699 c = base.TextClient(master)
700 c.run()
702 def statusgui(config):
703 from buildbot.clients import gtkPanes
704 opts = loadOptions()
705 master = config.get('master')
706 if not master:
707 master = opts.get('masterstatus')
708 if master is None:
709 raise usage.UsageError("master must be specified: on the command "
710 "line or in ~/.buildbot/options")
711 c = gtkPanes.GtkClient(master)
712 c.run()
714 class SendChangeOptions(usage.Options):
715 optParameters = [
716 ("master", "m", None,
717 "Location of the buildmaster's PBListener (host:port)"),
718 ("username", "u", None, "Username performing the commit"),
719 ("branch", "b", None, "Branch specifier"),
720 ("category", "c", None, "Category of repository"),
721 ("revision", "r", None, "Revision specifier (string)"),
722 ("revision_number", "n", None, "Revision specifier (integer)"),
723 ("revision_file", None, None, "Filename containing revision spec"),
724 ("comments", "m", None, "log message"),
725 ("logfile", "F", None,
726 "Read the log messages from this file (- for stdin)"),
728 def getSynopsis(self):
729 return "Usage: buildbot sendchange [options] filenames.."
730 def parseArgs(self, *args):
731 self['files'] = args
734 def sendchange(config, runReactor=False):
735 """Send a single change to the buildmaster's PBChangeSource. The
736 connection will be drpoped as soon as the Change has been sent."""
737 from buildbot.clients.sendchange import Sender
739 opts = loadOptions()
740 user = config.get('username', opts.get('username'))
741 master = config.get('master', opts.get('master'))
742 branch = config.get('branch', opts.get('branch'))
743 category = config.get('category', opts.get('category'))
744 revision = config.get('revision')
745 # SVN and P4 use numeric revisions
746 if config.get("revision_number"):
747 revision = int(config['revision_number'])
748 if config.get("revision_file"):
749 revision = open(config["revision_file"],"r").read()
751 comments = config.get('comments')
752 if not comments and config.get('logfile'):
753 if config['logfile'] == "-":
754 f = sys.stdin
755 else:
756 f = open(config['logfile'], "rt")
757 comments = f.read()
758 if comments is None:
759 comments = ""
761 files = config.get('files', [])
763 assert user, "you must provide a username"
764 assert master, "you must provide the master location"
766 s = Sender(master, user)
767 d = s.send(branch, revision, comments, files, category=category)
768 if runReactor:
769 d.addCallbacks(s.printSuccess, s.printFailure)
770 d.addBoth(s.stop)
771 s.run()
772 return d
775 class ForceOptions(usage.Options):
776 optParameters = [
777 ["builder", None, None, "which Builder to start"],
778 ["branch", None, None, "which branch to build"],
779 ["revision", None, None, "which revision to build"],
780 ["reason", None, None, "the reason for starting the build"],
783 def parseArgs(self, *args):
784 args = list(args)
785 if len(args) > 0:
786 if self['builder'] is not None:
787 raise usage.UsageError("--builder provided in two ways")
788 self['builder'] = args.pop(0)
789 if len(args) > 0:
790 if self['reason'] is not None:
791 raise usage.UsageError("--reason provided in two ways")
792 self['reason'] = " ".join(args)
795 class TryOptions(usage.Options):
796 optParameters = [
797 ["connect", "c", None,
798 "how to reach the buildmaster, either 'ssh' or 'pb'"],
799 # for ssh, use --tryhost, --username, and --trydir
800 ["tryhost", None, None,
801 "the hostname (used by ssh) for the buildmaster"],
802 ["trydir", None, None,
803 "the directory (on the tryhost) where tryjobs are deposited"],
804 ["username", "u", None, "Username performing the trial build"],
805 # for PB, use --master, --username, and --passwd
806 ["master", "m", None,
807 "Location of the buildmaster's PBListener (host:port)"],
808 ["passwd", None, None, "password for PB authentication"],
810 ["diff", None, None,
811 "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."],
812 ["patchlevel", "p", 0,
813 "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"],
815 ["baserev", None, None,
816 "Base revision to use instead of scanning a local tree."],
818 ["vc", None, None,
819 "The VC system in use, one of: cvs,svn,tla,baz,darcs"],
820 ["branch", None, None,
821 "The branch in use, for VC systems that can't figure it out"
822 " themselves"],
824 ["builder", "b", None,
825 "Run the trial build on this Builder. Can be used multiple times."],
826 ["properties", None, None,
827 "A set of properties made available in the build environment, format:prop=value,propb=valueb..."],
830 optFlags = [
831 ["wait", None, "wait until the builds have finished"],
832 ["dryrun", 'n', "Gather info, but don't actually submit."],
835 def __init__(self):
836 super(TryOptions, self).__init__()
837 self['builders'] = []
838 self['properties'] = {}
840 def opt_builder(self, option):
841 self['builders'].append(option)
843 def opt_properties(self, option):
844 # We need to split the value of this option into a dictionary of properties
845 properties = {}
846 propertylist = option.split(",")
847 for i in range(0,len(propertylist)):
848 print propertylist[i]
849 splitproperty = propertylist[i].split("=")
850 properties[splitproperty[0]] = splitproperty[1]
851 self['properties'] = properties
853 def opt_patchlevel(self, option):
854 self['patchlevel'] = int(option)
856 def getSynopsis(self):
857 return "Usage: buildbot try [options]"
859 def doTry(config):
860 from buildbot.scripts import tryclient
861 t = tryclient.Try(config)
862 t.run()
864 class TryServerOptions(usage.Options):
865 optParameters = [
866 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"],
869 def doTryServer(config):
870 import md5
871 jobdir = os.path.expanduser(config["jobdir"])
872 job = sys.stdin.read()
873 # now do a 'safecat'-style write to jobdir/tmp, then move atomically to
874 # jobdir/new . Rather than come up with a unique name randomly, I'm just
875 # going to MD5 the contents and prepend a timestamp.
876 timestring = "%d" % time.time()
877 jobhash = md5.new(job).hexdigest()
878 fn = "%s-%s" % (timestring, jobhash)
879 tmpfile = os.path.join(jobdir, "tmp", fn)
880 newfile = os.path.join(jobdir, "new", fn)
881 f = open(tmpfile, "w")
882 f.write(job)
883 f.close()
884 os.rename(tmpfile, newfile)
887 class CheckConfigOptions(usage.Options):
888 optFlags = [
889 ['quiet', 'q', "Don't display error messages or tracebacks"],
892 def getSynopsis(self):
893 return "Usage :buildbot checkconfig [configFile]\n" + \
894 " If not specified, 'master.cfg' will be used as 'configFile'"
896 def parseArgs(self, *args):
897 if len(args) >= 1:
898 self['configFile'] = args[0]
899 else:
900 self['configFile'] = 'master.cfg'
903 def doCheckConfig(config):
904 quiet = config.get('quiet')
905 configFileName = config.get('configFile')
906 try:
907 from buildbot.scripts.checkconfig import ConfigLoader
908 if os.path.isdir(configFileName):
909 ConfigLoader(basedir=configFileName)
910 else:
911 ConfigLoader(configFileName=configFileName)
912 except:
913 if not quiet:
914 # Print out the traceback in a nice format
915 t, v, tb = sys.exc_info()
916 traceback.print_exception(t, v, tb)
917 sys.exit(1)
919 if not quiet:
920 print "Config file is good!"
923 class Options(usage.Options):
924 synopsis = "Usage: buildbot <command> [command options]"
926 subCommands = [
927 # the following are all admin commands
928 ['create-master', None, MasterOptions,
929 "Create and populate a directory for a new buildmaster"],
930 ['upgrade-master', None, UpgradeMasterOptions,
931 "Upgrade an existing buildmaster directory for the current version"],
932 ['create-slave', None, SlaveOptions,
933 "Create and populate a directory for a new buildslave"],
934 ['start', None, StartOptions, "Start a buildmaster or buildslave"],
935 ['stop', None, StopOptions, "Stop a buildmaster or buildslave"],
936 ['restart', None, RestartOptions,
937 "Restart a buildmaster or buildslave"],
939 ['reconfig', None, ReconfigOptions,
940 "SIGHUP a buildmaster to make it re-read the config file"],
941 ['sighup', None, ReconfigOptions,
942 "SIGHUP a buildmaster to make it re-read the config file"],
944 ['sendchange', None, SendChangeOptions,
945 "Send a change to the buildmaster"],
947 ['debugclient', None, DebugClientOptions,
948 "Launch a small debug panel GUI"],
950 ['statuslog', None, StatusClientOptions,
951 "Emit current builder status to stdout"],
952 ['statusgui', None, StatusClientOptions,
953 "Display a small window showing current builder status"],
955 #['force', None, ForceOptions, "Run a build"],
956 ['try', None, TryOptions, "Run a build with your local changes"],
958 ['tryserver', None, TryServerOptions,
959 "buildmaster-side 'try' support function, not for users"],
961 ['checkconfig', None, CheckConfigOptions,
962 "test the validity of a master.cfg config file"],
964 # TODO: 'watch'
967 def opt_version(self):
968 import buildbot
969 print "Buildbot version: %s" % buildbot.version
970 usage.Options.opt_version(self)
972 def opt_verbose(self):
973 from twisted.python import log
974 log.startLogging(sys.stderr)
976 def postOptions(self):
977 if not hasattr(self, 'subOptions'):
978 raise usage.UsageError("must specify a command")
981 def run():
982 config = Options()
983 try:
984 config.parseOptions()
985 except usage.error, e:
986 print "%s: %s" % (sys.argv[0], e)
987 print
988 c = getattr(config, 'subOptions', config)
989 print str(c)
990 sys.exit(1)
992 command = config.subCommand
993 so = config.subOptions
995 if command == "create-master":
996 createMaster(so)
997 elif command == "upgrade-master":
998 upgradeMaster(so)
999 elif command == "create-slave":
1000 createSlave(so)
1001 elif command == "start":
1002 from buildbot.scripts.startup import start
1003 start(so)
1004 elif command == "stop":
1005 stop(so, wait=True)
1006 elif command == "restart":
1007 restart(so)
1008 elif command == "reconfig" or command == "sighup":
1009 from buildbot.scripts.reconfig import Reconfigurator
1010 Reconfigurator().run(so)
1011 elif command == "sendchange":
1012 sendchange(so, True)
1013 elif command == "debugclient":
1014 debugclient(so)
1015 elif command == "statuslog":
1016 statuslog(so)
1017 elif command == "statusgui":
1018 statusgui(so)
1019 elif command == "try":
1020 doTry(so)
1021 elif command == "tryserver":
1022 doTryServer(so)
1023 elif command == "checkconfig":
1024 doCheckConfig(so)