OLD | NEW |
| (Empty) |
1 | |
2 import os, sys, urllib, weakref | |
3 from itertools import count | |
4 | |
5 from zope.interface import implements | |
6 from twisted.python import log | |
7 from twisted.application import strports, service | |
8 from twisted.web import server, distrib, static, html | |
9 from twisted.spread import pb | |
10 | |
11 from buildbot.interfaces import IControl, IStatusReceiver | |
12 | |
13 from buildbot.status.web.base import HtmlResource, Box, \ | |
14 build_get_class, ICurrentBox, OneLineMixin, map_branches, \ | |
15 make_stop_form, make_force_build_form | |
16 from buildbot.status.web.feeds import Rss20StatusResource, \ | |
17 Atom10StatusResource | |
18 from buildbot.status.web.waterfall import WaterfallStatusResource | |
19 from buildbot.status.web.console import ConsoleStatusResource | |
20 from buildbot.status.web.grid import GridStatusResource, TransposedGridStatusRes
ource | |
21 from buildbot.status.web.changes import ChangesResource | |
22 from buildbot.status.web.builder import BuildersResource | |
23 from buildbot.status.web.buildstatus import BuildStatusStatusResource | |
24 from buildbot.status.web.slaves import BuildSlavesResource | |
25 from buildbot.status.web.status_json import JsonStatusResource | |
26 from buildbot.status.web.xmlrpc import XMLRPCServer | |
27 from buildbot.status.web.about import AboutBuildbot | |
28 from buildbot.status.web.auth import IAuth, AuthFailResource | |
29 | |
30 # this class contains the status services (WebStatus and the older Waterfall) | |
31 # which can be put in c['status']. It also contains some of the resources | |
32 # that are attached to the WebStatus at various well-known URLs, which the | |
33 # admin might wish to attach (using WebStatus.putChild) at other URLs. | |
34 | |
35 | |
36 class LastBuild(HtmlResource): | |
37 def body(self, request): | |
38 return "missing\n" | |
39 | |
40 def getLastNBuilds(status, numbuilds, builders=[], branches=[]): | |
41 """Return a list with the last few Builds, sorted by start time. | |
42 builder_names=None means all builders | |
43 """ | |
44 | |
45 # TODO: this unsorts the list of builder names, ick | |
46 builder_names = set(status.getBuilderNames()) | |
47 if builders: | |
48 builder_names = builder_names.intersection(set(builders)) | |
49 | |
50 # to make sure that we get everything, we must get 'numbuilds' builds | |
51 # from *each* source, then sort by ending time, then trim to the last | |
52 # 20. We could be more efficient, but it would require the same | |
53 # gnarly code that the Waterfall uses to generate one event at a | |
54 # time. TODO: factor that code out into some useful class. | |
55 events = [] | |
56 for builder_name in builder_names: | |
57 builder = status.getBuilder(builder_name) | |
58 for build_number in count(1): | |
59 if build_number > numbuilds: | |
60 break # enough from this builder, move on to another | |
61 build = builder.getBuild(-build_number) | |
62 if not build: | |
63 break # no more builds here, move on to the next builder | |
64 #if not build.isFinished(): | |
65 # continue | |
66 (build_start, build_end) = build.getTimes() | |
67 event = (build_start, builder_name, build) | |
68 events.append(event) | |
69 def _sorter(a, b): | |
70 return cmp( a[:2], b[:2] ) | |
71 events.sort(_sorter) | |
72 # now only return the actual build, and only return some of them | |
73 return [e[2] for e in events[-numbuilds:]] | |
74 | |
75 | |
76 # /one_line_per_build | |
77 # accepts builder=, branch=, numbuilds=, reload= | |
78 class OneLinePerBuild(HtmlResource, OneLineMixin): | |
79 """This shows one line per build, combining all builders together. Useful | |
80 query arguments: | |
81 | |
82 numbuilds=: how many lines to display | |
83 builder=: show only builds for this builder. Multiple builder= arguments | |
84 can be used to see builds from any builder in the set. | |
85 reload=: reload the page after this many seconds | |
86 """ | |
87 | |
88 title = "Recent Builds" | |
89 | |
90 def __init__(self, numbuilds=20): | |
91 HtmlResource.__init__(self) | |
92 self.numbuilds = numbuilds | |
93 | |
94 def getChild(self, path, req): | |
95 status = self.getStatus(req) | |
96 builder = status.getBuilder(path) | |
97 return OneLinePerBuildOneBuilder(builder, numbuilds=self.numbuilds) | |
98 | |
99 def get_reload_time(self, request): | |
100 if "reload" in request.args: | |
101 try: | |
102 reload_time = int(request.args["reload"][0]) | |
103 return max(reload_time, 15) | |
104 except ValueError: | |
105 pass | |
106 return None | |
107 | |
108 def head(self, request): | |
109 head = '' | |
110 reload_time = self.get_reload_time(request) | |
111 if reload_time is not None: | |
112 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time | |
113 return head | |
114 | |
115 def body(self, req): | |
116 status = self.getStatus(req) | |
117 control = self.getControl(req) | |
118 numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0]) | |
119 builders = req.args.get("builder", []) | |
120 branches = [b for b in req.args.get("branch", []) if b] | |
121 | |
122 g = status.generateFinishedBuilds(builders, map_branches(branches), | |
123 numbuilds, max_search=numbuilds) | |
124 | |
125 data = "" | |
126 | |
127 # really this is "up to %d builds" | |
128 html_branches = map(html.escape, branches) | |
129 data += "<h1>Last %d finished builds: %s</h1>\n" % \ | |
130 (numbuilds, ", ".join(html_branches)) | |
131 if builders: | |
132 html_builders = map(html.escape, builders) | |
133 data += ("<p>of builders: %s</p>\n" % (", ".join(html_builders))) | |
134 data += "<ul>\n" | |
135 got = 0 | |
136 building = False | |
137 online = 0 | |
138 for build in g: | |
139 got += 1 | |
140 data += " <li>" + self.make_line(req, build) + "</li>\n" | |
141 builder_status = build.getBuilder().getState()[0] | |
142 if builder_status == "building": | |
143 building = True | |
144 online += 1 | |
145 elif builder_status != "offline": | |
146 online += 1 | |
147 if not got: | |
148 data += " <li>No matching builds found</li>\n" | |
149 data += "</ul>\n" | |
150 | |
151 if control is not None: | |
152 if building: | |
153 stopURL = "builders/_all/stop" | |
154 data += make_stop_form(stopURL, self.isUsingUserPasswd(req), | |
155 True, "Builds") | |
156 if online: | |
157 forceURL = "builders/_all/force" | |
158 data += make_force_build_form(forceURL, | |
159 self.isUsingUserPasswd(req), True) | |
160 | |
161 return data | |
162 | |
163 | |
164 | |
165 # /one_line_per_build/$BUILDERNAME | |
166 # accepts branch=, numbuilds= | |
167 | |
168 class OneLinePerBuildOneBuilder(HtmlResource, OneLineMixin): | |
169 def __init__(self, builder, numbuilds=20): | |
170 HtmlResource.__init__(self) | |
171 self.builder = builder | |
172 self.builder_name = builder.getName() | |
173 self.numbuilds = numbuilds | |
174 self.title = "Recent Builds of %s" % self.builder_name | |
175 | |
176 def body(self, req): | |
177 status = self.getStatus(req) | |
178 numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0]) | |
179 branches = [b for b in req.args.get("branch", []) if b] | |
180 | |
181 # walk backwards through all builds of a single builder | |
182 g = self.builder.generateFinishedBuilds(map_branches(branches), | |
183 numbuilds) | |
184 | |
185 data = "" | |
186 html_branches = map(html.escape, branches) | |
187 data += ("<h1>Last %d builds of builder %s: %s</h1>\n" % | |
188 (numbuilds, self.builder_name, ", ".join(html_branches))) | |
189 data += "<ul>\n" | |
190 got = 0 | |
191 for build in g: | |
192 got += 1 | |
193 data += " <li>" + self.make_line(req, build) + "</li>\n" | |
194 if not got: | |
195 data += " <li>No matching builds found</li>\n" | |
196 data += "</ul>\n" | |
197 | |
198 return data | |
199 | |
200 # /one_box_per_builder | |
201 # accepts builder=, branch= | |
202 class OneBoxPerBuilder(HtmlResource): | |
203 """This shows a narrow table with one row per builder. The leftmost column | |
204 contains the builder name. The next column contains the results of the | |
205 most recent build. The right-hand column shows the builder's current | |
206 activity. | |
207 | |
208 builder=: show only builds for this builder. Multiple builder= arguments | |
209 can be used to see builds from any builder in the set. | |
210 """ | |
211 | |
212 title = "Latest Build" | |
213 | |
214 def body(self, req): | |
215 status = self.getStatus(req) | |
216 control = self.getControl(req) | |
217 | |
218 builders = req.args.get("builder", status.getBuilderNames()) | |
219 branches = [b for b in req.args.get("branch", []) if b] | |
220 | |
221 data = "" | |
222 | |
223 html_branches = map(html.escape, branches) | |
224 data += "<h2>Latest builds: %s</h2>\n" % ", ".join(html_branches) | |
225 data += "<table>\n" | |
226 | |
227 building = False | |
228 online = 0 | |
229 base_builders_url = self.path_to_root(req) + "builders/" | |
230 for bn in builders: | |
231 base_builder_url = base_builders_url + urllib.quote(bn, safe='') | |
232 builder = status.getBuilder(bn) | |
233 data += "<tr>\n" | |
234 data += '<td class="box"><a href="%s">%s</a></td>\n' \ | |
235 % (base_builder_url, html.escape(bn)) | |
236 builds = list(builder.generateFinishedBuilds(map_branches(branches), | |
237 num_builds=1)) | |
238 if builds: | |
239 b = builds[0] | |
240 url = (base_builder_url + "/builds/%d" % b.getNumber()) | |
241 try: | |
242 label = b.getProperty("got_revision") | |
243 except KeyError: | |
244 label = None | |
245 if not label or len(str(label)) > 20: | |
246 label = "#%d" % b.getNumber() | |
247 text = ['<a href="%s">%s</a>' % (url, label)] | |
248 text.extend(b.getText()) | |
249 box = Box(text, | |
250 class_="LastBuild box %s" % build_get_class(b)) | |
251 data += box.td(align="center") | |
252 else: | |
253 data += '<td class="LastBuild box" >no build</td>\n' | |
254 current_box = ICurrentBox(builder).getBox(status) | |
255 data += current_box.td(align="center") | |
256 | |
257 builder_status = builder.getState()[0] | |
258 if builder_status == "building": | |
259 building = True | |
260 online += 1 | |
261 elif builder_status != "offline": | |
262 online += 1 | |
263 | |
264 data += "</table>\n" | |
265 | |
266 if control is not None: | |
267 if building: | |
268 stopURL = "builders/_all/stop" | |
269 data += make_stop_form(stopURL, self.isUsingUserPasswd(req), | |
270 True, "Builds") | |
271 if online: | |
272 forceURL = "builders/_all/force" | |
273 data += make_force_build_form(forceURL, | |
274 self.isUsingUserPasswd(req), True) | |
275 | |
276 return data | |
277 | |
278 | |
279 | |
280 HEADER = ''' | |
281 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
282 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
283 | |
284 <html | |
285 xmlns="http://www.w3.org/1999/xhtml" | |
286 lang="en" | |
287 xml:lang="en"> | |
288 ''' | |
289 | |
290 HEAD_ELEMENTS = [ | |
291 '<title>%(title)s</title>', | |
292 '<link href="%(root)sbuildbot.css" rel="stylesheet" type="text/css" />', | |
293 ] | |
294 BODY_ATTRS = { | |
295 'vlink': "#800080", | |
296 } | |
297 | |
298 FOOTER = ''' | |
299 </html> | |
300 ''' | |
301 | |
302 | |
303 class WebStatus(service.MultiService): | |
304 implements(IStatusReceiver) | |
305 # TODO: IStatusReceiver is really about things which subscribe to hear | |
306 # about buildbot events. We need a different interface (perhaps a parent | |
307 # of IStatusReceiver) for status targets that don't subscribe, like the | |
308 # WebStatus class. buildbot.master.BuildMaster.loadConfig:737 asserts | |
309 # that everything in c['status'] provides IStatusReceiver, but really it | |
310 # should check that they provide IStatusTarget instead. | |
311 | |
312 """ | |
313 The webserver provided by this class has the following resources: | |
314 | |
315 /waterfall : the big time-oriented 'waterfall' display, with links | |
316 to individual changes, builders, builds, steps, and logs. | |
317 A number of query-arguments can be added to influence | |
318 the display. | |
319 /rss : a rss feed summarizing all failed builds. The same | |
320 query-arguments used by 'waterfall' can be added to | |
321 influence the feed output. | |
322 /atom : an atom feed summarizing all failed builds. The same | |
323 query-arguments used by 'waterfall' can be added to | |
324 influence the feed output. | |
325 /grid : another summary display that shows a grid of builds, with | |
326 sourcestamps on the x axis, and builders on the y. Query | |
327 arguments similar to those for the waterfall can be added. | |
328 /tgrid : similar to the grid display, but the commits are down the | |
329 left side, and the build hosts are across the top. | |
330 /builders/BUILDERNAME: a page summarizing the builder. This includes | |
331 references to the Schedulers that feed it, | |
332 any builds currently in the queue, which | |
333 buildslaves are designated or attached, and a | |
334 summary of the build process it uses. | |
335 /builders/BUILDERNAME/builds/NUM: a page describing a single Build | |
336 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step | |
337 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog | |
338 /builders/BUILDERNAME/builds/NUM/tests : summarize test results | |
339 /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test | |
340 /builders/_all/{force,stop}: force a build/stop building on all builders. | |
341 /changes : summarize all ChangeSources | |
342 /changes/CHANGENUM: a page describing a single Change | |
343 /schedulers/SCHEDULERNAME: a page describing a Scheduler, including | |
344 a description of its behavior, a list of the | |
345 Builders it triggers, and list of the Changes | |
346 that are queued awaiting the tree-stable | |
347 timer, and controls to accelerate the timer. | |
348 /buildslaves : list all BuildSlaves | |
349 /buildslaves/SLAVENAME : describe a single BuildSlave | |
350 /one_line_per_build : summarize the last few builds, one line each | |
351 /one_line_per_build/BUILDERNAME : same, but only for a single builder | |
352 /one_box_per_builder : show the latest build and current activity | |
353 /about : describe this buildmaster (Buildbot and support library versions) | |
354 /xmlrpc : (not yet implemented) an XMLRPC server with build status | |
355 | |
356 | |
357 All URLs for pages which are not defined here are used to look | |
358 for files in PUBLIC_HTML, which defaults to BASEDIR/public_html. | |
359 This means that /robots.txt or /buildbot.css or /favicon.ico can | |
360 be placed in that directory. | |
361 | |
362 If an index file (index.html, index.htm, or index, in that order) is | |
363 present in PUBLIC_HTML, it will be used for the root resource. If not, | |
364 the default behavior is to put a redirection to the /waterfall page. | |
365 | |
366 All of the resources provided by this service use relative URLs to reach | |
367 each other. The only absolute links are the c['projectURL'] links at the | |
368 top and bottom of the page, and the buildbot home-page link at the | |
369 bottom. | |
370 | |
371 This webserver defines class attributes on elements so they can be styled | |
372 with CSS stylesheets. All pages pull in PUBLIC_HTML/buildbot.css, and you | |
373 can cause additional stylesheets to be loaded by adding a suitable <link> | |
374 to the WebStatus instance's .head_elements attribute. | |
375 | |
376 Buildbot uses some generic classes to identify the type of object, and | |
377 some more specific classes for the various kinds of those types. It does | |
378 this by specifying both in the class attributes where applicable, | |
379 separated by a space. It is important that in your CSS you declare the | |
380 more generic class styles above the more specific ones. For example, | |
381 first define a style for .Event, and below that for .SUCCESS | |
382 | |
383 The following CSS class names are used: | |
384 - Activity, Event, BuildStep, LastBuild: general classes | |
385 - waiting, interlocked, building, offline, idle: Activity states | |
386 - start, running, success, failure, warnings, skipped, exception: | |
387 LastBuild and BuildStep states | |
388 - Change: box with change | |
389 - Builder: box for builder name (at top) | |
390 - Project | |
391 - Time | |
392 | |
393 """ | |
394 | |
395 # we are not a ComparableMixin, and therefore the webserver will be | |
396 # rebuilt every time we reconfig. This is because WebStatus.putChild() | |
397 # makes it too difficult to tell whether two instances are the same or | |
398 # not (we'd have to do a recursive traversal of all children to discover | |
399 # all the changes). | |
400 | |
401 def __init__(self, http_port=None, distrib_port=None, allowForce=False, | |
402 public_html="public_html", site=None, numbuilds=20, | |
403 num_events=200, num_events_max=None, auth=None, | |
404 order_console_by_time=False): | |
405 """Run a web server that provides Buildbot status. | |
406 | |
407 @type http_port: int or L{twisted.application.strports} string | |
408 @param http_port: a strports specification describing which port the | |
409 buildbot should use for its web server, with the | |
410 Waterfall display as the root page. For backwards | |
411 compatibility this can also be an int. Use | |
412 'tcp:8000' to listen on that port, or | |
413 'tcp:12345:interface=127.0.0.1' if you only want | |
414 local processes to connect to it (perhaps because | |
415 you are using an HTTP reverse proxy to make the | |
416 buildbot available to the outside world, and do not | |
417 want to make the raw port visible). | |
418 | |
419 @type distrib_port: int or L{twisted.application.strports} string | |
420 @param distrib_port: Use this if you want to publish the Waterfall | |
421 page using web.distrib instead. The most common | |
422 case is to provide a string that is an absolute | |
423 pathname to the unix socket on which the | |
424 publisher should listen | |
425 (C{os.path.expanduser(~/.twistd-web-pb)} will | |
426 match the default settings of a standard | |
427 twisted.web 'personal web server'). Another | |
428 possibility is to pass an integer, which means | |
429 the publisher should listen on a TCP socket, | |
430 allowing the web server to be on a different | |
431 machine entirely. Both forms are provided for | |
432 backwards compatibility; the preferred form is a | |
433 strports specification like | |
434 'unix:/home/buildbot/.twistd-web-pb'. Providing | |
435 a non-absolute pathname will probably confuse | |
436 the strports parser. | |
437 | |
438 @param allowForce: boolean, if True then the webserver will allow | |
439 visitors to trigger and cancel builds | |
440 | |
441 @param public_html: the path to the public_html directory for this displ
ay, | |
442 either absolute or relative to the basedir. The def
ault | |
443 is 'public_html', which selects BASEDIR/public_html. | |
444 | |
445 @type site: None or L{twisted.web.server.Site} | |
446 @param site: Use this if you want to define your own object instead of | |
447 using the default.` | |
448 | |
449 @type numbuilds: int | |
450 @param numbuilds: Default number of entries in lists at the /one_line_pe
r_build | |
451 and /builders/FOO URLs. This default can be overriden both programatica
lly --- | |
452 by passing the equally named argument to constructors of OneLinePerBuild
OneBuilder | |
453 and OneLinePerBuild --- and via the UI, by tacking ?numbuilds=xy onto th
e URL. | |
454 | |
455 @type num_events: int | |
456 @param num_events: Defaualt number of events to show in the waterfall. | |
457 | |
458 @type num_events_max: int | |
459 @param num_events_max: The maximum number of events that are allowed to
be | |
460 shown in the waterfall. The default value of C{None} will disable this | |
461 check | |
462 | |
463 @type auth: a L{status.web.auth.IAuth} or C{None} | |
464 @param auth: an object that performs authentication to restrict access | |
465 to the C{allowForce} features. Ignored if C{allowForce} | |
466 is not C{True}. If C{auth} is C{None}, people can force or | |
467 stop builds without auth. | |
468 | |
469 @type order_console_by_time: bool | |
470 @param order_console_by_time: Whether to order changes (commits) in the
console | |
471 view according to the time they were created (for VCS like
Git) or | |
472 according to their integer revision numbers (for VCS like S
VN). | |
473 """ | |
474 | |
475 service.MultiService.__init__(self) | |
476 if type(http_port) is int: | |
477 http_port = "tcp:%d" % http_port | |
478 self.http_port = http_port | |
479 if distrib_port is not None: | |
480 if type(distrib_port) is int: | |
481 distrib_port = "tcp:%d" % distrib_port | |
482 if distrib_port[0] in "/~.": # pathnames | |
483 distrib_port = "unix:%s" % distrib_port | |
484 self.distrib_port = distrib_port | |
485 self.allowForce = allowForce | |
486 self.num_events = num_events | |
487 if num_events_max: | |
488 assert num_events_max >= num_events | |
489 self.num_events_max = num_events_max | |
490 self.public_html = public_html | |
491 | |
492 if self.allowForce and auth: | |
493 assert IAuth.providedBy(auth) | |
494 self.auth = auth | |
495 else: | |
496 if auth: | |
497 log.msg("Warning: Ignoring authentication. allowForce must be" | |
498 " set to True use this") | |
499 self.auth = None | |
500 | |
501 self.orderConsoleByTime = order_console_by_time | |
502 | |
503 # If we were given a site object, go ahead and use it. | |
504 if site: | |
505 self.site = site | |
506 else: | |
507 # this will be replaced once we've been attached to a parent (and | |
508 # thus have a basedir and can reference BASEDIR) | |
509 root = static.Data("placeholder", "text/plain") | |
510 self.site = server.Site(root) | |
511 self.childrenToBeAdded = {} | |
512 | |
513 self.setupUsualPages(numbuilds=numbuilds, num_events=num_events, | |
514 num_events_max=num_events_max) | |
515 | |
516 # the following items are accessed by HtmlResource when it renders | |
517 # each page. | |
518 self.site.buildbot_service = self | |
519 self.header = HEADER | |
520 self.head_elements = HEAD_ELEMENTS[:] | |
521 self.body_attrs = BODY_ATTRS.copy() | |
522 self.footer = FOOTER | |
523 self.template_values = {} | |
524 | |
525 # keep track of cached connections so we can break them when we shut | |
526 # down. See ticket #102 for more details. | |
527 self.channels = weakref.WeakKeyDictionary() | |
528 | |
529 if self.http_port is not None: | |
530 s = strports.service(self.http_port, self.site) | |
531 s.setServiceParent(self) | |
532 if self.distrib_port is not None: | |
533 f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) | |
534 s = strports.service(self.distrib_port, f) | |
535 s.setServiceParent(self) | |
536 | |
537 def setupUsualPages(self, numbuilds, num_events, num_events_max): | |
538 #self.putChild("", IndexOrWaterfallRedirection()) | |
539 self.putChild("waterfall", WaterfallStatusResource(num_events=num_events
, | |
540 num_events_max=num_events_max)) | |
541 self.putChild("grid", GridStatusResource()) | |
542 self.putChild("console", ConsoleStatusResource( | |
543 orderByTime=self.orderConsoleByTime)) | |
544 self.putChild("tgrid", TransposedGridStatusResource()) | |
545 self.putChild("builders", BuildersResource()) # has builds/steps/logs | |
546 self.putChild("changes", ChangesResource()) | |
547 self.putChild("buildslaves", BuildSlavesResource()) | |
548 self.putChild("buildstatus", BuildStatusStatusResource()) | |
549 #self.putChild("schedulers", SchedulersResource()) | |
550 self.putChild("one_line_per_build", | |
551 OneLinePerBuild(numbuilds=numbuilds)) | |
552 self.putChild("one_box_per_builder", OneBoxPerBuilder()) | |
553 self.putChild("xmlrpc", XMLRPCServer()) | |
554 self.putChild("about", AboutBuildbot()) | |
555 self.putChild("authfail", AuthFailResource()) | |
556 | |
557 def __repr__(self): | |
558 if self.http_port is None: | |
559 return "<WebStatus on path %s at %s>" % (self.distrib_port, | |
560 hex(id(self))) | |
561 if self.distrib_port is None: | |
562 return "<WebStatus on port %s at %s>" % (self.http_port, | |
563 hex(id(self))) | |
564 return ("<WebStatus on port %s and path %s at %s>" % | |
565 (self.http_port, self.distrib_port, hex(id(self)))) | |
566 | |
567 def setServiceParent(self, parent): | |
568 service.MultiService.setServiceParent(self, parent) | |
569 | |
570 # this class keeps a *separate* link to the buildmaster, rather than | |
571 # just using self.parent, so that when we are "disowned" (and thus | |
572 # parent=None), any remaining HTTP clients of this WebStatus will still | |
573 # be able to get reasonable results. | |
574 self.master = parent | |
575 | |
576 self.setupSite() | |
577 | |
578 def setupSite(self): | |
579 # this is responsible for creating the root resource. It isn't done | |
580 # at __init__ time because we need to reference the parent's basedir. | |
581 htmldir = os.path.abspath(os.path.join(self.master.basedir, self.public_
html)) | |
582 if os.path.isdir(htmldir): | |
583 log.msg("WebStatus using (%s)" % htmldir) | |
584 else: | |
585 log.msg("WebStatus: warning: %s is missing. Do you need to run" | |
586 " 'buildbot upgrade-master' on this buildmaster?" % htmldir) | |
587 # all static pages will get a 404 until upgrade-master is used to | |
588 # populate this directory. Create the directory, though, since | |
589 # otherwise we get internal server errors instead of 404s. | |
590 os.mkdir(htmldir) | |
591 root = static.File(htmldir) | |
592 | |
593 for name, child_resource in self.childrenToBeAdded.iteritems(): | |
594 root.putChild(name, child_resource) | |
595 | |
596 status = self.getStatus() | |
597 # Disabled from Chromium. | |
598 # root.putChild("rss", Rss20StatusResource(status)) | |
599 # root.putChild("atom", Atom10StatusResource(status)) | |
600 root.putChild("json", JsonStatusResource(status)) | |
601 | |
602 self.site.resource = root | |
603 | |
604 def putChild(self, name, child_resource): | |
605 """This behaves a lot like root.putChild() . """ | |
606 self.childrenToBeAdded[name] = child_resource | |
607 | |
608 def registerChannel(self, channel): | |
609 self.channels[channel] = 1 # weakrefs | |
610 | |
611 def stopService(self): | |
612 for channel in self.channels: | |
613 try: | |
614 channel.transport.loseConnection() | |
615 except: | |
616 log.msg("WebStatus.stopService: error while disconnecting" | |
617 " leftover clients") | |
618 log.err() | |
619 return service.MultiService.stopService(self) | |
620 | |
621 def getStatus(self): | |
622 return self.master.getStatus() | |
623 | |
624 def getControl(self): | |
625 if self.allowForce: | |
626 return IControl(self.master) | |
627 return None | |
628 | |
629 def getChangeSvc(self): | |
630 return self.master.change_svc | |
631 | |
632 def getPortnum(self): | |
633 # this is for the benefit of unit tests | |
634 s = list(self)[0] | |
635 return s._port.getHost().port | |
636 | |
637 def isUsingUserPasswd(self): | |
638 """Returns boolean to indicate if this WebStatus uses authentication""" | |
639 if self.auth: | |
640 return True | |
641 return False | |
642 | |
643 def authUser(self, user, passwd): | |
644 """Check that user/passwd is a valid user/pass tuple and can should be | |
645 allowed to perform the action. If this WebStatus is not password | |
646 protected, this function returns False.""" | |
647 if not self.isUsingUserPasswd(): | |
648 return False | |
649 if self.auth.authenticate(user, passwd): | |
650 return True | |
651 log.msg("Authentication failed for '%s': %s" % (user, | |
652 self.auth.errmsg())) | |
653 return False | |
654 | |
655 # resources can get access to the IStatus by calling | |
656 # request.site.buildbot_service.getStatus() | |
657 | |
658 # this is the compatibility class for the old waterfall. It is exactly like a | |
659 # regular WebStatus except that the root resource (e.g. http://buildbot.net/) | |
660 # always redirects to a WaterfallStatusResource, and the old arguments are | |
661 # mapped into the new resource-tree approach. In the normal WebStatus, the | |
662 # root resource either redirects the browser to /waterfall or serves | |
663 # PUBLIC_HTML/index.html, and favicon/robots.txt are provided by | |
664 # having the admin write actual files into PUBLIC_HTML/ . | |
665 | |
666 # note: we don't use a util.Redirect here because HTTP requires that the | |
667 # Location: header provide an absolute URI, and it's non-trivial to figure | |
668 # out our absolute URI from here. | |
669 | |
670 class Waterfall(WebStatus): | |
671 | |
672 if hasattr(sys, "frozen"): | |
673 # all 'data' files are in the directory of our executable | |
674 here = os.path.dirname(sys.executable) | |
675 buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png")) | |
676 buildbot_css = os.path.abspath(os.path.join(here, "classic.css")) | |
677 else: | |
678 # running from source | |
679 # the icon is sibpath(__file__, "../buildbot.png") . This is for | |
680 # portability. | |
681 up = os.path.dirname | |
682 buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))), | |
683 "buildbot.png")) | |
684 buildbot_css = os.path.abspath(os.path.join(up(__file__), | |
685 "classic.css")) | |
686 | |
687 compare_attrs = ["http_port", "distrib_port", "allowForce", | |
688 "categories", "css", "favicon", "robots_txt"] | |
689 | |
690 def __init__(self, http_port=None, distrib_port=None, allowForce=True, | |
691 categories=None, css=buildbot_css, favicon=buildbot_icon, | |
692 robots_txt=None): | |
693 import warnings | |
694 m = ("buildbot.status.html.Waterfall is deprecated as of 0.7.6 " | |
695 "and will be removed from a future release. " | |
696 "Please use html.WebStatus instead.") | |
697 warnings.warn(m, DeprecationWarning) | |
698 | |
699 WebStatus.__init__(self, http_port, distrib_port, allowForce) | |
700 self.css = css | |
701 if css: | |
702 if os.path.exists(os.path.join("public_html", "buildbot.css")): | |
703 # they've upgraded, so defer to that copy instead | |
704 pass | |
705 else: | |
706 data = open(css, "rb").read() | |
707 self.putChild("buildbot.css", static.Data(data, "text/css")) | |
708 self.favicon = favicon | |
709 self.robots_txt = robots_txt | |
710 if favicon: | |
711 data = open(favicon, "rb").read() | |
712 self.putChild("favicon.ico", static.Data(data, "image/x-icon")) | |
713 if robots_txt: | |
714 data = open(robots_txt, "rb").read() | |
715 self.putChild("robots.txt", static.Data(data, "text/plain")) | |
716 self.putChild("", WaterfallStatusResource(categories)) | |
OLD | NEW |