web-refactoring: get most of the Waterfall sub-pages working again
[buildbot.git] / buildbot / status / web / baseweb.py
blobd67888552e6794d1086724d49960edc49d1a0d02
2 from itertools import count
3 from zope.interface import implements
5 from twisted.python import log
6 from twisted.application import service, strports
7 from twisted.web import server, distrib, static
8 from twisted.spread import pb
10 from buildbot.interfaces import IStatusReceiver, IControl
11 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
13 from buildbot.status.web.base import HtmlResource
14 from buildbot.status.web.waterfall import WaterfallStatusResource
15 from buildbot.status.web.changes import ChangesResource
16 from buildbot.status.web.builder import BuildersResource
18 # this class contains the status services (WebStatus and the older Waterfall)
19 # which can be put in c['status']. It also contains some of the resources
20 # that are attached to the WebStatus at various well-known URLs, which the
21 # admin might wish to attach (using WebStatus.putChild) at other URLs.
24 class TimelineOfEverything(WaterfallStatusResource):
26 def __init__(self):
27 HtmlResource.__init__(self)
29 def render(self, request):
30 webstatus = request.site.webstatus
31 self.css = webstatus.css
32 self.status = request.site.status
33 self.changemaster = webstatus.parent.change_svc
34 self.categories = None
35 self.title = self.status.getProjectName()
36 if self.title is None:
37 self.title = "BuildBot"
38 return WaterfallStatusResource.render(self, request)
41 class LastBuild(HtmlResource):
42 def body(self, request):
43 return "missing\n"
45 def getLastNBuilds(status, numbuilds, desired_builder_names=None):
46 """Return a list with the last few Builds, sorted by start time.
47 builder_names=None means all builders
48 """
50 # TODO: this unsorts the list of builder names, ick
51 builder_names = set(status.getBuilderNames())
52 if desired_builder_names is not None:
53 desired_builder_names = set(desired_builder_names)
54 builder_names = builder_names.intersection(desired_builder_names)
56 # to make sure that we get everything, we must get 'numbuilds' builds
57 # from *each* source, then sort by ending time, then trim to the last
58 # 20. We could be more efficient, but it would require the same
59 # gnarly code that the Waterfall uses to generate one event at a
60 # time.
61 events = []
62 for builder_name in builder_names:
63 builder = status.getBuilder(builder_name)
64 for build_number in count(1):
65 if build_number > numbuilds:
66 break # enough from this builder, move on to another
67 build = builder.getBuild(-build_number)
68 if not build:
69 break # no more builds here, move on to the next builder
70 #if not build.isFinished():
71 # continue
72 (build_start, build_end) = build.getTimes()
73 event = (build_start, builder_name, build)
74 events.append(event)
75 def _sorter(a, b):
76 return cmp( a[:2], b[:2] )
77 events.sort(_sorter)
78 # now only return the actual build, and only return some of them
79 return [e[2] for e in events[-numbuilds:]]
82 def oneLineForABuild(status, build):
83 css_classes = {SUCCESS: "success",
84 WARNINGS: "warnings",
85 FAILURE: "failure",
86 EXCEPTION: "exception",
89 builder_name = build.getBuilder().getName()
90 results = build.getResults()
91 rev = build.getProperty("got_revision")
92 if len(rev) > 20:
93 rev = "?"
94 values = {'class': css_classes[results],
95 'builder_name': builder_name,
96 'buildnum': build.getNumber(),
97 'results': css_classes[results],
98 'buildurl': status.getURLForThing(build),
99 'rev': rev,
101 fmt = ('<div class="%(class)s">Build '
102 '<a href="%(buildurl)s">#%(buildnum)d</a> of '
103 '%(builder_name)s [%(rev)s]: '
104 '<span class="%(class)s">%(results)s</span></div>\n')
105 data = fmt % values
106 return data
108 # /_buildbot/one_line_per_build
109 class OneLinePerBuild(HtmlResource):
110 """This shows one line per build, combining all builders together. Useful
111 query arguments:
113 numbuilds=: how many lines to display
114 builder=: show only builds for this builder. Multiple builder= arguments
115 can be used to see builds from any builder in the set.
118 def __init__(self, numbuilds=20):
119 HtmlResource.__init__(self)
120 self.numbuilds = numbuilds
122 def getChild(self, path, request):
123 status = request.site.status
124 builder = status.getBuilder(path)
125 return OneLinePerBuildOneBuilder(builder)
127 def body(self, request):
128 status = request.site.status
129 numbuilds = self.numbuilds
130 if "numbuilds" in request.args:
131 numbuilds = int(request.args["numbuilds"][0])
133 desired_builder_names = None
134 if "builder" in request.args:
135 desired_builder_names = request.args["builder"]
136 builds = getLastNBuilds(status, numbuilds, desired_builder_names)
137 data = ""
138 for build in reversed(builds):
139 data += oneLineForABuild(status, build)
140 else:
141 data += "<div>No matching builds found</div>"
142 return data
146 # /_buildbot/one_line_per_build/$BUILDERNAME
147 class OneLinePerBuildOneBuilder(HtmlResource):
148 def __init__(self, builder, numbuilds=20):
149 HtmlResource.__init__(self)
150 self.builder = builder
151 self.numbuilds = numbuilds
153 def body(self, request):
154 status = request.site.status
155 numbuilds = self.numbuilds
156 if "numbuilds" in request.args:
157 numbuilds = int(request.args["numbuilds"][0])
158 # walk backwards through all builds of a single builder
160 # islice is cool but not exactly what we need here
161 #events = itertools.islice(b.eventGenerator(), self.numbuilds)
163 css_classes = {SUCCESS: "success",
164 WARNINGS: "warnings",
165 FAILURE: "failure",
166 EXCEPTION: "exception",
169 data = ""
170 i = 1
171 while i < numbuilds:
172 build = self.builder.getBuild(-i)
173 if not build:
174 break
175 i += 1
177 data += oneLineForABuild(status, build)
179 return data
183 HEADER = '''
184 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
185 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
187 <html
188 xmlns="http://www.w3.org/1999/xhtml"
189 lang="en"
190 xml:lang="en">
192 <head>
193 <title>%(title)s</title>
194 <link href="%(css_path)s" rel="stylesheet" type="text/css" />
195 </head>
199 FOOTER = '''
200 </html>
204 class WebStatus(service.MultiService):
205 implements(IStatusReceiver)
208 The webserver provided by this class has the following resources:
210 /waterfall : the big time-oriented 'waterfall' display, with links
211 to individual changes, builders, builds, steps, and logs.
212 A number of query-arguments can be added to influence
213 the display.
214 /builders/BUILDERNAME: a page summarizing the builder. This includes
215 references to the Schedulers that feed it,
216 any builds currently in the queue, which
217 buildslaves are designated or attached, and a
218 summary of the build process it uses.
219 /builders/BUILDERNAME/builds/NUM: a page describing a single Build
220 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step
221 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog
222 /builders/BUILDERNAME/builds/NUM/tests : summarize test results
223 /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test
224 /changes : summarize all ChangeSources
225 /changes/CHANGENUM: a page describing a single Change
226 /schedulers/SCHEDULERNAME: a page describing a Scheduler, including
227 a description of its behavior, a list of the
228 Builders it triggers, and list of the Changes
229 that are queued awaiting the tree-stable
230 timer, and controls to accelerate the timer.
231 /others...
233 All URLs for pages which are not defined here are used to look for files
234 in BASEDIR/public_html/ , which means that /robots.txt or /buildbot.css
235 or /favicon.ico can be placed in that directory.
237 If an index file (index.html, index.htm, or index, in that order) is
238 present in public_html/, it will be used for the root resource. If not,
239 the default behavior is to put a redirection to the /waterfall page.
241 All of the resources provided by this service use relative URLs to reach
242 each other. The only absolute links are the c['projectURL'] links at the
243 top and bottom of the page, and the buildbot home-page link at the
244 bottom.
246 This webserver defines class attributes on elements so they can be styled
247 with CSS stylesheets. Buildbot uses some generic classes to identify the
248 type of object, and some more specific classes for the various kinds of
249 those types. It does this by specifying both in the class attributes
250 where applicable, separated by a space. It is important that in your CSS
251 you declare the more generic class styles above the more specific ones.
252 For example, first define a style for .Event, and below that for .SUCCESS
254 The following CSS class names are used:
255 - Activity, Event, BuildStep, LastBuild: general classes
256 - waiting, interlocked, building, offline, idle: Activity states
257 - start, running, success, failure, warnings, skipped, exception:
258 LastBuild and BuildStep states
259 - Change: box with change
260 - Builder: box for builder name (at top)
261 - Project
262 - Time
266 compare_attrs = ["http_port", "distrib_port", "allowForce", "css"]
267 # TODO: putChild should cause two instances to compare differently
269 def __init__(self, http_port=None, distrib_port=None,
270 allowForce=False, css="buildbot.css"):
271 """Run a web server that provides Buildbot status.
273 @type http_port: int or L{twisted.application.strports} string
274 @param http_port: a strports specification describing which port the
275 buildbot should use for its web server, with the
276 Waterfall display as the root page. For backwards
277 compatibility this can also be an int. Use
278 'tcp:8000' to listen on that port, or
279 'tcp:12345:interface=127.0.0.1' if you only want
280 local processes to connect to it (perhaps because
281 you are using an HTTP reverse proxy to make the
282 buildbot available to the outside world, and do not
283 want to make the raw port visible).
285 @type distrib_port: int or L{twisted.application.strports} string
286 @param distrib_port: Use this if you want to publish the Waterfall
287 page using web.distrib instead. The most common
288 case is to provide a string that is an absolute
289 pathname to the unix socket on which the
290 publisher should listen
291 (C{os.path.expanduser(~/.twistd-web-pb)} will
292 match the default settings of a standard
293 twisted.web 'personal web server'). Another
294 possibility is to pass an integer, which means
295 the publisher should listen on a TCP socket,
296 allowing the web server to be on a different
297 machine entirely. Both forms are provided for
298 backwards compatibility; the preferred form is a
299 strports specification like
300 'unix:/home/buildbot/.twistd-web-pb'. Providing
301 a non-absolute pathname will probably confuse
302 the strports parser.
303 @param allowForce: boolean, if True then the webserver will allow
304 visitors to trigger and cancel builds
305 @param css: a URL. If set, the header of each generated page will
306 include a link to add the given URL as a CSS stylesheet
307 for the page.
310 service.MultiService.__init__(self)
311 if type(http_port) is int:
312 http_port = "tcp:%d" % http_port
313 self.http_port = http_port
314 if distrib_port is not None:
315 if type(distrib_port) is int:
316 distrib_port = "tcp:%d" % distrib_port
317 if distrib_port[0] in "/~.": # pathnames
318 distrib_port = "unix:%s" % distrib_port
319 self.distrib_port = distrib_port
320 self.allowForce = allowForce
321 self.css = css
323 self.setupSite()
325 if self.http_port is not None:
326 s = strports.service(self.http_port, self.site)
327 s.setServiceParent(self)
328 if self.distrib_port is not None:
329 f = pb.PBServerFactory(distrib.ResourcePublisher(self.site))
330 s = strports.service(self.distrib_port, f)
331 s.setServiceParent(self)
333 def setupSite(self):
334 # this is responsible for setting self.root and self.site
335 self.root = static.File("public_html")
336 log.msg("WebStatus using (%s)" % self.root.path)
337 self.setupUsualPages(self.root)
338 # once we get enabled, we'll stash a reference to the main IStatus
339 # instance in site.status, so all of our childrens' render() methods
340 # can access it as request.site.status
341 self.site = server.Site(self.root)
342 self.site.buildbot_service = self
343 self.header = HEADER
344 self.footer = FOOTER
345 self.template_values = {}
347 def getStatus(self):
348 return self.parent.getStatus()
349 def getControl(self):
350 if self.allowForce:
351 return IControl(self.parent)
352 return None
354 def setupUsualPages(self, root):
355 #root.putChild("", IndexOrWaterfallRedirection())
356 root.putChild("waterfall", WaterfallStatusResource())
357 root.putChild("builders", BuildersResource())
358 root.putChild("changes", ChangesResource())
359 #root.putChild("schedulers", SchedulersResource())
361 root.putChild("one_line_per_build", OneLinePerBuild())
363 def putChild(self, name, child_resource):
364 self.root.putChild(name, child_resource)
366 def __repr__(self):
367 if self.http_port is None:
368 return "<WebStatus on path %s>" % self.distrib_port
369 if self.distrib_port is None:
370 return "<WebStatus on port %s>" % self.http_port
371 return "<WebStatus on port %s and path %s>" % (self.http_port,
372 self.distrib_port)
374 # resources can get access to the IStatus by calling
375 # request.site.buildbot_service.getStatus()
377 # this is the compatibility class for the old waterfall. It is exactly like a
378 # regular WebStatus except that the root resource (e.g. http://buildbot.net/)
379 # is a WaterfallStatusResource. In the normal WebStatus, the waterfall is at
380 # e.g. http://builbot.net/waterfall, and the root resource either redirects
381 # the browser to that or serves BASEDIR/public_html/index.html .
382 class Waterfall(WebStatus):
383 def setupSite(self):
384 WebStatus.setupSite(self)
385 self.root.putChild("", WaterfallStatusResource())