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