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