Ticket #58: factor-web.patch
| File factor-web.patch, 72.2 KB (added by dustin, 5 years ago) |
|---|
-
buildbot/status/html.py
old new 3 3 # compatibility wrapper. This is currently the preferred place for master.cfg 4 4 # to import from. 5 5 6 # compatibility function 6 7 from buildbot.status.web.waterfall import Waterfall 7 8 _hush_pyflakes = [Waterfall] 9 10 11 from buildbot.status.web.waterfall import WaterfallStatusResource 12 _hush_pyflakes = [WaterfallStatusResource] 13 14 from buildbot.status.web.site import WebStatus 15 _hush_pyflakes = [WebStatus] -
new file statusgrid/buildbot/status/web/site.py
- + 1 import sys, os.path 2 3 from twisted.web.resource import Resource 4 from twisted.application import strports 5 from twisted.web import static, html, server, distrib 6 from twisted.web.error import NoResource 7 from twisted.spread import pb 8 9 from buildbot import interfaces 10 from buildbot.status.base import StatusReceiverMultiService 11 from buildbot.status.web.base import HtmlResource, IToplevelResource 12 13 if 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")) 17 else: 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 25 class 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 139 class 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 151 class 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 19 19 from buildbot import interfaces, util 20 20 from buildbot import version 21 21 from buildbot.sourcestamp import SourceStamp 22 from buildbot.status import builder, base 22 from buildbot.status import builder 23 from buildbot.status.base import StatusReceiverMultiService 23 24 from buildbot.changes import changes 24 25 from buildbot.process.base import BuildRequest 26 from buildbot.status.web.site import WebStatus 27 from buildbot.status.web.base import HtmlResource, StaticHTML, IToplevelResource, IHTMLLog 28 from buildbot.status.web.changes import StatusResourceChanges 29 from buildbot.status.web.builder import StatusResourceBuilder 25 30 26 31 class ITopBox(Interface): 27 32 """I represent a box in the top row of the waterfall display: the one … … 36 41 """I represent a box in the waterfall display.""" 37 42 pass 38 43 39 class IHTMLLog(Interface):40 pass41 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 in54 any way.55 """56 label = html.escape(label)57 return ROW_TEMPLATE % {"label": label, "field": field}58 59 44 colormap = { 60 45 'green': '#72ff75', 61 46 } … … 140 125 return td(text, props, bgcolor=self.color, class_=self.class_) 141 126 142 127 143 class HtmlResource(Resource):144 css = None145 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 data157 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, so170 # this css can be used from child pages too171 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 data178 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 = body186 self.title = title187 def body(self, request):188 return self.bodyHTML189 190 # $builder/builds/NN/stepname191 class StatusResourceBuildStep(HtmlResource):192 title = "Build Step"193 194 def __init__(self, status, step):195 HtmlResource.__init__(self)196 self.status = status197 self.step = step198 199 def body(self, request):200 s = self.step201 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 broken227 # either way. If we quote it but say '/'s are safe,228 # it chops up the step name. If we quote it and '/'s229 # are not safe, it escapes the / that separates the230 # 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 data240 241 def getChild(self, path, request):242 logname = path243 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/TESTNAME252 class StatusResourceTestResult(HtmlResource):253 title = "Test Logs"254 255 def __init__(self, status, name, result):256 HtmlResource.__init__(self)257 self.status = status258 self.name = name259 self.result = result260 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 data272 273 274 # $builder/builds/NN/tests275 class StatusResourceTestResults(HtmlResource):276 title = "Test Results"277 278 def __init__(self, status, results):279 HtmlResource.__init__(self)280 self.status = status281 self.results = results282 283 def body(self, request):284 r = self.results285 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: " % dotname293 # TODO: this could break on weird test names. At the moment,294 # test names only come from Trial tests, where the name295 # components must be legal python names, but that won't always296 # 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 data302 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/NN313 class StatusResourceBuild(HtmlResource):314 title = "Build"315 316 def __init__(self, status, build, builderControl, buildControl):317 HtmlResource.__init__(self)318 self.status = status319 self.build = build320 self.builderControl = builderControl321 self.control = buildControl322 323 def body(self, request):324 b = self.build325 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 trouble330 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 .diff345 if b.getChanges():346 data += " <li>Changes: see below</li>\n"347 if (branch is None and revision is None and patch is None348 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" % url357 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 and364 push the 'Stop' button</p>\n""" % stopURL365 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()) # TODO429 return data430 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] and441 # we want to go to: http://localhost:8080/svn-hello/builds/5 or442 # 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 error462 else:463 self.builderControl.resubmitBuild(self.build, reason)464 # we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and465 # 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 # $builder489 class StatusResourceBuilder(HtmlResource):490 491 def __init__(self, status, builder, control):492 HtmlResource.__init__(self)493 self.status = status494 self.title = builder.getName() + " Builder"495 self.builder = builder496 self.control = control497 498 def body(self, request):499 b = self.builder500 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 and532 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 possible548 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 """ % pingURL560 561 return data562 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 denied576 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 = None589 if revision == "":590 revision = None591 592 # TODO: if we can authenticate that a particular User pushed the593 # button, use their name instead of None, so they'll be informed of594 # 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 be601 # honored602 pass603 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 ISlaveControl608 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 file637 if path == "builds":638 build = self.builder.getBuild(num)639 if build:640 control = None641 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/NN650 class StatusResourceChanges(HtmlResource):651 def __init__(self, status, changemaster):652 HtmlResource.__init__(self)653 self.status = status654 self.changemaster = changemaster655 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 data667 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 674 128 textlog_stylesheet = """ 675 129 <style type="text/css"> 676 130 div.data { … … 1035 489 if debug: log.msg(" fES1", starts) 1036 490 1037 491 492 if 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")) 496 else: 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 1038 503 class 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 1041 542 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 1043 553 HtmlResource.__init__(self) 1044 self. status = status1045 self. changemaster = changemaster554 self.addSlash = True 555 self.allowForce = allowForce 1046 556 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 1047 568 p = self.status.getProjectName() 1048 569 if p: 1049 570 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) 1051 587 1052 588 def body(self, request): 1053 589 "This method builds the main waterfall display." … … 1511 1047 data += " </tr>\n" 1512 1048 return data 1513 1049 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 1052 def 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
- + 1 from twisted.web.resource import Resource 2 3 from zope.interface import Interface 4 5 class 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 49 class 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 57 class 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 64 class IHTMLLog(Interface): 65 pass 66 -
new file statusgrid/buildbot/status/web/builder.py
- + 1 import urllib, re 2 3 from twisted.python import log 4 from twisted.web import static, html 5 from twisted.internet import defer, reactor 6 from twisted.web.error import NoResource 7 from twisted.web.util import Redirect, DeferredResource 8 9 from buildbot import interfaces, util 10 from buildbot.sourcestamp import SourceStamp 11 from buildbot.status.web.base import HtmlResource, StaticHTML, IHTMLLog 12 from buildbot.process.base import BuildRequest 13 14 ROW_TEMPLATE = ''' 15 <div class="row"> 16 <span class="label">%(label)s</span> 17 <span class="field">%(field)s</span> 18 </div>''' 19 20 def 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 31 class 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 96 class 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 123 class 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 164 class 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 342 class 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
- + 1 from buildbot.status.web.base import HtmlResource, StaticHTML 2 3 class 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
![[Buildbot Logo]](/chrome/site/header-text-transparent.png)