web: implement addSlash, fix path_to_root
[buildbot.git] / buildbot / status / web / baseweb.py
blob6b915bbe987fd97f70390806a828e61610a6f88a
2 import os, sys
3 from itertools import count
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
9 from twisted.spread import pb
11 from buildbot.interfaces import IControl, IStatusReceiver
12 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
14 from buildbot.status.web.base import HtmlResource
15 from buildbot.status.web.waterfall import WaterfallStatusResource
16 from buildbot.status.web.changes import ChangesResource
17 from buildbot.status.web.builder import BuildersResource
19 # this class contains the status services (WebStatus and the older Waterfall)
20 # which can be put in c['status']. It also contains some of the resources
21 # that are attached to the WebStatus at various well-known URLs, which the
22 # admin might wish to attach (using WebStatus.putChild) at other URLs.
25 class TimelineOfEverything(WaterfallStatusResource):
27 def __init__(self):
28 HtmlResource.__init__(self)
30 def render(self, request):
31 webstatus = request.site.webstatus
32 self.css = webstatus.css
33 self.status = request.site.status
34 self.changemaster = webstatus.parent.change_svc
35 self.categories = None
36 self.title = self.status.getProjectName()
37 if self.title is None:
38 self.title = "BuildBot"
39 return WaterfallStatusResource.render(self, request)
42 class LastBuild(HtmlResource):
43 def body(self, request):
44 return "missing\n"
46 def getLastNBuilds(status, numbuilds, desired_builder_names=None):
47 """Return a list with the last few Builds, sorted by start time.
48 builder_names=None means all builders
49 """
51 # TODO: this unsorts the list of builder names, ick
52 builder_names = set(status.getBuilderNames())
53 if desired_builder_names is not None:
54 desired_builder_names = set(desired_builder_names)
55 builder_names = builder_names.intersection(desired_builder_names)
57 # to make sure that we get everything, we must get 'numbuilds' builds
58 # from *each* source, then sort by ending time, then trim to the last
59 # 20. We could be more efficient, but it would require the same
60 # gnarly code that the Waterfall uses to generate one event at a
61 # time.
62 events = []
63 for builder_name in builder_names:
64 builder = status.getBuilder(builder_name)
65 for build_number in count(1):
66 if build_number > numbuilds:
67 break # enough from this builder, move on to another
68 build = builder.getBuild(-build_number)
69 if not build:
70 break # no more builds here, move on to the next builder
71 #if not build.isFinished():
72 # continue
73 (build_start, build_end) = build.getTimes()
74 event = (build_start, builder_name, build)
75 events.append(event)
76 def _sorter(a, b):
77 return cmp( a[:2], b[:2] )
78 events.sort(_sorter)
79 # now only return the actual build, and only return some of them
80 return [e[2] for e in events[-numbuilds:]]
83 def oneLineForABuild(status, build):
84 css_classes = {SUCCESS: "success",
85 WARNINGS: "warnings",
86 FAILURE: "failure",
87 EXCEPTION: "exception",
90 builder_name = build.getBuilder().getName()
91 results = build.getResults()
92 rev = build.getProperty("got_revision")
93 if len(rev) > 20:
94 rev = "?"
95 values = {'class': css_classes[results],
96 'builder_name': builder_name,
97 'buildnum': build.getNumber(),
98 'results': css_classes[results],
99 'buildurl': status.getURLForThing(build),
100 'rev': rev,
102 fmt = ('<div class="%(class)s">Build '
103 '<a href="%(buildurl)s">#%(buildnum)d</a> of '
104 '%(builder_name)s [%(rev)s]: '
105 '<span class="%(class)s">%(results)s</span></div>\n')
106 data = fmt % values
107 return data
109 # /_buildbot/one_line_per_build
110 class OneLinePerBuild(HtmlResource):
111 """This shows one line per build, combining all builders together. Useful
112 query arguments:
114 numbuilds=: how many lines to display
115 builder=: show only builds for this builder. Multiple builder= arguments
116 can be used to see builds from any builder in the set.
119 def __init__(self, numbuilds=20):
120 HtmlResource.__init__(self)
121 self.numbuilds = numbuilds
123 def getChild(self, path, request):
124 status = request.site.status
125 builder = status.getBuilder(path)
126 return OneLinePerBuildOneBuilder(builder)
128 def body(self, request):
129 status = request.site.status
130 numbuilds = self.numbuilds
131 if "numbuilds" in request.args:
132 numbuilds = int(request.args["numbuilds"][0])
134 desired_builder_names = None
135 if "builder" in request.args:
136 desired_builder_names = request.args["builder"]
137 builds = getLastNBuilds(status, numbuilds, desired_builder_names)
138 data = ""
139 for build in reversed(builds):
140 data += oneLineForABuild(status, build)
141 else:
142 data += "<div>No matching builds found</div>"
143 return data
147 # /_buildbot/one_line_per_build/$BUILDERNAME
148 class OneLinePerBuildOneBuilder(HtmlResource):
149 def __init__(self, builder, numbuilds=20):
150 HtmlResource.__init__(self)
151 self.builder = builder
152 self.numbuilds = numbuilds
154 def body(self, request):
155 status = request.site.status
156 numbuilds = self.numbuilds
157 if "numbuilds" in request.args:
158 numbuilds = int(request.args["numbuilds"][0])
159 # walk backwards through all builds of a single builder
161 # islice is cool but not exactly what we need here
162 #events = itertools.islice(b.eventGenerator(), self.numbuilds)
164 css_classes = {SUCCESS: "success",
165 WARNINGS: "warnings",
166 FAILURE: "failure",
167 EXCEPTION: "exception",
170 data = ""
171 i = 1
172 while i < numbuilds:
173 build = self.builder.getBuild(-i)
174 if not build:
175 break
176 i += 1
178 data += oneLineForABuild(status, build)
180 return data
184 HEADER = '''
185 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
186 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
188 <html
189 xmlns="http://www.w3.org/1999/xhtml"
190 lang="en"
191 xml:lang="en">
194 HEAD_ELEMENTS = [
195 '<title>%(title)s</title>',
196 '<link href="%(root)sbuildbot.css" rel="stylesheet" type="text/css" />',
198 BODY_ATTRS = {
199 'vlink': "#800080",
202 FOOTER = '''
203 </html>
207 class WebStatus(service.MultiService):
208 implements(IStatusReceiver)
209 # TODO: IStatusReceiver is really about things which subscribe to hear
210 # about buildbot events. We need a different interface (perhaps a parent
211 # of IStatusReceiver) for status targets that don't subscribe, like the
212 # WebStatus class. buildbot.master.BuildMaster.loadConfig:737 asserts
213 # that everything in c['status'] provides IStatusReceiver, but really it
214 # should check that they provide IStatusTarget instead.
217 The webserver provided by this class has the following resources:
219 /waterfall : the big time-oriented 'waterfall' display, with links
220 to individual changes, builders, builds, steps, and logs.
221 A number of query-arguments can be added to influence
222 the display.
223 /builders/BUILDERNAME: a page summarizing the builder. This includes
224 references to the Schedulers that feed it,
225 any builds currently in the queue, which
226 buildslaves are designated or attached, and a
227 summary of the build process it uses.
228 /builders/BUILDERNAME/builds/NUM: a page describing a single Build
229 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step
230 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog
231 /builders/BUILDERNAME/builds/NUM/tests : summarize test results
232 /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test
233 /changes : summarize all ChangeSources
234 /changes/CHANGENUM: a page describing a single Change
235 /schedulers/SCHEDULERNAME: a page describing a Scheduler, including
236 a description of its behavior, a list of the
237 Builders it triggers, and list of the Changes
238 that are queued awaiting the tree-stable
239 timer, and controls to accelerate the timer.
240 /others...
242 All URLs for pages which are not defined here are used to look for files
243 in BASEDIR/public_html/ , which means that /robots.txt or /buildbot.css
244 or /favicon.ico can be placed in that directory.
246 If an index file (index.html, index.htm, or index, in that order) is
247 present in public_html/, it will be used for the root resource. If not,
248 the default behavior is to put a redirection to the /waterfall page.
250 All of the resources provided by this service use relative URLs to reach
251 each other. The only absolute links are the c['projectURL'] links at the
252 top and bottom of the page, and the buildbot home-page link at the
253 bottom.
255 This webserver defines class attributes on elements so they can be styled
256 with CSS stylesheets. All pages pull in public_html/buildbot.css, and you
257 can cause additional stylesheets to be loaded by adding a suitable <link>
258 to the WebStatus instance's .head_elements attribute.
260 Buildbot uses some generic classes to identify the type of object, and
261 some more specific classes for the various kinds of those types. It does
262 this by specifying both in the class attributes where applicable,
263 separated by a space. It is important that in your CSS you declare the
264 more generic class styles above the more specific ones. For example,
265 first define a style for .Event, and below that for .SUCCESS
267 The following CSS class names are used:
268 - Activity, Event, BuildStep, LastBuild: general classes
269 - waiting, interlocked, building, offline, idle: Activity states
270 - start, running, success, failure, warnings, skipped, exception:
271 LastBuild and BuildStep states
272 - Change: box with change
273 - Builder: box for builder name (at top)
274 - Project
275 - Time
279 # we are not a ComparableMixin, and therefore the webserver will be
280 # rebuilt every time we reconfig.
282 def __init__(self, http_port=None, distrib_port=None, allowForce=False):
283 """Run a web server that provides Buildbot status.
285 @type http_port: int or L{twisted.application.strports} string
286 @param http_port: a strports specification describing which port the
287 buildbot should use for its web server, with the
288 Waterfall display as the root page. For backwards
289 compatibility this can also be an int. Use
290 'tcp:8000' to listen on that port, or
291 'tcp:12345:interface=127.0.0.1' if you only want
292 local processes to connect to it (perhaps because
293 you are using an HTTP reverse proxy to make the
294 buildbot available to the outside world, and do not
295 want to make the raw port visible).
297 @type distrib_port: int or L{twisted.application.strports} string
298 @param distrib_port: Use this if you want to publish the Waterfall
299 page using web.distrib instead. The most common
300 case is to provide a string that is an absolute
301 pathname to the unix socket on which the
302 publisher should listen
303 (C{os.path.expanduser(~/.twistd-web-pb)} will
304 match the default settings of a standard
305 twisted.web 'personal web server'). Another
306 possibility is to pass an integer, which means
307 the publisher should listen on a TCP socket,
308 allowing the web server to be on a different
309 machine entirely. Both forms are provided for
310 backwards compatibility; the preferred form is a
311 strports specification like
312 'unix:/home/buildbot/.twistd-web-pb'. Providing
313 a non-absolute pathname will probably confuse
314 the strports parser.
315 @param allowForce: boolean, if True then the webserver will allow
316 visitors to trigger and cancel builds
319 service.MultiService.__init__(self)
320 if type(http_port) is int:
321 http_port = "tcp:%d" % http_port
322 self.http_port = http_port
323 if distrib_port is not None:
324 if type(distrib_port) is int:
325 distrib_port = "tcp:%d" % distrib_port
326 if distrib_port[0] in "/~.": # pathnames
327 distrib_port = "unix:%s" % distrib_port
328 self.distrib_port = distrib_port
329 self.allowForce = allowForce
331 # this will be replaced once we've been attached to a parent (and
332 # thus have a basedir and can reference BASEDIR/public_html/)
333 root = static.Data("placeholder", "text/plain")
334 self.site = server.Site(root)
335 self.childrenToBeAdded = {}
337 self.setupUsualPages()
339 # the following items are accessed by HtmlResource when it renders
340 # each page.
341 self.site.buildbot_service = self
342 self.header = HEADER
343 self.head_elements = HEAD_ELEMENTS[:]
344 self.body_attrs = BODY_ATTRS.copy()
345 self.footer = FOOTER
346 self.template_values = {}
348 if self.http_port is not None:
349 s = strports.service(self.http_port, self.site)
350 s.setServiceParent(self)
351 if self.distrib_port is not None:
352 f = pb.PBServerFactory(distrib.ResourcePublisher(self.site))
353 s = strports.service(self.distrib_port, f)
354 s.setServiceParent(self)
356 def setupUsualPages(self):
357 #self.putChild("", IndexOrWaterfallRedirection())
358 self.putChild("waterfall", WaterfallStatusResource())
359 self.putChild("builders", BuildersResource())
360 self.putChild("changes", ChangesResource())
361 #self.putChild("schedulers", SchedulersResource())
363 self.putChild("one_line_per_build", OneLinePerBuild())
365 def __repr__(self):
366 if self.http_port is None:
367 return "<WebStatus on path %s>" % self.distrib_port
368 if self.distrib_port is None:
369 return "<WebStatus on port %s>" % self.http_port
370 return "<WebStatus on port %s and path %s>" % (self.http_port,
371 self.distrib_port)
373 def setServiceParent(self, parent):
374 service.MultiService.setServiceParent(self, parent)
375 self.setupSite()
377 def setupSite(self):
378 # this is responsible for creating the root resource. It isn't done
379 # at __init__ time because we need to reference the parent's basedir.
380 htmldir = os.path.join(self.parent.basedir, "public_html")
381 if not os.path.isdir(htmldir):
382 os.mkdir(htmldir)
383 root = static.File(htmldir)
384 log.msg("WebStatus using (%s)" % htmldir)
386 for name, child_resource in self.childrenToBeAdded.iteritems():
387 root.putChild(name, child_resource)
389 self.site.resource = root
391 def putChild(self, name, child_resource):
392 """This behaves a lot like root.putChild() . """
393 self.childrenToBeAdded[name] = child_resource
395 def getStatus(self):
396 return self.parent.getStatus()
397 def getControl(self):
398 if self.allowForce:
399 return IControl(self.parent)
400 return None
402 def getPortnum(self):
403 # this is for the benefit of unit tests
404 s = list(self)[0]
405 return s._port.getHost().port
407 # resources can get access to the IStatus by calling
408 # request.site.buildbot_service.getStatus()
410 # this is the compatibility class for the old waterfall. It is exactly like a
411 # regular WebStatus except that the root resource (e.g. http://buildbot.net/)
412 # always redirects to a WaterfallStatusResource, and the old arguments are
413 # mapped into the new resource-tree approach. In the normal WebStatus, the
414 # root resource either redirects the browser to /waterfall or serves
415 # BASEDIR/public_html/index.html, and favicon/robots.txt are provided by
416 # having the admin write actual files into BASEDIR/public_html/ .
418 # note: we don't use a util.Redirect here because HTTP requires that the
419 # Location: header provide an absolute URI, and it's non-trivial to figure
420 # out our absolute URI from here.
422 class Waterfall(WebStatus):
424 if hasattr(sys, "frozen"):
425 # all 'data' files are in the directory of our executable
426 here = os.path.dirname(sys.executable)
427 buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png"))
428 buildbot_css = os.path.abspath(os.path.join(here, "classic.css"))
429 else:
430 # running from source
431 # the icon is sibpath(__file__, "../buildbot.png") . This is for
432 # portability.
433 up = os.path.dirname
434 buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))),
435 "buildbot.png"))
436 buildbot_css = os.path.abspath(os.path.join(up(__file__),
437 "classic.css"))
439 compare_attrs = ["http_port", "distrib_port", "allowForce",
440 "categories", "css", "favicon", "robots_txt"]
442 def __init__(self, http_port=None, distrib_port=None, allowForce=True,
443 categories=None, css=buildbot_css, favicon=buildbot_icon,
444 robots_txt=None):
445 WebStatus.__init__(self, http_port, distrib_port, allowForce)
446 self.css = css
447 if css:
448 data = open(css, "rb").read()
449 self.putChild("buildbot.css", static.Data(data, "text/plain"))
450 self.favicon = favicon
451 self.robots_txt = robots_txt
452 if favicon:
453 data = open(favicon, "rb").read()
454 self.putChild("favicon.ico", static.Data(data, "image/x-icon"))
455 if robots_txt:
456 data = open(robots_txt, "rb").read()
457 self.putChild("robots.txt", static.Data(data, "text/plain"))
458 self.putChild("", WaterfallStatusResource(categories))