Package buildbot :: Package status :: Package web :: Module baseweb
[frames] | no frames]

Source Code for Module buildbot.status.web.baseweb

  1   
  2  import os, weakref 
  3   
  4  from zope.interface import implements 
  5  from twisted.python import log 
  6  from twisted.application import strports, service 
  7  from twisted.web import server, distrib, static 
  8  from twisted.spread import pb 
  9  from twisted.web.util import Redirect 
 10   
 11  from buildbot.interfaces import IStatusReceiver 
 12   
 13  from buildbot.status.web.base import StaticFile, createJinjaEnv 
 14  from buildbot.status.web.feeds import Rss20StatusResource, \ 
 15       Atom10StatusResource 
 16  from buildbot.status.web.waterfall import WaterfallStatusResource 
 17  from buildbot.status.web.console import ConsoleStatusResource 
 18  from buildbot.status.web.olpb import OneLinePerBuild 
 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.status_json import JsonStatusResource 
 25  from buildbot.status.web.about import AboutBuildbot 
 26  from buildbot.status.web.authz import Authz 
 27  from buildbot.status.web.auth import AuthFailResource 
 28  from buildbot.status.web.root import RootPage 
 29  from buildbot.status.web.change_hook import ChangeHookResource 
 30   
 31  # this class contains the WebStatus class.  Basic utilities are in base.py, 
 32  # and specific pages are each in their own module. 
 33   
