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