Package buildbot :: Package scripts :: Module runner
[hide private]
[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  # 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) 
  14   
  15  # the create/start/stop commands should all be run as the same user, 
  16  # preferably a separate 'buildbot' account. 
  17   
18 -class MakerBase(usage.Options):
19 optFlags = [ 20 ['help', 'h', "Display this message"], 21 ["quiet", "q", "Do not emit the commands being run"], 22 ] 23 24 #["basedir", "d", None, "Base directory for the buildmaster"], 25 opt_h = usage.Options.opt_help 26
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")
34
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'])
39 40 makefile_sample = """# -*- makefile -*- 41 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'. 45 46 # The 'reconfig' target will tell a buildmaster to reload its config file. 47 48 start: 49 twistd --no_save -y buildbot.tac 50 51 stop: 52 kill `cat twistd.pid` 53 54 reconfig: 55 kill -HUP `cat twistd.pid` 56 57 log: 58 tail -f twistd.log 59 """ 60
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']
67
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)
75
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
100
101 - def chdir(self):
102 if not self.quiet: print "chdir", self.basedir 103 os.chdir(self.basedir)
104
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)
122
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()
139
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)
158
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() 173 174 target = os.path.join(webdir, "buildbot.css") 175 f = open(target, "wt") 176 f.write(open(buildbot_css, "rt").read()) 177 f.close() 178 179 target = os.path.join(webdir, "robots.txt") 180 f = open(target, "wt") 181 f.write(open(robots_txt, "rt").read()) 182 f.close()
183
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)
204
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)
217
218 - def check_master_cfg(self):
219 from buildbot.master import BuildMaster 220 from twisted.python import log, failure 221 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 227 228 # side-effects of loading the config file: 229 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). 234 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. 239 240 if sys.path[0] != self.basedir: 241 sys.path.insert(0, self.basedir) 242 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
269
270 -class UpgradeMasterOptions(MakerBase):
271 optFlags = [ 272 ["replace", "r", "Replace any modified files without confirmation."], 273 ] 274
275 - def getSynopsis(self):
276 return "Usage: buildbot upgrade-master [options] <basedir>"
277 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. 283 284 Use this after you've upgraded your buildbot installation and before you 285 restart the buildmaster to use the new version. 286 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. 292 """
293
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"), 304 ) 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"
313 314
315 -class MasterOptions(MakerBase):
316 optFlags = [ 317 ["force", "f", 318 "Re-use an existing directory (will not overwrite master.cfg file)"], 319 ] 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"], 326 ]
327 - def getSynopsis(self):
328 return "Usage: buildbot create-master [options] <basedir>"
329 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. 333 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.""" 340
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")
349 350 351 masterTAC = """ 352 from twisted.application import service 353 from buildbot.master import BuildMaster 354 355 basedir = r'%(basedir)s' 356 configfile = r'%(config)s' 357 rotateLength = %(log-size)s 358 maxRotatedFiles = %(log-count)s 359 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) 371 372 """ 373
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"), 384 ) 385 m.makefile() 386 387 if not m.quiet: print "buildmaster configured in %s" % m.basedir
388
389 -class SlaveOptions(MakerBase):
390 optFlags = [ 391 ["force", "f", "Re-use an existing directory"], 392 ] 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)"], 399 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"], 412 ] 413 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. 420 421 <name>, <passwd>, and <master> will be provided by the buildmaster 422 administrator for your bot. You must choose <basedir> yourself. 423 """ 424
425 - def getSynopsis(self):
426 return "Usage: buildbot create-slave [options] <basedir> <master> <name> <passwd>"
427
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 if master[:5] == "http:": 433 raise usage.UsageError("<master> is not a URL - do not use URL") 434 self['basedir'] = basedir 435 self['master'] = master 436 self['name'] = name 437 self['passwd'] = passwd
438
439 - def postOptions(self):
440 MakerBase.postOptions(self) 441 self['usepty'] = int(self['usepty']) 442 self['keepalive'] = int(self['keepalive']) 443 self['maxdelay'] = int(self['maxdelay']) 444 if self['master'].find(":") == -1: 445 raise usage.UsageError("--master must be in the form host:portnum") 446 if not re.match('^\d+$', self['log-size']): 447 raise usage.UsageError("log-size parameter needs to be an int") 448 if not re.match('^\d+$', self['log-count']) and \ 449 self['log-count'] != 'None': 450 raise usage.UsageError("log-count parameter needs to be an int "+ 451 " or None")
452 453 slaveTAC = """ 454 from twisted.application import service 455 from buildbot.slave.bot import BuildSlave 456 457 basedir = r'%(basedir)s' 458 buildmaster_host = '%(host)s' 459 port = %(port)d 460 slavename = '%(name)s' 461 passwd = '%(passwd)s' 462 keepalive = %(keepalive)d 463 usepty = %(usepty)d 464 umask = %(umask)s 465 maxdelay = %(maxdelay)d 466 rotateLength = %(log-size)s 467 maxRotatedFiles = %(log-count)s 468 469 application = service.Application('buildslave') 470 try: 471 from twisted.python.logfile import LogFile 472 from twisted.python.log import ILogObserver, FileLogObserver 473 logfile = LogFile.fromFullPath("twistd.log", rotateLength=rotateLength, 474 maxRotatedFiles=maxRotatedFiles) 475 application.setComponent(ILogObserver, FileLogObserver(logfile).emit) 476 except ImportError: 477 # probably not yet twisted 8.2.0 and beyond, can't set log yet 478 pass 479 s = BuildSlave(buildmaster_host, port, slavename, passwd, basedir, 480 keepalive, usepty, umask=umask, maxdelay=maxdelay) 481 s.setServiceParent(application) 482 483 """ 484
485 -def createSlave(config):
486 m = Maker(config) 487 m.mkdir() 488 m.chdir() 489 try: 490 master = config['master'] 491 host, port = re.search(r'(.+):(\d+)', master).groups() 492 config['host'] = host 493 config['port'] = int(port) 494 except: 495 print "unparseable master location '%s'" % master 496 print " expecting something more like localhost:8007" 497 raise 498 contents = slaveTAC % config 499 500 m.makeTAC(contents, secret=True) 501 502 m.makefile() 503 m.mkinfo() 504 505 if not m.quiet: print "buildslave configured in %s" % m.basedir
506 507 508
509 -def stop(config, signame="TERM", wait=False):
510 import signal 511 basedir = config['basedir'] 512 quiet = config['quiet'] 513 os.chdir(basedir) 514 try: 515 f = open("twistd.pid", "rt") 516 except: 517 raise BuildbotNotRunningError 518 pid = int(f.read().strip()) 519 signum = getattr(signal, "SIG"+signame) 520 timer = 0 521 os.kill(pid, signum) 522 if not wait: 523 if not quiet: 524 print "sent SIG%s to process" % signame 525 return 526 time.sleep(0.1) 527 while timer < 10: 528 # poll once per second until twistd.pid goes away, up to 10 seconds 529 try: 530 os.kill(pid, 0) 531 except OSError: 532 if not quiet: 533 print "buildbot process %d is dead" % pid 534 return 535 timer += 1 536 time.sleep(1) 537 if not quiet: 538 print "never saw process go away"
539
540 -def restart(config):
541 quiet = config['quiet'] 542 from buildbot.scripts.startup import start 543 try: 544 stop(config, wait=True) 545 except BuildbotNotRunningError: 546 pass 547 if not quiet: 548 print "now restarting buildbot process.." 549 start(config)
550 551
552 -def loadOptions(filename="options", here=None, home=None):
553 """Find the .buildbot/FILENAME file. Crawl from the current directory up 554 towards the root, and also look in ~/.buildbot . The first directory 555 that's owned by the user and has the file we're looking for wins. Windows 556 skips the owned-by-user test. 557 558 @rtype: dict 559 @return: a dictionary of names defined in the options file. If no options 560 file was found, return an empty dict. 561 """ 562 563 if here is None: 564 here = os.getcwd() 565 here = os.path.abspath(here) 566 567 if home is None: 568 if runtime.platformType == 'win32': 569 home = os.path.join(os.environ['APPDATA'], "buildbot") 570 else: 571 home = os.path.expanduser("~/.buildbot") 572 573 searchpath = [] 574 toomany = 20 575 while True: 576 searchpath.append(os.path.join(here, ".buildbot")) 577 next = os.path.dirname(here) 578 if next == here: 579 break # we've hit the root 580 here = next 581 toomany -= 1 # just in case 582 if toomany == 0: 583 raise ValueError("Hey, I seem to have wandered up into the " 584 "infinite glories of the heavens. Oops.") 585 searchpath.append(home) 586 587 localDict = {} 588 589 for d in searchpath: 590 if os.path.isdir(d): 591 if runtime.platformType != 'win32': 592 if os.stat(d)[stat.ST_UID] != os.getuid(): 593 print "skipping %s because you don't own it" % d 594 continue # security, skip other people's directories 595 optfile = os.path.join(d, filename) 596 if os.path.exists(optfile): 597 try: 598 f = open(optfile, "r") 599 options = f.read() 600 exec options in localDict 601 except: 602 print "error while reading %s" % optfile 603 raise 604 break 605 606 for k in localDict.keys(): 607 if k.startswith("__"): 608 del localDict[k] 609 return localDict
610
611 -class StartOptions(MakerBase):
612 optFlags = [ 613 ['quiet', 'q', "Don't display startup log messages"], 614 ]
615 - def getSynopsis(self):
616 return "Usage: buildbot start <basedir>"
617
618 -class StopOptions(MakerBase):
619 - def getSynopsis(self):
620 return "Usage: buildbot stop <basedir>"
621
622 -class ReconfigOptions(MakerBase):
623 optFlags = [ 624 ['quiet', 'q', "Don't display log messages about reconfiguration"], 625 ]
626 - def getSynopsis(self):
627 return "Usage: buildbot reconfig <basedir>"
628 629 630
631 -class RestartOptions(MakerBase):
632 optFlags = [ 633 ['quiet', 'q', "Don't display startup log messages"], 634 ]
635 - def getSynopsis(self):
636 return "Usage: buildbot restart <basedir>"
637
638 -class DebugClientOptions(usage.Options):
639 optFlags = [ 640 ['help', 'h', "Display this message"], 641 ] 642 optParameters = [ 643 ["master", "m", None, 644 "Location of the buildmaster's slaveport (host:port)"], 645 ["passwd", "p", None, "Debug password to use"], 646 ] 647
648 - def parseArgs(self, *args):
649 if len(args) > 0: 650 self['master'] = args[0] 651 if len(args) > 1: 652 self['passwd'] = args[1] 653 if len(args) > 2: 654 raise usage.UsageError("I wasn't expecting so many arguments")
655
656 -def debugclient(config):
657 from buildbot.clients import debug 658 opts = loadOptions() 659 660 master = config.get('master') 661 if not master: 662 master = opts.get('master') 663 if master is None: 664 raise usage.UsageError("master must be specified: on the command " 665 "line or in ~/.buildbot/options") 666 667 passwd = config.get('passwd') 668 if not passwd: 669 passwd = opts.get('debugPassword') 670 if passwd is None: 671 raise usage.UsageError("passwd must be specified: on the command " 672 "line or in ~/.buildbot/options") 673 674 d = debug.DebugWidget(master, passwd) 675 d.run()
676
677 -class StatusClientOptions(usage.Options):
678 optFlags = [ 679 ['help', 'h', "Display this message"], 680 ] 681 optParameters = [ 682 ["master", "m", None, 683 "Location of the buildmaster's status port (host:port)"], 684 ] 685
686 - def parseArgs(self, *args):
687 if len(args) > 0: 688 self['master'] = args[0] 689 if len(args) > 1: 690 raise usage.UsageError("I wasn't expecting so many arguments")
691
692 -def statuslog(config):
693 from buildbot.clients import base 694 opts = loadOptions() 695 master = config.get('master') 696 if not master: 697 master = opts.get('masterstatus') 698 if master is None: 699 raise usage.UsageError("master must be specified: on the command " 700 "line or in ~/.buildbot/options") 701 c = base.TextClient(master) 702 c.run()
703
704 -def statusgui(config):
705 from buildbot.clients import gtkPanes 706 opts = loadOptions() 707 master = config.get('master') 708 if not master: 709 master = opts.get('masterstatus') 710 if master is None: 711 raise usage.UsageError("master must be specified: on the command " 712 "line or in ~/.buildbot/options") 713 c = gtkPanes.GtkClient(master) 714 c.run()
715
716 -class SendChangeOptions(usage.Options):
717 optParameters = [ 718 ("master", "m", None, 719 "Location of the buildmaster's PBListener (host:port)"), 720 ("username", "u", None, "Username performing the commit"), 721 ("branch", "b", None, "Branch specifier"), 722 ("category", "c", None, "Category of repository"), 723 ("revision", "r", None, "Revision specifier (string)"), 724 ("revision_number", "n", None, "Revision specifier (integer)"), 725 ("revision_file", None, None, "Filename containing revision spec"), 726 ("comments", "m", None, "log message"), 727 ("logfile", "F", None, 728 "Read the log messages from this file (- for stdin)"), 729 ("when", "w", None, "timestamp to use as the change time"), 730 ]
731 - def getSynopsis(self):
732 return "Usage: buildbot sendchange [options] filenames.."
733 - def parseArgs(self, *args):
734 self['files'] = args
735 736
737 -def sendchange(config, runReactor=False):
738 """Send a single change to the buildmaster's PBChangeSource. The 739 connection will be drpoped as soon as the Change has been sent.""" 740 from buildbot.clients.sendchange import Sender 741 742 opts = loadOptions() 743 user = config.get('username', opts.get('username')) 744 master = config.get('master', opts.get('master')) 745 branch = config.get('branch', opts.get('branch')) 746 category = config.get('category', opts.get('category')) 747 revision = config.get('revision') 748 if config.get('when'): 749 when = float(config.get('when')) 750 else: 751 when = None 752 # SVN and P4 use numeric revisions 753 if config.get("revision_number"): 754 revision = int(config['revision_number']) 755 if config.get("revision_file"): 756 revision = open(config["revision_file"],"r").read() 757 758 comments = config.get('comments') 759 if not comments and config.get('logfile'): 760 if config['logfile'] == "-": 761 f = sys.stdin 762 else: 763 f = open(config['logfile'], "rt") 764 comments = f.read() 765 if comments is None: 766 comments = "" 767 768 files = config.get('files', []) 769 770 assert user, "you must provide a username" 771 assert master, "you must provide the master location" 772 773 s = Sender(master, user) 774 d = s.send(branch, revision, comments, files, category=category, when=when) 775 if runReactor: 776 d.addCallbacks(s.printSuccess, s.printFailure) 777 d.addBoth(s.stop) 778 s.run() 779 return d
780 781
782 -class ForceOptions(usage.Options):
783 optParameters = [ 784 ["builder", None, None, "which Builder to start"], 785 ["branch", None, None, "which branch to build"], 786 ["revision", None, None, "which revision to build"], 787 ["reason", None, None, "the reason for starting the build"], 788 ] 789
790 - def parseArgs(self, *args):
791 args = list(args) 792 if len(args) > 0: 793 if self['builder'] is not None: 794 raise usage.UsageError("--builder provided in two ways") 795 self['builder'] = args.pop(0) 796 if len(args) > 0: 797 if self['reason'] is not None: 798 raise usage.UsageError("--reason provided in two ways") 799 self['reason'] = " ".join(args)
800 801
802 -class TryOptions(usage.Options):
803 optParameters = [ 804 ["connect", "c", None, 805 "how to reach the buildmaster, either 'ssh' or 'pb'"], 806 # for ssh, use --tryhost, --username, and --trydir 807 ["tryhost", None, None, 808 "the hostname (used by ssh) for the buildmaster"], 809 ["trydir", None, None, 810 "the directory (on the tryhost) where tryjobs are deposited"], 811 ["username", "u", None, "Username performing the trial build"], 812 # for PB, use --master, --username, and --passwd 813 ["master", "m", None, 814 "Location of the buildmaster's PBListener (host:port)"], 815 ["passwd", None, None, "password for PB authentication"], 816 817 ["diff", None, None, 818 "Filename of a patch to use instead of scanning a local tree. Use '-' for stdin."], 819 ["patchlevel", "p", 0, 820 "Number of slashes to remove from patch pathnames, like the -p option to 'patch'"], 821 822 ["baserev", None, None, 823 "Base revision to use instead of scanning a local tree."], 824 825 ["vc", None, None, 826 "The VC system in use, one of: cvs,svn,tla,baz,darcs"], 827 ["branch", None, None, 828 "The branch in use, for VC systems that can't figure it out" 829 " themselves"], 830 831 ["builder", "b", None, 832 "Run the trial build on this Builder. Can be used multiple times."], 833 ["properties", None, None, 834 "A set of properties made available in the build environment, format:prop=value,propb=valueb..."], 835 ] 836 837 optFlags = [ 838 ["wait", None, "wait until the builds have finished"], 839 ["dryrun", 'n', "Gather info, but don't actually submit."], 840 ] 841
842 - def __init__(self):
843 super(TryOptions, self).__init__() 844 self['builders'] = [] 845 self['properties'] = {}
846
847 - def opt_builder(self, option):
848 self['builders'].append(option)
849
850 - def opt_properties(self, option):
851 # We need to split the value of this option into a dictionary of properties 852 properties = {} 853 propertylist = option.split(",") 854 for i in range(0,len(propertylist)): 855 print propertylist[i] 856 splitproperty = propertylist[i].split("=") 857 properties[splitproperty[0]] = splitproperty[1] 858 self['properties'] = properties
859
860 - def opt_patchlevel(self, option):
861 self['patchlevel'] = int(option)
862
863 - def getSynopsis(self):
864 return "Usage: buildbot try [options]"
865
866 -def doTry(config):
867 from buildbot.scripts import tryclient 868 t = tryclient.Try(config) 869 t.run()
870
871 -class TryServerOptions(usage.Options):
872 optParameters = [ 873 ["jobdir", None, None, "the jobdir (maildir) for submitting jobs"], 874 ]
875
876 -def doTryServer(config):
877 import md5 878 jobdir = os.path.expanduser(config["jobdir"]) 879 job = sys.stdin.read() 880 # now do a 'safecat'-style write to jobdir/tmp, then move atomically to 881 # jobdir/new . Rather than come up with a unique name randomly, I'm just 882 # going to MD5 the contents and prepend a timestamp. 883 timestring = "%d" % time.time() 884 jobhash = md5.new(job).hexdigest() 885 fn = "%s-%s" % (timestring, jobhash) 886 tmpfile = os.path.join(jobdir, "tmp", fn) 887 newfile = os.path.join(jobdir, "new", fn) 888 f = open(tmpfile, "w") 889 f.write(job) 890 f.close() 891 os.rename(tmpfile, newfile)
892 893
894 -class CheckConfigOptions(usage.Options):
895 optFlags = [ 896 ['quiet', 'q', "Don't display error messages or tracebacks"], 897 ] 898
899 - def getSynopsis(self):
900 return "Usage :buildbot checkconfig [configFile]\n" + \ 901 " If not specified, 'master.cfg' will be used as 'configFile'"
902
903 - def parseArgs(self, *args):
904 if len(args) >= 1: 905 self['configFile'] = args[0] 906 else: 907 self['configFile'] = 'master.cfg'
908 909
910 -def doCheckConfig(config):
911 quiet = config.get('quiet') 912 configFileName = config.get('configFile') 913 try: 914 from buildbot.scripts.checkconfig import ConfigLoader 915 if os.path.isdir(configFileName): 916 ConfigLoader(basedir=configFileName) 917 else: 918 ConfigLoader(configFileName=configFileName) 919 except: 920 if not quiet: 921 # Print out the traceback in a nice format 922 t, v, tb = sys.exc_info() 923 traceback.print_exception(t, v, tb) 924 sys.exit(1) 925 926 if not quiet: 927 print "Config file is good!"
928 929
930 -class Options(usage.Options):
931 synopsis = "Usage: buildbot <command> [command options]" 932 933 subCommands = [ 934 # the following are all admin commands 935 ['create-master', None, MasterOptions, 936 "Create and populate a directory for a new buildmaster"], 937 ['upgrade-master', None, UpgradeMasterOptions, 938 "Upgrade an existing buildmaster directory for the current version"], 939 ['create-slave', None, SlaveOptions, 940 "Create and populate a directory for a new buildslave"], 941 ['start', None, StartOptions, "Start a buildmaster or buildslave"], 942 ['stop', None, StopOptions, "Stop a buildmaster or buildslave"], 943 ['restart', None, RestartOptions, 944 "Restart a buildmaster or buildslave"], 945 946 ['reconfig', None, ReconfigOptions, 947 "SIGHUP a buildmaster to make it re-read the config file"], 948 ['sighup', None, ReconfigOptions, 949 "SIGHUP a buildmaster to make it re-read the config file"], 950 951 ['sendchange', None, SendChangeOptions, 952 "Send a change to the buildmaster"], 953 954 ['debugclient', None, DebugClientOptions, 955 "Launch a small debug panel GUI"], 956 957 ['statuslog', None, StatusClientOptions, 958 "Emit current builder status to stdout"], 959 ['statusgui', None, StatusClientOptions, 960 "Display a small window showing current builder status"], 961 962 #['force', None, ForceOptions, "Run a build"], 963 ['try', None, TryOptions, "Run a build with your local changes"], 964 965 ['tryserver', None, TryServerOptions, 966 "buildmaster-side 'try' support function, not for users"], 967 968 ['checkconfig', None, CheckConfigOptions, 969 "test the validity of a master.cfg config file"], 970 971 # TODO: 'watch' 972 ] 973
974 - def opt_version(self):
975 import buildbot 976 print "Buildbot version: %s" % buildbot.version 977 usage.Options.opt_version(self)
978
979 - def opt_verbose(self):
980 from twisted.python import log 981 log.startLogging(sys.stderr)
982
983 - def postOptions(self):
984 if not hasattr(self, 'subOptions'): 985 raise usage.UsageError("must specify a command")
986 987
988 -def run():
989 config = Options() 990 try: 991 config.parseOptions() 992 except usage.error, e: 993 print "%s: %s" % (sys.argv[0], e) 994 print 995 c = getattr(config, 'subOptions', config) 996 print str(c) 997 sys.exit(1) 998 999 command = config.subCommand 1000 so = config.subOptions 1001 1002 if command == "create-master": 1003 createMaster(so) 1004 elif command == "upgrade-master": 1005 upgradeMaster(so) 1006 elif command == "create-slave": 1007 createSlave(so) 1008 elif command == "start": 1009 from buildbot.scripts.startup import start 1010 start(so) 1011 elif command == "stop": 1012 stop(so, wait=True) 1013 elif command == "restart": 1014 restart(so) 1015 elif command == "reconfig" or command == "sighup": 1016 from buildbot.scripts.reconfig import Reconfigurator 1017 Reconfigurator().run(so) 1018 elif command == "sendchange": 1019 sendchange(so, True) 1020 elif command == "debugclient": 1021 debugclient(so) 1022 elif command == "statuslog": 1023 statuslog(so) 1024 elif command == "statusgui": 1025 statusgui(so) 1026 elif command == "try": 1027 doTry(so) 1028 elif command == "tryserver": 1029 doTryServer(so) 1030 elif command == "checkconfig": 1031 doCheckConfig(so)
1032