34 -class WebStatus(service.MultiService):
35 implements(IStatusReceiver) 36 # TODO: IStatusReceiver is really about things which subscribe to hear 37 # about buildbot events. We need a different interface (perhaps a parent 38 # of IStatusReceiver) for status targets that don't subscribe, like the 39 # WebStatus class. buildbot.master.BuildMaster.loadConfig:737 asserts 40 # that everything in c['status'] provides IStatusReceiver, but really it 41 # should check that they provide IStatusTarget instead. 42 43 """ 44 The webserver provided by this class has the following resources: 45 46 /waterfall : the big time-oriented 'waterfall' display, with links 47 to individual changes, builders, builds, steps, and logs. 48 A number of query-arguments can be added to influence 49 the display. 50 /rss : a rss feed summarizing all failed builds. The same 51 query-arguments used by 'waterfall' can be added to 52 influence the feed output. 53 /atom : an atom feed summarizing all failed builds. The same 54 query-arguments used by 'waterfall' can be added to 55 influence the feed output. 56 /grid : another summary display that shows a grid of builds, with 57 sourcestamps on the x axis, and builders on the y. Query 58 arguments similar to those for the waterfall can be added. 59 /tgrid : similar to the grid display, but the commits are down the 60 left side, and the build hosts are across the top. 61 /builders/BUILDERNAME: a page summarizing the builder. This includes 62 references to the Schedulers that feed it, 63 any builds currently in the queue, which 64 buildslaves are designated or attached, and a 65 summary of the build process it uses. 66 /builders/BUILDERNAME/builds/NUM: a page describing a single Build 67 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step 68 /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog 69 /builders/_all/{force,stop}: force a build/stop building on all builders. 70 /buildstatus?builder=...&number=...: an embedded iframe for the console 71 /changes : summarize all ChangeSources 72 /changes/CHANGENUM: a page describing a single Change 73 /buildslaves : list all BuildSlaves 74 /buildslaves/SLAVENAME : describe a single BuildSlave 75 /one_line_per_build : summarize the last few builds, one line each 76 /one_line_per_build/BUILDERNAME : same, but only for a single builder 77 /about : describe this buildmaster (Buildbot and support library versions) 78 /change_hook[/DIALECT] : accepts changes from external sources, optionally 79 choosing the dialect that will be permitted 80 (i.e. github format, etc..) 81 82 and more! see the manual. 83 84 85 All URLs for pages which are not defined here are used to look 86 for files in PUBLIC_HTML, which defaults to BASEDIR/public_html. 87 This means that /robots.txt or /favicon.ico can be placed in 88 that directory 89 90 This webserver uses the jinja2 template system to generate the web pages 91 (see http://jinja.pocoo.org/2/) and by default loads pages from the 92 buildbot.status.web.templates package. Any file here can be overridden by placing 93 a corresponding file in the master's 'templates' directory. 94 95 The main customization points are layout.html which loads style sheet 96 (css) and provides header and footer content, and root.html, which 97 generates the root page. 98 99 All of the resources provided by this service use relative URLs to reach 100 each other. The only absolute links are the c['projectURL'] links at the 101 top and bottom of the page, and the buildbot home-page link at the 102 bottom. 103 104 Buildbot uses some generic classes to identify the type of object, and 105 some more specific classes for the various kinds of those types. It does 106 this by specifying both in the class attributes where applicable, 107 separated by a space. It is important that in your CSS you declare the 108 more generic class styles above the more specific ones. For example, 109 first define a style for .Event, and below that for .SUCCESS 110 111 The following CSS class names are used: 112 - Activity, Event, BuildStep, LastBuild: general classes 113 - waiting, interlocked, building, offline, idle: Activity states 114 - start, running, success, failure, warnings, skipped, exception: 115 LastBuild and BuildStep states 116 - Change: box with change 117 - Builder: box for builder name (at top) 118 - Project 119 - Time 120 121 """ 122 123 # we are not a ComparableMixin, and therefore the webserver will be 124 # rebuilt every time we reconfig. This is because WebStatus.putChild() 125 # makes it too difficult to tell whether two instances are the same or 126 # not (we'd have to do a recursive traversal of all children to discover 127 # all the changes). 128
129 - def __init__(self, http_port=None, distrib_port=None, allowForce=None, 130 public_html="public_html", site=None, numbuilds=20, 131 num_events=200, num_events_max=None, auth=None, 132 order_console_by_time=False, changecommentlink=None, 133 revlink=None, projects=None, repositories=None, 134 authz=None, logRotateLength=None, maxRotatedFiles=None, 135 change_hook_dialects = {}):
136 """Run a web server that provides Buildbot status. 137 138 @type http_port: int or L{twisted.application.strports} string 139 @param http_port: a strports specification describing which port the 140 buildbot should use for its web server, with the 141 Waterfall display as the root page. For backwards 142 compatibility this can also be an int. Use 143 'tcp:8000' to listen on that port, or 144 'tcp:12345:interface=127.0.0.1' if you only want 145 local processes to connect to it (perhaps because 146 you are using an HTTP reverse proxy to make the 147 buildbot available to the outside world, and do not 148 want to make the raw port visible). 149 150 @type distrib_port: int or L{twisted.application.strports} string 151 @param distrib_port: Use this if you want to publish the Waterfall 152 page using web.distrib instead. The most common 153 case is to provide a string that is an absolute 154 pathname to the unix socket on which the 155 publisher should listen 156 (C{os.path.expanduser(~/.twistd-web-pb)} will 157 match the default settings of a standard 158 twisted.web 'personal web server'). Another 159 possibility is to pass an integer, which means 160 the publisher should listen on a TCP socket, 161 allowing the web server to be on a different 162 machine entirely. Both forms are provided for 163 backwards compatibility; the preferred form is a 164 strports specification like 165 'unix:/home/buildbot/.twistd-web-pb'. Providing 166 a non-absolute pathname will probably confuse 167 the strports parser. 168 169 @param allowForce: deprecated; use authz instead 170 @param auth: deprecated; use with authz 171 172 @param authz: a buildbot.status.web.authz.Authz instance giving the authorization 173 parameters for this view 174 175 @param public_html: the path to the public_html directory for this display, 176 either absolute or relative to the basedir. The default 177 is 'public_html', which selects BASEDIR/public_html. 178 179 @type site: None or L{twisted.web.server.Site} 180 @param site: Use this if you want to define your own object instead of 181 using the default.` 182 183 @type numbuilds: int 184 @param numbuilds: Default number of entries in lists at the /one_line_per_build 185 and /builders/FOO URLs. This default can be overriden both programatically --- 186 by passing the equally named argument to constructors of OneLinePerBuildOneBuilder 187 and OneLinePerBuild --- and via the UI, by tacking ?numbuilds=xy onto the URL. 188 189 @type num_events: int 190 @param num_events: Default number of events to show in the waterfall. 191 192 @type num_events_max: int 193 @param num_events_max: The maximum number of events that are allowed to be 194 shown in the waterfall. The default value of C{None} will disable this 195 check 196 197 @type auth: a L{status.web.auth.IAuth} or C{None} 198 @param auth: an object that performs authentication to restrict access 199 to the C{allowForce} features. Ignored if C{allowForce} 200 is not C{True}. If C{auth} is C{None}, people can force or 201 stop builds without auth. 202 203 @type order_console_by_time: bool 204 @param order_console_by_time: Whether to order changes (commits) in the console 205 view according to the time they were created (for VCS like Git) or 206 according to their integer revision numbers (for VCS like SVN). 207 208 @type changecommentlink: callable, dict, tuple (2 or 3 strings) or C{None} 209 @param changecommentlink: adds links to ticket/bug ids in change comments, 210 see buildbot.status.web.base.changecommentlink for details 211 212 @type revlink: callable, dict, string or C{None} 213 @param revlink: decorations revision ids with links to a web-view, 214 see buildbot.status.web.base.revlink for details 215 216 @type projects: callable, dict or c{None} 217 @param projects: maps project identifiers to URLs, so that any project listed 218 is automatically decorated with a link to it's front page. 219 see buildbot.status.web.base.dictlink for details 220 221 @type repositories: callable, dict or c{None} 222 @param repositories: maps repository identifiers to URLs, so that any project listed 223 is automatically decorated with a link to it's web view. 224 see buildbot.status.web.base.dictlink for details 225 226 @type logRotateLength: None or int 227 @param logRotateLength: file size at which the http.log is rotated/reset. 228 If not set, the value set in the buildbot.tac will be used, 229 falling back to the BuildMaster's default value (1 Mb). 230 231 @type maxRotatedFiles: None or int 232 @param maxRotatedFiles: number of old http.log files to keep during log rotation. 233 If not set, the value set in the buildbot.tac will be used, 234 falling back to the BuildMaster's default value (10 files). 235 236 @type change_hook_dialects: None or dict 237 @param change_hook_dialects: If empty, disables change_hook support, otherwise 238 whitelists valid dialects. In the format of 239 {"dialect1": "Option1", "dialect2", None} 240 Where the values are options that will be passed 241 to the dialect 242 243 To enable the DEFAULT handler, use a key of DEFAULT 244 245 246 247 248 """ 249 250 service.MultiService.__init__(self) 251 if type(http_port) is int: 252 http_port = "tcp:%d" % http_port 253 self.http_port = http_port 254 if distrib_port is not None: 255 if type(distrib_port) is int: 256 distrib_port = "tcp:%d" % distrib_port 257 if distrib_port[0] in "/~.": # pathnames 258 distrib_port = "unix:%s" % distrib_port 259 self.distrib_port = distrib_port 260 self.num_events = num_events 261 if num_events_max: 262 assert num_events_max >= num_events 263 self.num_events_max = num_events_max 264 self.public_html = public_html 265 266 # make up an authz if allowForce was given 267 if authz: 268 if allowForce is not None: 269 raise ValueError("cannot use both allowForce and authz parameters") 270 if auth: 271 raise ValueError("cannot use both auth and authz parameters (pass " 272 "auth as an Authz parameter)") 273 else: 274 # invent an authz 275 if allowForce and auth: 276 authz = Authz(auth=auth, default_action="auth") 277 elif allowForce: 278 authz = Authz(default_action=True) 279 else: 280 if auth: 281 log.msg("Warning: Ignoring authentication. Search for 'authorization'" 282 " in the manual") 283 authz = Authz() # no authorization for anything 284 285 self.authz = authz 286 287 self.orderConsoleByTime = order_console_by_time 288 289 # If we were given a site object, go ahead and use it. (if not, we add one later) 290 self.site = site 291 292 # store the log settings until we create the site object 293 self.logRotateLength = logRotateLength 294 self.maxRotatedFiles = maxRotatedFiles 295 296 # create the web site page structure 297 self.childrenToBeAdded = {} 298 self.setupUsualPages(numbuilds=numbuilds, num_events=num_events, 299 num_events_max=num_events_max) 300 301 # Set up the jinja templating engine. 302 self.templates = createJinjaEnv(revlink, changecommentlink, 303 repositories, projects) 304 305 # keep track of cached connections so we can break them when we shut 306 # down. See ticket #102 for more details. 307 self.channels = weakref.WeakKeyDictionary() 308 309 # do we want to allow change_hook 310 self.change_hook_dialects = {} 311 if change_hook_dialects: 312 self.change_hook_dialects = change_hook_dialects 313 self.putChild("change_hook", ChangeHookResource(dialects = self.change_hook_dialects))
314
315 - def setupUsualPages(self, numbuilds, num_events, num_events_max):
316 #self.putChild("", IndexOrWaterfallRedirection()) 317 self.putChild("waterfall", WaterfallStatusResource(num_events=num_events, 318 num_events_max=num_events_max)) 319 self.putChild("grid", GridStatusResource()) 320 self.putChild("console", ConsoleStatusResource( 321 orderByTime=self.orderConsoleByTime)) 322 self.putChild("tgrid", TransposedGridStatusResource()) 323 self.putChild("builders", BuildersResource()) # has builds/steps/logs 324 self.putChild("one_box_per_builder", Redirect("builders")) 325 self.putChild("changes", ChangesResource()) 326 self.putChild("buildslaves", BuildSlavesResource()) 327 self.putChild("buildstatus", BuildStatusStatusResource()) 328 self.putChild("one_line_per_build", 329 OneLinePerBuild(numbuilds=numbuilds)) 330 self.putChild("about", AboutBuildbot()) 331 self.putChild("authfail", AuthFailResource())
332 333
334 - def __repr__(self):
335 if self.http_port is None: 336 return "<WebStatus on path %s at %s>" % (self.distrib_port, 337 hex(id(self))) 338 if self.distrib_port is None: 339 return "<WebStatus on port %s at %s>" % (self.http_port, 340 hex(id(self))) 341 return ("<WebStatus on port %s and path %s at %s>" % 342 (self.http_port, self.distrib_port, hex(id(self))))
343
344 - def setServiceParent(self, parent):
345 service.MultiService.setServiceParent(self, parent) 346 347 # this class keeps a *separate* link to the buildmaster, rather than 348 # just using self.parent, so that when we are "disowned" (and thus 349 # parent=None), any remaining HTTP clients of this WebStatus will still 350 # be able to get reasonable results. 351 self.master = parent 352 353 def either(a,b): # a if a else b for py2.4 354 if a: 355 return a 356 else: 357 return b
358 359 rotateLength = either(self.logRotateLength, self.master.log_rotation.rotateLength) 360 maxRotatedFiles = either(self.maxRotatedFiles, self.master.log_rotation.maxRotatedFiles) 361 362 if not self.site: 363 364 class RotateLogSite(server.Site): 365 def _openLogFile(self, path): 366 try: 367 from twisted.python.logfile import LogFile 368 log.msg("Setting up http.log rotating %s files of %s bytes each" % 369 (maxRotatedFiles, rotateLength)) 370 if hasattr(LogFile, "fromFullPath"): # not present in Twisted-2.5.0 371 return LogFile.fromFullPath(path, rotateLength=rotateLength, maxRotatedFiles=maxRotatedFiles) 372 else: 373 log.msg("WebStatus: rotated http logs are not supported on this version of Twisted") 374 except ImportError, e: 375 log.msg("WebStatus: Unable to set up rotating http.log: %s" % e) 376 377 # if all else fails, just call the parent method 378 return server.Site._openLogFile(self, path)
379 380 # this will be replaced once we've been attached to a parent (and 381 # thus have a basedir and can reference BASEDIR) 382 root = static.Data("placeholder", "text/plain") 383 httplog = os.path.abspath(os.path.join(self.master.basedir, "http.log")) 384 self.site = RotateLogSite(root, logPath=httplog) 385 386 # the following items are accessed by HtmlResource when it renders 387 # each page. 388 self.site.buildbot_service = self 389 390 if self.http_port is not None: 391 s = strports.service(self.http_port, self.site) 392 s.setServiceParent(self) 393 if self.distrib_port is not None: 394 f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) 395 s = strports.service(self.distrib_port, f) 396 s.setServiceParent(self) 397 398 self.setupSite() 399
400 - def setupSite(self):
401 # this is responsible for creating the root resource. It isn't done 402 # at __init__ time because we need to reference the parent's basedir. 403 htmldir = os.path.abspath(os.path.join(self.master.basedir, self.public_html)) 404 if os.path.isdir(htmldir): 405 log.msg("WebStatus using (%s)" % htmldir) 406 else: 407 log.msg("WebStatus: warning: %s is missing. Do you need to run" 408 " 'buildbot upgrade-master' on this buildmaster?" % htmldir) 409 # all static pages will get a 404 until upgrade-master is used to 410 # populate this directory. Create the directory, though, since 411 # otherwise we get internal server errors instead of 404s. 412 os.mkdir(htmldir) 413 414 root = StaticFile(htmldir) 415 root_page = RootPage() 416 root.putChild("", root_page) 417 root.putChild("shutdown", root_page) 418 root.putChild("cancel_shutdown", root_page) 419 420 for name, child_resource in self.childrenToBeAdded.iteritems(): 421 root.putChild(name, child_resource) 422 423 status = self.getStatus() 424 root.putChild("rss", Rss20StatusResource(status)) 425 root.putChild("atom", Atom10StatusResource(status)) 426 root.putChild("json", JsonStatusResource(status)) 427 428 self.site.resource = root
429
430 - def putChild(self, name, child_resource):
431 """This behaves a lot like root.putChild() . """ 432 self.childrenToBeAdded[name] = child_resource
433
434 - def registerChannel(self, channel):
435 self.channels[channel] = 1 # weakrefs
436
437 - def stopService(self):
438 for channel in self.channels: 439 try: 440 channel.transport.loseConnection() 441 except: 442 log.msg("WebStatus.stopService: error while disconnecting" 443 " leftover clients") 444 log.err() 445 return service.MultiService.stopService(self)
446
447 - def getStatus(self):
448 return self.master.getStatus()
449
450 - def getChangeSvc(self):
451 return self.master.change_svc
452
453 - def getPortnum(self):
454 # this is for the benefit of unit tests 455 s = list(self)[0] 456 return s._port.getHost().port
457 458 # What happened to getControl?! 459 # 460 # instead of passing control objects all over the place in the web 461 # code, at the few places where a control instance is required we 462 # find the requisite object manually, starting at the buildmaster. 463 # This is in preparation for removal of the IControl hierarchy 464 # entirely. 465 466 # resources can get access to the IStatus by calling 467 # request.site.buildbot_service.getStatus() 468