Ticket #58: factor-web.patch

File factor-web.patch, 72.2 KB (added by dustin, 5 years ago)

factor-web.patch

  • buildbot/status/html.py

    old new  
    33# compatibility wrapper. This is currently the preferred place for master.cfg 
    44# to import from. 
    55 
     6# compatibility function 
    67from buildbot.status.web.waterfall import Waterfall 
    78_hush_pyflakes = [Waterfall] 
     9 
     10 
     11from buildbot.status.web.waterfall import WaterfallStatusResource 
     12_hush_pyflakes = [WaterfallStatusResource] 
     13 
     14from buildbot.status.web.site import WebStatus 
     15_hush_pyflakes = [WebStatus] 
  • new file statusgrid/buildbot/status/web/site.py

    - +  
     1import sys, os.path 
     2 
     3from twisted.web.resource import Resource 
     4from twisted.application import strports 
     5from twisted.web import static, html, server, distrib 
     6from twisted.web.error import NoResource 
     7from twisted.spread import pb 
     8 
     9from buildbot import interfaces 
     10from buildbot.status.base import StatusReceiverMultiService 
     11from buildbot.status.web.base import HtmlResource, IToplevelResource 
     12 
     13if hasattr(sys, "frozen"): 
     14    # all 'data' files are in the directory of our executable 
     15    here = os.path.dirname(sys.executable) 
     16    buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png")) 
     17else: 
     18    # running from source 
     19    # the icon is sibpath(__file__, "../buildbot.png") . This is for 
     20    # portability. 
     21    up = os.path.dirname 
     22    buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))), 
     23                                                 "buildbot.png")) 
     24 
     25class WebStatus(StatusReceiverMultiService): 
     26    compare_attrs = ["http_port", "distrib_port", "favicon", "robots_txt"] 
     27 
     28    def __init__(self, top_page=None, http_port=None, distrib_port=None, 
     29                 favicon=None, robots_txt=None, **named_pages): 
     30        """To have the buildbot run its own web server, pass a port number to 
     31        C{http_port}. To have it run a web.distrib server 
     32 
     33        @type  top_page: IToplevelResource 
     34        @param top_page: an IToplevelResource that should be displayed 
     35                         as the main resource of this page 
     36 
     37        @type  http_port: int or L{twisted.application.strports} string 
     38        @param http_port: a strports specification describing which port the 
     39                          buildbot should use for its web server, with the 
     40                          Waterfall display as the root page. For backwards 
     41                          compatibility this can also be an int. Use 
     42                          'tcp:8000' to listen on that port, or 
     43                          'tcp:12345:interface=127.0.0.1' if you only want 
     44                          local processes to connect to it (perhaps because 
     45                          you are using an HTTP reverse proxy to make the 
     46                          buildbot available to the outside world, and do not 
     47                          want to make the raw port visible). 
     48 
     49        @type  distrib_port: int or L{twisted.application.strports} string 
     50        @param distrib_port: Use this if you want to publish the Waterfall 
     51                             page using web.distrib instead. The most common 
     52                             case is to provide a string that is an absolute 
     53                             pathname to the unix socket on which the 
     54                             publisher should listen 
     55                             (C{os.path.expanduser(~/.twistd-web-pb)} will 
     56                             match the default settings of a standard 
     57                             twisted.web 'personal web server'). Another 
     58                             possibility is to pass an integer, which means 
     59                             the publisher should listen on a TCP socket, 
     60                             allowing the web server to be on a different 
     61                             machine entirely. Both forms are provided for 
     62                             backwards compatibility; the preferred form is a 
     63                             strports specification like 
     64                             'unix:/home/buildbot/.twistd-web-pb'. Providing 
     65                             a non-absolute pathname will probably confuse 
     66                             the strports parser. 
     67 
     68        @type  favicon: string 
     69        @param favicon: if set, provide the pathname of an image file that 
     70                        will be used for the 'favicon.ico' resource. Many 
     71                        browsers automatically request this file and use it 
     72                        as an icon in any bookmark generated from this site. 
     73                        Defaults to the buildbot/buildbot.png image provided 
     74                        in the distribution. Can be set to None to avoid 
     75                        using a favicon at all. 
     76 
     77        @type  robots_txt: string 
     78        @param robots_txt: if set, provide the pathname of a robots.txt file. 
     79                           Many search engines request this file and obey the 
     80                           rules in it. E.g. to disallow them to crawl the 
     81                           status page, put the following two lines in 
     82                           robots.txt:: 
     83                              User-agent: * 
     84                              Disallow: / 
     85        """ 
     86        if favicon is None: favicon = buildbot_icon 
     87 
     88        StatusReceiverMultiService.__init__(self) 
     89        if type(http_port) is int: 
     90            http_port = "tcp:%d" % http_port 
     91        self.http_port = http_port 
     92        if distrib_port is not None: 
     93            if type(distrib_port) is int: 
     94                distrib_port = "tcp:%d" % distrib_port 
     95            if distrib_port[0] in "/~.": # pathnames 
     96                distrib_port = "unix:%s" % distrib_port 
     97        self.distrib_port = distrib_port 
     98        self.favicon = favicon 
     99        self.robots_txt = robots_txt 
     100 
     101        self.top_page = top_page 
     102        self.named_pages = named_pages 
     103 
     104    def __repr__(self): 
     105        if self.http_port is None: 
     106            return "<WebStatus on path %s>" % self.distrib_port 
     107        if self.distrib_port is None: 
     108            return "<WebStatus on port %s>" % self.http_port 
     109        return "<WebStatus on port %s and path %s>" % (self.http_port, 
     110                                                       self.distrib_port) 
     111 
     112    def setServiceParent(self, parent): 
     113        """ 
     114        @type  parent: L{buildbot.master.BuildMaster} 
     115        """ 
     116        StatusReceiverMultiService.setServiceParent(self, parent) 
     117        self.setup(parent) 
     118 
     119    def setup(self, parent): 
     120        # first, tell all of the named pages, and the top page, who 
     121        # the buildmaster is 
     122        if self.top_page: 
     123            IToplevelResource(self.top_page).setBuildmaster(parent) 
     124        for named_page in self.named_pages.values(): 
     125            IToplevelResource(named_page).setBuildmaster(parent) 
     126 
     127        wsr = WebStatusResource(self.parent, self.favicon, self.robots_txt, 
     128                self.top_page, self.named_pages) 
     129        self.site = server.Site(wsr) 
     130 
     131        if self.http_port is not None: 
     132            s = strports.service(self.http_port, self.site) 
     133            s.setServiceParent(self) 
     134        if self.distrib_port is not None: 
     135            f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) 
     136            s = strports.service(self.distrib_port, f) 
     137            s.setServiceParent(self) 
     138 
     139class MenuResource(HtmlResource): 
     140    title = "Menu" 
     141    def __init__(self, menu_items): 
     142        HtmlResource.__init__(self) 
     143        self.menu_items = menu_items 
     144 
     145    def body(self, request): 
     146        data = [ '<ul>' ] 
     147        data += [ '<li><a href="%s/">%s</a>' % (name,name) for name in self.menu_items.keys() ] 
     148        data += [ '</ul>' ] 
     149        return "\n".join(data) 
     150 
     151class WebStatusResource(Resource): 
     152    """ 
     153    A generic top-level resource for a site.  This resource handles at least 
     154    robots.txt and favicon.  If supplied with a top_page, then it delegates 
     155    all other methods to that resource.  Otherwise, it produces a simple HTML 
     156    menu of its named_pages. 
     157    """ 
     158    buildmaster = None 
     159 
     160    favicon = None 
     161    robots_txt = None 
     162 
     163    def __init__(self, buildmaster, favicon, robots_txt, top_page, named_pages): 
     164        """ 
     165        @type  buildmaster: L{buildbot.master.BuildMaster} 
     166        @type  favicon: string 
     167        @type  robots_txt: string 
     168        @type  top_page: Resource or None 
     169        @type  named_pages: dictionary mapping names to Resources 
     170        """ 
     171        Resource.__init__(self) 
     172        self.buildmaster = buildmaster 
     173        self.favicon = favicon 
     174        self.robots_txt = robots_txt 
     175        self.top_page = top_page 
     176        self.named_pages = named_pages 
     177 
     178        if self.top_page: 
     179            self.putChild("", self.top_page) 
     180        else: 
     181            self.putChild("", MenuResource(self.named_pages)) 
     182 
     183    def render(self, request): 
     184        # always add a slash if one isn't present 
     185        request.redirect(request.prePathURL() + '/') 
     186        request.finish() 
     187 
     188    def getChild(self, path, request): 
     189        if path == "robots.txt" and self.robots_txt: 
     190            return static.File(self.robots_txt) 
     191        if path == "favicon.ico": 
     192            if self.favicon: 
     193                return static.File(self.favicon) 
     194            return NoResource("No favicon.ico registered") 
     195 
     196        # point to a named page, if present 
     197        if self.named_pages.has_key(path): 
     198            return self.named_pages[path] 
     199 
     200        # delegate anything else to top_page, if present 
     201        if self.top_page: 
     202            return self.top_page.getChild(path, request) 
     203 
     204        return NoResource("No such page '%s'" % path) 
  • buildbot/status/web/waterfall.py

    old new  
    1919from buildbot import interfaces, util 
    2020from buildbot import version 
    2121from buildbot.sourcestamp import SourceStamp 
    22 from buildbot.status import builder, base 
     22from buildbot.status import builder 
     23from buildbot.status.base import StatusReceiverMultiService 
    2324from buildbot.changes import changes 
    2425from buildbot.process.base import BuildRequest 
     26from buildbot.status.web.site import WebStatus 
     27from buildbot.status.web.base import HtmlResource, StaticHTML, IToplevelResource, IHTMLLog 
     28from buildbot.status.web.changes import StatusResourceChanges 
     29from buildbot.status.web.builder import StatusResourceBuilder 
    2530 
    2631class ITopBox(Interface): 
    2732    """I represent a box in the top row of the waterfall display: the one 
     
    3641    """I represent a box in the waterfall display.""" 
    3742    pass 
    3843 
    39 class IHTMLLog(Interface): 
    40     pass 
    41  
    42 ROW_TEMPLATE = ''' 
    43 <div class="row"> 
    44   <span class="label">%(label)s</span> 
    45   <span class="field">%(field)s</span> 
    46 </div>''' 
    47  
    48 def make_row(label, field): 
    49     """Create a name/value row for the HTML. 
    50  
    51     `label` is plain text; it will be HTML-encoded. 
    52  
    53     `field` is a bit of HTML structure; it will not be encoded in 
    54     any way. 
    55     """ 
    56     label = html.escape(label) 
    57     return ROW_TEMPLATE % {"label": label, "field": field} 
    58  
    5944colormap = { 
    6045    'green': '#72ff75', 
    6146    } 
     
    140125        return td(text, props, bgcolor=self.color, class_=self.class_) 
    141126 
    142127 
    143 class HtmlResource(Resource): 
    144     css = None 
    145     contentType = "text/html; charset=UTF-8" 
    146     title = "Dummy" 
    147  
    148     def render(self, request): 
    149         data = self.content(request) 
    150         if isinstance(data, unicode): 
    151             data = data.encode("utf-8") 
    152         request.setHeader("content-type", self.contentType) 
    153         if request.method == "HEAD": 
    154             request.setHeader("content-length", len(data)) 
    155             return '' 
    156         return data 
    157  
    158     def content(self, request): 
    159         data = ('<!DOCTYPE html PUBLIC' 
    160                 ' "-//W3C//DTD XHTML 1.0 Transitional//EN"\n' 
    161                 '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' 
    162                 '<html' 
    163                 ' xmlns="http://www.w3.org/1999/xhtml"' 
    164                 ' lang="en"' 
    165                 ' xml:lang="en">\n') 
    166         data += "<head>\n" 
    167         data += "  <title>" + self.title + "</title>\n" 
    168         if self.css: 
    169             # TODO: use some sort of relative link up to the root page, so 
    170             # this css can be used from child pages too 
    171             data += ('  <link href="%s" rel="stylesheet" type="text/css"/>\n' 
    172                      % "buildbot.css") 
    173         data += "</head>\n" 
    174         data += '<body vlink="#800080">\n' 
    175         data += self.body(request) 
    176         data += "</body></html>\n" 
    177         return data 
    178  
    179     def body(self, request): 
    180         return "Dummy\n" 
    181  
    182 class StaticHTML(HtmlResource): 
    183     def __init__(self, body, title): 
    184         HtmlResource.__init__(self) 
    185         self.bodyHTML = body 
    186         self.title = title 
    187     def body(self, request): 
    188         return self.bodyHTML 
    189  
    190 # $builder/builds/NN/stepname 
    191 class StatusResourceBuildStep(HtmlResource): 
    192     title = "Build Step" 
    193  
    194     def __init__(self, status, step): 
    195         HtmlResource.__init__(self) 
    196         self.status = status 
    197         self.step = step 
    198  
    199     def body(self, request): 
    200         s = self.step 
    201         b = s.getBuild() 
    202         data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \ 
    203                (b.getBuilder().getName(), b.getNumber(), s.getName()) 
    204  
    205         if s.isFinished(): 
    206             data += ("<h2>Finished</h2>\n" 
    207                      "<p>%s</p>\n" % html.escape("%s" % s.getText())) 
    208         else: 
    209             data += ("<h2>Not Finished</h2>\n" 
    210                      "<p>ETA %s seconds</p>\n" % s.getETA()) 
    211  
    212         exp = s.getExpectations() 
    213         if exp: 
    214             data += ("<h2>Expectations</h2>\n" 
    215                      "<ul>\n") 
    216             for e in exp: 
    217                 data += "<li>%s: current=%s, target=%s</li>\n" % \ 
    218                         (html.escape(e[0]), e[1], e[2]) 
    219             data += "</ul>\n" 
    220         logs = s.getLogs() 
    221         if logs: 
    222             data += ("<h2>Logs</h2>\n" 
    223                      "<ul>\n") 
    224             for num in range(len(logs)): 
    225                 if logs[num].hasContents(): 
    226                     # FIXME: If the step name has a / in it, this is broken 
    227                     # either way.  If we quote it but say '/'s are safe, 
    228                     # it chops up the step name.  If we quote it and '/'s 
    229                     # are not safe, it escapes the / that separates the 
    230                     # step name from the log number. 
    231                     data += '<li><a href="%s">%s</a></li>\n' % \ 
    232                             (urllib.quote(request.childLink("%d" % num)), 
    233                              html.escape(logs[num].getName())) 
    234                 else: 
    235                     data += ('<li>%s</li>\n' % 
    236                              html.escape(logs[num].getName())) 
    237             data += "</ul>\n" 
    238  
    239         return data 
    240  
    241     def getChild(self, path, request): 
    242         logname = path 
    243         try: 
    244             log = self.step.getLogs()[int(logname)] 
    245             if log.hasContents(): 
    246                 return IHTMLLog(interfaces.IStatusLog(log)) 
    247             return NoResource("Empty Log '%s'" % logname) 
    248         except (IndexError, ValueError): 
    249             return NoResource("No such Log '%s'" % logname) 
    250  
    251 # $builder/builds/NN/tests/TESTNAME 
    252 class StatusResourceTestResult(HtmlResource): 
    253     title = "Test Logs" 
    254  
    255     def __init__(self, status, name, result): 
    256         HtmlResource.__init__(self) 
    257         self.status = status 
    258         self.name = name 
    259         self.result = result 
    260  
    261     def body(self, request): 
    262         dotname = ".".join(self.name) 
    263         logs = self.result.getLogs() 
    264         lognames = logs.keys() 
    265         lognames.sort() 
    266         data = "<h1>%s</h1>\n" % html.escape(dotname) 
    267         for name in lognames: 
    268             data += "<h2>%s</h2>\n" % html.escape(name) 
    269             data += "<pre>" + logs[name] + "</pre>\n\n" 
    270  
    271         return data 
    272  
    273  
    274 # $builder/builds/NN/tests 
    275 class StatusResourceTestResults(HtmlResource): 
    276     title = "Test Results" 
    277  
    278     def __init__(self, status, results): 
    279         HtmlResource.__init__(self) 
    280         self.status = status 
    281         self.results = results 
    282  
    283     def body(self, request): 
    284         r = self.results 
    285         data = "<h1>Test Results</h1>\n" 
    286         data += "<ul>\n" 
    287         testnames = r.keys() 
    288         testnames.sort() 
    289         for name in testnames: 
    290             res = r[name] 
    291             dotname = ".".join(name) 
    292             data += " <li>%s: " % dotname 
    293             # TODO: this could break on weird test names. At the moment, 
    294             # test names only come from Trial tests, where the name 
    295             # components must be legal python names, but that won't always 
    296             # be a restriction. 
    297             url = request.childLink(dotname) 
    298             data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText())) 
    299             data += "</li>\n" 
    300         data += "</ul>\n" 
    301         return data 
    302  
    303     def getChild(self, path, request): 
    304         try: 
    305             name = tuple(path.split(".")) 
    306             result = self.results[name] 
    307             return StatusResourceTestResult(self.status, name, result) 
    308         except KeyError: 
    309             return NoResource("No such test name '%s'" % path) 
    310  
    311  
    312 # $builder/builds/NN 
    313 class StatusResourceBuild(HtmlResource): 
    314     title = "Build" 
    315  
    316     def __init__(self, status, build, builderControl, buildControl): 
    317         HtmlResource.__init__(self) 
    318         self.status = status 
    319         self.build = build 
    320         self.builderControl = builderControl 
    321         self.control = buildControl 
    322  
    323     def body(self, request): 
    324         b = self.build 
    325         buildbotURL = self.status.getBuildbotURL() 
    326         projectName = self.status.getProjectName() 
    327         data = '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL, 
    328                                                                  projectName) 
    329         # the color in the following line gives python-mode trouble 
    330         data += ("<h1>Build <a href=\"%s\">%s</a>:#%d</h1>\n" 
    331                  % (self.status.getURLForThing(b.getBuilder()), 
    332                     b.getBuilder().getName(), b.getNumber())) 
    333         data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename()) 
    334         data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason()) 
    335  
    336         branch, revision, patch = b.getSourceStamp() 
    337         data += "<h2>SourceStamp:</h2>\n" 
    338         data += " <ul>\n" 
    339         if branch: 
    340             data += "  <li>Branch: %s</li>\n" % html.escape(branch) 
    341         if revision: 
    342             data += "  <li>Revision: %s</li>\n" % html.escape(str(revision)) 
    343         if patch: 
    344             data += "  <li>Patch: YES</li>\n" # TODO: provide link to .diff 
    345         if b.getChanges(): 
    346             data += "  <li>Changes: see below</li>\n" 
    347         if (branch is None and revision is None and patch is None 
    348             and not b.getChanges()): 
    349             data += "  <li>build of most recent revision</li>\n" 
    350         data += " </ul>\n" 
    351         if b.isFinished(): 
    352             data += "<h2>Results:</h2>\n" 
    353             data += " ".join(b.getText()) + "\n" 
    354             if b.getTestResults(): 
    355                 url = request.childLink("tests") 
    356                 data += "<h3><a href=\"%s\">test results</a></h3>\n" % url 
    357         else: 
    358             data += "<h2>Build In Progress</h2>" 
    359             if self.control is not None: 
    360                 stopURL = urllib.quote(request.childLink("stop")) 
    361                 data += """ 
    362                 <form action="%s" class='command stopbuild'> 
    363                 <p>To stop this build, fill out the following fields and 
    364                 push the 'Stop' button</p>\n""" % stopURL 
    365                 data += make_row("Your name:", 
    366                                  "<input type='text' name='username' />") 
    367                 data += make_row("Reason for stopping build:", 
    368                                  "<input type='text' name='comments' />") 
    369                 data += """<input type="submit" value="Stop Builder" /> 
    370                 </form> 
    371                 """ 
    372  
    373         if b.isFinished() and self.builderControl is not None: 
    374             data += "<h3>Resubmit Build:</h3>\n" 
    375             # can we rebuild it exactly? 
    376             exactly = (revision is not None) or b.getChanges() 
    377             if exactly: 
    378                 data += ("<p>This tree was built from a specific set of \n" 
    379                          "source files, and can be rebuilt exactly</p>\n") 
    380             else: 
    381                 data += ("<p>This tree was built from the most recent " 
    382                          "revision") 
    383                 if branch: 
    384                     data += " (along some branch)" 
    385                 data += (" and thus it might not be possible to rebuild it \n" 
    386                          "exactly. Any changes that have been committed \n" 
    387                          "after this build was started <b>will</b> be \n" 
    388                          "included in a rebuild.</p>\n") 
    389             rebuildURL = urllib.quote(request.childLink("rebuild")) 
    390             data += ('<form action="%s" class="command rebuild">\n' 
    391                      % rebuildURL) 
    392             data += make_row("Your name:", 
    393                              "<input type='text' name='username' />") 
    394             data += make_row("Reason for re-running build:", 
    395                              "<input type='text' name='comments' />") 
    396             data += '<input type="submit" value="Rebuild" />\n' 
    397             data += '</form>\n' 
    398  
    399         data += "<h2>Steps and Logfiles:</h2>\n" 
    400         if b.getLogs(): 
    401             data += "<ol>\n" 
    402             for s in b.getSteps(): 
    403                 data += (" <li><a href=\"%s\">%s</a> [%s]\n" 
    404                          % (self.status.getURLForThing(s), s.getName(), 
    405                             " ".join(s.getText()))) 
    406                 if s.getLogs(): 
    407                     data += "  <ol>\n" 
    408                     for logfile in s.getLogs(): 
    409                         data += ("   <li><a href=\"%s\">%s</a></li>\n" % 
    410                                  (self.status.getURLForThing(logfile), 
    411                                   logfile.getName())) 
    412                     data += "  </ol>\n" 
    413                 data += " </li>\n" 
    414             data += "</ol>\n" 
    415  
    416         data += ("<h2>Blamelist:</h2>\n" 
    417                  " <ol>\n") 
    418         for who in b.getResponsibleUsers(): 
    419             data += "  <li>%s</li>\n" % html.escape(who) 
    420         data += (" </ol>\n" 
    421                  "<h2>All Changes</h2>\n") 
    422         changes = b.getChanges() 
    423         if changes: 
    424             data += "<ol>\n" 
    425             for c in changes: 
    426                 data += "<li>" + c.asHTML() + "</li>\n" 
    427             data += "</ol>\n" 
    428         #data += html.PRE(b.changesText()) # TODO 
    429         return data 
    430  
    431     def stop(self, request): 
    432         log.msg("web stopBuild of build %s:%s" % \ 
    433                 (self.build.getBuilder().getName(), 
    434                  self.build.getNumber())) 
    435         name = request.args.get("username", ["<unknown>"])[0] 
    436         comments = request.args.get("comments", ["<no reason specified>"])[0] 
    437         reason = ("The web-page 'stop build' button was pressed by " 
    438                   "'%s': %s\n" % (name, comments)) 
    439         self.control.stopBuild(reason) 
    440         # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and 
    441         # we want to go to: http://localhost:8080/svn-hello/builds/5 or 
    442         # http://localhost:8080/ 
    443         # 
    444         #return Redirect("../%d" % self.build.getNumber()) 
    445         r = Redirect("../../..") 
    446         d = defer.Deferred() 
    447         reactor.callLater(1, d.callback, r) 
    448         return DeferredResource(d) 
    449  
    450     def rebuild(self, request): 
    451         log.msg("web rebuild of build %s:%s" % \ 
    452                 (self.build.getBuilder().getName(), 
    453                  self.build.getNumber())) 
    454         name = request.args.get("username", ["<unknown>"])[0] 
    455         comments = request.args.get("comments", ["<no reason specified>"])[0] 
    456         reason = ("The web-page 'rebuild' button was pressed by " 
    457                   "'%s': %s\n" % (name, comments)) 
    458         if not self.builderControl or not self.build.isFinished(): 
    459             log.msg("could not rebuild: bc=%s, isFinished=%s" 
    460                     % (self.builderControl, self.build.isFinished())) 
    461             # TODO: indicate an error 
    462         else: 
    463             self.builderControl.resubmitBuild(self.build, reason) 
    464         # we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and 
    465         # we want to go to the top, at http://localhost:8080/ 
    466         r = Redirect("../../..") 
    467         d = defer.Deferred() 
    468         reactor.callLater(1, d.callback, r) 
    469         return DeferredResource(d) 
    470  
    471     def getChild(self, path, request): 
    472         if path == "tests": 
    473             return StatusResourceTestResults(self.status, 
    474                                              self.build.getTestResults()) 
    475         if path == "stop": 
    476             return self.stop(request) 
    477         if path == "rebuild": 
    478             return self.rebuild(request) 
    479         if path.startswith("step-"): 
    480             stepname = path[len("step-"):] 
    481             steps = self.build.getSteps() 
    482             for s in steps: 
    483                 if s.getName() == stepname: 
    484                     return StatusResourceBuildStep(self.status, s) 
    485             return NoResource("No such BuildStep '%s'" % stepname) 
    486         return NoResource("No such resource '%s'" % path) 
    487  
    488 # $builder 
    489 class StatusResourceBuilder(HtmlResource): 
    490  
    491     def __init__(self, status, builder, control): 
    492         HtmlResource.__init__(self) 
    493         self.status = status 
    494         self.title = builder.getName() + " Builder" 
    495         self.builder = builder 
    496         self.control = control 
    497  
    498     def body(self, request): 
    499         b = self.builder 
    500         slaves = b.getSlaves() 
    501         connected_slaves = [s for s in slaves if s.isConnected()] 
    502  
    503         buildbotURL = self.status.getBuildbotURL() 
    504         projectName = self.status.getProjectName() 
    505         data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName) 
    506         data += make_row("Builder:", html.escape(b.getName())) 
    507         b1 = b.getBuild(-1) 
    508         if b1 is not None: 
    509             data += make_row("Current/last build:", str(b1.getNumber())) 
    510         data += "\n<br />BUILDSLAVES<br />\n" 
    511         data += "<ol>\n" 
    512         for slave in slaves: 
    513             data += "<li><b>%s</b>: " % html.escape(slave.getName()) 
    514             if slave.isConnected(): 
    515                 data += "CONNECTED\n" 
    516                 if slave.getAdmin(): 
    517                     data += make_row("Admin:", html.escape(slave.getAdmin())) 
    518                 if slave.getHost(): 
    519                     data += "<span class='label'>Host info:</span>\n" 
    520                     data += html.PRE(slave.getHost()) 
    521             else: 
    522                 data += ("NOT CONNECTED\n") 
    523             data += "</li>\n" 
    524         data += "</ol>\n" 
    525  
    526         if self.control is not None and connected_slaves: 
    527             forceURL = urllib.quote(request.childLink("force")) 
    528             data += ( 
    529                 """ 
    530                 <form action='%(forceURL)s' class='command forcebuild'> 
    531                 <p>To force a build, fill out the following fields and 
    532                 push the 'Force Build' button</p>""" 
    533                 + make_row("Your name:", 
    534                            "<input type='text' name='username' />") 
    535                 + make_row("Reason for build:", 
    536                            "<input type='text' name='comments' />") 
    537                 + make_row("Branch to build:", 
    538                            "<input type='text' name='branch' />") 
    539                 + make_row("Revision to build:", 
    540                            "<input type='text' name='revision' />") 
    541                 + """ 
    542                 <input type='submit' value='Force Build' /> 
    543                 </form> 
    544                 """) % {"forceURL": forceURL} 
    545         elif self.control is not None: 
    546             data += """ 
    547             <p>All buildslaves appear to be offline, so it's not possible 
    548             to force this build to execute at this time.</p> 
    549             """ 
    550  
    551         if self.control is not None: 
    552             pingURL = urllib.quote(request.childLink("ping")) 
    553             data += """ 
    554             <form action="%s" class='command pingbuilder'> 
    555             <p>To ping the buildslave(s), push the 'Ping' button</p> 
    556  
    557             <input type="submit" value="Ping Builder" /> 
    558             </form> 
    559             """ % pingURL 
    560  
    561         return data 
    562  
    563     def force(self, request): 
    564         name = request.args.get("username", ["<unknown>"])[0] 
    565         reason = request.args.get("comments", ["<no reason specified>"])[0] 
    566         branch = request.args.get("branch", [""])[0] 
    567         revision = request.args.get("revision", [""])[0] 
    568  
    569         r = "The web-page 'force build' button was pressed by '%s': %s\n" \ 
    570             % (name, reason) 
    571         log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'" 
    572                 % (self.builder.name, branch, revision)) 
    573  
    574         if not self.control: 
    575             # TODO: tell the web user that their request was denied 
    576             log.msg("but builder control is disabled") 
    577             return Redirect("..") 
    578  
    579         # keep weird stuff out of the branch and revision strings. TODO: 
    580         # centralize this somewhere. 
    581         if not re.match(r'^[\w\.\-\/]*$', branch): 
    582             log.msg("bad branch '%s'" % branch) 
    583             return Redirect("..") 
    584         if not re.match(r'^[\w\.\-\/]*$', revision): 
    585             log.msg("bad revision '%s'" % revision) 
    586             return Redirect("..") 
    587         if branch == "": 
    588             branch = None 
    589         if revision == "": 
    590             revision = None 
    591  
    592         # TODO: if we can authenticate that a particular User pushed the 
    593         # button, use their name instead of None, so they'll be informed of 
    594         # the results. 
    595         s = SourceStamp(branch=branch, revision=revision) 
    596         req = BuildRequest(r, s, self.builder.getName()) 
    597         try: 
    598             self.control.requestBuildSoon(req) 
    599         except interfaces.NoSlaveError: 
    600             # TODO: tell the web user that their request could not be 
    601             # honored 
    602             pass 
    603         return Redirect("..") 
    604  
    605     def ping(self, request): 
    606         log.msg("web ping of builder '%s'" % self.builder.name) 
    607         self.control.ping() # TODO: there ought to be an ISlaveControl 
    608         return Redirect("..") 
    609  
    610     def getChild(self, path, request): 
    611         if path == "force": 
    612             return self.force(request) 
    613         if path == "ping": 
    614             return self.ping(request) 
    615         if not path in ("events", "builds"): 
    616             return NoResource("Bad URL '%s'" % path) 
    617         num = request.postpath.pop(0) 
    618         request.prepath.append(num) 
    619         num = int(num) 
    620         if path == "events": 
    621             # TODO: is this dead code? .statusbag doesn't exist,right? 
    622             log.msg("getChild['path']: %s" % request.uri) 
    623             return NoResource("events are unavailable until code gets fixed") 
    624             filename = request.postpath.pop(0) 
    625             request.prepath.append(filename) 
    626             e = self.builder.statusbag.getEventNumbered(num) 
    627             if not e: 
    628                 return NoResource("No such event '%d'" % num) 
    629             file = e.files.get(filename, None) 
    630             if file == None: 
    631                 return NoResource("No such file '%s'" % filename) 
    632             if type(file) == type(""): 
    633                 if file[:6] in ("<HTML>", "<html>"): 
    634                     return static.Data(file, "text/html") 
    635                 return static.Data(file, "text/plain") 
    636             return file 
    637         if path == "builds": 
    638             build = self.builder.getBuild(num) 
    639             if build: 
    640                 control = None 
    641                 if self.control: 
    642                     control = self.control.getBuild(num) 
    643                 return StatusResourceBuild(self.status, build, 
    644                                            self.control, control) 
    645             else: 
    646                 return NoResource("No such build '%d'" % num) 
    647         return NoResource("really weird URL %s" % path) 
    648  
    649 # $changes/NN 
    650 class StatusResourceChanges(HtmlResource): 
    651     def __init__(self, status, changemaster): 
    652         HtmlResource.__init__(self) 
    653         self.status = status 
    654         self.changemaster = changemaster 
    655     def body(self, request): 
    656         data = "" 
    657         data += "Change sources:\n" 
    658         sources = list(self.changemaster) 
    659         if sources: 
    660             data += "<ol>\n" 
    661             for s in sources: 
    662                 data += "<li>%s</li>\n" % s.describe() 
    663             data += "</ol>\n" 
    664         else: 
    665             data += "none (push only)\n" 
    666         return data 
    667     def getChild(self, path, request): 
    668         num = int(path) 
    669         c = self.changemaster.getChangeNumbered(num) 
    670         if not c: 
    671             return NoResource("No change number '%d'" % num) 
    672         return StaticHTML(c.asHTML(), "Change #%d" % num) 
    673  
    674128textlog_stylesheet = """ 
    675129<style type="text/css"> 
    676130 div.data { 
     
    1035489        if debug: log.msg(" fES1", starts) 
    1036490 
    1037491 
     492if hasattr(sys, "frozen"): 
     493    # all 'data' files are in the directory of our executable 
     494    here = os.path.dirname(sys.executable) 
     495    buildbot_css = os.path.abspath(os.path.join(here, "classic.css")) 
     496else: 
     497    # running from source 
     498    # the icon is sibpath(__file__, "../buildbot.png") . This is for 
     499    # portability. 
     500    up = os.path.dirname 
     501    buildbot_css = os.path.abspath(os.path.join(up(__file__), "classic.css")) 
     502 
    1038503class WaterfallStatusResource(HtmlResource): 
    1039     """This builds the main status page, with the waterfall display, and 
    1040     all child pages.""" 
     504    """I implement the primary web-page status interface, called a 'Waterfall 
     505    Display' because builds and steps are presented in a grid of boxes which 
     506    move downwards over time. The top edge is always the present. Each column 
     507    represents a single builder. Each box describes a single Step, which may 
     508    have logfiles or other status information. 
     509 
     510    All these pages are served via a web server of some sort. The simplest 
     511    approach is to let the buildmaster run its own webserver, on a given TCP 
     512    port, but it can also publish its pages to a L{twisted.web.distrib} 
     513    distributed web server (which lets the buildbot pages be a subset of some 
     514    other web server). 
     515 
     516    Since 0.6.3, BuildBot defines class attributes on elements so they can be 
     517    styled with CSS stylesheets. Buildbot uses some generic classes to 
     518    identify the type of object, and some more specific classes for the 
     519    various kinds of those types. It does this by specifying both in the 
     520    class attributes where applicable, separated by a space. It is important 
     521    that in your CSS you declare the more generic class styles above the more 
     522    specific ones. For example, first define a style for .Event, and below 
     523    that for .SUCCESS 
     524 
     525    The following CSS class names are used: 
     526        - Activity, Event, BuildStep, LastBuild: general classes 
     527        - waiting, interlocked, building, offline, idle: Activity states 
     528        - start, running, success, failure, warnings, skipped, exception: 
     529          LastBuild and BuildStep states 
     530        - Change: box with change 
     531        - Builder: box for builder name (at top) 
     532        - Project 
     533        - Time 
     534 
     535    @type buildmaster: L{buildbot.master.BuildMaster} 
     536    @ivar buildmaster: like all status plugins, this object is a child of the 
     537                  BuildMaster, so C{.parent} points to a 
     538                  L{buildbot.master.BuildMaster} instance, through which 
     539                  the status-reporting object is acquired. 
     540    """ 
     541 
    1041542    title = "BuildBot" 
    1042     def __init__(self, status, changemaster, categories, css=None): 
     543 
     544    implements(IToplevelResource) 
     545 
     546    status = None 
     547    control = None 
     548    changemaster = None 
     549 
     550    def __init__(self, allowForce=True, categories=None, css=None): 
     551        if not css: css = buildbot_css 
     552 
    1043553        HtmlResource.__init__(self) 
    1044         self.status = status 
    1045         self.changemaster = changemaster 
     554        self.addSlash = True 
     555        self.allowForce = allowForce 
    1046556        self.categories = categories 
     557        self.css = css 
     558 
     559    def setBuildmaster(self, buildmaster): 
     560        self.status = buildmaster.getStatus() 
     561        if self.allowForce: 
     562            self.control = interfaces.IControl(buildmaster) 
     563        else: 
     564            self.control = None 
     565        self.changemaster = buildmaster.change_svc 
     566 
     567        # try to set the page title 
    1047568        p = self.status.getProjectName() 
    1048569        if p: 
    1049570            self.title = "BuildBot: %s" % p 
    1050         self.css = css 
     571 
     572    def getChild(self, path, request): 
     573        if path == "": return self # follow the trailing / 
     574        if path == "buildbot.css" and self.css: 
     575            return static.File(self.css) 
     576        if path == "changes": 
     577            return StatusResourceChanges(self.status, self.changemaster) 
     578 
     579        if path in self.status.getBuilderNames(): 
     580            builder = self.status.getBuilder(path) 
     581            control = None 
     582            if self.control: 
     583                control = self.control.getBuilder(path) 
     584            return StatusResourceBuilder(self.status, builder, control) 
     585 
     586        return NoResource("No such Builder '%s'" % path) 
    1051587 
    1052588    def body(self, request): 
    1053589        "This method builds the main waterfall display." 
     
    15111047            data += " </tr>\n" 
    15121048        return data 
    15131049 
    1514  
    1515 class StatusResource(Resource): 
    1516     status = None 
    1517     control = None 
    1518     favicon = None 
    1519     robots_txt = None 
    1520  
    1521     def __init__(self, status, control, changemaster, categories, css): 
    1522         """ 
    1523         @type  status:       L{buildbot.status.builder.Status} 
    1524         @type  control:      L{buildbot.master.Control} 
    1525         @type  changemaster: L{buildbot.changes.changes.ChangeMaster} 
    1526         """ 
    1527         Resource.__init__(self) 
    1528         self.status = status 
    1529         self.control = control 
    1530         self.changemaster = changemaster 
    1531         self.categories = categories 
    1532         self.css = css 
    1533         waterfall = WaterfallStatusResource(self.status, changemaster, 
    1534                                             categories, css) 
    1535         self.putChild("", waterfall) 
    1536  
    1537     def render(self, request): 
    1538         request.redirect(request.prePathURL() + '/') 
    1539         request.finish() 
    1540  
    1541     def getChild(self, path, request): 
    1542         if path == "robots.txt" and self.robots_txt: 
    1543             return static.File(self.robots_txt) 
    1544         if path == "buildbot.css" and self.css: 
    1545             return static.File(self.css) 
    1546         if path == "changes": 
    1547             return StatusResourceChanges(self.status, self.changemaster) 
    1548         if path == "favicon.ico": 
    1549             if self.favicon: 
    1550                 return static.File(self.favicon) 
    1551             return NoResource("No favicon.ico registered") 
    1552  
    1553         if path in self.status.getBuilderNames(): 
    1554             builder = self.status.getBuilder(path) 
    1555             control = None 
    1556             if self.control: 
    1557                 control = self.control.getBuilder(path) 
    1558             return StatusResourceBuilder(self.status, builder, control) 
    1559  
    1560         return NoResource("No such Builder '%s'" % path) 
    1561  
    1562 if hasattr(sys, "frozen"): 
    1563     # all 'data' files are in the directory of our executable 
    1564     here = os.path.dirname(sys.executable) 
    1565     buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png")) 
    1566     buildbot_css = os.path.abspath(os.path.join(here, "classic.css")) 
    1567 else: 
    1568     # running from source 
    1569     # the icon is sibpath(__file__, "../buildbot.png") . This is for 
    1570     # portability. 
    1571     up = os.path.dirname 
    1572     buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))), 
    1573                                                  "buildbot.png")) 
    1574     buildbot_css = os.path.abspath(os.path.join(up(__file__), "classic.css")) 
    1575  
    1576 class Waterfall(base.StatusReceiverMultiService): 
    1577     """I implement the primary web-page status interface, called a 'Waterfall 
    1578     Display' because builds and steps are presented in a grid of boxes which 
    1579     move downwards over time. The top edge is always the present. Each column 
    1580     represents a single builder. Each box describes a single Step, which may 
    1581     have logfiles or other status information. 
    1582  
    1583     All these pages are served via a web server of some sort. The simplest 
    1584     approach is to let the buildmaster run its own webserver, on a given TCP 
    1585     port, but it can also publish its pages to a L{twisted.web.distrib} 
    1586     distributed web server (which lets the buildbot pages be a subset of some 
    1587     other web server). 
    1588  
    1589     Since 0.6.3, BuildBot defines class attributes on elements so they can be 
    1590     styled with CSS stylesheets. Buildbot uses some generic classes to 
    1591     identify the type of object, and some more specific classes for the 
    1592     various kinds of those types. It does this by specifying both in the 
    1593     class attributes where applicable, separated by a space. It is important 
    1594     that in your CSS you declare the more generic class styles above the more 
    1595     specific ones. For example, first define a style for .Event, and below 
    1596     that for .SUCCESS 
    1597  
    1598     The following CSS class names are used: 
    1599         - Activity, Event, BuildStep, LastBuild: general classes 
    1600         - waiting, interlocked, building, offline, idle: Activity states 
    1601         - start, running, success, failure, warnings, skipped, exception: 
    1602           LastBuild and BuildStep states 
    1603         - Change: box with change 
    1604         - Builder: box for builder name (at top) 
    1605         - Project 
    1606         - Time 
    1607  
    1608     @type parent: L{buildbot.master.BuildMaster} 
    1609     @ivar parent: like all status plugins, this object is a child of the 
    1610                   BuildMaster, so C{.parent} points to a 
    1611                   L{buildbot.master.BuildMaster} instance, through which 
    1612                   the status-reporting object is acquired. 
    1613     """ 
    1614  
    1615     compare_attrs = ["http_port", "distrib_port", "allowForce", 
    1616                      "categories", "css", "favicon", "robots_txt"] 
    1617  
    1618     def __init__(self, http_port=None, distrib_port=None, allowForce=True, 
    1619                  categories=None, css=buildbot_css, favicon=buildbot_icon, 
    1620                  robots_txt=None): 
    1621         """To have the buildbot run its own web server, pass a port number to 
    1622         C{http_port}. To have it run a web.distrib server 
    1623  
    1624         @type  http_port: int or L{twisted.application.strports} string 
    1625         @param http_port: a strports specification describing which port the 
    1626                           buildbot should use for its web server, with the 
    1627                           Waterfall display as the root page. For backwards 
    1628                           compatibility this can also be an int. Use 
    1629                           'tcp:8000' to listen on that port, or 
    1630                           'tcp:12345:interface=127.0.0.1' if you only want 
    1631                           local processes to connect to it (perhaps because 
    1632                           you are using an HTTP reverse proxy to make the 
    1633                           buildbot available to the outside world, and do not 
    1634                           want to make the raw port visible). 
    1635  
    1636         @type  distrib_port: int or L{twisted.application.strports} string 
    1637         @param distrib_port: Use this if you want to publish the Waterfall 
    1638                              page using web.distrib instead. The most common 
    1639                              case is to provide a string that is an absolute 
    1640                              pathname to the unix socket on which the 
    1641                              publisher should listen 
    1642                              (C{os.path.expanduser(~/.twistd-web-pb)} will 
    1643                              match the default settings of a standard 
    1644                              twisted.web 'personal web server'). Another 
    1645                              possibility is to pass an integer, which means 
    1646                              the publisher should listen on a TCP socket, 
    1647                              allowing the web server to be on a different 
    1648                              machine entirely. Both forms are provided for 
    1649                              backwards compatibility; the preferred form is a 
    1650                              strports specification like 
    1651                              'unix:/home/buildbot/.twistd-web-pb'. Providing 
    1652                              a non-absolute pathname will probably confuse 
    1653                              the strports parser. 
    1654  
    1655         @type  allowForce: bool 
    1656         @param allowForce: if True, present a 'Force Build' button on the 
    1657                            per-Builder page that allows visitors to the web 
    1658                            site to initiate a build. If False, don't provide 
    1659                            this button. 
    1660  
    1661         @type  favicon: string 
    1662         @param favicon: if set, provide the pathname of an image file that 
    1663                         will be used for the 'favicon.ico' resource. Many 
    1664                         browsers automatically request this file and use it 
    1665                         as an icon in any bookmark generated from this site. 
    1666                         Defaults to the buildbot/buildbot.png image provided 
    1667                         in the distribution. Can be set to None to avoid 
    1668                         using a favicon at all. 
    1669  
    1670         @type  robots_txt: string 
    1671         @param robots_txt: if set, provide the pathname of a robots.txt file. 
    1672                            Many search engines request this file and obey the 
    1673                            rules in it. E.g. to disallow them to crawl the 
    1674                            status page, put the following two lines in 
    1675                            robots.txt:: 
    1676                               User-agent: * 
    1677                               Disallow: / 
    1678         """ 
    1679  
    1680         base.StatusReceiverMultiService.__init__(self) 
    1681         assert allowForce in (True, False) # TODO: implement others 
    1682         if type(http_port) is int: 
    1683             http_port = "tcp:%d" % http_port 
    1684         self.http_port = http_port 
    1685         if distrib_port is not None: 
    1686             if type(distrib_port) is int: 
    1687                 distrib_port = "tcp:%d" % distrib_port 
    1688             if distrib_port[0] in "/~.": # pathnames 
    1689                 distrib_port = "unix:%s" % distrib_port 
    1690         self.distrib_port = distrib_port 
    1691         self.allowForce = allowForce 
    1692         self.categories = categories 
    1693         self.css = css 
    1694         self.favicon = favicon 
    1695         self.robots_txt = robots_txt 
    1696  
    1697     def __repr__(self): 
    1698         if self.http_port is None: 
    1699             return "<Waterfall on path %s>" % self.distrib_port 
    1700         if self.distrib_port is None: 
    1701             return "<Waterfall on port %s>" % self.http_port 
    1702         return "<Waterfall on port %s and path %s>" % (self.http_port, 
    1703                                                        self.distrib_port) 
    1704  
    1705     def setServiceParent(self, parent): 
    1706         """ 
    1707         @type  parent: L{buildbot.master.BuildMaster} 
    1708         """ 
    1709         base.StatusReceiverMultiService.setServiceParent(self, parent) 
    1710         self.setup() 
    1711  
    1712     def setup(self): 
    1713         status = self.parent.getStatus() 
    1714         if self.allowForce: 
    1715             control = interfaces.IControl(self.parent) 
    1716         else: 
    1717             control = None 
    1718         change_svc = self.parent.change_svc 
    1719         sr = StatusResource(status, control, change_svc, self.categories, 
    1720                             self.css) 
    1721         sr.favicon = self.favicon 
    1722         sr.robots_txt = self.robots_txt 
    1723         self.site = server.Site(sr) 
    1724  
    1725         if self.http_port is not None: 
    1726             s = strports.service(self.http_port, self.site) 
    1727             s.setServiceParent(self) 
    1728         if self.distrib_port is not None: 
    1729             f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) 
    1730             s = strports.service(self.distrib_port, f) 
    1731             s.setServiceParent(self) 
     1050# Compatibility function: sets up a new site with a waterfall as its 
     1051# top_page 
     1052def Waterfall(http_port=None, distrib_port=None, favicon=None, robots_txt=None, 
     1053            allowForce=True, categories=None, css=None): 
     1054    return WebStatus( 
     1055        WaterfallStatusResource(allowForce=allowForce, categories=categories, css=css), 
     1056        http_port=http_port, 
     1057        distrib_port=distrib_port, 
     1058        favicon=favicon, 
     1059        robots_txt=robots_txt) 
  • new file statusgrid/buildbot/status/web/base.py

    - +  
     1from twisted.web.resource import Resource 
     2 
     3from zope.interface import Interface 
     4 
     5class HtmlResource(Resource): 
     6    css = None 
     7    contentType = "text/html; charset=UTF-8" 
     8    title = "Dummy" 
     9    addSlash = False 
     10 
     11    def render(self, request): 
     12        if self.addSlash and request.prePathURL()[-1] != '/': 
     13            request.redirect(request.prePathURL() + '/') 
     14            request.finish() 
     15 
     16        data = self.content(request) 
     17        if isinstance(data, unicode): 
     18            data = data.encode("utf-8") 
     19        request.setHeader("content-type", self.contentType) 
     20        if request.method == "HEAD": 
     21            request.setHeader("content-length", len(data)) 
     22            return '' 
     23        return data 
     24 
     25    def content(self, request): 
     26        data = ('<!DOCTYPE html PUBLIC' 
     27                ' "-//W3C//DTD XHTML 1.0 Transitional//EN"\n' 
     28                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' 
     29                '<html' 
     30                ' xmlns="http://www.w3.org/1999/xhtml"' 
     31                ' lang="en"' 
     32                ' xml:lang="en">\n') 
     33        data += "<head>\n" 
     34        data += "  <title>" + self.title + "</title>\n" 
     35        if self.css: 
     36            # TODO: use some sort of relative link up to the root page, so 
     37            # this css can be used from child pages too 
     38            data += ('  <link href="%s" rel="stylesheet" type="text/css"/>\n' 
     39                     % "buildbot.css") 
     40        data += "</head>\n" 
     41        data += '<body vlink="#800080">\n' 
     42        data += self.body(request) 
     43        data += "</body></html>\n" 
     44        return data 
     45 
     46    def body(self, request): 
     47        return "Dummy\n" 
     48 
     49class StaticHTML(HtmlResource): 
     50    def __init__(self, body, title): 
     51        HtmlResource.__init__(self) 
     52        self.bodyHTML = body 
     53        self.title = title 
     54    def body(self, request): 
     55        return self.bodyHTML 
     56 
     57class IToplevelResource(Interface): 
     58    """ 
     59    I represent a Resource which is instantiated once, and which expects 
     60    to be told about a buildmaster before I am rendered. 
     61    """ 
     62    def setBuildmaster(buildmaster): pass 
     63 
     64class IHTMLLog(Interface): 
     65    pass 
     66 
  • new file statusgrid/buildbot/status/web/builder.py

    - +  
     1import urllib, re 
     2 
     3from twisted.python import log 
     4from twisted.web import static, html 
     5from twisted.internet import defer, reactor 
     6from twisted.web.error import NoResource 
     7from twisted.web.util import Redirect, DeferredResource 
     8 
     9from buildbot import interfaces, util 
     10from buildbot.sourcestamp import SourceStamp 
     11from buildbot.status.web.base import HtmlResource, StaticHTML, IHTMLLog 
     12from buildbot.process.base import BuildRequest 
     13 
     14ROW_TEMPLATE = ''' 
     15<div class="row"> 
     16  <span class="label">%(label)s</span> 
     17  <span class="field">%(field)s</span> 
     18</div>''' 
     19 
     20def make_row(label, field): 
     21    """Create a name/value row for the HTML. 
     22 
     23    `label` is plain text; it will be HTML-encoded. 
     24 
     25    `field` is a bit of HTML structure; it will not be encoded in 
     26    any way. 
     27    """ 
     28    label = html.escape(label) 
     29    return ROW_TEMPLATE % {"label": label, "field": field} 
     30 
     31class StatusResourceBuildStep(HtmlResource): 
     32    """ 
     33    I represent a particular step of a particular build, at a URL like 
     34    BUILDER/builds/NN/STEPNAME. 
     35    """ 
     36    title = "Build Step" 
     37 
     38    def __init__(self, status, step): 
     39        HtmlResource.__init__(self) 
     40        self.status = status 
     41        self.step = step 
     42 
     43    def body(self, request): 
     44        s = self.step 
     45        b = s.getBuild() 
     46        data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \ 
     47               (b.getBuilder().getName(), b.getNumber(), s.getName()) 
     48 
     49        if s.isFinished(): 
     50            data += ("<h2>Finished</h2>\n" 
     51                     "<p>%s</p>\n" % html.escape("%s" % s.getText())) 
     52        else: 
     53            data += ("<h2>Not Finished</h2>\n" 
     54                     "<p>ETA %s seconds</p>\n" % s.getETA()) 
     55 
     56        exp = s.getExpectations() 
     57        if exp: 
     58            data += ("<h2>Expectations</h2>\n" 
     59                     "<ul>\n") 
     60            for e in exp: 
     61                data += "<li>%s: current=%s, target=%s</li>\n" % \ 
     62                        (html.escape(e[0]), e[1], e[2]) 
     63            data += "</ul>\n" 
     64        logs = s.getLogs() 
     65        if logs: 
     66            data += ("<h2>Logs</h2>\n" 
     67                     "<ul>\n") 
     68            for num in range(len(logs)): 
     69                if logs[num].hasContents(): 
     70                    # FIXME: If the step name has a / in it, this is broken 
     71                    # either way.  If we quote it but say '/'s are safe, 
     72                    # it chops up the step name.  If we quote it and '/'s 
     73                    # are not safe, it escapes the / that separates the 
     74                    # step name from the log number. 
     75                    data += '<li><a href="%s">%s</a></li>\n' % \ 
     76                            (urllib.quote(request.childLink("%d" % num)), 
     77                             html.escape(logs[num].getName())) 
     78                else: 
     79                    data += ('<li>%s</li>\n' % 
     80                             html.escape(logs[num].getName())) 
     81            data += "</ul>\n" 
     82 
     83        return data 
     84 
     85    def getChild(self, path, request): 
     86        logname = path 
     87        try: 
     88            log = self.step.getLogs()[int(logname)] 
     89            if log.hasContents(): 
     90                return IHTMLLog(interfaces.IStatusLog(log)) 
     91            return NoResource("Empty Log '%s'" % logname) 
     92        except (IndexError, ValueError): 
     93            return NoResource("No such Log '%s'" % logname) 
     94 
     95# $builder/builds/NN/tests/TESTNAME 
     96class StatusResourceTestResult(HtmlResource): 
     97    """ 
     98    I represent the result of a particular test of a particular 
     99    build, at a URL like BUILDER/builds/NN/tests/TESTNAME. 
     100    """ 
     101    title = "Test Logs" 
     102 
     103    def __init__(self, status, name, result): 
     104        HtmlResource.__init__(self) 
     105        self.status = status 
     106        self.name = name 
     107        self.result = result 
     108 
     109    def body(self, request): 
     110        dotname = ".".join(self.name) 
     111        logs = self.result.getLogs() 
     112        lognames = logs.keys() 
     113        lognames.sort() 
     114        data = "<h1>%s</h1>\n" % html.escape(dotname) 
     115        for name in lognames: 
     116            data += "<h2>%s</h2>\n" % html.escape(name) 
     117            data += "<pre>" + logs[name] + "</pre>\n\n" 
     118 
     119        return data 
     120 
     121 
     122# $builder/builds/NN/tests 
     123class StatusResourceTestResults(HtmlResource): 
     124    """ 
     125    I represent a summary of the tests in a particular build, at a 
     126    URL like BUILDER/builds/NN/tests. 
     127    """ 
     128    title = "Test Results" 
     129 
     130    def __init__(self, status, results): 
     131        HtmlResource.__init__(self) 
     132        self.status = status 
     133        self.results = results 
     134 
     135    def body(self, request): 
     136        r = self.results 
     137        data = "<h1>Test Results</h1>\n" 
     138        data += "<ul>\n" 
     139        testnames = r.keys() 
     140        testnames.sort() 
     141        for name in testnames: 
     142            res = r[name] 
     143            dotname = ".".join(name) 
     144            data += " <li>%s: " % dotname 
     145            # TODO: this could break on weird test names. At the moment, 
     146            # test names only come from Trial tests, where the name 
     147            # components must be legal python names, but that won't always 
     148            # be a restriction. 
     149            url = request.childLink(dotname) 
     150            data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText())) 
     151            data += "</li>\n" 
     152        data += "</ul>\n" 
     153        return data 
     154 
     155    def getChild(self, path, request): 
     156        try: 
     157            name = tuple(path.split(".")) 
     158            result = self.results[name] 
     159            return StatusResourceTestResult(self.status, name, result) 
     160        except KeyError: 
     161            return NoResource("No such test name '%s'" % path) 
     162 
     163 
     164class StatusResourceBuild(HtmlResource): 
     165    """ 
     166    I represent a particular build, at a URL like BUILDER/builds/NN. 
     167    """ 
     168    title = "Build" 
     169 
     170    def __init__(self, status, build, builderControl, buildControl): 
     171        HtmlResource.__init__(self) 
     172        self.status = status 
     173        self.build = build 
     174        self.builderControl = builderControl 
     175        self.control = buildControl 
     176 
     177    def body(self, request): 
     178        b = self.build 
     179        buildbotURL = self.status.getBuildbotURL() 
     180        projectName = self.status.getProjectName() 
     181        data = '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL, 
     182                                                                 projectName) 
     183        # the color in the following line gives python-mode trouble 
     184        data += ("<h1>Build <a href=\"%s\">%s</a>:#%d</h1>\n" 
     185                 % (self.status.getURLForThing(b.getBuilder()), 
     186                    b.getBuilder().getName(), b.getNumber())) 
     187        data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename()) 
     188        data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason()) 
     189 
     190        branch, revision, patch = b.getSourceStamp() 
     191        data += "<h2>SourceStamp:</h2>\n" 
     192        data += " <ul>\n" 
     193        if branch: 
     194            data += "  <li>Branch: %s</li>\n" % html.escape(branch) 
     195        if revision: 
     196            data += "  <li>Revision: %s</li>\n" % html.escape(str(revision)) 
     197        if patch: 
     198            data += "  <li>Patch: YES</li>\n" # TODO: provide link to .diff 
     199        if b.getChanges(): 
     200            data += "  <li>Changes: see below</li>\n" 
     201        if (branch is None and revision is None and patch is None 
     202            and not b.getChanges()): 
     203            data += "  <li>build of most recent revision</li>\n" 
     204        data += " </ul>\n" 
     205        if b.isFinished(): 
     206            data += "<h2>Results:</h2>\n" 
     207            data += " ".join(b.getText()) + "\n" 
     208            if b.getTestResults(): 
     209                url = request.childLink("tests") 
     210                data += "<h3><a href=\"%s\">test results</a></h3>\n" % url 
     211        else: 
     212            data += "<h2>Build In Progress</h2>" 
     213            if self.control is not None: 
     214                stopURL = urllib.quote(request.childLink("stop")) 
     215                data += """ 
     216                <form action="%s" class='command stopbuild'> 
     217                <p>To stop this build, fill out the following fields and 
     218                push the 'Stop' button</p>\n""" % stopURL 
     219                data += make_row("Your name:", 
     220                                 "<input type='text' name='username' />") 
     221                data += make_row("Reason for stopping build:", 
     222                                 "<input type='text' name='comments' />") 
     223                data += """<input type="submit" value="Stop Builder" /> 
     224                </form> 
     225                """ 
     226 
     227        if b.isFinished() and self.builderControl is not None: 
     228            data += "<h3>Resubmit Build:</h3>\n" 
     229            # can we rebuild it exactly? 
     230            exactly = (revision is not None) or b.getChanges() 
     231            if exactly: 
     232                data += ("<p>This tree was built from a specific set of \n" 
     233                         "source files, and can be rebuilt exactly</p>\n") 
     234            else: 
     235                data += ("<p>This tree was built from the most recent " 
     236                         "revision") 
     237                if branch: 
     238                    data += " (along some branch)" 
     239                data += (" and thus it might not be possible to rebuild it \n" 
     240                         "exactly. Any changes that have been committed \n" 
     241                         "after this build was started <b>will</b> be \n" 
     242                         "included in a rebuild.</p>\n") 
     243            rebuildURL = urllib.quote(request.childLink("rebuild")) 
     244            data += ('<form action="%s" class="command rebuild">\n' 
     245                     % rebuildURL) 
     246            data += make_row("Your name:", 
     247                             "<input type='text' name='username' />") 
     248            data += make_row("Reason for re-running build:", 
     249                             "<input type='text' name='comments' />") 
     250            data += '<input type="submit" value="Rebuild" />\n' 
     251            data += '</form>\n' 
     252 
     253        data += "<h2>Steps and Logfiles:</h2>\n" 
     254        if b.getLogs(): 
     255            data += "<ol>\n" 
     256            for s in b.getSteps(): 
     257                data += (" <li><a href=\"%s\">%s</a> [%s]\n" 
     258                         % (self.status.getURLForThing(s), s.getName(), 
     259                            " ".join(s.getText()))) 
     260                if s.getLogs(): 
     261                    data += "  <ol>\n" 
     262                    for logfile in s.getLogs(): 
     263                        data += ("   <li><a href=\"%s\">%s</a></li>\n" % 
     264                                 (self.status.getURLForThing(logfile), 
     265                                  logfile.getName())) 
     266                    data += "  </ol>\n" 
     267                data += " </li>\n" 
     268            data += "</ol>\n" 
     269 
     270        data += ("<h2>Blamelist:</h2>\n" 
     271                 " <ol>\n") 
     272        for who in b.getResponsibleUsers(): 
     273            data += "  <li>%s</li>\n" % html.escape(who) 
     274        data += (" </ol>\n" 
     275                 "<h2>All Changes</h2>\n") 
     276        changes = b.getChanges() 
     277        if changes: 
     278            data += "<ol>\n" 
     279            for c in changes: 
     280                data += "<li>" + c.asHTML() + "</li>\n" 
     281            data += "</ol>\n" 
     282        #data += html.PRE(b.changesText()) # TODO 
     283        return data 
     284 
     285    def stop(self, request): 
     286        log.msg("web stopBuild of build %s:%s" % \ 
     287                (self.build.getBuilder().getName(), 
     288                 self.build.getNumber())) 
     289        name = request.args.get("username", ["<unknown>"])[0] 
     290        comments = request.args.get("comments", ["<no reason specified>"])[0] 
     291        reason = ("The web-page 'stop build' button was pressed by " 
     292                  "'%s': %s\n" % (name, comments)) 
     293        self.control.stopBuild(reason) 
     294        # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and 
     295        # we want to go to: http://localhost:8080/svn-hello/builds/5 or 
     296        # http://localhost:8080/ 
     297        # 
     298        #return Redirect("../%d" % self.build.getNumber()) 
     299        r = Redirect("../../..") 
     300        d = defer.Deferred() 
     301        reactor.callLater(1, d.callback, r) 
     302        return DeferredResource(d) 
     303 
     304    def rebuild(self, request): 
     305        log.msg("web rebuild of build %s:%s" % \ 
     306                (self.build.getBuilder().getName(), 
     307                 self.build.getNumber())) 
     308        name = request.args.get("username", ["<unknown>"])[0] 
     309        comments = request.args.get("comments", ["<no reason specified>"])[0] 
     310        reason = ("The web-page 'rebuild' button was pressed by " 
     311                  "'%s': %s\n" % (name, comments)) 
     312        if not self.builderControl or not self.build.isFinished(): 
     313            log.msg("could not rebuild: bc=%s, isFinished=%s" 
     314                    % (self.builderControl, self.build.isFinished())) 
     315            # TODO: indicate an error 
     316        else: 
     317            self.builderControl.resubmitBuild(self.build, reason) 
     318        # we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and 
     319        # we want to go to the top, at http://localhost:8080/ 
     320        r = Redirect("../../..") 
     321        d = defer.Deferred() 
     322        reactor.callLater(1, d.callback, r) 
     323        return DeferredResource(d) 
     324 
     325    def getChild(self, path, request): 
     326        if path == "tests": 
     327            return StatusResourceTestResults(self.status, 
     328                                             self.build.getTestResults()) 
     329        if path == "stop": 
     330            return self.stop(request) 
     331        if path == "rebuild": 
     332            return self.rebuild(request) 
     333        if path.startswith("step-"): 
     334            stepname = path[len("step-"):] 
     335            steps = self.build.getSteps() 
     336            for s in steps: 
     337                if s.getName() == stepname: 
     338                    return StatusResourceBuildStep(self.status, s) 
     339            return NoResource("No such BuildStep '%s'" % stepname) 
     340        return NoResource("No such resource '%s'" % path) 
     341 
     342class StatusResourceBuilder(HtmlResource): 
     343    """ 
     344    I display a builder's status.  I have several child pages containing 
     345    other interesting data, implemented by the other classes in this module. 
     346    """ 
     347    def __init__(self, status, builder, control): 
     348        HtmlResource.__init__(self) 
     349        self.status = status 
     350        self.title = builder.getName() + " Builder" 
     351        self.builder = builder 
     352        self.control = control 
     353 
     354    def body(self, request): 
     355        b = self.builder 
     356        slaves = b.getSlaves() 
     357        connected_slaves = [s for s in slaves if s.isConnected()] 
     358 
     359        buildbotURL = self.status.getBuildbotURL() 
     360        projectName = self.status.getProjectName() 
     361        data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName) 
     362        data += make_row("Builder:", html.escape(b.getName())) 
     363        b1 = b.getBuild(-1) 
     364        if b1 is not None: 
     365            data += make_row("Current/last build:", str(b1.getNumber())) 
     366        data += "\n<br />BUILDSLAVES<br />\n" 
     367        data += "<ol>\n" 
     368        for slave in slaves: 
     369            data += "<li><b>%s</b>: " % html.escape(slave.getName()) 
     370            if slave.isConnected(): 
     371                data += "CONNECTED\n" 
     372                if slave.getAdmin(): 
     373                    data += make_row("Admin:", html.escape(slave.getAdmin())) 
     374                if slave.getHost(): 
     375                    data += "<span class='label'>Host info:</span>\n" 
     376                    data += html.PRE(slave.getHost()) 
     377            else: 
     378                data += ("NOT CONNECTED\n") 
     379            data += "</li>\n" 
     380        data += "</ol>\n" 
     381 
     382        if self.control is not None and connected_slaves: 
     383            forceURL = urllib.quote(request.childLink("force")) 
     384            data += ( 
     385                """ 
     386                <form action='%(forceURL)s' class='command forcebuild'> 
     387                <p>To force a build, fill out the following fields and 
     388                push the 'Force Build' button</p>""" 
     389                + make_row("Your name:", 
     390                           "<input type='text' name='username' />") 
     391                + make_row("Reason for build:", 
     392                           "<input type='text' name='comments' />") 
     393                + make_row("Branch to build:", 
     394                           "<input type='text' name='branch' />") 
     395                + make_row("Revision to build:", 
     396                           "<input type='text' name='revision' />") 
     397                + """ 
     398                <input type='submit' value='Force Build' /> 
     399                </form> 
     400                """) % {"forceURL": forceURL} 
     401        elif self.control is not None: 
     402            data += """ 
     403            <p>All buildslaves appear to be offline, so it's not possible 
     404            to force this build to execute at this time.</p> 
     405            """ 
     406 
     407        if self.control is not None: 
     408            pingURL = urllib.quote(request.childLink("ping")) 
     409            data += """ 
     410            <form action="%s" class='command pingbuilder'> 
     411            <p>To ping the buildslave(s), push the 'Ping' button</p> 
     412 
     413            <input type="submit" value="Ping Builder" /> 
     414            </form> 
     415            """ % pingURL 
     416 
     417        return data 
     418 
     419    def force(self, request): 
     420        name = request.args.get("username", ["<unknown>"])[0] 
     421        reason = request.args.get("comments", ["<no reason specified>"])[0] 
     422        branch = request.args.get("branch", [""])[0] 
     423        revision = request.args.get("revision", [""])[0] 
     424 
     425        r = "The web-page 'force build' button was pressed by '%s': %s\n" \ 
     426            % (name, reason) 
     427        log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'" 
     428                % (self.builder.name, branch, revision)) 
     429 
     430        if not self.control: 
     431            # TODO: tell the web user that their request was denied 
     432            log.msg("but builder control is disabled") 
     433            return Redirect("..") 
     434 
     435        # keep weird stuff out of the branch and revision strings. TODO: 
     436        # centralize this somewhere. 
     437        if not re.match(r'^[\w\.\-\/]*$', branch): 
     438            log.msg("bad branch '%s'" % branch) 
     439            return Redirect("..") 
     440        if not re.match(r'^[\w\.\-\/]*$', revision): 
     441            log.msg("bad revision '%s'" % revision) 
     442            return Redirect("..") 
     443        if branch == "": 
     444            branch = None 
     445        if revision == "": 
     446            revision = None 
     447 
     448        # TODO: if we can authenticate that a particular User pushed the 
     449        # button, use their name instead of None, so they'll be informed of 
     450        # the results. 
     451        s = SourceStamp(branch=branch, revision=revision) 
     452        req = BuildRequest(r, s, self.builder.getName()) 
     453        try: 
     454            self.control.requestBuildSoon(req) 
     455        except interfaces.NoSlaveError: 
     456            # TODO: tell the web user that their request could not be 
     457            # honored 
     458            pass 
     459        return Redirect("..") 
     460 
     461    def ping(self, request): 
     462        log.msg("web ping of builder '%s'" % self.builder.name) 
     463        self.control.ping() # TODO: there ought to be an ISlaveControl 
     464        return Redirect("..") 
     465 
     466    def getChild(self, path, request): 
     467        if path == "force": 
     468            return self.force(request) 
     469        if path == "ping": 
     470            return self.ping(request) 
     471        if not path in ("events", "builds"): 
     472            return NoResource("Bad URL '%s'" % path) 
     473        num = request.postpath.pop(0) 
     474        request.prepath.append(num) 
     475        num = int(num) 
     476        if path == "events": 
     477            # TODO: is this dead code? .statusbag doesn't exist,right? 
     478            log.msg("getChild['path']: %s" % request.uri) 
     479            return NoResource("events are unavailable until code gets fixed") 
     480            filename = request.postpath.pop(0) 
     481            request.prepath.append(filename) 
     482            e = self.builder.statusbag.getEventNumbered(num) 
     483            if not e: 
     484                return NoResource("No such event '%d'" % num) 
     485            file = e.files.get(filename, None) 
     486            if file == None: 
     487                return NoResource("No such file '%s'" % filename) 
     488            if type(file) == type(""): 
     489                if file[:6] in ("<HTML>", "<html>"): 
     490                    return static.Data(file, "text/html") 
     491                return static.Data(file, "text/plain") 
     492            return file 
     493        if path == "builds": 
     494            build = self.builder.getBuild(num) 
     495            if build: 
     496                control = None 
     497                if self.control: 
     498                    control = self.control.getBuild(num) 
     499                return StatusResourceBuild(self.status, build, 
     500                                           self.control, control) 
     501            else: 
     502                return NoResource("No such build '%d'" % num) 
     503        return NoResource("really weird URL %s" % path) 
     504 
     505 
  • new file statusgrid/buildbot/status/web/changes.py

    - +  
     1from buildbot.status.web.base import HtmlResource, StaticHTML 
     2 
     3class StatusResourceChanges(HtmlResource): 
     4    """ 
     5    I display the list of change sources in the changemaster by default, 
     6    and my children display specific numbered changes. 
     7    """ 
     8    def __init__(self, status, changemaster): 
     9        HtmlResource.__init__(self) 
     10        self.status = status 
     11        self.changemaster = changemaster 
     12    def body(self, request): 
     13        data = "" 
     14        data += "Change sources:\n" 
     15        sources = list(self.changemaster) 
     16        if sources: 
     17            data += "<ol>\n" 
     18            for s in sources: 
     19                data += "<li>%s</li>\n" % s.describe() 
     20            data += "</ol>\n" 
     21        else: 
     22            data += "none (push only)\n" 
     23        return data 
     24    def getChild(self, path, request): 
     25        num = int(path) 
     26        c = self.changemaster.getChangeNumbered(num) 
     27        if not c: 
     28            return NoResource("No change number '%d'" % num) 
     29        return StaticHTML(c.asHTML(), "Change #%d" % num) 
     30 
     31