Package buildbot :: Package scripts :: Module runner
[frames] | no frames]

Source Code for Module buildbot.scripts.runner

   1  # -*- test-case-name: buildbot.test.test_runner -*- 
   2   
   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 
   8   
   9  from buildbot.interfaces import BuildbotNotRunningError 
  10   
11 -def isBuildmasterDir(dir):
12 buildbot_tac = os.path.join(dir, "buildbot.tac") 13 if not os.path.isfile(buildbot_tac): 14 print "no buildbot.tac" 15 return False 16 17 contents = open(buildbot_tac, "r").read() 18 return "Application('buildmaster')" in contents
19 20 # the create/start/stop commands should all be run as the same user, 21 # preferably a separate 'buildbot' account. 22 23 # Note that the terms 'options' and 'config' are used intechangeably here - in 24 # fact, they are intercanged several times. Caveat legator. 25
26 -class OptionsWithOptionsFile(usage.Options):
27 # subclasses should set this to a list-of-lists in order to source the 28 # .buildbot/options file. 29 # buildbotOptions = [ [ 'optfile-name', 'option-name' ], .. ] 30 buildbotOptions = None 31
32 - def __init__(self, *args):
33 # for options in self.buildbotOptions, optParameters, and the options 34 # file, change the default in optParameters *before* calling through 35 # to the parent constructor 36 37 if self.buildbotOptions: 38 optfile = loadOptionsFile() 39 for optfile_name, option_name in self.buildbotOptions: 40 for i in range(len(self.optParameters)): 41 if self.optParameters[i][0] == option_name and optfile_name in optfile: 42 self.optParameters[i][2] = optfile[optfile_name] 43 usage.Options.__init__(self, *args)
44
45 -def loadOptionsFile(filename="options", here=None, home=None):
46 """Find the .buildbot/FILENAME file. Crawl from the current directory up 47 towards the root, and also look in ~/.buildbot . The first directory 48 that's owned by the user and has the file we're looking for wins. Windows 49 skips the owned-by-user test. 50 51 @rtype: dict 52 @return: a dictionary of names defined in the options file. If no options 53 file was found, return an empty dict. 54 """ 55 56 if here is None: 57 here = os.getcwd() 58 here = os.path.abspath(here) 59 60 if home is None: 61 if runtime.platformType == 'win32': 62 # never trust env-vars, use the proper API 63 from win32com.shell import shellcon, shell 64 appdata = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) 65 home = os.path.join(appdata, "buildbot") 66 else: 67 home = os.path.expanduser("~/.buildbot") 68 69 searchpath = [] 70 toomany = 20 71 while True: 72 searchpath.append(os.path.join(here, ".buildbot")) 73 next = os.path.dirname(here) 74 if next == here: 75 break # we've hit the root 76 here = next 77 toomany -= 1 # just in case 78 if toomany == 0: 79 raise ValueError("Hey, I seem to have wandered up into the " 80 "infinite glories of the heavens. Oops.") 81 searchpath.append(home) 82 83 localDict = {} 84 85 for d in searchpath: 86 if os.path.isdir(d): 87 if runtime.platformType != 'win32': 88 if os.stat(d)[stat.ST_UID] != os.getuid(): 89 print "skipping %s because you don't own it" % d 90 continue # security, skip other people's directories 91 optfile = os.path.join(d, filename) 92 if os.path.exists(optfile): 93 try: 94 f = open(optfile, "r") 95 options = f.read() 96 exec options in localDict 97 except: 98 print "error while reading %s" % optfile 99 raise 100 break 101 102 for k in localDict.keys(): 103 if k.startswith("__"): 104 del localDict[k] 105 return localDict
106
107 -class MakerBase(OptionsWithOptionsFile):
108 optFlags = [ 109 ['help', 'h', "Display this message"], 110 ["quiet", "q", "Do not emit the commands being run"], 111 ] 112 113 longdesc = """ 114 Operates upon the specified <basedir> (or the current directory, if not 115 specified). 116 """ 117 118 opt_h = usage.Options.opt_help 119
120 - def parseArgs(self, *args):
121 if len(args) > 0: 122 self['basedir'] = args[0] 123 else: 124 # Use the current directory if no basedir was specified. 125 self['basedir'] = os.getcwd() 126 if len(args) > 1: 127 raise usage.UsageError("I wasn't expecting so many arguments")
128
129 - def postOptions(self):
130 self['basedir'] = os.path.abspath(self['basedir'])
131 132 makefile_sample = """# -*- makefile -*- 133 134 # This is a simple makefile which lives in a buildmaster 135 # directory (next to the buildbot.tac file). It allows you to start/stop the 136 # master by doing 'make start' or 'make stop'. 137 138 # The 'reconfig' target will tell a buildmaster to reload its config file. 139 140 start: 141 twistd --no_save -y buildbot.tac 142 143 stop: 144 if [ -e twistd.pid ]; \\ 145 then kill `cat twistd.pid`; \\ 146 else echo "Nothing to stop."; \\ 147 fi 148 149 reconfig: 150 if [ -e twistd.pid ]; \\ 151 then kill -HUP `cat twistd.pid`; \\ 152 else echo "Nothing to reconfig."; \\ 153 fi 154 155 log: 156 if [ -e twistd.log ]; \\ 157 then tail -f twistd.log; \\ 158 else echo "Nothing to tail."; \\ 159 fi 160 """ 161
162 -class Maker:
163 - def __init__(self, config):
164 self.config = config 165 self.basedir = config['basedir'] 166 self.force = config.get('force', False) 167 self.quiet = config['quiet']
168
169 - def mkdir(self):
170 if os.path.exists(self.basedir): 171 if not self.quiet: 172 print "updating existing installation" 173 return 174 if not self.quiet: print "mkdir", self.basedir 175 os.mkdir(self.basedir)
176
177 - def mkinfo(self):
178 path = os.path.join(self.basedir, "info") 179 if not os.path.exists(path): 180 if not self.quiet: print "mkdir", path 181 os.mkdir(path) 182 created = False 183 admin = os.path.join(path, "admin") 184 if not os.path.exists(admin): 185 if not self.quiet: 186 print "Creating info/admin, you need to edit it appropriately" 187 f = open(admin, "wt") 188 f.write("Your Name Here <admin@youraddress.invalid>\n") 189 f.close() 190 created = True 191 host = os.path.join(path, "host") 192 if not os.path.exists(host): 193 if not self.quiet: 194 print "Creating info/host, you need to edit it appropriately" 195 f = open(host, "wt") 196 f.write("Please put a description of this build host here\n") 197 f.close() 198 created = True 199 access_uri = os.path.join(path, "access_uri") 200 if not os.path.exists(access_uri): 201 if not self.quiet: 202 print "Not creating info/access_uri - add it if you wish" 203 if created and not self.quiet: 204 print "Please edit the files in %s appropriately." % path
205
206 - def chdir(self):
207 if not self.quiet: print "chdir", self.basedir 208 os.chdir(self.basedir)
209
210 - def makeTAC(self, contents, secret=False):
211 tacfile = "buildbot.tac" 212 if os.path.exists(tacfile): 213 oldcontents = open(tacfile, "rt").read() 214 if oldcontents == contents: 215 if not self.quiet: 216 print "buildbot.tac already exists and is correct" 217 return 218 if not self.quiet: 219 print "not touching existing buildbot.tac" 220 print "creating buildbot.tac.new instead" 221 tacfile = "buildbot.tac.new" 222 f = open(tacfile, "wt") 223 f.write(contents) 224 f.close() 225 if secret: 226 os.chmod(tacfile, 0600)
227
228 - def makefile(self):
229 target = "Makefile.sample" 230 if os.path.exists(target): 231 oldcontents = open(target, "rt").read() 232 if oldcontents == makefile_sample: 233 if not self.quiet: 234 print "Makefile.sample already exists and is correct" 235 return 236 if not self.quiet: 237 print "replacing Makefile.sample" 238 else: 239 if not self.quiet: 240 print "creating Makefile.sample" 241 f = open(target, "wt") 242 f.write(makefile_sample) 243 f.close()
244
245 - def sampleconfig(self, source):
246 target = "master.cfg.sample" 247 config_sample = open(source, "rt").read() 248 if os.path.exists(target): 249 oldcontents = open(target, "rt").read() 250 if oldcontents == config_sample: 251 if not self.quiet: 252 print "master.cfg.sample already exists and is up-to-date" 253 return 254 if not self.quiet: 255 print "replacing master.cfg.sample" 256 else: 257 if not self.quiet: 258 print "creating master.cfg.sample" 259 f = open(target, "wt") 260 f.write(config_sample) 261 f.close() 262 os.chmod(target, 0600)
263
264 - def public_html(self, files):
265 webdir = os.path.join(self.basedir, "public_html") 266 if os.path.exists(webdir): 267 if not self.quiet: 268 print "public_html/ already exists: not replacing" 269 return 270 else: 271 os.mkdir(webdir) 272 if not self.quiet: 273 print "populating public_html/" 274 for target, source in files.iteritems(): 275 target = os.path.join(webdir, target) 276 f = open(target, "wt") 277 f.write(open(source, "rt").read()) 278 f.close()
279
280 - def create_db(self):
281 from buildbot.db import dbspec, exceptions 282 spec = dbspec.DBSpec.from_url(self.config["db"], self.basedir) 283 if not self.config['quiet']: print "creating database" 284 285 # upgrade from "nothing" 286 from buildbot.db.schema import manager 287 sm = manager.DBSchemaManager(spec, self.basedir) 288 if sm.get_db_version() != 0: 289 raise exceptions.DBAlreadyExistsError 290 sm.upgrade()
291
292 - def populate_if_missing(self, target, source, overwrite=False):
293 new_contents = open(source, "rt").read() 294 if os.path.exists(target): 295 old_contents = open(target, "rt").read() 296 if old_contents != new_contents: 297 if overwrite: 298 if not self.quiet: 299 print "%s has old/modified contents" % target 300 print " overwriting it with new contents" 301 open(target, "wt").write(new_contents) 302 else: 303 if not self.quiet: 304 print "%s has old/modified contents" % target 305 print " writing new contents to %s.new" % target 306 open(target + ".new", "wt").write(new_contents) 307 # otherwise, it's up to date 308 else: 309 if not self.quiet: 310 print "populating %s" % target 311 open(target, "wt").write(new_contents)
312
313 - def move_if_present(self, source, dest):
314 if os.path.exists(source): 315 if os.path.exists(dest): 316 print "Notice: %s now overrides %s" % (dest, source) 317 print " as the latter is not used by buildbot anymore." 318 print " Decide which one you want to keep." 319 else: 320 try: 321 print "Notice: Moving %s to %s." % (source, dest) 322 print " You can (and probably want to) remove it if you haven't modified this file." 323 os.renames(source, dest) 324 except Exception, e: 325 print "Error moving %s to %s: %s" % (source, dest, str(e))
326
327 - def upgrade_public_html(self, files):
328 webdir = os.path.join(self.basedir, "public_html") 329 if not os.path.exists(webdir): 330 if not self.quiet: 331 print "populating public_html/" 332 os.mkdir(webdir) 333 for target, source in files.iteritems(): 334 self.populate_if_missing(os.path.join(webdir, target), 335 source)
336
337 - def check_master_cfg(self):
338 from buildbot.master import BuildMaster 339 from twisted.python import log, failure 340 341 master_cfg = os.path.join(self.basedir, "master.cfg") 342 if not os.path.exists(master_cfg): 343 if not self.quiet: 344 print "No master.cfg found" 345 return 1 346 347 # side-effects of loading the config file: 348 349 # for each Builder defined in c['builders'], if the status directory 350 # didn't already exist, it will be created, and the 351 # $BUILDERNAME/builder pickle might be created (with a single 352 # "builder created" event). 353 354 # we put basedir in front of sys.path, because that's how the 355 # buildmaster itself will run, and it is quite common to have the 356 # buildmaster import helper classes from other .py files in its 357 # basedir. 358 359 if sys.path[0] != self.basedir: 360 sys.path.insert(0, self.basedir) 361 362 m = BuildMaster(self.basedir) 363 # we need to route log.msg to stdout, so any problems can be seen 364 # there. But if everything goes well, I'd rather not clutter stdout 365 # with log messages. So instead we add a logObserver which gathers 366 # messages and only displays them if something goes wrong. 367 messages = [] 368 log.addObserver(messages.append) 369 try: 370 # this will raise an exception if there's something wrong with 371 # the config file. Note that this BuildMaster instance is never 372 # started, so it won't actually do anything with the 373 # configuration. 374 m.loadConfig(open(master_cfg, "r"), check_synchronously_only=True) 375 except: 376 f = failure.Failure() 377 if not self.quiet: 378 print 379 for m in messages: 380 print "".join(m['message']) 381 print f 382 print 383 print "An error was detected in the master.cfg file." 384 print "Please correct the problem and run 'buildbot upgrade-master' again." 385 print 386 return 1 387 return 0
388 389 DB_HELP = """ 390 The --db string is evaluated to build the DB object, which specifies 391 which database the buildmaster should use to hold scheduler state and 392 status information. The default (which creates an SQLite database in 393 BASEDIR/state.sqlite) is equivalent to: 394 395 --db='DBSpec("sqlite3", basedir+"/state.sqlite"))' 396 --db='sqlite:///state.sqlite' 397 398 To use a remote MySQL database instead, use something like: 399 400 --db='mysql://bbuser:bbpasswd@dbhost/bbdb' 401 """ 402
403 -class UpgradeMasterOptions(MakerBase):
404 optFlags = [ 405 ["replace", "r", "Replace any modified files without confirmation."], 406 ] 407 optParameters = [ 408 ["db", None, "sqlite:///state.sqlite", 409 "which DB to use for scheduler/status state. See below for syntax."], 410 ] 411
412 - def getSynopsis(self):
413 return "Usage: buildbot upgrade-master [options] [<basedir>]"
414 415 longdesc = """ 416 This command takes an existing buildmaster working directory and 417 adds/modifies the files there to work with the current version of 418 buildbot. When this command is finished, the buildmaster directory should 419 look much like a brand-new one created by the 'create-master' command. 420 421 Use this after you've upgraded your buildbot installation and before you 422 restart the buildmaster to use the new version. 423 424 If you have modified the files in your working directory, this command 425 will leave them untouched, but will put the new recommended contents in a 426 .new file (for example, if index.html has been modified, this command 427 will create index.html.new). You can then look at the new version and 428 decide how to merge its contents into your modified file. 429 """+DB_HELP+""" 430 When upgrading from a pre-0.8.0 release (which did not use a database), 431 this command will create the given database and migrate data from the old 432 pickle files into it, then move the pickle files out of the way (e.g. to 433 changes.pck.old). To revert to an older release, rename the pickle files 434 back. When you are satisfied with the new version, you can delete the old 435 pickle files. 436 """
437
438 -def upgradeMaster(config):
439 basedir = os.path.expanduser(config['basedir']) 440 m = Maker(config) 441 # TODO: check Makefile 442 # TODO: check TAC file 443 # check web files: index.html, default.css, robots.txt 444 m.upgrade_public_html({ 445 'bg_gradient.jpg' : util.sibpath(__file__, "../status/web/files/bg_gradient.jpg"), 446 'default.css' : util.sibpath(__file__, "../status/web/files/default.css"), 447 'robots.txt' : util.sibpath(__file__, "../status/web/files/robots.txt"), 448 'favicon.ico' : util.sibpath(__file__, "../status/web/files/favicon.ico"), 449 }) 450 m.populate_if_missing(os.path.join(basedir, "master.cfg.sample"), 451 util.sibpath(__file__, "sample.cfg"), 452 overwrite=True) 453 # if index.html exists, use it to override the root page tempalte 454 m.move_if_present(os.path.join(basedir, "public_html/index.html"), 455 os.path.join(basedir, "templates/root.html")) 456 457 from buildbot.db import connector, dbspec 458 spec = dbspec.DBSpec.from_url(config["db"], basedir) 459 # TODO: check that TAC file specifies the right spec 460 461 # upgrade the db 462 from buildbot.db.schema import manager 463 sm = manager.DBSchemaManager(spec, basedir) 464 sm.upgrade() 465 466 # check the configuration 467 rc = m.check_master_cfg() 468 if rc: 469 return rc 470 if not config['quiet']: print "upgrade complete" 471 return 0
472 473
474 -class MasterOptions(MakerBase):
475 optFlags = [ 476 ["force", "f", 477 "Re-use an existing directory (will not overwrite master.cfg file)"], 478 ["relocatable", "r", 479 "Create a relocatable buildbot.tac"], 480 ] 481 optParameters = [ 482 ["config", "c", "master.cfg", "name of the buildmaster config file"], 483 ["log-size", "s", "1000000", 484 "size at which to rotate twisted log files"], 485 ["log-count", "l", "None", 486 "limit the number of kept old twisted log files"], 487 ["db", None, "sqlite:///state.sqlite", 488 "which DB to use for scheduler/status state. See below for syntax."], 489 ]
490 - def getSynopsis(self):
491 return "Usage: buildbot create-master [options] [<basedir>]"
492 493 longdesc = """ 494 This command creates a buildmaster working directory and buildbot.tac file. 495 The master will live in <dir> and create various files there. If 496 --relocatable is given, then the resulting buildbot.tac file will be 497 written such that its containing directory is assumed to be the basedir. 498 This is generally a good idea. 499 500 At runtime, the master will read a configuration file (named 501 'master.cfg' by default) in its basedir. This file should contain python 502 code which eventually defines a dictionary named 'BuildmasterConfig'. 503 The elements of this dictionary are used to configure the Buildmaster. 504 See doc/config.xhtml for details about what can be controlled through 505 this interface. 506 """ + DB_HELP + """ 507 The --db string is stored verbatim in the buildbot.tac file, and 508 evaluated as 'buildbot start' time to pass a DBConnector instance into 509 the newly-created BuildMaster object. 510 """ 511
512 - def postOptions(self):
513 MakerBase.postOptions(self) 514 if not re.match('^\d+$', self['log-size']): 515 raise usage.UsageError("log-size parameter needs to be an int") 516 if not re.match('^\d+$', self['log-count']) and \ 517 self['log-count'] != 'None': 518 raise usage.UsageError("log-count parameter needs to be an int "+ 519 " or None")
520 521 522 masterTAC = """ 523 import os 524 525 from twisted.application import service 526 from buildbot.master import BuildMaster 527 528 basedir = r'%(basedir)s' 529 rotateLength = %(log-size)s 530 maxRotatedFiles = %(log-count)s 531 532 # if this is a relocatable tac file, get the directory containing the TAC 533 if basedir == '.': 534 import os.path 535 basedir = os.path.abspath(os.path.dirname(__file__)) 536 537 # note: this line is matched against to check that this is a buildmaster 538 # directory; do not edit it. 539 application = service.Application('buildmaster') 540 try: 541 from twisted.python.logfile import LogFile 542 from twisted.python.log import ILogObserver, FileLogObserver 543 logfile = LogFile.fromFullPath(os.path.join(basedir, "twistd.log"), rotateLength=rotateLength, 544 maxRotatedFiles=maxRotatedFiles) 545 application.setComponent(ILogObserver, FileLogObserver(logfile).emit) 546 except ImportError: 547 # probably not yet twisted 8.2.0 and beyond, can't set log yet 548 pass 549 550 configfile = r'%(config)s' 551 552 m = BuildMaster(basedir, configfile) 553 m.setServiceParent(application) 554 m.log_rotation.rotateLength = rotateLength 555 m.log_rotation.maxRotatedFiles = maxRotatedFiles 556 557 """ 558
559 -def createMaster(config):
560 m = Maker(config) 561 m.mkdir() 562 m.chdir() 563 if config['relocatable']: 564 config['basedir'] = '.' 565 contents = masterTAC % config 566 m.makeTAC(contents) 567 m.sampleconfig(util.sibpath(__file__, "sample.cfg")) 568 m.public_html({ 569 'bg_gradient.jpg' : util.sibpath(__file__, "../status/web/files/bg_gradient.jpg"), 570 'default.css' : util.sibpath(__file__, "../status/web/files/default.css"), 571 'robots.txt' : util.sibpath(__file__, "../status/web/files/robots.txt"), 572 'favicon.ico' : util.sibpath(__file__, "../status/web/files/favicon.ico"), 573 }) 574 m.makefile() 575 m.create_db() 576 577 if not m.quiet: print "buildmaster configured in %s" % m.basedir
578
579 -def stop(config, signame="TERM", wait=False):
580 import signal 581 basedir = config['basedir'] 582 quiet = config['quiet'] 583 584 if not isBuildmasterDir(config['basedir']): 585 print "not a buildmaster directory" 586 sys.exit(1) 587 588 os.chdir(basedir) 589 try: 590 f = open("twistd.pid", "rt") 591 except: 592 raise BuildbotNotRunningError 593 pid = int(f.read().strip()) 594 signum = getattr(signal, "SIG"+signame) 595 timer = 0 596 try: 597 os.kill(pid, signum) 598 except OSError, e: 599 if e.errno != 3: 600 raise 601 602 if not wait: 603 if not quiet: 604 print "sent SIG%s to process" % signame 605 return 606 time.sleep(0.1) 607 while timer < 10: 608 # poll once per second until twistd.pid goes away, up to 10 seconds 609 try: 610 os.kill(pid, 0) 611 except OSError: 612 if not quiet: 613 print "buildbot process %d is dead" % pid 614 return 615 timer += 1 616 time.sleep(1) 617 if not quiet: 618 print "never saw process go away"
619
620 -def restart(config):
621 basedir = config['basedir'] 622 quiet = config['quiet'] 623 624 if not isBuildmasterDir(config['basedir']): 625 print "not a buildmaster directory" 626 sys.exit(1) 627 628 from buildbot.scripts.startup import start 629 try: 630 stop(config, wait=True) 631 except BuildbotNotRunningError: 632 pass 633 if not quiet: 634 print "now restarting buildbot process.." 635 start(config)
636 637
638 -class StartOptions(MakerBase):
639 optFlags = [ 640 ['quiet', 'q', "Don't display startup log messages"], 641 ]
642 - def getSynopsis(self):
643 return "Usage: buildbot start [<basedir>]"
644
645 -class StopOptions(MakerBase):
646 - def getSynopsis(self):
647 return "Usage: buildbot stop [<basedir>]"
648
649 -class ReconfigOptions(MakerBase):
650 optFlags = [ 651 ['quiet', 'q', "Don't display log messages about reconfiguration"], 652 ]
653 - def getSynopsis(self):
654 return "Usage: buildbot reconfig [<basedir>]"
655 656 657
658 -class RestartOptions(MakerBase):
659 optFlags = [ 660 ['quiet', 'q', "Don't display startup log messages"], 661 ]
662 - def getSynopsis(self):
663 return "Usage: buildbot restart [<basedir>]"
664
665 -class DebugClientOptions(OptionsWithOptionsFile):
666 optFlags = [ 667 ['help', 'h', "Display this message"], 668 ] 669 optParameters = [ 670 ["master", "m", None, 671 "Location of the buildmaster's slaveport (host:port)"], 672 ["passwd", "p", None, "Debug password to use"], 673 ] 674 buildbotOptions = [ 675 [ 'debugMaster', 'passwd' ], 676 [ 'master', 'master' ], 677 ] 678
679 - def parseArgs(self, *args):
680 if len(args) > 0: 681 self['master'] = args[0] 682 if len(args) > 1: 683 self['passwd'] = args[1] 684 if len(args) > 2: 685 raise usage.UsageError("I wasn't expecting so many arguments")
686
687 -def debugclient(config):
688 from buildbot.clients import debug 689 690 master = config.get('master') 691 if master is None: 692 raise usage.UsageError("master must be specified: on the command " 693 "line or in ~/.buildbot/options") 694 695 passwd = config.get('passwd') 696 if passwd is None: 697 raise usage.UsageError("passwd must be specified: on the command " 698 "line or in ~/.buildbot/options") 699 700 d = debug.DebugWidget(master, passwd) 701 d.run()
702
703 -class StatusClientOptions(OptionsWithOptionsFile):
704 optFlags = [ 705 ['help', 'h', "Display this message"], 706 ] 707 optParameters = [ 708 ["master", "m", None, 709 "Location of the buildmaster's status port (host:port)"], 710 ["username", "u", "statusClient", "Username performing the trial build"], 711 ["passwd", None, "clientpw", "password for PB authentication"], 712 ] 713 buildbotOptions = [ 714 [ 'masterstatus', 'master' ], 715 ] 716
717 - def parseArgs(self, *args):
718 if len(args) > 0: 719 self['master'] = args[0] 720 if len(args) > 1: 721 raise usage.UsageError("I wasn't expecting so many arguments")
722
723 -def statuslog(config):
724 from buildbot.clients import base 725 master = config.get('master') 726 if master is None: 727 raise usage.UsageError("master must be specified: on the command " 728 "line or in ~/.buildbot/options") 729 passwd = config.get('passwd') 730 username = config.get('username') 731 c = base.TextClient(master, username=username, passwd=passwd) 732 c.run()
733
734 -def statusgui(config):
735 from buildbot.clients import gtkPanes 736 master = config.get('master') 737 if master is None: 738 raise usage.UsageError("master must be specified: on the command " 739 "line or in ~/.buildbot/options") 740 c = gtkPanes.GtkClient(master) 741 c.run()
742
743 -class SendChangeOptions(OptionsWithOptionsFile):
744 - def __init__(self):
745 OptionsWithOptionsFile.__init__(self) 746 self['properties'] = {}
747 748 optParameters = [ 749 ("master", "m", None, 750 "Location of the buildmaster's PBListener (host:port)"), 751 ("username", "u", None, "Username performing the commit"), 752 ("repository", "R", None, "Repository specifier"), 753 ("project", "P", None, "Project specifier"), 754 ("branch", "b", None, "Branch specifier"), 755 ("category", "c", None, "Category of repository"), 756 ("revision", "r", None, "Revision specifier"), 757 ("revision_file", None, None, "Filename containing revision spec"), 758 ("property", "p", None, 759 "A property for the change, in the format: name:value"), 760 ("comments", "m", None, "log message"), 761 ("logfile", "F", None, 762 "Read the log messages from this file (- for stdin)"), 763 ("when", "w", None, "timestamp to use as the change time"), 764 ] 765 766 buildbotOptions = [ 767 [ 'master', 'master' ], 768 [ 'username', 'username' ], 769 [ 'branch', 'branch' ], 770 [ 'category', 'category' ], 771 ] 772
773 - def getSynopsis(self):
774 return "Usage: buildbot sendchange [options] filenames.."
775 - def parseArgs(self, *args):
776 self['files'] = args
777 - def opt_property(self, property):
778 name,value = property.split(':') 779 self['properties'][name] = value
780 781
782 -def sendchange(config, runReactor=False):
783 """Send a single change to the buildmaster's PBChangeSource. The 784 connection will be drpoped as soon as the Change has been sent.""" 785 from buildbot.clients.sendchange import Sender 786 787 user = config.get('username') 788 master = config.get('master') 789 branch = config.get('branch') 790 category = config.get('category') 791 revision = config.get('revision') 792 properties = config.get('properties', {}) 793 repository = config.get('repository', '') 794 project = config.get('project', '') 795 if config.get('when'): 796 when = float(config.get('when')) 797 else: 798 when = None 799 if config.get("revision_file"): 800 revision = open(config["revision_file"],"r").read() 801 802 comments = config.get('comments') 803 if not comments and config.get('logfile'): 804 if config['logfile'] == "-": 805 f = sys.stdin 806 else: 807 f = open(config['logfile'], "rt") 808 comments = f.read() 809 if comments is None: 810 comments = "" 811 812 files = config.get('files', []) 813 814 assert user, "you must provide a username" 815 assert master, "you must provide the master location" 816 817 s = Sender(master, user) 818 d = s.send(branch, revision, comments, files, category=category, when=when, 819 properties=properties, repository=repository, project=project) 820 if runReactor: 821 d.addCallbacks(s.printSuccess, s.printFailure) 822 d.addBoth(s.stop) 823 s.run() 824 return d
825 826
827 -class ForceOptions(OptionsWithOptionsFile):
828 optParameters = [ 829 ["builder", None, None, "which Builder to start"], 830 ["branch", None, None, "which branch to build"], 831 ["revision", None, None, "which revision to build"], 832 ["reason", None, None, "the reason for starting the build"], 833 ] 834
835 - def parseArgs(self, *args):
836 args = list(args) 837 if len(args) > 0: 838 if self['builder'] is not None: 839 raise usage.UsageError("--builder provided in two ways") 840 self['builder'] = args.pop(0) 841 if len(args) > 0: 842 if self['reason'] is not None: 843 raise usage.UsageError("--reason provided in two ways") 844 self['reason'] = " ".join(args)
845 846
847 -class TryOptions(OptionsWithOptionsFile):
848 optParameters = [ 849 ["connect", "c", None, 850 "how to reach the buildmaster, either 'ssh' or 'pb'"], 851 # for ssh, use --tryhost, --username, and --trydir 852 ["tryhost", None, None, 853 "the hostname (used by ssh) for the buildmaster"], 854 ["trydir", None, None, 855 "the directory (on the tryhost) where tryjobs are deposited"], 856 ["username", "u", None, "Username performing the trial build"], 857 # for PB, use --master, --username, and --passwd 858 ["master", "m", None, 859 "Location of the buildmaster's PBListener (host:port)"], 860 ["passwd", None, None, "password for PB authentication"], 861 862 ["diff", None, None, 863 "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."], 864 ["patchlevel", "p", 0, 865 "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"], 866 867 ["baserev", None, None, 868 "Base revision to use instead of scanning a local tree."], 869 870 ["vc", None, None, 871 "The VC system in use, one of: cvs,svn,tla,baz,darcs,p4"], 872 ["branch", None, None, 873 "The branch in use, for VC systems that can't figure it out" 874 " themselves"], 875 876 ["builder", "b", None, 877 "Run the trial build on this Builder. Can be used multiple times."], 878 ["properties", None, None, 879 "A set of properties made available in the build environment, format:prop=value,propb=valueb..."], 880 881 ["try-topfile", None, None, 882 "Name of a file at the top of the tree, used to find the top. Only needed for SVN and CVS."], 883 ["try-topdir", None, None, 884 "Path to the top of the working copy. Only needed for SVN and CVS."], 885 886 ] 887 888 optFlags = [ 889 ["wait", None, "wait until the builds have finished"], 890 ["dryrun", 'n', "Gather info, but don't actually submit."], 891 ["get-builder-names", None, "Get the names of available builders. Doesn't submit anything. Only supported for 'pb' connections."], 892 ] 893 894 # here it is, the definitive, quirky mapping of .buildbot/options names to 895 # command-line options. Design by committee, anyone? 896 buildbotOptions = [ 897 [ 'try_connect', 'connect' ], 898 #[ 'try_builders', 'builders' ], <-- handled in postOptions 899 [ 'try_vc', 'vc' ], 900 [ 'try_branch', 'branch' ], 901 [ 'try_topdir', 'try-topdir' ], 902 [ 'try_topfile', 'try-topfile' ], 903 [ 'try_host', 'tryhost' ], 904 [ 'try_username', 'username' ], 905 [ 'try_dir', 'trydir' ], 906 [ 'try_password', 'passwd' ], 907 [ 'try_master', 'master' ], 908 #[ 'try_wait', 'wait' ], <-- handled in postOptions 909 [ 'masterstatus', 'master' ], 910 ] 911
912 - def __init__(self):
913 OptionsWithOptionsFile.__init__(self) 914 self['builders'] = [] 915 self['properties'] = {}
916
917 - def opt_builder(self, option):
918 self['builders'].append(option)
919
920 - def opt_properties(self, option):
921 # We need to split the value of this option into a dictionary of properties 922 properties = {} 923 propertylist = option.split(",") 924 for i in range(0,len(propertylist)): 925 print propertylist[i] 926 splitproperty = propertylist[i].split("=") 927 properties[splitproperty[0]] = splitproperty[1] 928 self['properties'] = properties
929
930 - def opt_patchlevel(self, option):
931 self['patchlevel'] = int(option)
932
933 - def getSynopsis(self):
934 return "Usage: buildbot try [options]"
935
936 - def postOptions(self):
937 opts = loadOptionsFile() 938 if not self['builders']: 939 self['builders'] = opts.get('try_builders', []) 940 if opts.get('try_wait', False): 941 self['wait'] = True
942
943 -def doTry(config):
944 from buildbot.clients import tryclient 945 t = tryclient.Try(config) 946 t.run()
947
948 -class TryServerOptions(OptionsWithOptionsFile):
949 optParameters = [ 950 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"], 951 ]
952
953 -def doTryServer(config):
954 import md5 955 jobdir = os.path.expanduser(config["jobdir"]) 956 job = sys.stdin.read() 957 # now do a 'safecat'-style write to jobdir/tmp, then move atomically to 958 # jobdir/new . Rather than come up with a unique name randomly, I'm just 959 # going to MD5 the contents and prepend a timestamp. 960 timestring = "%d" % time.time() 961 jobhash = md5.new(job).hexdigest() 962 fn = "%s-%s" % (timestring, jobhash) 963 tmpfile = os.path.join(jobdir, "tmp", fn) 964 newfile = os.path.join(jobdir, "new", fn) 965 f = open(tmpfile, "w") 966 f.write(job) 967 f.close() 968 os.rename(tmpfile, newfile)
969 970
971 -class CheckConfigOptions(OptionsWithOptionsFile):
972 optFlags = [ 973 ['quiet', 'q', "Don't display error messages or tracebacks"], 974 ] 975
976 - def getSynopsis(self):
977 return "Usage :buildbot checkconfig [configFile]\n" + \ 978 " If not specified, 'master.cfg' will be used as 'configFile'"
979
980 - def parseArgs(self, *args):
981 if len(args) >= 1: 982 self['configFile'] = args[0] 983 else: 984 self['configFile'] = 'master.cfg'
985 986
987 -def doCheckConfig(config):
988 quiet = config.get('quiet') 989 configFileName = config.get('configFile') 990 try: 991 from buildbot.scripts.checkconfig import ConfigLoader 992 if os.path.isdir(configFileName): 993 ConfigLoader(basedir=configFileName) 994 else: 995 ConfigLoader(configFileName=configFileName) 996 except: 997 if not quiet: 998 # Print out the traceback in a nice format 999 t, v, tb = sys.exc_info() 1000 traceback.print_exception(t, v, tb) 1001 sys.exit(1) 1002 1003 if not quiet: 1004 print "Config file is good!"
1005 1006
1007 -class Options(usage.Options):
1008 synopsis = "Usage: buildbot <command> [command options]" 1009 1010 subCommands = [ 1011 # the following are all admin commands 1012 ['create-master', None, MasterOptions, 1013 "Create and populate a directory for a new buildmaster"], 1014 ['upgrade-master', None, UpgradeMasterOptions, 1015 "Upgrade an existing buildmaster directory for the current version"], 1016 ['start', None, StartOptions, "Start a buildmaster"], 1017 ['stop', None, StopOptions, "Stop a buildmaster"], 1018 ['restart', None, RestartOptions, 1019 "Restart a buildmaster"], 1020 1021 ['reconfig', None, ReconfigOptions, 1022 "SIGHUP a buildmaster to make it re-read the config file"], 1023 ['sighup', None, ReconfigOptions, 1024 "SIGHUP a buildmaster to make it re-read the config file"], 1025 1026 ['sendchange', None, SendChangeOptions, 1027 "Send a change to the buildmaster"], 1028 1029 ['debugclient', None, DebugClientOptions, 1030 "Launch a small debug panel GUI"], 1031 1032 ['statuslog', None, StatusClientOptions, 1033 "Emit current builder status to stdout"], 1034 ['statusgui', None, StatusClientOptions, 1035 "Display a small window showing current builder status"], 1036 1037 #['force', None, ForceOptions, "Run a build"], 1038 ['try', None, TryOptions, "Run a build with your local changes"], 1039 1040 ['tryserver', None, TryServerOptions, 1041 "buildmaster-side 'try' support function, not for users"], 1042 1043 ['checkconfig', None, CheckConfigOptions, 1044 "test the validity of a master.cfg config file"], 1045 1046 # TODO: 'watch' 1047 ] 1048
1049 - def opt_version(self):
1050 import buildbot 1051 print "Buildbot version: %s" % buildbot.version 1052 usage.Options.opt_version(self)
1053
1054 - def opt_verbose(self):
1055 from twisted.python import log 1056 log.startLogging(sys.stderr)
1057
1058 - def postOptions(self):
1059 if not hasattr(self, 'subOptions'): 1060 raise usage.UsageError("must specify a command")
1061 1062
1063 -def run():
1064 config = Options() 1065 try: 1066 config.parseOptions() 1067 except usage.error, e: 1068 print "%s: %s" % (sys.argv[0], e) 1069 print 1070 c = getattr(config, 'subOptions', config) 1071 print str(c) 1072 sys.exit(1) 1073 1074 command = config.subCommand 1075 so = config.subOptions 1076 1077 if command == "create-master": 1078 createMaster(so) 1079 elif command == "upgrade-master": 1080 upgradeMaster(so) 1081 elif command == "start": 1082 from buildbot.scripts.startup import start 1083 1084 if not isBuildmasterDir(so['basedir']): 1085 print "not a buildmaster directory" 1086 sys.exit(1) 1087 1088 start(so) 1089 elif command == "stop": 1090 stop(so, wait=True) 1091 elif command == "restart": 1092 restart(so) 1093 elif command == "reconfig" or command == "sighup": 1094 from buildbot.scripts.reconfig import Reconfigurator 1095 Reconfigurator().run(so) 1096 elif command == "sendchange": 1097 sendchange(so, True) 1098 elif command == "debugclient": 1099 debugclient(so) 1100 elif command == "statuslog": 1101 statuslog(so) 1102 elif command == "statusgui": 1103 statusgui(so) 1104 elif command == "try": 1105 doTry(so) 1106 elif command == "tryserver": 1107 doTryServer(so) 1108 elif command == "checkconfig": 1109 doCheckConfig(so) 1110 sys.exit(0)
1111