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