| 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 |