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
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
):
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
):
29 self
['basedir'] = args
[0]
31 self
['basedir'] = None
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.
49 twistd --no_save -y buildbot.tac
55 kill -HUP `cat twistd.pid`
62 def __init__(self
, config
):
64 self
.basedir
= config
['basedir']
65 self
.force
= config
.get('force', False)
66 self
.quiet
= config
['quiet']
69 if os
.path
.exists(self
.basedir
):
71 print "updating existing installation"
73 if not self
.quiet
: print "mkdir", self
.basedir
74 os
.mkdir(self
.basedir
)
77 path
= os
.path
.join(self
.basedir
, "info")
78 if not os
.path
.exists(path
):
79 if not self
.quiet
: print "mkdir", path
82 admin
= os
.path
.join(path
, "admin")
83 if not os
.path
.exists(admin
):
85 print "Creating info/admin, you need to edit it appropriately"
87 f
.write("Your Name Here <admin@youraddress.invalid>\n")
90 host
= os
.path
.join(path
, "host")
91 if not os
.path
.exists(host
):
93 print "Creating info/host, you need to edit it appropriately"
95 f
.write("Please put a description of this build host here\n")
98 if created
and not self
.quiet
:
99 print "Please edit the files in %s appropriately." % path
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
:
111 print "buildbot.tac already exists and is correct"
114 print "not touching existing buildbot.tac"
115 print "creating buildbot.tac.new instead"
116 tacfile
= "buildbot.tac.new"
117 f
= open(tacfile
, "wt")
121 os
.chmod(tacfile
, 0600)
124 target
= "Makefile.sample"
125 if os
.path
.exists(target
):
126 oldcontents
= open(target
, "rt").read()
127 if oldcontents
== makefile_sample
:
129 print "Makefile.sample already exists and is correct"
132 print "replacing Makefile.sample"
135 print "creating Makefile.sample"
136 f
= open(target
, "wt")
137 f
.write(makefile_sample
)
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
:
147 print "master.cfg.sample already exists and is up-to-date"
150 print "replacing master.cfg.sample"
153 print "creating master.cfg.sample"
154 f
= open(target
, "wt")
155 f
.write(config_sample
)
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
):
163 print "public_html/ already exists: not replacing"
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())
174 target
= os
.path
.join(webdir
, "buildbot.css")
175 f
= open(target
, "wt")
176 f
.write(open(buildbot_css
, "rt").read())
179 target
= os
.path
.join(webdir
, "robots.txt")
180 f
= open(target
, "wt")
181 f
.write(open(robots_txt
, "rt").read())
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
:
191 print "%s has old/modified contents" % target
192 print " overwriting it with new contents"
193 open(target
, "wt").write(new_contents
)
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
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
):
209 print "populating public_html/"
211 self
.populate_if_missing(os
.path
.join(webdir
, "index.html"),
213 self
.populate_if_missing(os
.path
.join(webdir
, "buildbot.css"),
215 self
.populate_if_missing(os
.path
.join(webdir
, "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
):
225 print "No master.cfg found"
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
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.
249 log
.addObserver(messages
.append
)
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
255 m
.loadConfig(open(master_cfg
, "r"))
257 f
= failure
.Failure()
261 print "".join(m
['message'])
264 print "An error was detected in the master.cfg file."
265 print "Please correct the problem and run 'buildbot upgrade-master' again."
270 class UpgradeMasterOptions(MakerBase
):
272 ["replace", "r", "Replace any modified files without confirmation."],
275 def getSynopsis(self
):
276 return "Usage: buildbot upgrade-master [options] <basedir>"
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']
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"),
308 rc
= m
.check_master_cfg()
311 if not config
['quiet']:
312 print "upgrade complete"
315 class MasterOptions(MakerBase
):
318 "Re-use an existing directory (will not overwrite master.cfg file)"],
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>"
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
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 "+
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')
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)
368 # probably not yet twisted 8.2.0 and beyond, can't set log yet
370 BuildMaster(basedir, configfile).setServiceParent(application)
374 def createMaster(config
):
378 contents
= masterTAC
% config
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"),
387 if not m
.quiet
: print "buildmaster configured in %s" % m
.basedir
389 class SlaveOptions(MakerBase
):
391 ["force", "f", "Re-use an existing directory"],
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)"],
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"],
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
):
430 raise usage
.UsageError("command needs more arguments")
431 basedir
, master
, name
, passwd
= args
432 self
['basedir'] = basedir
433 self
['master'] = master
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 "+
452 from twisted.application import service
453 from buildbot.slave.bot import BuildSlave
455 basedir = r'%(basedir)s'
456 buildmaster_host = '%(host)s'
458 slavename = '%(name)s'
459 passwd = '%(passwd)s'
460 keepalive = %(keepalive)d
463 maxdelay = %(maxdelay)d
464 rotateLength = %(log-size)s
465 maxRotatedFiles = %(log-count)s
467 application = service.Application('buildslave')
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)
475 # probably not yet twisted 8.2.0 and beyond, can't set log yet
477 s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir,
478 keepalive, usepty, umask=umask, maxdelay=maxdelay)
479 s.setServiceParent(application)
483 def createSlave(config
):
488 master
= config
['master']
489 host
, port
= re
.search(r
'(.+):(\d+)', master
).groups()
490 config
['host'] = host
491 config
['port'] = int(port
)
493 print "unparseable master location '%s'" % master
494 print " expecting something more like localhost:8007"
496 contents
= slaveTAC
% config
498 m
.makeTAC(contents
, secret
=True)
503 if not m
.quiet
: print "buildslave configured in %s" % m
.basedir
507 def stop(config
, signame
="TERM", wait
=False):
509 basedir
= config
['basedir']
510 quiet
= config
['quiet']
513 f
= open("twistd.pid", "rt")
515 raise BuildbotNotRunningError
516 pid
= int(f
.read().strip())
517 signum
= getattr(signal
, "SIG"+signame
)
522 print "sent SIG%s to process" % signame
526 # poll once per second until twistd.pid goes away, up to 10 seconds
531 print "buildbot process %d is dead" % pid
536 print "never saw process go away"
539 quiet
= config
['quiet']
540 from buildbot
.scripts
.startup
import start
542 stop(config
, wait
=True)
543 except BuildbotNotRunningError
:
546 print "now restarting buildbot process.."
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.
557 @return: a dictionary of names defined in the options file. If no options
558 file was found, return an empty dict.
563 here
= os
.path
.abspath(here
)
566 if runtime
.platformType
== 'win32':
567 home
= os
.path
.join(os
.environ
['APPDATA'], "buildbot")
569 home
= os
.path
.expanduser("~/.buildbot")
574 searchpath
.append(os
.path
.join(here
, ".buildbot"))
575 next
= os
.path
.dirname(here
)
577 break # we've hit the root
579 toomany
-= 1 # just in case
581 raise ValueError("Hey, I seem to have wandered up into the "
582 "infinite glories of the heavens. Oops.")
583 searchpath
.append(home
)
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
):
596 f
= open(optfile
, "r")
598 exec options
in localDict
600 print "error while reading %s" % optfile
604 for k
in localDict
.keys():
605 if k
.startswith("__"):
609 class StartOptions(MakerBase
):
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
):
622 ['quiet', 'q', "Don't display log messages about reconfiguration"],
624 def getSynopsis(self
):
625 return "Usage: buildbot reconfig <basedir>"
629 class RestartOptions(MakerBase
):
631 ['quiet', 'q', "Don't display startup log messages"],
633 def getSynopsis(self
):
634 return "Usage: buildbot restart <basedir>"
636 class DebugClientOptions(usage
.Options
):
638 ['help', 'h', "Display this message"],
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
):
648 self
['master'] = args
[0]
650 self
['passwd'] = args
[1]
652 raise usage
.UsageError("I wasn't expecting so many arguments")
654 def debugclient(config
):
655 from buildbot
.clients
import debug
658 master
= config
.get('master')
660 master
= opts
.get('master')
662 raise usage
.UsageError("master must be specified: on the command "
663 "line or in ~/.buildbot/options")
665 passwd
= config
.get('passwd')
667 passwd
= opts
.get('debugPassword')
669 raise usage
.UsageError("passwd must be specified: on the command "
670 "line or in ~/.buildbot/options")
672 d
= debug
.DebugWidget(master
, passwd
)
675 class StatusClientOptions(usage
.Options
):
677 ['help', 'h', "Display this message"],
680 ["master", "m", None,
681 "Location of the buildmaster's status port (host:port)"],
684 def parseArgs(self
, *args
):
686 self
['master'] = args
[0]
688 raise usage
.UsageError("I wasn't expecting so many arguments")
690 def statuslog(config
):
691 from buildbot
.clients
import base
693 master
= config
.get('master')
695 master
= opts
.get('masterstatus')
697 raise usage
.UsageError("master must be specified: on the command "
698 "line or in ~/.buildbot/options")
699 c
= base
.TextClient(master
)
702 def statusgui(config
):
703 from buildbot
.clients
import gtkPanes
705 master
= config
.get('master')
707 master
= opts
.get('masterstatus')
709 raise usage
.UsageError("master must be specified: on the command "
710 "line or in ~/.buildbot/options")
711 c
= gtkPanes
.GtkClient(master
)
714 class SendChangeOptions(usage
.Options
):
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
):
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
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'] == "-":
756 f
= open(config
['logfile'], "rt")
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
)
769 d
.addCallbacks(s
.printSuccess
, s
.printFailure
)
775 class ForceOptions(usage
.Options
):
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
):
786 if self
['builder'] is not None:
787 raise usage
.UsageError("--builder provided in two ways")
788 self
['builder'] = args
.pop(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
):
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"],
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."],
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"
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..."],
831 ["wait", None, "wait until the builds have finished"],
832 ["dryrun", 'n', "Gather info, but don't actually submit."],
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
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]"
860 from buildbot
.scripts
import tryclient
861 t
= tryclient
.Try(config
)
864 class TryServerOptions(usage
.Options
):
866 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"],
869 def doTryServer(config
):
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")
884 os
.rename(tmpfile
, newfile
)
887 class CheckConfigOptions(usage
.Options
):
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
):
898 self
['configFile'] = args
[0]
900 self
['configFile'] = 'master.cfg'
903 def doCheckConfig(config
):
904 quiet
= config
.get('quiet')
905 configFileName
= config
.get('configFile')
907 from buildbot
.scripts
.checkconfig
import ConfigLoader
908 if os
.path
.isdir(configFileName
):
909 ConfigLoader(basedir
=configFileName
)
911 ConfigLoader(configFileName
=configFileName
)
914 # Print out the traceback in a nice format
915 t
, v
, tb
= sys
.exc_info()
916 traceback
.print_exception(t
, v
, tb
)
920 print "Config file is good!"
923 class Options(usage
.Options
):
924 synopsis
= "Usage: buildbot <command> [command options]"
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"],
967 def opt_version(self
):
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")
984 config
.parseOptions()
985 except usage
.error
, e
:
986 print "%s: %s" % (sys
.argv
[0], e
)
988 c
= getattr(config
, 'subOptions', config
)
992 command
= config
.subCommand
993 so
= config
.subOptions
995 if command
== "create-master":
997 elif command
== "upgrade-master":
999 elif command
== "create-slave":
1001 elif command
== "start":
1002 from buildbot
.scripts
.startup
import start
1004 elif command
== "stop":
1006 elif command
== "restart":
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":
1015 elif command
== "statuslog":
1017 elif command
== "statusgui":
1019 elif command
== "try":
1021 elif command
== "tryserver":
1023 elif command
== "checkconfig":