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 from twisted
.python
import usage
, util
, runtime
8 # this is mostly just a front-end for mktap, twistd, and kill(1), but in the
9 # future it will also provide an interface to some developer tools that talk
10 # directly to a remote buildmaster (like 'try' and a status client)
12 # the create/start/stop commands should all be run as the same user,
13 # preferably a separate 'buildbot' account.
15 class MakerBase(usage
.Options
):
17 ['help', 'h', "Display this message"],
18 ["quiet", "q", "Do not emit the commands being run"],
21 #["basedir", "d", None, "Base directory for the buildmaster"],
22 opt_h
= usage
.Options
.opt_help
24 def parseArgs(self
, *args
):
26 self
['basedir'] = args
[0]
28 self
['basedir'] = None
30 raise usage
.UsageError("I wasn't expecting so many arguments")
32 def postOptions(self
):
33 if self
['basedir'] is None:
34 raise usage
.UsageError("<basedir> parameter is required")
35 self
['basedir'] = os
.path
.abspath(self
['basedir'])
37 makefile_sample
= """# -*- makefile -*-
39 # This is a simple makefile which lives in a buildmaster/buildslave
40 # directory (next to the buildbot.tac file). It allows you to start/stop the
41 # master or slave by doing 'make start' or 'make stop'.
43 # The 'reconfig' target will tell a buildmaster to reload its config file.
46 twistd --no_save -y buildbot.tac
52 kill -HUP `cat twistd.pid`
59 def __init__(self
, config
):
61 self
.basedir
= config
['basedir']
62 self
.force
= config
.get('force', False)
63 self
.quiet
= config
['quiet']
66 if os
.path
.exists(self
.basedir
):
68 print "updating existing installation"
70 if not self
.quiet
: print "mkdir", self
.basedir
71 os
.mkdir(self
.basedir
)
74 path
= os
.path
.join(self
.basedir
, "info")
75 if not os
.path
.exists(path
):
76 if not self
.quiet
: print "mkdir", path
79 admin
= os
.path
.join(path
, "admin")
80 if not os
.path
.exists(admin
):
82 print "Creating info/admin, you need to edit it appropriately"
84 f
.write("Your Name Here <admin@youraddress.invalid>\n")
87 host
= os
.path
.join(path
, "host")
88 if not os
.path
.exists(host
):
90 print "Creating info/host, you need to edit it appropriately"
92 f
.write("Please put a description of this build host here\n")
95 if created
and not self
.quiet
:
96 print "Please edit the files in %s appropriately." % path
99 if not self
.quiet
: print "chdir", self
.basedir
100 os
.chdir(self
.basedir
)
102 def makeTAC(self
, contents
, secret
=False):
103 tacfile
= "buildbot.tac"
104 if os
.path
.exists(tacfile
):
105 oldcontents
= open(tacfile
, "rt").read()
106 if oldcontents
== contents
:
108 print "buildbot.tac already exists and is correct"
111 print "not touching existing buildbot.tac"
112 print "creating buildbot.tac.new instead"
113 tacfile
= "buildbot.tac.new"
114 f
= open(tacfile
, "wt")
118 os
.chmod(tacfile
, 0600)
121 target
= "Makefile.sample"
122 if os
.path
.exists(target
):
123 oldcontents
= open(target
, "rt").read()
124 if oldcontents
== makefile_sample
:
126 print "Makefile.sample already exists and is correct"
129 print "replacing Makefile.sample"
132 print "creating Makefile.sample"
133 f
= open(target
, "wt")
134 f
.write(makefile_sample
)
137 def sampleconfig(self
, source
):
138 target
= "master.cfg.sample"
139 config_sample
= open(source
, "rt").read()
140 if os
.path
.exists(target
):
141 oldcontents
= open(target
, "rt").read()
142 if oldcontents
== config_sample
:
144 print "master.cfg.sample already exists and is up-to-date"
147 print "replacing master.cfg.sample"
150 print "creating master.cfg.sample"
151 f
= open(target
, "wt")
152 f
.write(config_sample
)
154 os
.chmod(target
, 0600)
156 def public_html(self
, index_html
, buildbot_css
, robots_txt
,
158 webdir
= os
.path
.join(self
.basedir
, "public_html")
159 if os
.path
.exists(webdir
):
162 print "public_html/ already exists: not replacing"
167 print "populating public_html/"
168 target
= os
.path
.join(webdir
, "index.html")
169 f
= open(target
, "wt")
170 f
.write(open(index_html
, "rt").read())
173 target
= os
.path
.join(webdir
, "buildbot.css")
174 f
= open(target
, "wt")
175 f
.write(open(buildbot_css
, "rt").read())
178 target
= os
.path
.join(webdir
, "robots.txt")
179 f
= open(target
, "wt")
180 f
.write(open(robots_txt
, "rt").read())
184 class UpgradeMasterOptions(MakerBase
):
186 ["replace", "r", "Replace any modified files without confirmation."],
189 def getSynopsis(self
):
190 return "Usage: buildbot upgrade-master [options] <basedir>"
193 This command takes an existing buildmaster working directory and
194 adds/modifies the files there to work with the current version of
195 buildbot. When this command is finished, the buildmaster directory should
196 look much like a brand-new one created by the 'create-master' command.
198 Use this after you've upgraded your buildbot installation and before you
199 restart the buildmaster to use the new version.
201 If you have modified the files in your working directory, this command
202 will leave them untouched, but will put the new recommended contents in a
203 .new file (for example, if index.html has been modified, this command
204 will create index.html.new). You can then look at the new version and
205 decide how to merge its contents into your modified file.
208 def upgradeMaster(config
):
209 basedir
= config
['basedir']
214 # check web files: index.html, classic.css, robots.txt
215 webdir
= os
.path
.join(basedir
, "public_html")
216 m
.public_html(util
.sibpath(__file__
, "../status/web/index.html"),
217 util
.sibpath(__file__
, "../status/web/classic.css"),
218 util
.sibpath(__file__
, "../status/web/robots.txt"),
223 class MasterOptions(MakerBase
):
226 "Re-use an existing directory (will not overwrite master.cfg file)"],
229 ["config", "c", "master.cfg", "name of the buildmaster config file"],
231 def getSynopsis(self
):
232 return "Usage: buildbot create-master [options] <basedir>"
235 This command creates a buildmaster working directory and buildbot.tac
236 file. The master will live in <dir> and create various files there.
238 At runtime, the master will read a configuration file (named
239 'master.cfg' by default) in its basedir. This file should contain python
240 code which eventually defines a dictionary named 'BuildmasterConfig'.
241 The elements of this dictionary are used to configure the Buildmaster.
242 See doc/config.xhtml for details about what can be controlled through
246 from twisted.application import service
247 from buildbot.master import BuildMaster
249 basedir = r'%(basedir)s'
250 configfile = r'%(config)s'
252 application = service.Application('buildmaster')
253 BuildMaster(basedir, configfile).setServiceParent(application)
257 def createMaster(config
):
261 contents
= masterTAC
% config
263 m
.sampleconfig(util
.sibpath(__file__
, "sample.cfg"))
264 m
.public_html(util
.sibpath(__file__
, "../status/web/index.html"),
265 util
.sibpath(__file__
, "../status/web/classic.css"),
266 util
.sibpath(__file__
, "../status/web/robots.txt"),
270 if not m
.quiet
: print "buildmaster configured in %s" % m
.basedir
272 class SlaveOptions(MakerBase
):
274 ["force", "f", "Re-use an existing directory"],
277 # ["name", "n", None, "Name for this build slave"],
278 # ["passwd", "p", None, "Password for this build slave"],
279 # ["basedir", "d", ".", "Base directory to use"],
280 # ["master", "m", "localhost:8007",
281 # "Location of the buildmaster (host:port)"],
283 ["keepalive", "k", 600,
284 "Interval at which keepalives should be sent (in seconds)"],
286 "(1 or 0) child processes should be run in a pty"],
287 ["umask", None, "None",
288 "controls permissions of generated files. Use --umask=022 to be world-readable"],
292 This command creates a buildslave working directory and buildbot.tac
293 file. The bot will use the <name> and <passwd> arguments to authenticate
294 itself when connecting to the master. All commands are run in a
295 build-specific subdirectory of <basedir>. <master> is a string of the
296 form 'hostname:port', and specifies where the buildmaster can be reached.
298 <name>, <passwd>, and <master> will be provided by the buildmaster
299 administrator for your bot. You must choose <basedir> yourself.
302 def getSynopsis(self
):
303 return "Usage: buildbot create-slave [options] <basedir> <master> <name> <passwd>"
305 def parseArgs(self
, *args
):
307 raise usage
.UsageError("command needs more arguments")
308 basedir
, master
, name
, passwd
= args
309 self
['basedir'] = basedir
310 self
['master'] = master
312 self
['passwd'] = passwd
314 def postOptions(self
):
315 MakerBase
.postOptions(self
)
316 self
['usepty'] = int(self
['usepty'])
317 self
['keepalive'] = int(self
['keepalive'])
318 if self
['master'].find(":") == -1:
319 raise usage
.UsageError("--master must be in the form host:portnum")
322 from twisted.application import service
323 from buildbot.slave.bot import BuildSlave
325 basedir = r'%(basedir)s'
326 buildmaster_host = '%(host)s'
328 slavename = '%(name)s'
329 passwd = '%(passwd)s'
330 keepalive = %(keepalive)d
334 application = service.Application('buildslave')
335 s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir,
336 keepalive, usepty, umask=umask)
337 s.setServiceParent(application)
341 def createSlave(config
):
346 master
= config
['master']
347 host
, port
= re
.search(r
'(.+):(\d+)', master
).groups()
348 config
['host'] = host
349 config
['port'] = int(port
)
351 print "unparseable master location '%s'" % master
352 print " expecting something more like localhost:8007"
354 contents
= slaveTAC
% config
356 m
.makeTAC(contents
, secret
=True)
361 if not m
.quiet
: print "buildslave configured in %s" % m
.basedir
365 def stop(config
, signame
="TERM", wait
=False):
367 basedir
= config
['basedir']
368 quiet
= config
['quiet']
370 f
= open("twistd.pid", "rt")
371 pid
= int(f
.read().strip())
372 signum
= getattr(signal
, "SIG"+signame
)
377 print "sent SIG%s to process" % signame
381 # poll once per second until twistd.pid goes away, up to 10 seconds
386 print "buildbot process %d is dead" % pid
391 print "never saw process go away"
394 quiet
= config
['quiet']
395 from buildbot
.scripts
.startup
import start
396 stop(config
, wait
=True)
398 print "now restarting buildbot process.."
402 def loadOptions(filename
="options", here
=None, home
=None):
403 """Find the .buildbot/FILENAME file. Crawl from the current directory up
404 towards the root, and also look in ~/.buildbot . The first directory
405 that's owned by the user and has the file we're looking for wins. Windows
406 skips the owned-by-user test.
409 @return: a dictionary of names defined in the options file. If no options
410 file was found, return an empty dict.
415 here
= os
.path
.abspath(here
)
418 if runtime
.platformType
== 'win32':
419 home
= os
.path
.join(os
.environ
['APPDATA'], "buildbot")
421 home
= os
.path
.expanduser("~/.buildbot")
426 searchpath
.append(os
.path
.join(here
, ".buildbot"))
427 next
= os
.path
.dirname(here
)
429 break # we've hit the root
431 toomany
-= 1 # just in case
433 raise ValueError("Hey, I seem to have wandered up into the "
434 "infinite glories of the heavens. Oops.")
435 searchpath
.append(home
)
441 if runtime
.platformType
!= 'win32':
442 if os
.stat(d
)[stat
.ST_UID
] != os
.getuid():
443 print "skipping %s because you don't own it" % d
444 continue # security, skip other people's directories
445 optfile
= os
.path
.join(d
, filename
)
446 if os
.path
.exists(optfile
):
448 f
= open(optfile
, "r")
450 exec options
in localDict
452 print "error while reading %s" % optfile
456 for k
in localDict
.keys():
457 if k
.startswith("__"):
461 class StartOptions(MakerBase
):
463 ['quiet', 'q', "Don't display startup log messages"],
465 def getSynopsis(self
):
466 return "Usage: buildbot start <basedir>"
468 class StopOptions(MakerBase
):
469 def getSynopsis(self
):
470 return "Usage: buildbot stop <basedir>"
472 class ReconfigOptions(MakerBase
):
474 ['quiet', 'q', "Don't display log messages about reconfiguration"],
476 def getSynopsis(self
):
477 return "Usage: buildbot reconfig <basedir>"
481 class RestartOptions(MakerBase
):
483 ['quiet', 'q', "Don't display startup log messages"],
485 def getSynopsis(self
):
486 return "Usage: buildbot restart <basedir>"
488 class DebugClientOptions(usage
.Options
):
490 ['help', 'h', "Display this message"],
493 ["master", "m", None,
494 "Location of the buildmaster's slaveport (host:port)"],
495 ["passwd", "p", None, "Debug password to use"],
498 def parseArgs(self
, *args
):
500 self
['master'] = args
[0]
502 self
['passwd'] = args
[1]
504 raise usage
.UsageError("I wasn't expecting so many arguments")
506 def debugclient(config
):
507 from buildbot
.clients
import debug
510 master
= config
.get('master')
512 master
= opts
.get('master')
514 raise usage
.UsageError("master must be specified: on the command "
515 "line or in ~/.buildbot/options")
517 passwd
= config
.get('passwd')
519 passwd
= opts
.get('debugPassword')
521 raise usage
.UsageError("passwd must be specified: on the command "
522 "line or in ~/.buildbot/options")
524 d
= debug
.DebugWidget(master
, passwd
)
527 class StatusClientOptions(usage
.Options
):
529 ['help', 'h', "Display this message"],
532 ["master", "m", None,
533 "Location of the buildmaster's status port (host:port)"],
536 def parseArgs(self
, *args
):
538 self
['master'] = args
[0]
540 raise usage
.UsageError("I wasn't expecting so many arguments")
542 def statuslog(config
):
543 from buildbot
.clients
import base
545 master
= config
.get('master')
547 master
= opts
.get('masterstatus')
549 raise usage
.UsageError("master must be specified: on the command "
550 "line or in ~/.buildbot/options")
551 c
= base
.TextClient(master
)
554 def statusgui(config
):
555 from buildbot
.clients
import gtkPanes
557 master
= config
.get('master')
559 master
= opts
.get('masterstatus')
561 raise usage
.UsageError("master must be specified: on the command "
562 "line or in ~/.buildbot/options")
563 c
= gtkPanes
.GtkClient(master
)
566 class SendChangeOptions(usage
.Options
):
568 ("master", "m", None,
569 "Location of the buildmaster's PBListener (host:port)"),
570 ("username", "u", None, "Username performing the commit"),
571 ("branch", "b", None, "Branch specifier"),
572 ("revision", "r", None, "Revision specifier (string)"),
573 ("revision_number", "n", None, "Revision specifier (integer)"),
574 ("revision_file", None, None, "Filename containing revision spec"),
575 ("comments", "m", None, "log message"),
576 ("logfile", "F", None,
577 "Read the log messages from this file (- for stdin)"),
579 def getSynopsis(self
):
580 return "Usage: buildbot sendchange [options] filenames.."
581 def parseArgs(self
, *args
):
585 def sendchange(config
, runReactor
=False):
586 """Send a single change to the buildmaster's PBChangeSource. The
587 connection will be drpoped as soon as the Change has been sent."""
588 from buildbot
.clients
.sendchange
import Sender
591 user
= config
.get('username', opts
.get('username'))
592 master
= config
.get('master', opts
.get('master'))
593 branch
= config
.get('branch', opts
.get('branch'))
594 revision
= config
.get('revision')
595 # SVN and P4 use numeric revisions
596 if config
.get("revision_number"):
597 revision
= int(config
['revision_number'])
598 if config
.get("revision_file"):
599 revision
= open(config
["revision_file"],"r").read()
601 comments
= config
.get('comments')
602 if not comments
and config
.get('logfile'):
603 if config
['logfile'] == "-":
606 f
= open(config
['logfile'], "rt")
611 files
= config
.get('files', [])
613 assert user
, "you must provide a username"
614 assert master
, "you must provide the master location"
616 s
= Sender(master
, user
)
617 d
= s
.send(branch
, revision
, comments
, files
)
619 d
.addCallbacks(s
.printSuccess
, s
.printFailure
)
625 class ForceOptions(usage
.Options
):
627 ["builder", None, None, "which Builder to start"],
628 ["branch", None, None, "which branch to build"],
629 ["revision", None, None, "which revision to build"],
630 ["reason", None, None, "the reason for starting the build"],
633 def parseArgs(self
, *args
):
636 if self
['builder'] is not None:
637 raise usage
.UsageError("--builder provided in two ways")
638 self
['builder'] = args
.pop(0)
640 if self
['reason'] is not None:
641 raise usage
.UsageError("--reason provided in two ways")
642 self
['reason'] = " ".join(args
)
645 class TryOptions(usage
.Options
):
647 ["connect", "c", None,
648 "how to reach the buildmaster, either 'ssh' or 'pb'"],
649 # for ssh, use --tryhost, --username, and --trydir
650 ["tryhost", None, None,
651 "the hostname (used by ssh) for the buildmaster"],
652 ["trydir", None, None,
653 "the directory (on the tryhost) where tryjobs are deposited"],
654 ["username", "u", None, "Username performing the trial build"],
655 # for PB, use --master, --username, and --passwd
656 ["master", "m", None,
657 "Location of the buildmaster's PBListener (host:port)"],
658 ["passwd", None, None, "password for PB authentication"],
661 "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."],
662 ["patchlevel", "p", 0,
663 "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"],
665 ["baserev", None, None,
666 "Base revision to use instead of scanning a local tree."],
669 "The VC system in use, one of: cvs,svn,tla,baz,darcs"],
670 ["branch", None, None,
671 "The branch in use, for VC systems that can't figure it out"
674 ["builder", "b", None,
675 "Run the trial build on this Builder. Can be used multiple times."],
679 ["wait", None, "wait until the builds have finished"],
683 super(TryOptions
, self
).__init
__()
684 self
['builders'] = []
686 def opt_builder(self
, option
):
687 self
['builders'].append(option
)
689 def opt_patchlevel(self
, option
):
690 self
['patchlevel'] = int(option
)
692 def getSynopsis(self
):
693 return "Usage: buildbot try [options]"
696 from buildbot
.scripts
import tryclient
697 t
= tryclient
.Try(config
)
700 class TryServerOptions(usage
.Options
):
702 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"],
705 def doTryServer(config
):
707 jobdir
= os
.path
.expanduser(config
["jobdir"])
708 job
= sys
.stdin
.read()
709 # now do a 'safecat'-style write to jobdir/tmp, then move atomically to
710 # jobdir/new . Rather than come up with a unique name randomly, I'm just
711 # going to MD5 the contents and prepend a timestamp.
712 timestring
= "%d" % time
.time()
713 jobhash
= md5
.new(job
).hexdigest()
714 fn
= "%s-%s" % (timestring
, jobhash
)
715 tmpfile
= os
.path
.join(jobdir
, "tmp", fn
)
716 newfile
= os
.path
.join(jobdir
, "new", fn
)
717 f
= open(tmpfile
, "w")
720 os
.rename(tmpfile
, newfile
)
723 class Options(usage
.Options
):
724 synopsis
= "Usage: buildbot <command> [command options]"
727 # the following are all admin commands
728 ['create-master', None, MasterOptions
,
729 "Create and populate a directory for a new buildmaster"],
730 ['upgrade-master', None, UpgradeMasterOptions
,
731 "Upgrade an existing buildmaster directory for the current version"],
732 ['create-slave', None, SlaveOptions
,
733 "Create and populate a directory for a new buildslave"],
734 ['start', None, StartOptions
, "Start a buildmaster or buildslave"],
735 ['stop', None, StopOptions
, "Stop a buildmaster or buildslave"],
736 ['restart', None, RestartOptions
,
737 "Restart a buildmaster or buildslave"],
739 ['reconfig', None, ReconfigOptions
,
740 "SIGHUP a buildmaster to make it re-read the config file"],
741 ['sighup', None, ReconfigOptions
,
742 "SIGHUP a buildmaster to make it re-read the config file"],
744 ['sendchange', None, SendChangeOptions
,
745 "Send a change to the buildmaster"],
747 ['debugclient', None, DebugClientOptions
,
748 "Launch a small debug panel GUI"],
750 ['statuslog', None, StatusClientOptions
,
751 "Emit current builder status to stdout"],
752 ['statusgui', None, StatusClientOptions
,
753 "Display a small window showing current builder status"],
755 #['force', None, ForceOptions, "Run a build"],
756 ['try', None, TryOptions
, "Run a build with your local changes"],
758 ['tryserver', None, TryServerOptions
,
759 "buildmaster-side 'try' support function, not for users"],
764 def opt_version(self
):
766 print "Buildbot version: %s" % buildbot
.version
767 usage
.Options
.opt_version(self
)
769 def opt_verbose(self
):
770 from twisted
.python
import log
771 log
.startLogging(sys
.stderr
)
773 def postOptions(self
):
774 if not hasattr(self
, 'subOptions'):
775 raise usage
.UsageError("must specify a command")
781 config
.parseOptions()
782 except usage
.error
, e
:
783 print "%s: %s" % (sys
.argv
[0], e
)
785 c
= getattr(config
, 'subOptions', config
)
789 command
= config
.subCommand
790 so
= config
.subOptions
792 if command
== "create-master":
794 elif command
== "upgrade-master":
796 elif command
== "create-slave":
798 elif command
== "start":
799 from buildbot
.scripts
.startup
import start
801 elif command
== "stop":
803 elif command
== "restart":
805 elif command
== "reconfig" or command
== "sighup":
806 from buildbot
.scripts
.reconfig
import Reconfigurator
807 Reconfigurator().run(so
)
808 elif command
== "sendchange":
810 elif command
== "debugclient":
812 elif command
== "statuslog":
814 elif command
== "statusgui":
816 elif command
== "try":
818 elif command
== "tryserver":