Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/status/web
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/status/web')
-rw-r--r--buildbot/buildbot/status/web/__init__.py0
-rw-r--r--buildbot/buildbot/status/web/about.py33
-rw-r--r--buildbot/buildbot/status/web/base.py421
-rw-r--r--buildbot/buildbot/status/web/baseweb.py614
-rw-r--r--buildbot/buildbot/status/web/build.py302
-rw-r--r--buildbot/buildbot/status/web/builder.py312
-rw-r--r--buildbot/buildbot/status/web/changes.py41
-rw-r--r--buildbot/buildbot/status/web/classic.css78
-rw-r--r--buildbot/buildbot/status/web/feeds.py359
-rw-r--r--buildbot/buildbot/status/web/grid.py252
-rw-r--r--buildbot/buildbot/status/web/index.html32
-rw-r--r--buildbot/buildbot/status/web/logs.py171
-rw-r--r--buildbot/buildbot/status/web/robots.txt9
-rw-r--r--buildbot/buildbot/status/web/slaves.py181
-rw-r--r--buildbot/buildbot/status/web/step.py97
-rw-r--r--buildbot/buildbot/status/web/tests.py64
-rw-r--r--buildbot/buildbot/status/web/waterfall.py962
-rw-r--r--buildbot/buildbot/status/web/xmlrpc.py203
18 files changed, 0 insertions, 4131 deletions
diff --git a/buildbot/buildbot/status/web/__init__.py b/buildbot/buildbot/status/web/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/buildbot/buildbot/status/web/__init__.py
+++ /dev/null
diff --git a/buildbot/buildbot/status/web/about.py b/buildbot/buildbot/status/web/about.py
deleted file mode 100644
index 09748e6..0000000
--- a/buildbot/buildbot/status/web/about.py
+++ /dev/null
@@ -1,33 +0,0 @@
-
-from twisted.web import html
-from buildbot.status.web.base import HtmlResource
-import buildbot
-import twisted
-import sys
-
-class AboutBuildbot(HtmlResource):
- title = "About this Buildbot"
-
- def body(self, request):
- data = ''
- data += '<h1>Welcome to the Buildbot</h1>\n'
- data += '<h2>Version Information</h2>\n'
- data += '<ul>\n'
- data += ' <li>Buildbot: %s</li>\n' % html.escape(buildbot.version)
- data += ' <li>Twisted: %s</li>\n' % html.escape(twisted.__version__)
- data += ' <li>Python: %s</li>\n' % html.escape(sys.version)
- data += ' <li>Buildmaster platform: %s</li>\n' % html.escape(sys.platform)
- data += '</ul>\n'
-
- data += '''
-<h2>Source code</h2>
-
-<p>Buildbot is a free software project, released under the terms of the
-<a href="http://www.gnu.org/licenses/gpl.html">GNU GPL</a>.</p>
-
-<p>Please visit the <a href="http://buildbot.net/">Buildbot Home Page</a> for
-more information, including documentation, bug reports, and source
-downloads.</p>
-'''
- return data
-
diff --git a/buildbot/buildbot/status/web/base.py b/buildbot/buildbot/status/web/base.py
deleted file mode 100644
index e515a25..0000000
--- a/buildbot/buildbot/status/web/base.py
+++ /dev/null
@@ -1,421 +0,0 @@
-
-import urlparse, urllib, time
-from zope.interface import Interface
-from twisted.web import html, resource
-from buildbot.status import builder
-from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION
-from buildbot import version, util
-
-class ITopBox(Interface):
- """I represent a box in the top row of the waterfall display: the one
- which shows the status of the last build for each builder."""
- def getBox(self, request):
- """Return a Box instance, which can produce a <td> cell.
- """
-
-class ICurrentBox(Interface):
- """I represent the 'current activity' box, just above the builder name."""
- def getBox(self, status):
- """Return a Box instance, which can produce a <td> cell.
- """
-
-class IBox(Interface):
- """I represent a box in the waterfall display."""
- def getBox(self, request):
- """Return a Box instance, which wraps an Event and can produce a <td>
- cell.
- """
-
-class IHTMLLog(Interface):
- pass
-
-css_classes = {SUCCESS: "success",
- WARNINGS: "warnings",
- FAILURE: "failure",
- SKIPPED: "skipped",
- EXCEPTION: "exception",
- }
-
-ROW_TEMPLATE = '''
-<div class="row">
- <span class="label">%(label)s</span>
- <span class="field">%(field)s</span>
-</div>
-'''
-
-def make_row(label, field):
- """Create a name/value row for the HTML.
-
- `label` is plain text; it will be HTML-encoded.
-
- `field` is a bit of HTML structure; it will not be encoded in
- any way.
- """
- label = html.escape(label)
- return ROW_TEMPLATE % {"label": label, "field": field}
-
-def make_stop_form(stopURL, on_all=False, label="Build"):
- if on_all:
- data = """<form action="%s" class='command stopbuild'>
- <p>To stop all builds, fill out the following fields and
- push the 'Stop' button</p>\n""" % stopURL
- else:
- data = """<form action="%s" class='command stopbuild'>
- <p>To stop this build, fill out the following fields and
- push the 'Stop' button</p>\n""" % stopURL
- data += make_row("Your name:",
- "<input type='text' name='username' />")
- data += make_row("Reason for stopping build:",
- "<input type='text' name='comments' />")
- data += '<input type="submit" value="Stop %s" /></form>\n' % label
- return data
-
-def make_force_build_form(forceURL, on_all=False):
- if on_all:
- data = """<form action="%s" class="command forcebuild">
- <p>To force a build on all Builders, fill out the following fields
- and push the 'Force Build' button</p>""" % forceURL
- else:
- data = """<form action="%s" class="command forcebuild">
- <p>To force a build, fill out the following fields and
- push the 'Force Build' button</p>""" % forceURL
- return (data
- + make_row("Your name:",
- "<input type='text' name='username' />")
- + make_row("Reason for build:",
- "<input type='text' name='comments' />")
- + make_row("Branch to build:",
- "<input type='text' name='branch' />")
- + make_row("Revision to build:",
- "<input type='text' name='revision' />")
- + '<input type="submit" value="Force Build" /></form>\n')
-
-def td(text="", parms={}, **props):
- data = ""
- data += " "
- #if not props.has_key("border"):
- # props["border"] = 1
- props.update(parms)
- comment = props.get("comment", None)
- if comment:
- data += "<!-- %s -->" % comment
- data += "<td"
- class_ = props.get('class_', None)
- if class_:
- props["class"] = class_
- for prop in ("align", "colspan", "rowspan", "border",
- "valign", "halign", "class"):
- p = props.get(prop, None)
- if p != None:
- data += " %s=\"%s\"" % (prop, p)
- data += ">"
- if not text:
- text = "&nbsp;"
- if isinstance(text, list):
- data += "<br />".join(text)
- else:
- data += text
- data += "</td>\n"
- return data
-
-def build_get_class(b):
- """
- Return the class to use for a finished build or buildstep,
- based on the result.
- """
- # FIXME: this getResults duplicity might need to be fixed
- result = b.getResults()
- #print "THOMAS: result for b %r: %r" % (b, result)
- if isinstance(b, builder.BuildStatus):
- result = b.getResults()
- elif isinstance(b, builder.BuildStepStatus):
- result = b.getResults()[0]
- # after forcing a build, b.getResults() returns ((None, []), []), ugh
- if isinstance(result, tuple):
- result = result[0]
- else:
- raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
-
- if result == None:
- # FIXME: this happens when a buildstep is running ?
- return "running"
- return builder.Results[result]
-
-def path_to_root(request):
- # /waterfall : ['waterfall'] -> ''
- # /somewhere/lower : ['somewhere', 'lower'] -> '../'
- # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../'
- # / : [] -> ''
- if request.prepath:
- segs = len(request.prepath) - 1
- else:
- segs = 0
- root = "../" * segs
- return root
-
-def path_to_builder(request, builderstatus):
- return (path_to_root(request) +
- "builders/" +
- urllib.quote(builderstatus.getName(), safe=''))
-
-def path_to_build(request, buildstatus):
- return (path_to_builder(request, buildstatus.getBuilder()) +
- "/builds/%d" % buildstatus.getNumber())
-
-def path_to_step(request, stepstatus):
- return (path_to_build(request, stepstatus.getBuild()) +
- "/steps/%s" % urllib.quote(stepstatus.getName(), safe=''))
-
-def path_to_slave(request, slave):
- return (path_to_root(request) +
- "buildslaves/" +
- urllib.quote(slave.getName(), safe=''))
-
-class Box:
- # a Box wraps an Event. The Box has HTML <td> parameters that Events
- # lack, and it has a base URL to which each File's name is relative.
- # Events don't know about HTML.
- spacer = False
- def __init__(self, text=[], class_=None, urlbase=None,
- **parms):
- self.text = text
- self.class_ = class_
- self.urlbase = urlbase
- self.show_idle = 0
- if parms.has_key('show_idle'):
- del parms['show_idle']
- self.show_idle = 1
-
- self.parms = parms
- # parms is a dict of HTML parameters for the <td> element that will
- # represent this Event in the waterfall display.
-
- def td(self, **props):
- props.update(self.parms)
- text = self.text
- if not text and self.show_idle:
- text = ["[idle]"]
- return td(text, props, class_=self.class_)
-
-
-class HtmlResource(resource.Resource):
- # this is a cheap sort of template thingy
- contentType = "text/html; charset=UTF-8"
- title = "Buildbot"
- addSlash = False # adapted from Nevow
-
- def getChild(self, path, request):
- if self.addSlash and path == "" and len(request.postpath) == 0:
- return self
- return resource.Resource.getChild(self, path, request)
-
- def render(self, request):
- # tell the WebStatus about the HTTPChannel that got opened, so they
- # can close it if we get reconfigured and the WebStatus goes away.
- # They keep a weakref to this, since chances are good that it will be
- # closed by the browser or by us before we get reconfigured. See
- # ticket #102 for details.
- if hasattr(request, "channel"):
- # web.distrib.Request has no .channel
- request.site.buildbot_service.registerChannel(request.channel)
-
- # Our pages no longer require that their URL end in a slash. Instead,
- # they all use request.childLink() or some equivalent which takes the
- # last path component into account. This clause is left here for
- # historical and educational purposes.
- if False and self.addSlash and request.prepath[-1] != '':
- # this is intended to behave like request.URLPath().child('')
- # but we need a relative URL, since we might be living behind a
- # reverse proxy
- #
- # note that the Location: header (as used in redirects) are
- # required to have absolute URIs, and my attempt to handle
- # reverse-proxies gracefully violates rfc2616. This frequently
- # works, but single-component paths sometimes break. The best
- # strategy is to avoid these redirects whenever possible by using
- # HREFs with trailing slashes, and only use the redirects for
- # manually entered URLs.
- url = request.prePathURL()
- scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
- new_url = request.prepath[-1] + "/"
- if query:
- new_url += "?" + query
- request.redirect(new_url)
- return ''
-
- data = self.content(request)
- if isinstance(data, unicode):
- data = data.encode("utf-8")
- request.setHeader("content-type", self.contentType)
- if request.method == "HEAD":
- request.setHeader("content-length", len(data))
- return ''
- return data
-
- def getStatus(self, request):
- return request.site.buildbot_service.getStatus()
- def getControl(self, request):
- return request.site.buildbot_service.getControl()
-
- def getChangemaster(self, request):
- return request.site.buildbot_service.getChangeSvc()
-
- def path_to_root(self, request):
- return path_to_root(request)
-
- def footer(self, s, req):
- # TODO: this stuff should be generated by a template of some sort
- projectURL = s.getProjectURL()
- projectName = s.getProjectName()
- data = '<hr /><div class="footer">\n'
-
- welcomeurl = self.path_to_root(req) + "index.html"
- data += '[<a href="%s">welcome</a>]\n' % welcomeurl
- data += "<br />\n"
-
- data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>'
- data += "-%s " % version
- if projectName:
- data += "working for the "
- if projectURL:
- data += "<a href=\"%s\">%s</a> project." % (projectURL,
- projectName)
- else:
- data += "%s project." % projectName
- data += "<br />\n"
- data += ("Page built: " +
- time.strftime("%a %d %b %Y %H:%M:%S",
- time.localtime(util.now()))
- + "\n")
- data += '</div>\n'
-
- return data
-
- def getTitle(self, request):
- return self.title
-
- def fillTemplate(self, template, request):
- s = request.site.buildbot_service
- values = s.template_values.copy()
- values['root'] = self.path_to_root(request)
- # e.g. to reference the top-level 'buildbot.css' page, use
- # "%(root)sbuildbot.css"
- values['title'] = self.getTitle(request)
- return template % values
-
- def content(self, request):
- s = request.site.buildbot_service
- data = ""
- data += self.fillTemplate(s.header, request)
- data += "<head>\n"
- for he in s.head_elements:
- data += " " + self.fillTemplate(he, request) + "\n"
- data += self.head(request)
- data += "</head>\n\n"
-
- data += '<body %s>\n' % " ".join(['%s="%s"' % (k,v)
- for (k,v) in s.body_attrs.items()])
- data += self.body(request)
- data += "</body>\n"
- data += self.fillTemplate(s.footer, request)
- return data
-
- def head(self, request):
- return ""
-
- def body(self, request):
- return "Dummy\n"
-
-class StaticHTML(HtmlResource):
- def __init__(self, body, title):
- HtmlResource.__init__(self)
- self.bodyHTML = body
- self.title = title
- def body(self, request):
- return self.bodyHTML
-
-MINUTE = 60
-HOUR = 60*MINUTE
-DAY = 24*HOUR
-WEEK = 7*DAY
-MONTH = 30*DAY
-
-def plural(word, words, num):
- if int(num) == 1:
- return "%d %s" % (num, word)
- else:
- return "%d %s" % (num, words)
-
-def abbreviate_age(age):
- if age <= 90:
- return "%s ago" % plural("second", "seconds", age)
- if age < 90*MINUTE:
- return "about %s ago" % plural("minute", "minutes", age / MINUTE)
- if age < DAY:
- return "about %s ago" % plural("hour", "hours", age / HOUR)
- if age < 2*WEEK:
- return "about %s ago" % plural("day", "days", age / DAY)
- if age < 2*MONTH:
- return "about %s ago" % plural("week", "weeks", age / WEEK)
- return "a long time ago"
-
-
-class OneLineMixin:
- LINE_TIME_FORMAT = "%b %d %H:%M"
-
- def get_line_values(self, req, build):
- '''
- Collect the data needed for each line display
- '''
- builder_name = build.getBuilder().getName()
- results = build.getResults()
- text = build.getText()
- try:
- rev = build.getProperty("got_revision")
- if rev is None:
- rev = "??"
- except KeyError:
- rev = "??"
- rev = str(rev)
- if len(rev) > 40:
- rev = "version is too-long"
- root = self.path_to_root(req)
- css_class = css_classes.get(results, "")
- values = {'class': css_class,
- 'builder_name': builder_name,
- 'buildnum': build.getNumber(),
- 'results': css_class,
- 'text': " ".join(build.getText()),
- 'buildurl': path_to_build(req, build),
- 'builderurl': path_to_builder(req, build.getBuilder()),
- 'rev': rev,
- 'time': time.strftime(self.LINE_TIME_FORMAT,
- time.localtime(build.getTimes()[0])),
- }
- return values
-
- def make_line(self, req, build, include_builder=True):
- '''
- Format and render a single line into HTML
- '''
- values = self.get_line_values(req, build)
- fmt_pieces = ['<font size="-1">(%(time)s)</font>',
- 'rev=[%(rev)s]',
- '<span class="%(class)s">%(results)s</span>',
- ]
- if include_builder:
- fmt_pieces.append('<a href="%(builderurl)s">%(builder_name)s</a>')
- fmt_pieces.append('<a href="%(buildurl)s">#%(buildnum)d</a>:')
- fmt_pieces.append('%(text)s')
- data = " ".join(fmt_pieces) % values
- return data
-
-def map_branches(branches):
- # when the query args say "trunk", present that to things like
- # IBuilderStatus.generateFinishedBuilds as None, since that's the
- # convention in use. But also include 'trunk', because some VC systems
- # refer to it that way. In the long run we should clean this up better,
- # maybe with Branch objects or something.
- if "trunk" in branches:
- return branches + [None]
- return branches
diff --git a/buildbot/buildbot/status/web/baseweb.py b/buildbot/buildbot/status/web/baseweb.py
deleted file mode 100644
index a963a9a..0000000
--- a/buildbot/buildbot/status/web/baseweb.py
+++ /dev/null
@@ -1,614 +0,0 @@
-
-import os, sys, urllib, weakref
-from itertools import count
-
-from zope.interface import implements
-from twisted.python import log
-from twisted.application import strports, service
-from twisted.web import server, distrib, static, html
-from twisted.spread import pb
-
-from buildbot.interfaces import IControl, IStatusReceiver
-
-from buildbot.status.web.base import HtmlResource, Box, \
- build_get_class, ICurrentBox, OneLineMixin, map_branches, \
- make_stop_form, make_force_build_form
-from buildbot.status.web.feeds import Rss20StatusResource, \
- Atom10StatusResource
-from buildbot.status.web.waterfall import WaterfallStatusResource
-from buildbot.status.web.grid import GridStatusResource
-from buildbot.status.web.changes import ChangesResource
-from buildbot.status.web.builder import BuildersResource
-from buildbot.status.web.slaves import BuildSlavesResource
-from buildbot.status.web.xmlrpc import XMLRPCServer
-from buildbot.status.web.about import AboutBuildbot
-
-# this class contains the status services (WebStatus and the older Waterfall)
-# which can be put in c['status']. It also contains some of the resources
-# that are attached to the WebStatus at various well-known URLs, which the
-# admin might wish to attach (using WebStatus.putChild) at other URLs.
-
-
-class LastBuild(HtmlResource):
- def body(self, request):
- return "missing\n"
-
-def getLastNBuilds(status, numbuilds, builders=[], branches=[]):
- """Return a list with the last few Builds, sorted by start time.
- builder_names=None means all builders
- """
-
- # TODO: this unsorts the list of builder names, ick
- builder_names = set(status.getBuilderNames())
- if builders:
- builder_names = builder_names.intersection(set(builders))
-
- # to make sure that we get everything, we must get 'numbuilds' builds
- # from *each* source, then sort by ending time, then trim to the last
- # 20. We could be more efficient, but it would require the same
- # gnarly code that the Waterfall uses to generate one event at a
- # time. TODO: factor that code out into some useful class.
- events = []
- for builder_name in builder_names:
- builder = status.getBuilder(builder_name)
- for build_number in count(1):
- if build_number > numbuilds:
- break # enough from this builder, move on to another
- build = builder.getBuild(-build_number)
- if not build:
- break # no more builds here, move on to the next builder
- #if not build.isFinished():
- # continue
- (build_start, build_end) = build.getTimes()
- event = (build_start, builder_name, build)
- events.append(event)
- def _sorter(a, b):
- return cmp( a[:2], b[:2] )
- events.sort(_sorter)
- # now only return the actual build, and only return some of them
- return [e[2] for e in events[-numbuilds:]]
-
-
-# /one_line_per_build
-# accepts builder=, branch=, numbuilds=
-class OneLinePerBuild(HtmlResource, OneLineMixin):
- """This shows one line per build, combining all builders together. Useful
- query arguments:
-
- numbuilds=: how many lines to display
- builder=: show only builds for this builder. Multiple builder= arguments
- can be used to see builds from any builder in the set.
- """
-
- title = "Recent Builds"
-
- def __init__(self, numbuilds=20):
- HtmlResource.__init__(self)
- self.numbuilds = numbuilds
-
- def getChild(self, path, req):
- status = self.getStatus(req)
- builder = status.getBuilder(path)
- return OneLinePerBuildOneBuilder(builder)
-
- def body(self, req):
- status = self.getStatus(req)
- control = self.getControl(req)
- numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0])
- builders = req.args.get("builder", [])
- branches = [b for b in req.args.get("branch", []) if b]
-
- g = status.generateFinishedBuilds(builders, map_branches(branches),
- numbuilds)
-
- data = ""
-
- # really this is "up to %d builds"
- data += "<h1>Last %d finished builds: %s</h1>\n" % \
- (numbuilds, ", ".join(branches))
- if builders:
- data += ("<p>of builders: %s</p>\n" % (", ".join(builders)))
- data += "<ul>\n"
- got = 0
- building = False
- online = 0
- for build in g:
- got += 1
- data += " <li>" + self.make_line(req, build) + "</li>\n"
- builder_status = build.getBuilder().getState()[0]
- if builder_status == "building":
- building = True
- online += 1
- elif builder_status != "offline":
- online += 1
- if not got:
- data += " <li>No matching builds found</li>\n"
- data += "</ul>\n"
-
- if control is not None:
- if building:
- stopURL = "builders/_all/stop"
- data += make_stop_form(stopURL, True, "Builds")
- if online:
- forceURL = "builders/_all/force"
- data += make_force_build_form(forceURL, True)
-
- return data
-
-
-
-# /one_line_per_build/$BUILDERNAME
-# accepts branch=, numbuilds=
-
-class OneLinePerBuildOneBuilder(HtmlResource, OneLineMixin):
- def __init__(self, builder, numbuilds=20):
- HtmlResource.__init__(self)
- self.builder = builder
- self.builder_name = builder.getName()
- self.numbuilds = numbuilds
- self.title = "Recent Builds of %s" % self.builder_name
-
- def body(self, req):
- status = self.getStatus(req)
- numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0])
- branches = [b for b in req.args.get("branch", []) if b]
-
- # walk backwards through all builds of a single builder
- g = self.builder.generateFinishedBuilds(map_branches(branches),
- numbuilds)
-
- data = ""
- data += ("<h1>Last %d builds of builder %s: %s</h1>\n" %
- (numbuilds, self.builder_name, ", ".join(branches)))
- data += "<ul>\n"
- got = 0
- for build in g:
- got += 1
- data += " <li>" + self.make_line(req, build) + "</li>\n"
- if not got:
- data += " <li>No matching builds found</li>\n"
- data += "</ul>\n"
-
- return data
-
-# /one_box_per_builder
-# accepts builder=, branch=
-class OneBoxPerBuilder(HtmlResource):
- """This shows a narrow table with one row per builder. The leftmost column
- contains the builder name. The next column contains the results of the
- most recent build. The right-hand column shows the builder's current
- activity.
-
- builder=: show only builds for this builder. Multiple builder= arguments
- can be used to see builds from any builder in the set.
- """
-
- title = "Latest Build"
-
- def body(self, req):
- status = self.getStatus(req)
- control = self.getControl(req)
-
- builders = req.args.get("builder", status.getBuilderNames())
- branches = [b for b in req.args.get("branch", []) if b]
-
- data = ""
-
- data += "<h2>Latest builds: %s</h2>\n" % ", ".join(branches)
- data += "<table>\n"
-
- building = False
- online = 0
- base_builders_url = self.path_to_root(req) + "builders/"
- for bn in builders:
- base_builder_url = base_builders_url + urllib.quote(bn, safe='')
- builder = status.getBuilder(bn)
- data += "<tr>\n"
- data += '<td class="box"><a href="%s">%s</a></td>\n' \
- % (base_builder_url, html.escape(bn))
- builds = list(builder.generateFinishedBuilds(map_branches(branches),
- num_builds=1))
- if builds:
- b = builds[0]
- url = (base_builder_url + "/builds/%d" % b.getNumber())
- try:
- label = b.getProperty("got_revision")
- except KeyError:
- label = None
- if not label or len(str(label)) > 20:
- label = "#%d" % b.getNumber()
- text = ['<a href="%s">%s</a>' % (url, label)]
- text.extend(b.getText())
- box = Box(text,
- class_="LastBuild box %s" % build_get_class(b))
- data += box.td(align="center")
- else:
- data += '<td class="LastBuild box" >no build</td>\n'
- current_box = ICurrentBox(builder).getBox(status)
- data += current_box.td(align="center")
-
- builder_status = builder.getState()[0]
- if builder_status == "building":
- building = True
- online += 1
- elif builder_status != "offline":
- online += 1
-
- data += "</table>\n"
-
- if control is not None:
- if building:
- stopURL = "builders/_all/stop"
- data += make_stop_form(stopURL, True, "Builds")
- if online:
- forceURL = "builders/_all/force"
- data += make_force_build_form(forceURL, True)
-
- return data
-
-
-
-HEADER = '''
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- lang="en"
- xml:lang="en">
-'''
-
-HEAD_ELEMENTS = [
- '<title>%(title)s</title>',
- '<link href="%(root)sbuildbot.css" rel="stylesheet" type="text/css" />',
- ]
-BODY_ATTRS = {
- 'vlink': "#800080",
- }
-
-FOOTER = '''
-</html>
-'''
-
-
-class WebStatus(service.MultiService):
- implements(IStatusReceiver)
- # TODO: IStatusReceiver is really about things which subscribe to hear
- # about buildbot events. We need a different interface (perhaps a parent
- # of IStatusReceiver) for status targets that don't subscribe, like the
- # WebStatus class. buildbot.master.BuildMaster.loadConfig:737 asserts
- # that everything in c['status'] provides IStatusReceiver, but really it
- # should check that they provide IStatusTarget instead.
-
- """
- The webserver provided by this class has the following resources:
-
- /waterfall : the big time-oriented 'waterfall' display, with links
- to individual changes, builders, builds, steps, and logs.
- A number of query-arguments can be added to influence
- the display.
- /rss : a rss feed summarizing all failed builds. The same
- query-arguments used by 'waterfall' can be added to
- influence the feed output.
- /atom : an atom feed summarizing all failed builds. The same
- query-arguments used by 'waterfall' can be added to
- influence the feed output.
- /grid : another summary display that shows a grid of builds, with
- sourcestamps on the x axis, and builders on the y. Query
- arguments similar to those for the waterfall can be added.
- /builders/BUILDERNAME: a page summarizing the builder. This includes
- references to the Schedulers that feed it,
- any builds currently in the queue, which
- buildslaves are designated or attached, and a
- summary of the build process it uses.
- /builders/BUILDERNAME/builds/NUM: a page describing a single Build
- /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step
- /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog
- /builders/BUILDERNAME/builds/NUM/tests : summarize test results
- /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test
- /builders/_all/{force,stop}: force a build/stop building on all builders.
- /changes : summarize all ChangeSources
- /changes/CHANGENUM: a page describing a single Change
- /schedulers/SCHEDULERNAME: a page describing a Scheduler, including
- a description of its behavior, a list of the
- Builders it triggers, and list of the Changes
- that are queued awaiting the tree-stable
- timer, and controls to accelerate the timer.
- /buildslaves : list all BuildSlaves
- /buildslaves/SLAVENAME : describe a single BuildSlave
- /one_line_per_build : summarize the last few builds, one line each
- /one_line_per_build/BUILDERNAME : same, but only for a single builder
- /one_box_per_builder : show the latest build and current activity
- /about : describe this buildmaster (Buildbot and support library versions)
- /xmlrpc : (not yet implemented) an XMLRPC server with build status
-
-
- All URLs for pages which are not defined here are used to look
- for files in PUBLIC_HTML, which defaults to BASEDIR/public_html.
- This means that /robots.txt or /buildbot.css or /favicon.ico can
- be placed in that directory.
-
- If an index file (index.html, index.htm, or index, in that order) is
- present in PUBLIC_HTML, it will be used for the root resource. If not,
- the default behavior is to put a redirection to the /waterfall page.
-
- All of the resources provided by this service use relative URLs to reach
- each other. The only absolute links are the c['projectURL'] links at the
- top and bottom of the page, and the buildbot home-page link at the
- bottom.
-
- This webserver defines class attributes on elements so they can be styled
- with CSS stylesheets. All pages pull in PUBLIC_HTML/buildbot.css, and you
- can cause additional stylesheets to be loaded by adding a suitable <link>
- to the WebStatus instance's .head_elements attribute.
-
- Buildbot uses some generic classes to identify the type of object, and
- some more specific classes for the various kinds of those types. It does
- this by specifying both in the class attributes where applicable,
- separated by a space. It is important that in your CSS you declare the
- more generic class styles above the more specific ones. For example,
- first define a style for .Event, and below that for .SUCCESS
-
- The following CSS class names are used:
- - Activity, Event, BuildStep, LastBuild: general classes
- - waiting, interlocked, building, offline, idle: Activity states
- - start, running, success, failure, warnings, skipped, exception:
- LastBuild and BuildStep states
- - Change: box with change
- - Builder: box for builder name (at top)
- - Project
- - Time
-
- """
-
- # we are not a ComparableMixin, and therefore the webserver will be
- # rebuilt every time we reconfig. This is because WebStatus.putChild()
- # makes it too difficult to tell whether two instances are the same or
- # not (we'd have to do a recursive traversal of all children to discover
- # all the changes).
-
- def __init__(self, http_port=None, distrib_port=None, allowForce=False,
- public_html="public_html", site=None):
- """Run a web server that provides Buildbot status.
-
- @type http_port: int or L{twisted.application.strports} string
- @param http_port: a strports specification describing which port the
- buildbot should use for its web server, with the
- Waterfall display as the root page. For backwards
- compatibility this can also be an int. Use
- 'tcp:8000' to listen on that port, or
- 'tcp:12345:interface=127.0.0.1' if you only want
- local processes to connect to it (perhaps because
- you are using an HTTP reverse proxy to make the
- buildbot available to the outside world, and do not
- want to make the raw port visible).
-
- @type distrib_port: int or L{twisted.application.strports} string
- @param distrib_port: Use this if you want to publish the Waterfall
- page using web.distrib instead. The most common
- case is to provide a string that is an absolute
- pathname to the unix socket on which the
- publisher should listen
- (C{os.path.expanduser(~/.twistd-web-pb)} will
- match the default settings of a standard
- twisted.web 'personal web server'). Another
- possibility is to pass an integer, which means
- the publisher should listen on a TCP socket,
- allowing the web server to be on a different
- machine entirely. Both forms are provided for
- backwards compatibility; the preferred form is a
- strports specification like
- 'unix:/home/buildbot/.twistd-web-pb'. Providing
- a non-absolute pathname will probably confuse
- the strports parser.
-
- @param allowForce: boolean, if True then the webserver will allow
- visitors to trigger and cancel builds
-
- @param public_html: the path to the public_html directory for this display,
- either absolute or relative to the basedir. The default
- is 'public_html', which selects BASEDIR/public_html.
-
- @type site: None or L{twisted.web.server.Site}
- @param site: Use this if you want to define your own object instead of
- using the default.`
- """
-
- service.MultiService.__init__(self)
- if type(http_port) is int:
- http_port = "tcp:%d" % http_port
- self.http_port = http_port
- if distrib_port is not None:
- if type(distrib_port) is int:
- distrib_port = "tcp:%d" % distrib_port
- if distrib_port[0] in "/~.": # pathnames
- distrib_port = "unix:%s" % distrib_port
- self.distrib_port = distrib_port
- self.allowForce = allowForce
- self.public_html = public_html
-
- # If we were given a site object, go ahead and use it.
- if site:
- self.site = site
- else:
- # this will be replaced once we've been attached to a parent (and
- # thus have a basedir and can reference BASEDIR)
- root = static.Data("placeholder", "text/plain")
- self.site = server.Site(root)
- self.childrenToBeAdded = {}
-
- self.setupUsualPages()
-
- # the following items are accessed by HtmlResource when it renders
- # each page.
- self.site.buildbot_service = self
- self.header = HEADER
- self.head_elements = HEAD_ELEMENTS[:]
- self.body_attrs = BODY_ATTRS.copy()
- self.footer = FOOTER
- self.template_values = {}
-
- # keep track of cached connections so we can break them when we shut
- # down. See ticket #102 for more details.
- self.channels = weakref.WeakKeyDictionary()
-
- if self.http_port is not None:
- s = strports.service(self.http_port, self.site)
- s.setServiceParent(self)
- if self.distrib_port is not None:
- f = pb.PBServerFactory(distrib.ResourcePublisher(self.site))
- s = strports.service(self.distrib_port, f)
- s.setServiceParent(self)
-
- def setupUsualPages(self):
- #self.putChild("", IndexOrWaterfallRedirection())
- self.putChild("waterfall", WaterfallStatusResource())
- self.putChild("grid", GridStatusResource())
- self.putChild("builders", BuildersResource()) # has builds/steps/logs
- self.putChild("changes", ChangesResource())
- self.putChild("buildslaves", BuildSlavesResource())
- #self.putChild("schedulers", SchedulersResource())
- self.putChild("one_line_per_build", OneLinePerBuild())
- self.putChild("one_box_per_builder", OneBoxPerBuilder())
- self.putChild("xmlrpc", XMLRPCServer())
- self.putChild("about", AboutBuildbot())
-
- def __repr__(self):
- if self.http_port is None:
- return "<WebStatus on path %s at %s>" % (self.distrib_port,
- hex(id(self)))
- if self.distrib_port is None:
- return "<WebStatus on port %s at %s>" % (self.http_port,
- hex(id(self)))
- return ("<WebStatus on port %s and path %s at %s>" %
- (self.http_port, self.distrib_port, hex(id(self))))
-
- def setServiceParent(self, parent):
- service.MultiService.setServiceParent(self, parent)
-
- # this class keeps a *separate* link to the buildmaster, rather than
- # just using self.parent, so that when we are "disowned" (and thus
- # parent=None), any remaining HTTP clients of this WebStatus will still
- # be able to get reasonable results.
- self.master = parent
-
- self.setupSite()
-
- def setupSite(self):
- # this is responsible for creating the root resource. It isn't done
- # at __init__ time because we need to reference the parent's basedir.
- htmldir = os.path.abspath(os.path.join(self.master.basedir, self.public_html))
- if os.path.isdir(htmldir):
- log.msg("WebStatus using (%s)" % htmldir)
- else:
- log.msg("WebStatus: warning: %s is missing. Do you need to run"
- " 'buildbot upgrade-master' on this buildmaster?" % htmldir)
- # all static pages will get a 404 until upgrade-master is used to
- # populate this directory. Create the directory, though, since
- # otherwise we get internal server errors instead of 404s.
- os.mkdir(htmldir)
- root = static.File(htmldir)
-
- for name, child_resource in self.childrenToBeAdded.iteritems():
- root.putChild(name, child_resource)
-
- status = self.getStatus()
- root.putChild("rss", Rss20StatusResource(status))
- root.putChild("atom", Atom10StatusResource(status))
-
- self.site.resource = root
-
- def putChild(self, name, child_resource):
- """This behaves a lot like root.putChild() . """
- self.childrenToBeAdded[name] = child_resource
-
- def registerChannel(self, channel):
- self.channels[channel] = 1 # weakrefs
-
- def stopService(self):
- for channel in self.channels:
- try:
- channel.transport.loseConnection()
- except:
- log.msg("WebStatus.stopService: error while disconnecting"
- " leftover clients")
- log.err()
- return service.MultiService.stopService(self)
-
- def getStatus(self):
- return self.master.getStatus()
-
- def getControl(self):
- if self.allowForce:
- return IControl(self.master)
- return None
-
- def getChangeSvc(self):
- return self.master.change_svc
- def getPortnum(self):
- # this is for the benefit of unit tests
- s = list(self)[0]
- return s._port.getHost().port
-
-# resources can get access to the IStatus by calling
-# request.site.buildbot_service.getStatus()
-
-# this is the compatibility class for the old waterfall. It is exactly like a
-# regular WebStatus except that the root resource (e.g. http://buildbot.net/)
-# always redirects to a WaterfallStatusResource, and the old arguments are
-# mapped into the new resource-tree approach. In the normal WebStatus, the
-# root resource either redirects the browser to /waterfall or serves
-# PUBLIC_HTML/index.html, and favicon/robots.txt are provided by
-# having the admin write actual files into PUBLIC_HTML/ .
-
-# note: we don't use a util.Redirect here because HTTP requires that the
-# Location: header provide an absolute URI, and it's non-trivial to figure
-# out our absolute URI from here.
-
-class Waterfall(WebStatus):
-
- if hasattr(sys, "frozen"):
- # all 'data' files are in the directory of our executable
- here = os.path.dirname(sys.executable)
- buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png"))
- buildbot_css = os.path.abspath(os.path.join(here, "classic.css"))
- else:
- # running from source
- # the icon is sibpath(__file__, "../buildbot.png") . This is for
- # portability.
- up = os.path.dirname
- buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))),
- "buildbot.png"))
- buildbot_css = os.path.abspath(os.path.join(up(__file__),
- "classic.css"))
-
- compare_attrs = ["http_port", "distrib_port", "allowForce",
- "categories", "css", "favicon", "robots_txt"]
-
- def __init__(self, http_port=None, distrib_port=None, allowForce=True,
- categories=None, css=buildbot_css, favicon=buildbot_icon,
- robots_txt=None):
- import warnings
- m = ("buildbot.status.html.Waterfall is deprecated as of 0.7.6 "
- "and will be removed from a future release. "
- "Please use html.WebStatus instead.")
- warnings.warn(m, DeprecationWarning)
-
- WebStatus.__init__(self, http_port, distrib_port, allowForce)
- self.css = css
- if css:
- if os.path.exists(os.path.join("public_html", "buildbot.css")):
- # they've upgraded, so defer to that copy instead
- pass
- else:
- data = open(css, "rb").read()
- self.putChild("buildbot.css", static.Data(data, "text/plain"))
- self.favicon = favicon
- self.robots_txt = robots_txt
- if favicon:
- data = open(favicon, "rb").read()
- self.putChild("favicon.ico", static.Data(data, "image/x-icon"))
- if robots_txt:
- data = open(robots_txt, "rb").read()
- self.putChild("robots.txt", static.Data(data, "text/plain"))
- self.putChild("", WaterfallStatusResource(categories))
diff --git a/buildbot/buildbot/status/web/build.py b/buildbot/buildbot/status/web/build.py
deleted file mode 100644
index 5d01358..0000000
--- a/buildbot/buildbot/status/web/build.py
+++ /dev/null
@@ -1,302 +0,0 @@
-
-from twisted.web import html
-from twisted.web.util import Redirect, DeferredResource
-from twisted.internet import defer, reactor
-
-import urllib, time
-from twisted.python import log
-from buildbot.status.web.base import HtmlResource, make_row, make_stop_form, \
- css_classes, path_to_builder, path_to_slave
-
-from buildbot.status.web.tests import TestsResource
-from buildbot.status.web.step import StepsResource
-from buildbot import version, util
-
-# /builders/$builder/builds/$buildnum
-class StatusResourceBuild(HtmlResource):
- addSlash = True
-
- def __init__(self, build_status, build_control, builder_control):
- HtmlResource.__init__(self)
- self.build_status = build_status
- self.build_control = build_control
- self.builder_control = builder_control
-
- def getTitle(self, request):
- return ("Buildbot: %s Build #%d" %
- (html.escape(self.build_status.getBuilder().getName()),
- self.build_status.getNumber()))
-
- def body(self, req):
- b = self.build_status
- status = self.getStatus(req)
- projectURL = status.getProjectURL()
- projectName = status.getProjectName()
- data = ('<div class="title"><a href="%s">%s</a></div>\n'
- % (self.path_to_root(req), projectName))
- builder_name = b.getBuilder().getName()
- data += ("<h1><a href=\"%s\">Builder %s</a>: Build #%d</h1>\n"
- % (path_to_builder(req, b.getBuilder()),
- builder_name, b.getNumber()))
-
- if not b.isFinished():
- data += "<h2>Build In Progress</h2>"
- when = b.getETA()
- if when is not None:
- when_time = time.strftime("%H:%M:%S",
- time.localtime(time.time() + when))
- data += "<div>ETA %ds (%s)</div>\n" % (when, when_time)
-
- if self.build_control is not None:
- stopURL = urllib.quote(req.childLink("stop"))
- data += make_stop_form(stopURL)
-
- if b.isFinished():
- results = b.getResults()
- data += "<h2>Results:</h2>\n"
- text = " ".join(b.getText())
- data += '<span class="%s">%s</span>\n' % (css_classes[results],
- text)
- if b.getTestResults():
- url = req.childLink("tests")
- data += "<h3><a href=\"%s\">test results</a></h3>\n" % url
-
- ss = b.getSourceStamp()
- data += "<h2>SourceStamp:</h2>\n"
- data += " <ul>\n"
- if ss.branch:
- data += " <li>Branch: %s</li>\n" % html.escape(ss.branch)
- if ss.revision:
- data += " <li>Revision: %s</li>\n" % html.escape(str(ss.revision))
- if ss.patch:
- data += " <li>Patch: YES</li>\n" # TODO: provide link to .diff
- if ss.changes:
- data += " <li>Changes: see below</li>\n"
- if (ss.branch is None and ss.revision is None and ss.patch is None
- and not ss.changes):
- data += " <li>build of most recent revision</li>\n"
- got_revision = None
- try:
- got_revision = b.getProperty("got_revision")
- except KeyError:
- pass
- if got_revision:
- got_revision = str(got_revision)
- if len(got_revision) > 40:
- got_revision = "[revision string too long]"
- data += " <li>Got Revision: %s</li>\n" % got_revision
- data += " </ul>\n"
-
- # TODO: turn this into a table, or some other sort of definition-list
- # that doesn't take up quite so much vertical space
- try:
- slaveurl = path_to_slave(req, status.getSlave(b.getSlavename()))
- data += "<h2>Buildslave:</h2>\n <a href=\"%s\">%s</a>\n" % (html.escape(slaveurl), html.escape(b.getSlavename()))
- except KeyError:
- data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename())
- data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason())
-
- data += "<h2>Steps and Logfiles:</h2>\n"
- # TODO:
-# urls = self.original.getURLs()
-# ex_url_class = "BuildStep external"
-# for name, target in urls.items():
-# text.append('[<a href="%s" class="%s">%s</a>]' %
-# (target, ex_url_class, html.escape(name)))
- if b.getLogs():
- data += "<ol>\n"
- for s in b.getSteps():
- name = s.getName()
- data += (" <li><a href=\"%s\">%s</a> [%s]\n"
- % (req.childLink("steps/%s" % urllib.quote(name)),
- name,
- " ".join(s.getText())))
- if s.getLogs():
- data += " <ol>\n"
- for logfile in s.getLogs():
- logname = logfile.getName()
- logurl = req.childLink("steps/%s/logs/%s" %
- (urllib.quote(name),
- urllib.quote(logname)))
- data += (" <li><a href=\"%s\">%s</a></li>\n" %
- (logurl, logfile.getName()))
- data += " </ol>\n"
- data += " </li>\n"
- data += "</ol>\n"
-
- data += "<h2>Build Properties:</h2>\n"
- data += "<table><tr><th valign=\"left\">Name</th><th valign=\"left\">Value</th><th valign=\"left\">Source</th></tr>\n"
- for name, value, source in b.getProperties().asList():
- value = str(value)
- if len(value) > 500:
- value = value[:500] + " .. [property value too long]"
- data += "<tr>"
- data += "<td>%s</td>" % html.escape(name)
- data += "<td>%s</td>" % html.escape(value)
- data += "<td>%s</td>" % html.escape(source)
- data += "</tr>\n"
- data += "</table>"
-
- data += "<h2>Blamelist:</h2>\n"
- if list(b.getResponsibleUsers()):
- data += " <ol>\n"
- for who in b.getResponsibleUsers():
- data += " <li>%s</li>\n" % html.escape(who)
- data += " </ol>\n"
- else:
- data += "<div>no responsible users</div>\n"
-
-
- (start, end) = b.getTimes()
- data += "<h2>Timing</h2>\n"
- data += "<table>\n"
- data += "<tr><td>Start</td><td>%s</td></tr>\n" % time.ctime(start)
- if end:
- data += "<tr><td>End</td><td>%s</td></tr>\n" % time.ctime(end)
- data += "<tr><td>Elapsed</td><td>%s</td></tr>\n" % util.formatInterval(end - start)
- data += "</table>\n"
-
- if ss.changes:
- data += "<h2>All Changes</h2>\n"
- data += "<ol>\n"
- for c in ss.changes:
- data += "<li>" + c.asHTML() + "</li>\n"
- data += "</ol>\n"
- #data += html.PRE(b.changesText()) # TODO
-
- if b.isFinished() and self.builder_control is not None:
- data += "<h3>Resubmit Build:</h3>\n"
- # can we rebuild it exactly?
- exactly = (ss.revision is not None) or b.getChanges()
- if exactly:
- data += ("<p>This tree was built from a specific set of \n"
- "source files, and can be rebuilt exactly</p>\n")
- else:
- data += ("<p>This tree was built from the most recent "
- "revision")
- if ss.branch:
- data += " (along some branch)"
- data += (" and thus it might not be possible to rebuild it \n"
- "exactly. Any changes that have been committed \n"
- "after this build was started <b>will</b> be \n"
- "included in a rebuild.</p>\n")
- rebuildURL = urllib.quote(req.childLink("rebuild"))
- data += ('<form action="%s" class="command rebuild">\n'
- % rebuildURL)
- data += make_row("Your name:",
- "<input type='text' name='username' />")
- data += make_row("Reason for re-running build:",
- "<input type='text' name='comments' />")
- data += '<input type="submit" value="Rebuild" />\n'
- data += '</form>\n'
-
- # TODO: this stuff should be generated by a template of some sort
- data += '<hr /><div class="footer">\n'
-
- welcomeurl = self.path_to_root(req) + "index.html"
- data += '[<a href="%s">welcome</a>]\n' % welcomeurl
- data += "<br />\n"
-
- data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>'
- data += "-%s " % version
- if projectName:
- data += "working for the "
- if projectURL:
- data += "<a href=\"%s\">%s</a> project." % (projectURL,
- projectName)
- else:
- data += "%s project." % projectName
- data += "<br />\n"
- data += ("Page built: " +
- time.strftime("%a %d %b %Y %H:%M:%S",
- time.localtime(util.now()))
- + "\n")
- data += '</div>\n'
-
- return data
-
- def stop(self, req):
- b = self.build_status
- c = self.build_control
- log.msg("web stopBuild of build %s:%s" % \
- (b.getBuilder().getName(), b.getNumber()))
- name = req.args.get("username", ["<unknown>"])[0]
- comments = req.args.get("comments", ["<no reason specified>"])[0]
- reason = ("The web-page 'stop build' button was pressed by "
- "'%s': %s\n" % (name, comments))
- c.stopBuild(reason)
- # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
- # we want to go to: http://localhost:8080/svn-hello
- r = Redirect("../..")
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def rebuild(self, req):
- b = self.build_status
- bc = self.builder_control
- builder_name = b.getBuilder().getName()
- log.msg("web rebuild of build %s:%s" % (builder_name, b.getNumber()))
- name = req.args.get("username", ["<unknown>"])[0]
- comments = req.args.get("comments", ["<no reason specified>"])[0]
- reason = ("The web-page 'rebuild' button was pressed by "
- "'%s': %s\n" % (name, comments))
- if not bc or not b.isFinished():
- log.msg("could not rebuild: bc=%s, isFinished=%s"
- % (bc, b.isFinished()))
- # TODO: indicate an error
- else:
- bc.resubmitBuild(b, reason)
- # we're at
- # http://localhost:8080/builders/NAME/builds/5/rebuild?[args]
- # Where should we send them?
- #
- # Ideally it would be to the per-build page that they just started,
- # but we don't know the build number for it yet (besides, it might
- # have to wait for a current build to finish). The next-most
- # preferred place is somewhere that the user can see tangible
- # evidence of their build starting (or to see the reason that it
- # didn't start). This should be the Builder page.
- r = Redirect("../..") # the Builder's page
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def getChild(self, path, req):
- if path == "stop":
- return self.stop(req)
- if path == "rebuild":
- return self.rebuild(req)
- if path == "steps":
- return StepsResource(self.build_status)
- if path == "tests":
- return TestsResource(self.build_status)
-
- return HtmlResource.getChild(self, path, req)
-
-# /builders/$builder/builds
-class BuildsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, builder_status, builder_control):
- HtmlResource.__init__(self)
- self.builder_status = builder_status
- self.builder_control = builder_control
-
- def getChild(self, path, req):
- try:
- num = int(path)
- except ValueError:
- num = None
- if num is not None:
- build_status = self.builder_status.getBuild(num)
- if build_status:
- if self.builder_control:
- build_control = self.builder_control.getBuild(num)
- else:
- build_control = None
- return StatusResourceBuild(build_status, build_control,
- self.builder_control)
-
- return HtmlResource.getChild(self, path, req)
-
diff --git a/buildbot/buildbot/status/web/builder.py b/buildbot/buildbot/status/web/builder.py
deleted file mode 100644
index 35f65e9..0000000
--- a/buildbot/buildbot/status/web/builder.py
+++ /dev/null
@@ -1,312 +0,0 @@
-
-from twisted.web.error import NoResource
-from twisted.web import html, static
-from twisted.web.util import Redirect
-
-import re, urllib, time
-from twisted.python import log
-from buildbot import interfaces
-from buildbot.status.web.base import HtmlResource, make_row, \
- make_force_build_form, OneLineMixin, path_to_build, path_to_slave, path_to_builder
-from buildbot.process.base import BuildRequest
-from buildbot.sourcestamp import SourceStamp
-
-from buildbot.status.web.build import BuildsResource, StatusResourceBuild
-
-# /builders/$builder
-class StatusResourceBuilder(HtmlResource, OneLineMixin):
- addSlash = True
-
- def __init__(self, builder_status, builder_control):
- HtmlResource.__init__(self)
- self.builder_status = builder_status
- self.builder_control = builder_control
-
- def getTitle(self, request):
- return "Buildbot: %s" % html.escape(self.builder_status.getName())
-
- def build_line(self, build, req):
- buildnum = build.getNumber()
- buildurl = path_to_build(req, build)
- data = '<a href="%s">#%d</a> ' % (buildurl, buildnum)
-
- when = build.getETA()
- if when is not None:
- when_time = time.strftime("%H:%M:%S",
- time.localtime(time.time() + when))
- data += "ETA %ds (%s) " % (when, when_time)
- step = build.getCurrentStep()
- if step:
- data += "[%s]" % step.getName()
- else:
- data += "[waiting for Lock]"
- # TODO: is this necessarily the case?
-
- if self.builder_control is not None:
- stopURL = path_to_build(req, build) + '/stop'
- data += '''
-<form action="%s" class="command stopbuild" style="display:inline">
- <input type="submit" value="Stop Build" />
-</form>''' % stopURL
- return data
-
- def body(self, req):
- b = self.builder_status
- control = self.builder_control
- status = self.getStatus(req)
-
- slaves = b.getSlaves()
- connected_slaves = [s for s in slaves if s.isConnected()]
-
- projectName = status.getProjectName()
-
- data = '<a href="%s">%s</a>\n' % (self.path_to_root(req), projectName)
-
- data += "<h1>Builder: %s</h1>\n" % html.escape(b.getName())
-
- # the first section shows builds which are currently running, if any.
-
- current = b.getCurrentBuilds()
- if current:
- data += "<h2>Currently Building:</h2>\n"
- data += "<ul>\n"
- for build in current:
- data += " <li>" + self.build_line(build, req) + "</li>\n"
- data += "</ul>\n"
- else:
- data += "<h2>no current builds</h2>\n"
-
- # Then a section with the last 5 builds, with the most recent build
- # distinguished from the rest.
-
- data += "<h2>Recent Builds:</h2>\n"
- data += "<ul>\n"
- for i,build in enumerate(b.generateFinishedBuilds(num_builds=5)):
- data += " <li>" + self.make_line(req, build, False) + "</li>\n"
- if i == 0:
- data += "<br />\n" # separator
- # TODO: or empty list?
- data += "</ul>\n"
-
-
- data += "<h2>Buildslaves:</h2>\n"
- data += "<ol>\n"
- for slave in slaves:
- slaveurl = path_to_slave(req, slave)
- data += "<li><b><a href=\"%s\">%s</a></b>: " % (html.escape(slaveurl), html.escape(slave.getName()))
- if slave.isConnected():
- data += "CONNECTED\n"
- if slave.getAdmin():
- data += make_row("Admin:", html.escape(slave.getAdmin()))
- if slave.getHost():
- data += "<span class='label'>Host info:</span>\n"
- data += html.PRE(slave.getHost())
- else:
- data += ("NOT CONNECTED\n")
- data += "</li>\n"
- data += "</ol>\n"
-
- if control is not None and connected_slaves:
- forceURL = path_to_builder(req, b) + '/force'
- data += make_force_build_form(forceURL)
- elif control is not None:
- data += """
- <p>All buildslaves appear to be offline, so it's not possible
- to force this build to execute at this time.</p>
- """
-
- if control is not None:
- pingURL = path_to_builder(req, b) + '/ping'
- data += """
- <form action="%s" class='command pingbuilder'>
- <p>To ping the buildslave(s), push the 'Ping' button</p>
-
- <input type="submit" value="Ping Builder" />
- </form>
- """ % pingURL
-
- data += self.footer(status, req)
-
- return data
-
- def force(self, req):
- """
-
- Custom properties can be passed from the web form. To do
- this, subclass this class, overriding the force() method. You
- can then determine the properties (usually from form values,
- by inspecting req.args), then pass them to this superclass
- force method.
-
- """
- name = req.args.get("username", ["<unknown>"])[0]
- reason = req.args.get("comments", ["<no reason specified>"])[0]
- branch = req.args.get("branch", [""])[0]
- revision = req.args.get("revision", [""])[0]
-
- r = "The web-page 'force build' button was pressed by '%s': %s\n" \
- % (name, reason)
- log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
- % (self.builder_status.getName(), branch, revision))
-
- if not self.builder_control:
- # TODO: tell the web user that their request was denied
- log.msg("but builder control is disabled")
- return Redirect("..")
-
- # keep weird stuff out of the branch and revision strings. TODO:
- # centralize this somewhere.
- if not re.match(r'^[\w\.\-\/]*$', branch):
- log.msg("bad branch '%s'" % branch)
- return Redirect("..")
- if not re.match(r'^[\w\.\-\/]*$', revision):
- log.msg("bad revision '%s'" % revision)
- return Redirect("..")
- if not branch:
- branch = None
- if not revision:
- revision = None
-
- # TODO: if we can authenticate that a particular User pushed the
- # button, use their name instead of None, so they'll be informed of
- # the results.
- s = SourceStamp(branch=branch, revision=revision)
- req = BuildRequest(r, s, builderName=self.builder_status.getName())
- try:
- self.builder_control.requestBuildSoon(req)
- except interfaces.NoSlaveError:
- # TODO: tell the web user that their request could not be
- # honored
- pass
- # send the user back to the builder page
- return Redirect(".")
-
- def ping(self, req):
- log.msg("web ping of builder '%s'" % self.builder_status.getName())
- self.builder_control.ping() # TODO: there ought to be an ISlaveControl
- # send the user back to the builder page
- return Redirect(".")
-
- def getChild(self, path, req):
- if path == "force":
- return self.force(req)
- if path == "ping":
- return self.ping(req)
- if path == "events":
- num = req.postpath.pop(0)
- req.prepath.append(num)
- num = int(num)
- # TODO: is this dead code? .statusbag doesn't exist,right?
- log.msg("getChild['path']: %s" % req.uri)
- return NoResource("events are unavailable until code gets fixed")
- filename = req.postpath.pop(0)
- req.prepath.append(filename)
- e = self.builder_status.getEventNumbered(num)
- if not e:
- return NoResource("No such event '%d'" % num)
- file = e.files.get(filename, None)
- if file == None:
- return NoResource("No such file '%s'" % filename)
- if type(file) == type(""):
- if file[:6] in ("<HTML>", "<html>"):
- return static.Data(file, "text/html")
- return static.Data(file, "text/plain")
- return file
- if path == "builds":
- return BuildsResource(self.builder_status, self.builder_control)
-
- return HtmlResource.getChild(self, path, req)
-
-
-# /builders/_all
-class StatusResourceAllBuilders(HtmlResource, OneLineMixin):
-
- def __init__(self, status, control):
- HtmlResource.__init__(self)
- self.status = status
- self.control = control
-
- def getChild(self, path, req):
- if path == "force":
- return self.force(req)
- if path == "stop":
- return self.stop(req)
-
- return HtmlResource.getChild(self, path, req)
-
- def force(self, req):
- for bname in self.status.getBuilderNames():
- builder_status = self.status.getBuilder(bname)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(bname)
- build = StatusResourceBuilder(builder_status, builder_control)
- build.force(req)
- # back to the welcome page
- return Redirect("../..")
-
- def stop(self, req):
- for bname in self.status.getBuilderNames():
- builder_status = self.status.getBuilder(bname)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(bname)
- (state, current_builds) = builder_status.getState()
- if state != "building":
- continue
- for b in current_builds:
- build_status = builder_status.getBuild(b.number)
- if not build_status:
- continue
- if builder_control:
- build_control = builder_control.getBuild(b.number)
- else:
- build_control = None
- build = StatusResourceBuild(build_status, build_control,
- builder_control)
- build.stop(req)
- # go back to the welcome page
- return Redirect("../..")
-
-
-# /builders
-class BuildersResource(HtmlResource):
- title = "Builders"
- addSlash = True
-
- def body(self, req):
- s = self.getStatus(req)
- data = ""
- data += "<h1>Builders</h1>\n"
-
- # TODO: this is really basic. It should be expanded to include a
- # brief one-line summary of the builder (perhaps with whatever the
- # builder is currently doing)
- data += "<ol>\n"
- for bname in s.getBuilderNames():
- data += (' <li><a href="%s">%s</a></li>\n' %
- (req.childLink(urllib.quote(bname, safe='')),
- bname))
- data += "</ol>\n"
-
- data += self.footer(s, req)
-
- return data
-
- def getChild(self, path, req):
- s = self.getStatus(req)
- if path in s.getBuilderNames():
- builder_status = s.getBuilder(path)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(path)
- return StatusResourceBuilder(builder_status, builder_control)
- if path == "_all":
- return StatusResourceAllBuilders(self.getStatus(req),
- self.getControl(req))
-
- return HtmlResource.getChild(self, path, req)
-
diff --git a/buildbot/buildbot/status/web/changes.py b/buildbot/buildbot/status/web/changes.py
deleted file mode 100644
index ff562c6..0000000
--- a/buildbot/buildbot/status/web/changes.py
+++ /dev/null
@@ -1,41 +0,0 @@
-
-from zope.interface import implements
-from twisted.python import components
-from twisted.web.error import NoResource
-
-from buildbot.changes.changes import Change
-from buildbot.status.web.base import HtmlResource, StaticHTML, IBox, Box
-
-# /changes/NN
-class ChangesResource(HtmlResource):
-
- def body(self, req):
- data = ""
- data += "Change sources:\n"
- sources = self.getStatus(req).getChangeSources()
- if sources:
- data += "<ol>\n"
- for s in sources:
- data += "<li>%s</li>\n" % s.describe()
- data += "</ol>\n"
- else:
- data += "none (push only)\n"
- return data
-
- def getChild(self, path, req):
- num = int(path)
- c = self.getStatus(req).getChange(num)
- if not c:
- return NoResource("No change number '%d'" % num)
- return StaticHTML(c.asHTML(), "Change #%d" % num)
-
-
-class ChangeBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- url = req.childLink("../changes/%d" % self.original.number)
- text = self.original.get_HTML_box(url)
- return Box([text], class_="Change")
-components.registerAdapter(ChangeBox, Change, IBox)
-
diff --git a/buildbot/buildbot/status/web/classic.css b/buildbot/buildbot/status/web/classic.css
deleted file mode 100644
index 5a5b0ea..0000000
--- a/buildbot/buildbot/status/web/classic.css
+++ /dev/null
@@ -1,78 +0,0 @@
-a:visited {
- color: #800080;
-}
-
-td.Event, td.BuildStep, td.Activity, td.Change, td.Time, td.Builder {
- border-top: 1px solid;
- border-right: 1px solid;
-}
-
-td.box {
- border: 1px solid;
-}
-
-/* Activity states */
-.offline {
- background-color: gray;
-}
-.idle {
- background-color: white;
-}
-.waiting {
- background-color: yellow;
-}
-.building {
- background-color: yellow;
-}
-
-/* LastBuild, BuildStep states */
-.success {
- background-color: #72ff75;
-}
-.failure {
- background-color: red;
-}
-.warnings {
- background-color: #ff8000;
-}
-.exception {
- background-color: #c000c0;
-}
-.start,.running {
- background-color: yellow;
-}
-
-/* grid styles */
-
-table.Grid {
- border-collapse: collapse;
-}
-
-table.Grid tr td {
- padding: 0.2em;
- margin: 0px;
- text-align: center;
-}
-
-table.Grid tr td.title {
- font-size: 90%;
- border-right: 1px gray solid;
- border-bottom: 1px gray solid;
-}
-
-table.Grid tr td.sourcestamp {
- font-size: 90%;
-}
-
-table.Grid tr td.builder {
- text-align: right;
- font-size: 90%;
-}
-
-table.Grid tr td.build {
- border: 1px gray solid;
-}
-
-div.footer {
- font-size: 80%;
-}
diff --git a/buildbot/buildbot/status/web/feeds.py b/buildbot/buildbot/status/web/feeds.py
deleted file mode 100644
index c86ca3b..0000000
--- a/buildbot/buildbot/status/web/feeds.py
+++ /dev/null
@@ -1,359 +0,0 @@
-# This module enables ATOM and RSS feeds from webstatus.
-#
-# It is based on "feeder.py" which was part of the Buildbot
-# configuration for the Subversion project. The original file was
-# created by Lieven Gobaerts and later adjusted by API
-# (apinheiro@igalia.coma) and also here
-# http://code.google.com/p/pybots/source/browse/trunk/master/Feeder.py
-#
-# All subsequent changes to feeder.py where made by Chandan-Dutta
-# Chowdhury <chandan-dutta.chowdhury @ hp.com> and Gareth Armstrong
-# <gareth.armstrong @ hp.com>.
-#
-# Those modifications are as follows:
-# 1) the feeds are usable from baseweb.WebStatus
-# 2) feeds are fully validated ATOM 1.0 and RSS 2.0 feeds, verified
-# with code from http://feedvalidator.org
-# 3) nicer xml output
-# 4) feeds can be filtered as per the /waterfall display with the
-# builder and category filters
-# 5) cleaned up white space and imports
-#
-# Finally, the code was directly integrated into these two files,
-# buildbot/status/web/feeds.py (you're reading it, ;-)) and
-# buildbot/status/web/baseweb.py.
-
-import os
-import re
-import sys
-import time
-from twisted.web import resource
-from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
-
-class XmlResource(resource.Resource):
- contentType = "text/xml; charset=UTF-8"
- def render(self, request):
- data = self.content(request)
- request.setHeader("content-type", self.contentType)
- if request.method == "HEAD":
- request.setHeader("content-length", len(data))
- return ''
- return data
- docType = ''
- def header (self, request):
- data = ('<?xml version="1.0"?>\n')
- return data
- def footer(self, request):
- data = ''
- return data
- def content(self, request):
- data = self.docType
- data += self.header(request)
- data += self.body(request)
- data += self.footer(request)
- return data
- def body(self, request):
- return ''
-
-class FeedResource(XmlResource):
- title = None
- link = 'http://dummylink'
- language = 'en-us'
- description = 'Dummy rss'
- status = None
-
- def __init__(self, status, categories=None, title=None):
- self.status = status
- self.categories = categories
- self.title = title
- self.link = self.status.getBuildbotURL()
- self.description = 'List of FAILED builds'
- self.pubdate = time.gmtime(int(time.time()))
-
- def getBuilds(self, request):
- builds = []
- # THIS is lifted straight from the WaterfallStatusResource Class in
- # status/web/waterfall.py
- #
- # we start with all Builders available to this Waterfall: this is
- # limited by the config-file -time categories= argument, and defaults
- # to all defined Builders.
- allBuilderNames = self.status.getBuilderNames(categories=self.categories)
- builders = [self.status.getBuilder(name) for name in allBuilderNames]
-
- # but if the URL has one or more builder= arguments (or the old show=
- # argument, which is still accepted for backwards compatibility), we
- # use that set of builders instead. We still don't show anything
- # outside the config-file time set limited by categories=.
- showBuilders = request.args.get("show", [])
- showBuilders.extend(request.args.get("builder", []))
- if showBuilders:
- builders = [b for b in builders if b.name in showBuilders]
-
- # now, if the URL has one or category= arguments, use them as a
- # filter: only show those builders which belong to one of the given
- # categories.
- showCategories = request.args.get("category", [])
- if showCategories:
- builders = [b for b in builders if b.category in showCategories]
-
- maxFeeds = 25
-
- # Copy all failed builds in a new list.
- # This could clearly be implemented much better if we had
- # access to a global list of builds.
- for b in builders:
- lastbuild = b.getLastFinishedBuild()
- if lastbuild is None:
- continue
-
- lastnr = lastbuild.getNumber()
-
- totalbuilds = 0
- i = lastnr
- while i >= 0:
- build = b.getBuild(i)
- i -= 1
- if not build:
- continue
-
- results = build.getResults()
-
- # only add entries for failed builds!
- if results == FAILURE:
- totalbuilds += 1
- builds.append(build)
-
- # stop for this builder when our total nr. of feeds is reached
- if totalbuilds >= maxFeeds:
- break
-
- # Sort build list by date, youngest first.
- if sys.version_info[:3] >= (2,4,0):
- builds.sort(key=lambda build: build.getTimes(), reverse=True)
- else:
- # If you need compatibility with python < 2.4, use this for
- # sorting instead:
- # We apply Decorate-Sort-Undecorate
- deco = [(build.getTimes(), build) for build in builds]
- deco.sort()
- deco.reverse()
- builds = [build for (b1, build) in deco]
-
- if builds:
- builds = builds[:min(len(builds), maxFeeds)]
- return builds
-
- def body (self, request):
- data = ''
- builds = self.getBuilds(request)
-
- for build in builds:
- start, finished = build.getTimes()
- finishedTime = time.gmtime(int(finished))
- projectName = self.status.getProjectName()
- link = re.sub(r'index.html', "", self.status.getURLForThing(build))
-
- # title: trunk r22191 (plus patch) failed on 'i686-debian-sarge1 shared gcc-3.3.5'
- ss = build.getSourceStamp()
- source = ""
- if ss.branch:
- source += "Branch %s " % ss.branch
- if ss.revision:
- source += "Revision %s " % str(ss.revision)
- if ss.patch:
- source += " (plus patch)"
- if ss.changes:
- pass
- if (ss.branch is None and ss.revision is None and ss.patch is None
- and not ss.changes):
- source += "Latest revision "
- got_revision = None
- try:
- got_revision = build.getProperty("got_revision")
- except KeyError:
- pass
- if got_revision:
- got_revision = str(got_revision)
- if len(got_revision) > 40:
- got_revision = "[revision string too long]"
- source += "(Got Revision: %s)" % got_revision
- title = ('%s failed on "%s"' %
- (source, build.getBuilder().getName()))
-
- # get name of the failed step and the last 30 lines of its log.
- if build.getLogs():
- log = build.getLogs()[-1]
- laststep = log.getStep().getName()
- try:
- lastlog = log.getText()
- except IOError:
- # Probably the log file has been removed
- lastlog='<b>log file not available</b>'
-
- lines = re.split('\n', lastlog)
- lastlog = ''
- for logline in lines[max(0, len(lines)-30):]:
- lastlog = lastlog + logline + '<br/>'
- lastlog = lastlog.replace('\n', '<br/>')
-
- description = ''
- description += ('Date: %s<br/><br/>' %
- time.strftime("%a, %d %b %Y %H:%M:%S GMT",
- finishedTime))
- description += ('Full details available here: <a href="%s">%s</a><br/>' %
- (self.link, projectName))
- builder_summary_link = ('%s/builders/%s' %
- (re.sub(r'/index.html', '', self.link),
- build.getBuilder().getName()))
- description += ('Build summary: <a href="%s">%s</a><br/><br/>' %
- (builder_summary_link,
- build.getBuilder().getName()))
- description += ('Build details: <a href="%s">%s</a><br/><br/>' %
- (link, self.link + link[1:]))
- description += ('Author list: <b>%s</b><br/><br/>' %
- ",".join(build.getResponsibleUsers()))
- description += ('Failed step: <b>%s</b><br/><br/>' % laststep)
- description += 'Last lines of the build log:<br/>'
-
- data += self.item(title, description=description, lastlog=lastlog,
- link=link, pubDate=finishedTime)
-
- return data
-
- def item(self, title='', link='', description='', pubDate=''):
- """Generates xml for one item in the feed."""
-
-class Rss20StatusResource(FeedResource):
- def __init__(self, status, categories=None, title=None):
- FeedResource.__init__(self, status, categories, title)
- contentType = 'application/rss+xml'
-
- def header(self, request):
- data = FeedResource.header(self, request)
- data += ('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n')
- data += (' <channel>\n')
- if self.title is None:
- title = 'Build status of ' + status.getProjectName()
- else:
- title = self.title
- data += (' <title>%s</title>\n' % title)
- if self.link is not None:
- data += (' <link>%s</link>\n' % self.link)
- link = re.sub(r'/index.html', '', self.link)
- data += (' <atom:link href="%s/rss" rel="self" type="application/rss+xml"/>\n' % link)
- if self.language is not None:
- data += (' <language>%s</language>\n' % self.language)
- if self.description is not None:
- data += (' <description>%s</description>\n' % self.description)
- if self.pubdate is not None:
- rfc822_pubdate = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
- self.pubdate)
- data += (' <pubDate>%s</pubDate>\n' % rfc822_pubdate)
- return data
-
- def item(self, title='', link='', description='', lastlog='', pubDate=''):
- data = (' <item>\n')
- data += (' <title>%s</title>\n' % title)
- if link is not None:
- data += (' <link>%s</link>\n' % link)
- if (description is not None and lastlog is not None):
- lastlog = re.sub(r'<br/>', "\n", lastlog)
- lastlog = re.sub(r'&', "&amp;", lastlog)
- lastlog = re.sub(r"'", "&apos;", lastlog)
- lastlog = re.sub(r'"', "&quot;", lastlog)
- lastlog = re.sub(r'<', '&lt;', lastlog)
- lastlog = re.sub(r'>', '&gt;', lastlog)
- lastlog = lastlog.replace('\n', '<br/>')
- content = '<![CDATA['
- content += description
- content += lastlog
- content += ']]>'
- data += (' <description>%s</description>\n' % content)
- if pubDate is not None:
- rfc822pubDate = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
- pubDate)
- data += (' <pubDate>%s</pubDate>\n' % rfc822pubDate)
- # Every RSS item must have a globally unique ID
- guid = ('tag:%s@%s,%s:%s' % (os.environ['USER'],
- os.environ['HOSTNAME'],
- time.strftime("%Y-%m-%d", pubDate),
- time.strftime("%Y%m%d%H%M%S",
- pubDate)))
- data += (' <guid isPermaLink="false">%s</guid>\n' % guid)
- data += (' </item>\n')
- return data
-
- def footer(self, request):
- data = (' </channel>\n'
- '</rss>')
- return data
-
-class Atom10StatusResource(FeedResource):
- def __init__(self, status, categories=None, title=None):
- FeedResource.__init__(self, status, categories, title)
- contentType = 'application/atom+xml'
-
- def header(self, request):
- data = FeedResource.header(self, request)
- data += '<feed xmlns="http://www.w3.org/2005/Atom">\n'
- data += (' <id>%s</id>\n' % self.status.getBuildbotURL())
- if self.title is None:
- title = 'Build status of ' + status.getProjectName()
- else:
- title = self.title
- data += (' <title>%s</title>\n' % title)
- if self.link is not None:
- link = re.sub(r'/index.html', '', self.link)
- data += (' <link rel="self" href="%s/atom"/>\n' % link)
- data += (' <link rel="alternate" href="%s/"/>\n' % link)
- if self.description is not None:
- data += (' <subtitle>%s</subtitle>\n' % self.description)
- if self.pubdate is not None:
- rfc3339_pubdate = time.strftime("%Y-%m-%dT%H:%M:%SZ",
- self.pubdate)
- data += (' <updated>%s</updated>\n' % rfc3339_pubdate)
- data += (' <author>\n')
- data += (' <name>Build Bot</name>\n')
- data += (' </author>\n')
- return data
-
- def item(self, title='', link='', description='', lastlog='', pubDate=''):
- data = (' <entry>\n')
- data += (' <title>%s</title>\n' % title)
- if link is not None:
- data += (' <link href="%s"/>\n' % link)
- if (description is not None and lastlog is not None):
- lastlog = re.sub(r'<br/>', "\n", lastlog)
- lastlog = re.sub(r'&', "&amp;", lastlog)
- lastlog = re.sub(r"'", "&apos;", lastlog)
- lastlog = re.sub(r'"', "&quot;", lastlog)
- lastlog = re.sub(r'<', '&lt;', lastlog)
- lastlog = re.sub(r'>', '&gt;', lastlog)
- data += (' <content type="xhtml">\n')
- data += (' <div xmlns="http://www.w3.org/1999/xhtml">\n')
- data += (' %s\n' % description)
- data += (' <pre xml:space="preserve">%s</pre>\n' % lastlog)
- data += (' </div>\n')
- data += (' </content>\n')
- if pubDate is not None:
- rfc3339pubDate = time.strftime("%Y-%m-%dT%H:%M:%SZ",
- pubDate)
- data += (' <updated>%s</updated>\n' % rfc3339pubDate)
- # Every Atom entry must have a globally unique ID
- # http://diveintomark.org/archives/2004/05/28/howto-atom-id
- guid = ('tag:%s@%s,%s:%s' % (os.environ['USER'],
- os.environ['HOSTNAME'],
- time.strftime("%Y-%m-%d", pubDate),
- time.strftime("%Y%m%d%H%M%S",
- pubDate)))
- data += (' <id>%s</id>\n' % guid)
- data += (' <author>\n')
- data += (' <name>Build Bot</name>\n')
- data += (' </author>\n')
- data += (' </entry>\n')
- return data
-
- def footer(self, request):
- data = ('</feed>')
- return data
diff --git a/buildbot/buildbot/status/web/grid.py b/buildbot/buildbot/status/web/grid.py
deleted file mode 100644
index 79527d8..0000000
--- a/buildbot/buildbot/status/web/grid.py
+++ /dev/null
@@ -1,252 +0,0 @@
-from __future__ import generators
-
-import sys, time, os.path
-import urllib
-
-from buildbot import util
-from buildbot import version
-from buildbot.status.web.base import HtmlResource
-#from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
-# ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches
-from buildbot.status.web.base import build_get_class
-
-# set grid_css to the full pathname of the css file
-if hasattr(sys, "frozen"):
- # all 'data' files are in the directory of our executable
- here = os.path.dirname(sys.executable)
- grid_css = os.path.abspath(os.path.join(here, "grid.css"))
-else:
- # running from source; look for a sibling to __file__
- up = os.path.dirname
- grid_css = os.path.abspath(os.path.join(up(__file__), "grid.css"))
-
-class ANYBRANCH: pass # a flag value, used below
-
-class GridStatusResource(HtmlResource):
- # TODO: docs
- status = None
- control = None
- changemaster = None
-
- def __init__(self, allowForce=True, css=None):
- HtmlResource.__init__(self)
-
- self.allowForce = allowForce
- self.css = css or grid_css
-
- def getTitle(self, request):
- status = self.getStatus(request)
- p = status.getProjectName()
- if p:
- return "BuildBot: %s" % p
- else:
- return "BuildBot"
-
- def getChangemaster(self, request):
- # TODO: this wants to go away, access it through IStatus
- return request.site.buildbot_service.getChangeSvc()
-
- # handle reloads through an http header
- # TODO: send this as a real header, rather than a tag
- def get_reload_time(self, request):
- if "reload" in request.args:
- try:
- reload_time = int(request.args["reload"][0])
- return max(reload_time, 15)
- except ValueError:
- pass
- return None
-
- def head(self, request):
- head = ''
- reload_time = self.get_reload_time(request)
- if reload_time is not None:
- head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
- return head
-
-# def setBuildmaster(self, buildmaster):
-# self.status = buildmaster.getStatus()
-# if self.allowForce:
-# self.control = interfaces.IControl(buildmaster)
-# else:
-# self.control = None
-# self.changemaster = buildmaster.change_svc
-#
-# # try to set the page title
-# p = self.status.getProjectName()
-# if p:
-# self.title = "BuildBot: %s" % p
-#
- def build_td(self, request, build):
- if not build:
- return '<td class="build">&nbsp;</td>\n'
-
- if build.isFinished():
- # get the text and annotate the first line with a link
- text = build.getText()
- if not text: text = [ "(no information)" ]
- if text == [ "build", "successful" ]: text = [ "OK" ]
- else:
- text = [ 'building' ]
-
- name = build.getBuilder().getName()
- number = build.getNumber()
- url = "builders/%s/builds/%d" % (name, number)
- text[0] = '<a href="%s">%s</a>' % (url, text[0])
- text = '<br />\n'.join(text)
- class_ = build_get_class(build)
-
- return '<td class="build %s">%s</td>\n' % (class_, text)
-
- def builder_td(self, request, builder):
- state, builds = builder.getState()
-
- # look for upcoming builds. We say the state is "waiting" if the
- # builder is otherwise idle and there is a scheduler which tells us a
- # build will be performed some time in the near future. TODO: this
- # functionality used to be in BuilderStatus.. maybe this code should
- # be merged back into it.
- upcoming = []
- builderName = builder.getName()
- for s in self.getStatus(request).getSchedulers():
- if builderName in s.listBuilderNames():
- upcoming.extend(s.getPendingBuildTimes())
- if state == "idle" and upcoming:
- state = "waiting"
-
- # TODO: for now, this pending/upcoming stuff is in the "current
- # activity" box, but really it should go into a "next activity" row
- # instead. The only times it should show up in "current activity" is
- # when the builder is otherwise idle.
-
- # are any builds pending? (waiting for a slave to be free)
- url = 'builders/%s/' % urllib.quote(builder.getName(), safe='')
- text = '<a href="%s">%s</a>' % (url, builder.getName())
- pbs = builder.getPendingBuilds()
- if state != 'idle' or pbs:
- if pbs:
- text += "<br />(%s with %d pending)" % (state, len(pbs))
- else:
- text += "<br />(%s)" % state
-
- return '<td valign="center" class="builder %s">%s</td>\n' % \
- (state, text)
-
- def stamp_td(self, stamp):
- text = stamp.getText()
- return '<td valign="bottom" class="sourcestamp">%s</td>\n' % \
- "<br />".join(text)
-
- def body(self, request):
- "This method builds the main waterfall display."
-
- # get url parameters
- numBuilds = int(request.args.get("width", [5])[0])
- categories = request.args.get("category", [])
- branch = request.args.get("branch", [ANYBRANCH])[0]
- if branch == 'trunk': branch = None
-
- # and the data we want to render
- status = self.getStatus(request)
- stamps = self.getRecentSourcestamps(status, numBuilds, categories, branch)
-
- projectURL = status.getProjectURL()
- projectName = status.getProjectName()
-
- data = '<table class="Grid" border="0" cellspacing="0">\n'
- data += '<tr>\n'
- data += '<td class="title"><a href="%s">%s</a>' % (projectURL, projectName)
- if categories:
- if len(categories) > 1:
- data += '\n<br /><b>Categories:</b><br/>%s' % ('<br/>'.join(categories))
- else:
- data += '\n<br /><b>Category:</b> %s' % categories[0]
- if branch != ANYBRANCH:
- data += '\n<br /><b>Branch:</b> %s' % (branch or 'trunk')
- data += '</td>\n'
- for stamp in stamps:
- data += self.stamp_td(stamp)
- data += '</tr>\n'
-
- sortedBuilderNames = status.getBuilderNames()[:]
- sortedBuilderNames.sort()
- for bn in sortedBuilderNames:
- builds = [None] * len(stamps)
-
- builder = status.getBuilder(bn)
- if categories and builder.category not in categories:
- continue
-
- build = builder.getBuild(-1)
- while build and None in builds:
- ss = build.getSourceStamp(absolute=True)
- for i in range(len(stamps)):
- if ss == stamps[i] and builds[i] is None:
- builds[i] = build
- build = build.getPreviousBuild()
-
- data += '<tr>\n'
- data += self.builder_td(request, builder)
- for build in builds:
- data += self.build_td(request, build)
- data += '</tr>\n'
-
- data += '</table>\n'
-
- # TODO: this stuff should be generated by a template of some sort
- data += '<hr /><div class="footer">\n'
-
- welcomeurl = self.path_to_root(request) + "index.html"
- data += '[<a href="%s">welcome</a>]\n' % welcomeurl
- data += "<br />\n"
-
- data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>'
- data += "-%s " % version
- if projectName:
- data += "working for the "
- if projectURL:
- data += "<a href=\"%s\">%s</a> project." % (projectURL,
- projectName)
- else:
- data += "%s project." % projectName
- data += "<br />\n"
- data += ("Page built: " +
- time.strftime("%a %d %b %Y %H:%M:%S",
- time.localtime(util.now()))
- + "\n")
- data += '</div>\n'
- return data
-
- def getRecentSourcestamps(self, status, numBuilds, categories, branch):
- """
- get a list of the most recent NUMBUILDS SourceStamp tuples, sorted
- by the earliest start we've seen for them
- """
- # TODO: use baseweb's getLastNBuilds?
- sourcestamps = { } # { ss-tuple : earliest time }
- for bn in status.getBuilderNames():
- builder = status.getBuilder(bn)
- if categories and builder.category not in categories:
- continue
- build = builder.getBuild(-1)
- while build:
- ss = build.getSourceStamp(absolute=True)
- start = build.getTimes()[0]
- build = build.getPreviousBuild()
-
- # skip un-started builds
- if not start: continue
-
- # skip non-matching branches
- if branch != ANYBRANCH and ss.branch != branch: continue
-
- sourcestamps[ss] = min(sourcestamps.get(ss, sys.maxint), start)
-
- # now sort those and take the NUMBUILDS most recent
- sourcestamps = sourcestamps.items()
- sourcestamps.sort(lambda x, y: cmp(x[1], y[1]))
- sourcestamps = map(lambda tup : tup[0], sourcestamps)
- sourcestamps = sourcestamps[-numBuilds:]
-
- return sourcestamps
-
diff --git a/buildbot/buildbot/status/web/index.html b/buildbot/buildbot/status/web/index.html
deleted file mode 100644
index 23e6650..0000000
--- a/buildbot/buildbot/status/web/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-<html>
-<head>
-<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15">
-<title>Welcome to the Buildbot</title>
-</head>
-
-<body>
-<h1>Welcome to the Buildbot!</h1>
-
-<ul>
- <li>the <a href="waterfall">Waterfall Display</a> will give you a
- time-oriented summary of recent buildbot activity.</li>
-
- <li>the <a href="grid">Grid Display</a> will give you a
- developer-oriented summary of recent buildbot activity.</li>
-
- <li>The <a href="one_box_per_builder">Latest Build</a> for each builder is
- here.</li>
-
- <li><a href="one_line_per_build">Recent Builds</a> are summarized here, one
- per line.</li>
-
- <li><a href="buildslaves">Buildslave</a> information</li>
- <li><a href="changes">ChangeSource</a> information.</li>
-
- <br />
- <li><a href="about">About this Buildbot</a></li>
-</ul>
-
-
-</body> </html>
diff --git a/buildbot/buildbot/status/web/logs.py b/buildbot/buildbot/status/web/logs.py
deleted file mode 100644
index dfcf7f0..0000000
--- a/buildbot/buildbot/status/web/logs.py
+++ /dev/null
@@ -1,171 +0,0 @@
-
-from zope.interface import implements
-from twisted.python import components
-from twisted.spread import pb
-from twisted.web import html, server
-from twisted.web.resource import Resource
-from twisted.web.error import NoResource
-
-from buildbot import interfaces
-from buildbot.status import builder
-from buildbot.status.web.base import IHTMLLog, HtmlResource
-
-
-textlog_stylesheet = """
-<style type="text/css">
- div.data {
- font-family: "Courier New", courier, monotype;
- }
- span.stdout {
- font-family: "Courier New", courier, monotype;
- }
- span.stderr {
- font-family: "Courier New", courier, monotype;
- color: red;
- }
- span.header {
- font-family: "Courier New", courier, monotype;
- color: blue;
- }
-</style>
-"""
-
-class ChunkConsumer:
- implements(interfaces.IStatusLogConsumer)
-
- def __init__(self, original, textlog):
- self.original = original
- self.textlog = textlog
- def registerProducer(self, producer, streaming):
- self.producer = producer
- self.original.registerProducer(producer, streaming)
- def unregisterProducer(self):
- self.original.unregisterProducer()
- def writeChunk(self, chunk):
- formatted = self.textlog.content([chunk])
- try:
- self.original.write(formatted)
- except pb.DeadReferenceError:
- self.producing.stopProducing()
- def finish(self):
- self.textlog.finished()
-
-
-# /builders/$builder/builds/$buildnum/steps/$stepname/logs/$logname
-class TextLog(Resource):
- # a new instance of this Resource is created for each client who views
- # it, so we can afford to track the request in the Resource.
- implements(IHTMLLog)
-
- asText = False
- subscribed = False
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def getChild(self, path, req):
- if path == "text":
- self.asText = True
- return self
- return HtmlResource.getChild(self, path, req)
-
- def htmlHeader(self, request):
- title = "Log File contents"
- data = "<html>\n<head><title>" + title + "</title>\n"
- data += textlog_stylesheet
- data += "</head>\n"
- data += "<body vlink=\"#800080\">\n"
- texturl = request.childLink("text")
- data += '<a href="%s">(view as text)</a><br />\n' % texturl
- data += "<pre>\n"
- return data
-
- def content(self, entries):
- spanfmt = '<span class="%s">%s</span>'
- data = ""
- for type, entry in entries:
- if type >= len(builder.ChunkTypes) or type < 0:
- # non-std channel, don't display
- continue
- if self.asText:
- if type != builder.HEADER:
- data += entry
- else:
- data += spanfmt % (builder.ChunkTypes[type],
- html.escape(entry))
- return data
-
- def htmlFooter(self):
- data = "</pre>\n"
- data += "</body></html>\n"
- return data
-
- def render_HEAD(self, request):
- if self.asText:
- request.setHeader("content-type", "text/plain")
- else:
- request.setHeader("content-type", "text/html")
-
- # vague approximation, ignores markup
- request.setHeader("content-length", self.original.length)
- return ''
-
- def render_GET(self, req):
- self.req = req
-
- if self.asText:
- req.setHeader("content-type", "text/plain")
- else:
- req.setHeader("content-type", "text/html")
-
- if not self.asText:
- req.write(self.htmlHeader(req))
-
- self.original.subscribeConsumer(ChunkConsumer(req, self))
- return server.NOT_DONE_YET
-
- def finished(self):
- if not self.req:
- return
- try:
- if not self.asText:
- self.req.write(self.htmlFooter())
- self.req.finish()
- except pb.DeadReferenceError:
- pass
- # break the cycle, the Request's .notifications list includes the
- # Deferred (from req.notifyFinish) that's pointing at us.
- self.req = None
-
-components.registerAdapter(TextLog, interfaces.IStatusLog, IHTMLLog)
-
-
-class HTMLLog(Resource):
- implements(IHTMLLog)
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def render(self, request):
- request.setHeader("content-type", "text/html")
- return self.original.html
-
-components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
-
-
-class LogsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, step_status):
- HtmlResource.__init__(self)
- self.step_status = step_status
-
- def getChild(self, path, req):
- for log in self.step_status.getLogs():
- if path == log.getName():
- if log.hasContents():
- return IHTMLLog(interfaces.IStatusLog(log))
- return NoResource("Empty Log '%s'" % path)
- return HtmlResource.getChild(self, path, req)
diff --git a/buildbot/buildbot/status/web/robots.txt b/buildbot/buildbot/status/web/robots.txt
deleted file mode 100644
index 47a9d27..0000000
--- a/buildbot/buildbot/status/web/robots.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-User-agent: *
-Disallow: /waterfall
-Disallow: /builders
-Disallow: /changes
-Disallow: /buildslaves
-Disallow: /schedulers
-Disallow: /one_line_per_build
-Disallow: /one_box_per_builder
-Disallow: /xmlrpc
diff --git a/buildbot/buildbot/status/web/slaves.py b/buildbot/buildbot/status/web/slaves.py
deleted file mode 100644
index 5782873..0000000
--- a/buildbot/buildbot/status/web/slaves.py
+++ /dev/null
@@ -1,181 +0,0 @@
-
-import time, urllib
-from twisted.python import log
-from twisted.web import html
-from twisted.web.util import Redirect
-
-from buildbot.status.web.base import HtmlResource, abbreviate_age, OneLineMixin, path_to_slave
-from buildbot import version, util
-
-# /buildslaves/$slavename
-class OneBuildSlaveResource(HtmlResource, OneLineMixin):
- addSlash = False
- def __init__(self, slavename):
- HtmlResource.__init__(self)
- self.slavename = slavename
-
- def getTitle(self, req):
- return "Buildbot: %s" % html.escape(self.slavename)
-
- def getChild(self, path, req):
- if path == "shutdown":
- s = self.getStatus(req)
- slave = s.getSlave(self.slavename)
- slave.setGraceful(True)
- return Redirect(path_to_slave(req, slave))
-
- def body(self, req):
- s = self.getStatus(req)
- slave = s.getSlave(self.slavename)
- my_builders = []
- for bname in s.getBuilderNames():
- b = s.getBuilder(bname)
- for bs in b.getSlaves():
- slavename = bs.getName()
- if bs.getName() == self.slavename:
- my_builders.append(b)
-
- # Current builds
- current_builds = []
- for b in my_builders:
- for cb in b.getCurrentBuilds():
- if cb.getSlavename() == self.slavename:
- current_builds.append(cb)
-
- data = []
-
- projectName = s.getProjectName()
-
- data.append("<a href=\"%s\">%s</a>\n" % (self.path_to_root(req), projectName))
-
- data.append("<h1>Build Slave: %s</h1>\n" % self.slavename)
-
- shutdown_url = req.childLink("shutdown")
-
- if not slave.isConnected():
- data.append("<h2>NOT CONNECTED</h2>\n")
- elif not slave.getGraceful():
- data.append('''<form method="POST" action="%s">
-<input type="submit" value="Gracefully Shutdown">
-</form>''' % shutdown_url)
- else:
- data.append("Gracefully shutting down...\n")
-
- if current_builds:
- data.append("<h2>Currently building:</h2>\n")
- data.append("<ul>\n")
- for build in current_builds:
- data.append("<li>%s</li>\n" % self.make_line(req, build, True))
- data.append("</ul>\n")
-
- else:
- data.append("<h2>no current builds</h2>\n")
-
- # Recent builds
- data.append("<h2>Recent builds:</h2>\n")
- data.append("<ul>\n")
- n = 0
- try:
- max_builds = int(req.args.get('builds')[0])
- except:
- max_builds = 10
- for build in s.generateFinishedBuilds(builders=[b.getName() for b in my_builders]):
- if build.getSlavename() == self.slavename:
- n += 1
- data.append("<li>%s</li>\n" % self.make_line(req, build, True))
- if n > max_builds:
- break
- data.append("</ul>\n")
-
- projectURL = s.getProjectURL()
- projectName = s.getProjectName()
- data.append('<hr /><div class="footer">\n')
-
- welcomeurl = self.path_to_root(req) + "index.html"
- data.append("[<a href=\"%s\">welcome</a>]\n" % welcomeurl)
- data.append("<br />\n")
-
- data.append('<a href="http://buildbot.sourceforge.net/">Buildbot</a>')
- data.append("-%s " % version)
- if projectName:
- data.append("working for the ")
- if projectURL:
- data.append("<a href=\"%s\">%s</a> project." % (projectURL,
- projectName))
- else:
- data.append("%s project." % projectName)
- data.append("<br />\n")
- data.append("Page built: " +
- time.strftime("%a %d %b %Y %H:%M:%S",
- time.localtime(util.now()))
- + "\n")
- data.append("</div>\n")
-
- return "".join(data)
-
-# /buildslaves
-class BuildSlavesResource(HtmlResource):
- title = "BuildSlaves"
- addSlash = True
-
- def body(self, req):
- s = self.getStatus(req)
- data = ""
- data += "<h1>Build Slaves</h1>\n"
-
- used_by_builder = {}
- for bname in s.getBuilderNames():
- b = s.getBuilder(bname)
- for bs in b.getSlaves():
- slavename = bs.getName()
- if slavename not in used_by_builder:
- used_by_builder[slavename] = []
- used_by_builder[slavename].append(bname)
-
- data += "<ol>\n"
- for name in util.naturalSort(s.getSlaveNames()):
- slave = s.getSlave(name)
- slave_status = s.botmaster.slaves[name].slave_status
- isBusy = len(slave_status.getRunningBuilds())
- data += " <li><a href=\"%s\">%s</a>:\n" % (req.childLink(urllib.quote(name,'')), name)
- data += " <ul>\n"
- builder_links = ['<a href="%s">%s</a>'
- % (req.childLink("../builders/%s" % bname),bname)
- for bname in used_by_builder.get(name, [])]
- if builder_links:
- data += (" <li>Used by Builders: %s</li>\n" %
- ", ".join(builder_links))
- else:
- data += " <li>Not used by any Builders</li>\n"
- if slave.isConnected():
- data += " <li>Slave is currently connected</li>\n"
- admin = slave.getAdmin()
- if admin:
- # munge it to avoid feeding the spambot harvesters
- admin = admin.replace("@", " -at- ")
- data += " <li>Admin: %s</li>\n" % admin
- last = slave.lastMessageReceived()
- if last:
- lt = time.strftime("%Y-%b-%d %H:%M:%S",
- time.localtime(last))
- age = abbreviate_age(time.time() - last)
- data += " <li>Last heard from: %s " % age
- data += '<font size="-1">(%s)</font>' % lt
- data += "</li>\n"
- if isBusy:
- data += "<li>Slave is currently building.</li>"
- else:
- data += "<li>Slave is idle.</li>"
- else:
- data += " <li><b>Slave is NOT currently connected</b></li>\n"
-
- data += " </ul>\n"
- data += " </li>\n"
- data += "\n"
-
- data += "</ol>\n"
-
- return data
-
- def getChild(self, path, req):
- return OneBuildSlaveResource(path)
diff --git a/buildbot/buildbot/status/web/step.py b/buildbot/buildbot/status/web/step.py
deleted file mode 100644
index b65626f..0000000
--- a/buildbot/buildbot/status/web/step.py
+++ /dev/null
@@ -1,97 +0,0 @@
-
-from twisted.web import html
-
-import urllib
-from buildbot.status.web.base import HtmlResource, path_to_builder, \
- path_to_build
-from buildbot.status.web.logs import LogsResource
-from buildbot import util
-from time import ctime
-
-# /builders/$builder/builds/$buildnum/steps/$stepname
-class StatusResourceBuildStep(HtmlResource):
- title = "Build Step"
- addSlash = True
-
- def __init__(self, build_status, step_status):
- HtmlResource.__init__(self)
- self.status = build_status
- self.step_status = step_status
-
- def body(self, req):
- s = self.step_status
- b = s.getBuild()
- builder_name = b.getBuilder().getName()
- build_num = b.getNumber()
- data = ""
- data += ('<h1>BuildStep <a href="%s">%s</a>:' %
- (path_to_builder(req, b.getBuilder()), builder_name))
- data += '<a href="%s">#%d</a>' % (path_to_build(req, b), build_num)
- data += ":%s</h1>\n" % s.getName()
-
- if s.isFinished():
- data += ("<h2>Finished</h2>\n"
- "<p>%s</p>\n" % html.escape("%s" % s.getText()))
- else:
- data += ("<h2>Not Finished</h2>\n"
- "<p>ETA %s seconds</p>\n" % s.getETA())
-
- exp = s.getExpectations()
- if exp:
- data += ("<h2>Expectations</h2>\n"
- "<ul>\n")
- for e in exp:
- data += "<li>%s: current=%s, target=%s</li>\n" % \
- (html.escape(e[0]), e[1], e[2])
- data += "</ul>\n"
-
- (start, end) = s.getTimes()
- data += "<h2>Timing</h2>\n"
- data += "<table>\n"
- data += "<tr><td>Start</td><td>%s</td></tr>\n" % ctime(start)
- if end:
- data += "<tr><td>End</td><td>%s</td></tr>\n" % ctime(end)
- data += "<tr><td>Elapsed</td><td>%s</td></tr>\n" % util.formatInterval(end - start)
- data += "</table>\n"
-
- logs = s.getLogs()
- if logs:
- data += ("<h2>Logs</h2>\n"
- "<ul>\n")
- for logfile in logs:
- if logfile.hasContents():
- # FIXME: If the step name has a / in it, this is broken
- # either way. If we quote it but say '/'s are safe,
- # it chops up the step name. If we quote it and '/'s
- # are not safe, it escapes the / that separates the
- # step name from the log number.
- logname = logfile.getName()
- logurl = req.childLink("logs/%s" % urllib.quote(logname))
- data += ('<li><a href="%s">%s</a></li>\n' %
- (logurl, html.escape(logname)))
- else:
- data += '<li>%s</li>\n' % html.escape(logname)
- data += "</ul>\n"
-
- return data
-
- def getChild(self, path, req):
- if path == "logs":
- return LogsResource(self.step_status)
- return HtmlResource.getChild(self, path, req)
-
-
-
-# /builders/$builder/builds/$buildnum/steps
-class StepsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, build_status):
- HtmlResource.__init__(self)
- self.build_status = build_status
-
- def getChild(self, path, req):
- for s in self.build_status.getSteps():
- if s.getName() == path:
- return StatusResourceBuildStep(self.build_status, s)
- return HtmlResource.getChild(self, path, req)
diff --git a/buildbot/buildbot/status/web/tests.py b/buildbot/buildbot/status/web/tests.py
deleted file mode 100644
index b96bba2..0000000
--- a/buildbot/buildbot/status/web/tests.py
+++ /dev/null
@@ -1,64 +0,0 @@
-
-from twisted.web.error import NoResource
-from twisted.web import html
-
-from buildbot.status.web.base import HtmlResource
-
-# /builders/$builder/builds/$buildnum/tests/$testname
-class TestResult(HtmlResource):
- title = "Test Logs"
-
- def __init__(self, name, test_result):
- HtmlResource.__init__(self)
- self.name = name
- self.test_result = test_result
-
- def body(self, request):
- dotname = ".".join(self.name)
- logs = self.test_result.getLogs()
- lognames = logs.keys()
- lognames.sort()
- data = "<h1>%s</h1>\n" % html.escape(dotname)
- for name in lognames:
- data += "<h2>%s</h2>\n" % html.escape(name)
- data += "<pre>" + logs[name] + "</pre>\n\n"
-
- return data
-
-
-# /builders/$builder/builds/$buildnum/tests
-class TestsResource(HtmlResource):
- title = "Test Results"
-
- def __init__(self, build_status):
- HtmlResource.__init__(self)
- self.build_status = build_status
- self.test_results = build_status.getTestResults()
-
- def body(self, request):
- r = self.test_results
- data = "<h1>Test Results</h1>\n"
- data += "<ul>\n"
- testnames = r.keys()
- testnames.sort()
- for name in testnames:
- res = r[name]
- dotname = ".".join(name)
- data += " <li>%s: " % dotname
- # TODO: this could break on weird test names. At the moment,
- # test names only come from Trial tests, where the name
- # components must be legal python names, but that won't always
- # be a restriction.
- url = request.childLink(dotname)
- data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText()))
- data += "</li>\n"
- data += "</ul>\n"
- return data
-
- def getChild(self, path, request):
- try:
- name = tuple(path.split("."))
- result = self.test_results[name]
- return TestResult(name, result)
- except KeyError:
- return NoResource("No such test name '%s'" % path)
diff --git a/buildbot/buildbot/status/web/waterfall.py b/buildbot/buildbot/status/web/waterfall.py
deleted file mode 100644
index 1d3ab60..0000000
--- a/buildbot/buildbot/status/web/waterfall.py
+++ /dev/null
@@ -1,962 +0,0 @@
-# -*- test-case-name: buildbot.test.test_web -*-
-
-from zope.interface import implements
-from twisted.python import log, components
-from twisted.web import html
-import urllib
-
-import time
-import operator
-
-from buildbot import interfaces, util
-from buildbot import version
-from buildbot.status import builder
-
-from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
- ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches
-
-
-
-class CurrentBox(components.Adapter):
- # this provides the "current activity" box, just above the builder name
- implements(ICurrentBox)
-
- def formatETA(self, prefix, eta):
- if eta is None:
- return []
- if eta < 60:
- return ["< 1 min"]
- eta_parts = ["~"]
- eta_secs = eta
- if eta_secs > 3600:
- eta_parts.append("%d hrs" % (eta_secs / 3600))
- eta_secs %= 3600
- if eta_secs > 60:
- eta_parts.append("%d mins" % (eta_secs / 60))
- eta_secs %= 60
- abstime = time.strftime("%H:%M", time.localtime(util.now()+eta))
- return [prefix, " ".join(eta_parts), "at %s" % abstime]
-
- def getBox(self, status):
- # getState() returns offline, idle, or building
- state, builds = self.original.getState()
-
- # look for upcoming builds. We say the state is "waiting" if the
- # builder is otherwise idle and there is a scheduler which tells us a
- # build will be performed some time in the near future. TODO: this
- # functionality used to be in BuilderStatus.. maybe this code should
- # be merged back into it.
- upcoming = []
- builderName = self.original.getName()
- for s in status.getSchedulers():
- if builderName in s.listBuilderNames():
- upcoming.extend(s.getPendingBuildTimes())
- if state == "idle" and upcoming:
- state = "waiting"
-
- if state == "building":
- text = ["building"]
- if builds:
- for b in builds:
- eta = b.getETA()
- text.extend(self.formatETA("ETA in", eta))
- elif state == "offline":
- text = ["offline"]
- elif state == "idle":
- text = ["idle"]
- elif state == "waiting":
- text = ["waiting"]
- else:
- # just in case I add a state and forget to update this
- text = [state]
-
- # TODO: for now, this pending/upcoming stuff is in the "current
- # activity" box, but really it should go into a "next activity" row
- # instead. The only times it should show up in "current activity" is
- # when the builder is otherwise idle.
-
- # are any builds pending? (waiting for a slave to be free)
- pbs = self.original.getPendingBuilds()
- if pbs:
- text.append("%d pending" % len(pbs))
- for t in upcoming:
- eta = t - util.now()
- text.extend(self.formatETA("next in", eta))
- return Box(text, class_="Activity " + state)
-
-components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
-
-
-class BuildTopBox(components.Adapter):
- # this provides a per-builder box at the very top of the display,
- # showing the results of the most recent build
- implements(IBox)
-
- def getBox(self, req):
- assert interfaces.IBuilderStatus(self.original)
- branches = [b for b in req.args.get("branch", []) if b]
- builder = self.original
- builds = list(builder.generateFinishedBuilds(map_branches(branches),
- num_builds=1))
- if not builds:
- return Box(["none"], class_="LastBuild")
- b = builds[0]
- name = b.getBuilder().getName()
- number = b.getNumber()
- url = path_to_build(req, b)
- text = b.getText()
- tests_failed = b.getSummaryStatistic('tests-failed', operator.add, 0)
- if tests_failed: text.extend(["Failed tests: %d" % tests_failed])
- # TODO: maybe add logs?
- # TODO: add link to the per-build page at 'url'
- class_ = build_get_class(b)
- return Box(text, class_="LastBuild %s" % class_)
-components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
-
-class BuildBox(components.Adapter):
- # this provides the yellow "starting line" box for each build
- implements(IBox)
-
- def getBox(self, req):
- b = self.original
- number = b.getNumber()
- url = path_to_build(req, b)
- reason = b.getReason()
- text = ('<a title="Reason: %s" href="%s">Build %d</a>'
- % (html.escape(reason), url, number))
- class_ = "start"
- if b.isFinished() and not b.getSteps():
- # the steps have been pruned, so there won't be any indication
- # of whether it succeeded or failed.
- class_ = build_get_class(b)
- return Box([text], class_="BuildStep " + class_)
-components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
-
-class StepBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- urlbase = path_to_step(req, self.original)
- text = self.original.getText()
- if text is None:
- log.msg("getText() gave None", urlbase)
- text = []
- text = text[:]
- logs = self.original.getLogs()
- for num in range(len(logs)):
- name = logs[num].getName()
- if logs[num].hasContents():
- url = urlbase + "/logs/%s" % urllib.quote(name)
- text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name)))
- else:
- text.append(html.escape(name))
- urls = self.original.getURLs()
- ex_url_class = "BuildStep external"
- for name, target in urls.items():
- text.append('[<a href="%s" class="%s">%s</a>]' %
- (target, ex_url_class, html.escape(name)))
- class_ = "BuildStep " + build_get_class(self.original)
- return Box(text, class_=class_)
-components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
-
-
-class EventBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- text = self.original.getText()
- class_ = "Event"
- return Box(text, class_=class_)
-components.registerAdapter(EventBox, builder.Event, IBox)
-
-
-class Spacer:
- implements(interfaces.IStatusEvent)
-
- def __init__(self, start, finish):
- self.started = start
- self.finished = finish
-
- def getTimes(self):
- return (self.started, self.finished)
- def getText(self):
- return []
-
-class SpacerBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- #b = Box(["spacer"], "white")
- b = Box([])
- b.spacer = True
- return b
-components.registerAdapter(SpacerBox, Spacer, IBox)
-
-def insertGaps(g, lastEventTime, idleGap=2):
- debug = False
-
- e = g.next()
- starts, finishes = e.getTimes()
- if debug: log.msg("E0", starts, finishes)
- if finishes == 0:
- finishes = starts
- if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
- (finishes, idleGap, lastEventTime))
- if finishes is not None and finishes + idleGap < lastEventTime:
- if debug: log.msg(" spacer0")
- yield Spacer(finishes, lastEventTime)
-
- followingEventStarts = starts
- if debug: log.msg(" fES0", starts)
- yield e
-
- while 1:
- e = g.next()
- starts, finishes = e.getTimes()
- if debug: log.msg("E2", starts, finishes)
- if finishes == 0:
- finishes = starts
- if finishes is not None and finishes + idleGap < followingEventStarts:
- # there is a gap between the end of this event and the beginning
- # of the next one. Insert an idle event so the waterfall display
- # shows a gap here.
- if debug:
- log.msg(" finishes=%s, gap=%s, fES=%s" % \
- (finishes, idleGap, followingEventStarts))
- yield Spacer(finishes, followingEventStarts)
- yield e
- followingEventStarts = starts
- if debug: log.msg(" fES1", starts)
-
-HELP = '''
-<form action="../waterfall" method="GET">
-
-<h1>The Waterfall Display</h1>
-
-<p>The Waterfall display can be controlled by adding query arguments to the
-URL. For example, if your Waterfall is accessed via the URL
-<tt>http://buildbot.example.org:8080</tt>, then you could add a
-<tt>branch=</tt> argument (described below) by going to
-<tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that
-query arguments are separated from each other with ampersands, but they are
-separated from the main URL with a question mark, so to add a
-<tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use
-<tt>http://buildbot.example.org:8080?branch=beta4&amp;builder=unix&amp;builder=macos</tt>.</p>
-
-<h2>Limiting the Displayed Interval</h2>
-
-<p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the
-start of 1970) that will be used as an upper bound on the interval of events
-displayed: nothing will be shown that is more recent than the given time.
-When no argument is provided, all events up to and including the most recent
-steps are included.</p>
-
-<p>The <tt>first_time=</tt> argument provides the lower bound. No events will
-be displayed that occurred <b>before</b> this timestamp. Instead of providing
-<tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case,
-<tt>first_time</tt> will be set equal to <tt>last_time</tt> minus
-<tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p>
-
-<p>The display normally shows the latest 200 events that occurred in the
-given interval, where each timestamp on the left hand edge counts as a single
-event. You can add a <tt>num_events=</tt> argument to override this this.</p>
-
-<h2>Hiding non-Build events</h2>
-
-<p>By passing <tt>show_events=false</tt>, you can remove the "buildslave
-attached", "buildslave detached", and "builder reconfigured" events that
-appear in-between the actual builds.</p>
-
-%(show_events_input)s
-
-<h2>Showing only Certain Branches</h2>
-
-<p>If you provide one or more <tt>branch=</tt> arguments, the display will be
-limited to builds that used one of the given branches. If no <tt>branch=</tt>
-arguments are given, builds from all branches will be displayed.</p>
-
-Erase the text from these "Show Branch:" boxes to remove that branch filter.
-
-%(show_branches_input)s
-
-<h2>Limiting the Builders that are Displayed</h2>
-
-<p>By adding one or more <tt>builder=</tt> arguments, the display will be
-limited to showing builds that ran on the given builders. This serves to
-limit the display to the specific named columns. If no <tt>builder=</tt>
-arguments are provided, all Builders will be displayed.</p>
-
-<p>To view a Waterfall page with only a subset of Builders displayed, select
-the Builders you are interested in here.</p>
-
-%(show_builders_input)s
-
-
-<h2>Auto-reloading the Page</h2>
-
-<p>Adding a <tt>reload=</tt> argument will cause the page to automatically
-reload itself after that many seconds.</p>
-
-%(show_reload_input)s
-
-<h2>Reload Waterfall Page</h2>
-
-<input type="submit" value="View Waterfall" />
-</form>
-'''
-
-class WaterfallHelp(HtmlResource):
- title = "Waterfall Help"
-
- def __init__(self, categories=None):
- HtmlResource.__init__(self)
- self.categories = categories
-
- def body(self, request):
- data = ''
- status = self.getStatus(request)
-
- showEvents_checked = 'checked="checked"'
- if request.args.get("show_events", ["true"])[0].lower() == "true":
- showEvents_checked = ''
- show_events_input = ('<p>'
- '<input type="checkbox" name="show_events" '
- 'value="false" %s>'
- 'Hide non-Build events'
- '</p>\n'
- ) % showEvents_checked
-
- branches = [b
- for b in request.args.get("branch", [])
- if b]
- branches.append('')
- show_branches_input = '<table>\n'
- for b in branches:
- show_branches_input += ('<tr>'
- '<td>Show Branch: '
- '<input type="text" name="branch" '
- 'value="%s">'
- '</td></tr>\n'
- ) % (b,)
- show_branches_input += '</table>\n'
-
- # this has a set of toggle-buttons to let the user choose the
- # builders
- showBuilders = request.args.get("show", [])
- showBuilders.extend(request.args.get("builder", []))
- allBuilders = status.getBuilderNames(categories=self.categories)
-
- show_builders_input = '<table>\n'
- for bn in allBuilders:
- checked = ""
- if bn in showBuilders:
- checked = 'checked="checked"'
- show_builders_input += ('<tr>'
- '<td><input type="checkbox"'
- ' name="builder" '
- 'value="%s" %s></td> '
- '<td>%s</td></tr>\n'
- ) % (bn, checked, bn)
- show_builders_input += '</table>\n'
-
- # a couple of radio-button selectors for refresh time will appear
- # just after that text
- show_reload_input = '<table>\n'
- times = [("none", "None"),
- ("60", "60 seconds"),
- ("300", "5 minutes"),
- ("600", "10 minutes"),
- ]
- current_reload_time = request.args.get("reload", ["none"])
- if current_reload_time:
- current_reload_time = current_reload_time[0]
- if current_reload_time not in [t[0] for t in times]:
- times.insert(0, (current_reload_time, current_reload_time) )
- for value, name in times:
- checked = ""
- if value == current_reload_time:
- checked = 'checked="checked"'
- show_reload_input += ('<tr>'
- '<td><input type="radio" name="reload" '
- 'value="%s" %s></td> '
- '<td>%s</td></tr>\n'
- ) % (value, checked, name)
- show_reload_input += '</table>\n'
-
- fields = {"show_events_input": show_events_input,
- "show_branches_input": show_branches_input,
- "show_builders_input": show_builders_input,
- "show_reload_input": show_reload_input,
- }
- data += HELP % fields
- return data
-
-class WaterfallStatusResource(HtmlResource):
- """This builds the main status page, with the waterfall display, and
- all child pages."""
-
- def __init__(self, categories=None):
- HtmlResource.__init__(self)
- self.categories = categories
- self.putChild("help", WaterfallHelp(categories))
-
- def getTitle(self, request):
- status = self.getStatus(request)
- p = status.getProjectName()
- if p:
- return "BuildBot: %s" % p
- else:
- return "BuildBot"
-
- def getChangemaster(self, request):
- # TODO: this wants to go away, access it through IStatus
- return request.site.buildbot_service.getChangeSvc()
-
- def get_reload_time(self, request):
- if "reload" in request.args:
- try:
- reload_time = int(request.args["reload"][0])
- return max(reload_time, 15)
- except ValueError:
- pass
- return None
-
- def head(self, request):
- head = ''
- reload_time = self.get_reload_time(request)
- if reload_time is not None:
- head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
- return head
-
- def body(self, request):
- "This method builds the main waterfall display."
-
- status = self.getStatus(request)
- data = ''
-
- projectName = status.getProjectName()
- projectURL = status.getProjectURL()
-
- phase = request.args.get("phase",["2"])
- phase = int(phase[0])
-
- # we start with all Builders available to this Waterfall: this is
- # limited by the config-file -time categories= argument, and defaults
- # to all defined Builders.
- allBuilderNames = status.getBuilderNames(categories=self.categories)
- builders = [status.getBuilder(name) for name in allBuilderNames]
-
- # but if the URL has one or more builder= arguments (or the old show=
- # argument, which is still accepted for backwards compatibility), we
- # use that set of builders instead. We still don't show anything
- # outside the config-file time set limited by categories=.
- showBuilders = request.args.get("show", [])
- showBuilders.extend(request.args.get("builder", []))
- if showBuilders:
- builders = [b for b in builders if b.name in showBuilders]
-
- # now, if the URL has one or category= arguments, use them as a
- # filter: only show those builders which belong to one of the given
- # categories.
- showCategories = request.args.get("category", [])
- if showCategories:
- builders = [b for b in builders if b.category in showCategories]
-
- builderNames = [b.name for b in builders]
-
- if phase == -1:
- return self.body0(request, builders)
- (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
- self.buildGrid(request, builders)
- if phase == 0:
- return self.phase0(request, (changeNames + builderNames),
- timestamps, eventGrid)
- # start the table: top-header material
- data += '<table border="0" cellspacing="0">\n'
-
- if projectName and projectURL:
- # TODO: this is going to look really ugly
- topleft = '<a href="%s">%s</a><br />last build' % \
- (projectURL, projectName)
- else:
- topleft = "last build"
- data += ' <tr class="LastBuild">\n'
- data += td(topleft, align="right", colspan=2, class_="Project")
- for b in builders:
- box = ITopBox(b).getBox(request)
- data += box.td(align="center")
- data += " </tr>\n"
-
- data += ' <tr class="Activity">\n'
- data += td('current activity', align='right', colspan=2)
- for b in builders:
- box = ICurrentBox(b).getBox(status)
- data += box.td(align="center")
- data += " </tr>\n"
-
- data += " <tr>\n"
- TZ = time.tzname[time.localtime()[-1]]
- data += td("time (%s)" % TZ, align="center", class_="Time")
- data += td('<a href="%s">changes</a>' % request.childLink("../changes"),
- align="center", class_="Change")
- for name in builderNames:
- safename = urllib.quote(name, safe='')
- data += td('<a href="%s">%s</a>' %
- (request.childLink("../builders/%s" % safename), name),
- align="center", class_="Builder")
- data += " </tr>\n"
-
- if phase == 1:
- f = self.phase1
- else:
- f = self.phase2
- data += f(request, changeNames + builderNames, timestamps, eventGrid,
- sourceEvents)
-
- data += "</table>\n"
-
- data += '<hr /><div class="footer">\n'
-
- def with_args(req, remove_args=[], new_args=[], new_path=None):
- # sigh, nevow makes this sort of manipulation easier
- newargs = req.args.copy()
- for argname in remove_args:
- newargs[argname] = []
- if "branch" in newargs:
- newargs["branch"] = [b for b in newargs["branch"] if b]
- for k,v in new_args:
- if k in newargs:
- newargs[k].append(v)
- else:
- newargs[k] = [v]
- newquery = "&".join(["%s=%s" % (k, v)
- for k in newargs
- for v in newargs[k]
- ])
- if new_path:
- new_url = new_path
- elif req.prepath:
- new_url = req.prepath[-1]
- else:
- new_url = ''
- if newquery:
- new_url += "?" + newquery
- return new_url
-
- if timestamps:
- bottom = timestamps[-1]
- nextpage = with_args(request, ["last_time"],
- [("last_time", str(int(bottom)))])
- data += '[<a href="%s">next page</a>]\n' % nextpage
-
- helpurl = self.path_to_root(request) + "waterfall/help"
- helppage = with_args(request, new_path=helpurl)
- data += '[<a href="%s">help</a>]\n' % helppage
-
- welcomeurl = self.path_to_root(request) + "index.html"
- data += '[<a href="%s">welcome</a>]\n' % welcomeurl
-
- if self.get_reload_time(request) is not None:
- no_reload_page = with_args(request, remove_args=["reload"])
- data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page
-
- data += "<br />\n"
-
-
- bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version)
- data += '<a href="%s">Buildbot-%s</a> ' % (bburl, version)
- if projectName:
- data += "working for the "
- if projectURL:
- data += '<a href="%s">%s</a> project.' % (projectURL,
- projectName)
- else:
- data += "%s project." % projectName
- data += "<br />\n"
- # TODO: push this to the right edge, if possible
- data += ("Page built: " +
- time.strftime("%a %d %b %Y %H:%M:%S",
- time.localtime(util.now()))
- + "\n")
- data += '</div>\n'
- return data
-
- def body0(self, request, builders):
- # build the waterfall display
- data = ""
- data += "<h2>Basic display</h2>\n"
- data += '<p>See <a href="%s">here</a>' % request.childLink("../waterfall")
- data += " for the waterfall display</p>\n"
-
- data += '<table border="0" cellspacing="0">\n'
- names = map(lambda builder: builder.name, builders)
-
- # the top row is two blank spaces, then the top-level status boxes
- data += " <tr>\n"
- data += td("", colspan=2)
- for b in builders:
- text = ""
- state, builds = b.getState()
- if state != "offline":
- text += "%s<br />\n" % state #b.getCurrentBig().text[0]
- else:
- text += "OFFLINE<br />\n"
- data += td(text, align="center")
-
- # the next row has the column headers: time, changes, builder names
- data += " <tr>\n"
- data += td("Time", align="center")
- data += td("Changes", align="center")
- for name in names:
- data += td('<a href="%s">%s</a>' %
- (request.childLink("../" + urllib.quote(name)), name),
- align="center")
- data += " </tr>\n"
-
- # all further rows involve timestamps, commit events, and build events
- data += " <tr>\n"
- data += td("04:00", align="bottom")
- data += td("fred", align="center")
- for name in names:
- data += td("stuff", align="center")
- data += " </tr>\n"
-
- data += "</table>\n"
- return data
-
- def buildGrid(self, request, builders):
- debug = False
- # TODO: see if we can use a cached copy
-
- showEvents = False
- if request.args.get("show_events", ["true"])[0].lower() == "true":
- showEvents = True
- filterBranches = [b for b in request.args.get("branch", []) if b]
- filterBranches = map_branches(filterBranches)
- maxTime = int(request.args.get("last_time", [util.now()])[0])
- if "show_time" in request.args:
- minTime = maxTime - int(request.args["show_time"][0])
- elif "first_time" in request.args:
- minTime = int(request.args["first_time"][0])
- else:
- minTime = None
- spanLength = 10 # ten-second chunks
- maxPageLen = int(request.args.get("num_events", [200])[0])
-
- # first step is to walk backwards in time, asking each column
- # (commit, all builders) if they have any events there. Build up the
- # array of events, and stop when we have a reasonable number.
-
- commit_source = self.getChangemaster(request)
-
- lastEventTime = util.now()
- sources = [commit_source] + builders
- changeNames = ["changes"]
- builderNames = map(lambda builder: builder.getName(), builders)
- sourceNames = changeNames + builderNames
- sourceEvents = []
- sourceGenerators = []
-
- def get_event_from(g):
- try:
- while True:
- e = g.next()
- # e might be builder.BuildStepStatus,
- # builder.BuildStatus, builder.Event,
- # waterfall.Spacer(builder.Event), or changes.Change .
- # The showEvents=False flag means we should hide
- # builder.Event .
- if not showEvents and isinstance(e, builder.Event):
- continue
- break
- event = interfaces.IStatusEvent(e)
- if debug:
- log.msg("gen %s gave1 %s" % (g, event.getText()))
- except StopIteration:
- event = None
- return event
-
- for s in sources:
- gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
- sourceGenerators.append(gen)
- # get the first event
- sourceEvents.append(get_event_from(gen))
- eventGrid = []
- timestamps = []
-
- lastEventTime = 0
- for e in sourceEvents:
- if e and e.getTimes()[0] > lastEventTime:
- lastEventTime = e.getTimes()[0]
- if lastEventTime == 0:
- lastEventTime = util.now()
-
- spanStart = lastEventTime - spanLength
- debugGather = 0
-
- while 1:
- if debugGather: log.msg("checking (%s,]" % spanStart)
- # the tableau of potential events is in sourceEvents[]. The
- # window crawls backwards, and we examine one source at a time.
- # If the source's top-most event is in the window, is it pushed
- # onto the events[] array and the tableau is refilled. This
- # continues until the tableau event is not in the window (or is
- # missing).
-
- spanEvents = [] # for all sources, in this span. row of eventGrid
- firstTimestamp = None # timestamp of first event in the span
- lastTimestamp = None # last pre-span event, for next span
-
- for c in range(len(sourceGenerators)):
- events = [] # for this source, in this span. cell of eventGrid
- event = sourceEvents[c]
- while event and spanStart < event.getTimes()[0]:
- # to look at windows that don't end with the present,
- # condition the .append on event.time <= spanFinish
- if not IBox(event, None):
- log.msg("BAD EVENT", event, event.getText())
- assert 0
- if debug:
- log.msg("pushing", event.getText(), event)
- events.append(event)
- starts, finishes = event.getTimes()
- firstTimestamp = util.earlier(firstTimestamp, starts)
- event = get_event_from(sourceGenerators[c])
- if debug:
- log.msg("finished span")
-
- if event:
- # this is the last pre-span event for this source
- lastTimestamp = util.later(lastTimestamp,
- event.getTimes()[0])
- if debugGather:
- log.msg(" got %s from %s" % (events, sourceNames[c]))
- sourceEvents[c] = event # refill the tableau
- spanEvents.append(events)
-
- # only show events older than maxTime. This makes it possible to
- # visit a page that shows what it would be like to scroll off the
- # bottom of this one.
- if firstTimestamp is not None and firstTimestamp <= maxTime:
- eventGrid.append(spanEvents)
- timestamps.append(firstTimestamp)
-
- if lastTimestamp:
- spanStart = lastTimestamp - spanLength
- else:
- # no more events
- break
- if minTime is not None and lastTimestamp < minTime:
- break
-
- if len(timestamps) > maxPageLen:
- break
-
-
- # now loop
-
- # loop is finished. now we have eventGrid[] and timestamps[]
- if debugGather: log.msg("finished loop")
- assert(len(timestamps) == len(eventGrid))
- return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
-
- def phase0(self, request, sourceNames, timestamps, eventGrid):
- # phase0 rendering
- if not timestamps:
- return "no events"
- data = ""
- for r in range(0, len(timestamps)):
- data += "<p>\n"
- data += "[%s]<br />" % timestamps[r]
- row = eventGrid[r]
- assert(len(row) == len(sourceNames))
- for c in range(0, len(row)):
- if row[c]:
- data += "<b>%s</b><br />\n" % sourceNames[c]
- for e in row[c]:
- log.msg("Event", r, c, sourceNames[c], e.getText())
- lognames = [loog.getName() for loog in e.getLogs()]
- data += "%s: %s: %s<br />" % (e.getText(),
- e.getTimes()[0],
- lognames)
- else:
- data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
- return data
-
- def phase1(self, request, sourceNames, timestamps, eventGrid,
- sourceEvents):
- # phase1 rendering: table, but boxes do not overlap
- data = ""
- if not timestamps:
- return data
- lastDate = None
- for r in range(0, len(timestamps)):
- chunkstrip = eventGrid[r]
- # chunkstrip is a horizontal strip of event blocks. Each block
- # is a vertical list of events, all for the same source.
- assert(len(chunkstrip) == len(sourceNames))
- maxRows = reduce(lambda x,y: max(x,y),
- map(lambda x: len(x), chunkstrip))
- for i in range(maxRows):
- data += " <tr>\n";
- if i == 0:
- stuff = []
- # add the date at the beginning, and each time it changes
- today = time.strftime("<b>%d %b %Y</b>",
- time.localtime(timestamps[r]))
- todayday = time.strftime("<b>%a</b>",
- time.localtime(timestamps[r]))
- if today != lastDate:
- stuff.append(todayday)
- stuff.append(today)
- lastDate = today
- stuff.append(
- time.strftime("%H:%M:%S",
- time.localtime(timestamps[r])))
- data += td(stuff, valign="bottom", align="center",
- rowspan=maxRows, class_="Time")
- for c in range(0, len(chunkstrip)):
- block = chunkstrip[c]
- assert(block != None) # should be [] instead
- # bottom-justify
- offset = maxRows - len(block)
- if i < offset:
- data += td("")
- else:
- e = block[i-offset]
- box = IBox(e).getBox(request)
- box.parms["show_idle"] = 1
- data += box.td(valign="top", align="center")
- data += " </tr>\n"
-
- return data
-
- def phase2(self, request, sourceNames, timestamps, eventGrid,
- sourceEvents):
- data = ""
- if not timestamps:
- return data
- # first pass: figure out the height of the chunks, populate grid
- grid = []
- for i in range(1+len(sourceNames)):
- grid.append([])
- # grid is a list of columns, one for the timestamps, and one per
- # event source. Each column is exactly the same height. Each element
- # of the list is a single <td> box.
- lastDate = time.strftime("<b>%d %b %Y</b>",
- time.localtime(util.now()))
- for r in range(0, len(timestamps)):
- chunkstrip = eventGrid[r]
- # chunkstrip is a horizontal strip of event blocks. Each block
- # is a vertical list of events, all for the same source.
- assert(len(chunkstrip) == len(sourceNames))
- maxRows = reduce(lambda x,y: max(x,y),
- map(lambda x: len(x), chunkstrip))
- for i in range(maxRows):
- if i != maxRows-1:
- grid[0].append(None)
- else:
- # timestamp goes at the bottom of the chunk
- stuff = []
- # add the date at the beginning (if it is not the same as
- # today's date), and each time it changes
- todayday = time.strftime("<b>%a</b>",
- time.localtime(timestamps[r]))
- today = time.strftime("<b>%d %b %Y</b>",
- time.localtime(timestamps[r]))
- if today != lastDate:
- stuff.append(todayday)
- stuff.append(today)
- lastDate = today
- stuff.append(
- time.strftime("%H:%M:%S",
- time.localtime(timestamps[r])))
- grid[0].append(Box(text=stuff, class_="Time",
- valign="bottom", align="center"))
-
- # at this point the timestamp column has been populated with
- # maxRows boxes, most None but the last one has the time string
- for c in range(0, len(chunkstrip)):
- block = chunkstrip[c]
- assert(block != None) # should be [] instead
- for i in range(maxRows - len(block)):
- # fill top of chunk with blank space
- grid[c+1].append(None)
- for i in range(len(block)):
- # so the events are bottom-justified
- b = IBox(block[i]).getBox(request)
- b.parms['valign'] = "top"
- b.parms['align'] = "center"
- grid[c+1].append(b)
- # now all the other columns have maxRows new boxes too
- # populate the last row, if empty
- gridlen = len(grid[0])
- for i in range(len(grid)):
- strip = grid[i]
- assert(len(strip) == gridlen)
- if strip[-1] == None:
- if sourceEvents[i-1]:
- filler = IBox(sourceEvents[i-1]).getBox(request)
- else:
- # this can happen if you delete part of the build history
- filler = Box(text=["?"], align="center")
- strip[-1] = filler
- strip[-1].parms['rowspan'] = 1
- # second pass: bubble the events upwards to un-occupied locations
- # Every square of the grid that has a None in it needs to have
- # something else take its place.
- noBubble = request.args.get("nobubble",['0'])
- noBubble = int(noBubble[0])
- if not noBubble:
- for col in range(len(grid)):
- strip = grid[col]
- if col == 1: # changes are handled differently
- for i in range(2, len(strip)+1):
- # only merge empty boxes. Don't bubble commit boxes.
- if strip[-i] == None:
- next = strip[-i+1]
- assert(next)
- if next:
- #if not next.event:
- if next.spacer:
- # bubble the empty box up
- strip[-i] = next
- strip[-i].parms['rowspan'] += 1
- strip[-i+1] = None
- else:
- # we are above a commit box. Leave it
- # be, and turn the current box into an
- # empty one
- strip[-i] = Box([], rowspan=1,
- comment="commit bubble")
- strip[-i].spacer = True
- else:
- # we are above another empty box, which
- # somehow wasn't already converted.
- # Shouldn't happen
- pass
- else:
- for i in range(2, len(strip)+1):
- # strip[-i] will go from next-to-last back to first
- if strip[-i] == None:
- # bubble previous item up
- assert(strip[-i+1] != None)
- strip[-i] = strip[-i+1]
- strip[-i].parms['rowspan'] += 1
- strip[-i+1] = None
- else:
- strip[-i].parms['rowspan'] = 1
- # third pass: render the HTML table
- for i in range(gridlen):
- data += " <tr>\n";
- for strip in grid:
- b = strip[i]
- if b:
- data += b.td()
- else:
- if noBubble:
- data += td([])
- # Nones are left empty, rowspan should make it all fit
- data += " </tr>\n"
- return data
-
diff --git a/buildbot/buildbot/status/web/xmlrpc.py b/buildbot/buildbot/status/web/xmlrpc.py
deleted file mode 100644
index 234e7ff..0000000
--- a/buildbot/buildbot/status/web/xmlrpc.py
+++ /dev/null
@@ -1,203 +0,0 @@
-
-from twisted.python import log
-from twisted.web import xmlrpc
-from buildbot.status.builder import Results
-from itertools import count
-
-class XMLRPCServer(xmlrpc.XMLRPC):
- def __init__(self):
- xmlrpc.XMLRPC.__init__(self)
-
- def render(self, req):
- # extract the IStatus and IControl objects for later use, since they
- # come from the request object. They'll be the same each time, but
- # they aren't available until the first request arrives.
- self.status = req.site.buildbot_service.getStatus()
- self.control = req.site.buildbot_service.getControl()
- return xmlrpc.XMLRPC.render(self, req)
-
- def xmlrpc_getAllBuilders(self):
- """Return a list of all builder names
- """
- log.msg("getAllBuilders")
- return self.status.getBuilderNames()
-
- def xmlrpc_getLastBuildResults(self, builder_name):
- """Return the result of the last build for the given builder
- """
- builder = self.status.getBuilder(builder_name)
- lastbuild = builder.getBuild(-1)
- return Results[lastbuild.getResults()]
-
- def xmlrpc_getLastBuilds(self, builder_name, num_builds):
- """Return the last N completed builds for the given builder.
- 'builder_name' is the name of the builder to query
- 'num_builds' is the number of builds to return
-
- Each build is returned in the same form as xmlrpc_getAllBuildsInInterval
- """
- log.msg("getLastBuilds: %s - %d" % (builder_name, num_builds))
- builder = self.status.getBuilder(builder_name)
- all_builds = []
- for build_number in range(1, num_builds+1):
- build = builder.getBuild(-build_number)
- if not build:
- break
- if not build.isFinished():
- continue
- (build_start, build_end) = build.getTimes()
-
- ss = build.getSourceStamp()
- branch = ss.branch
- if branch is None:
- branch = ""
- try:
- revision = build.getProperty("got_revision")
- except KeyError:
- revision = ""
- revision = str(revision)
-
- answer = (builder_name,
- build.getNumber(),
- build_end,
- branch,
- revision,
- Results[build.getResults()],
- build.getText(),
- )
- all_builds.append((build_end, answer))
-
- # now we've gotten all the builds we're interested in. Sort them by
- # end time.
- all_builds.sort(lambda a,b: cmp(a[0], b[0]))
- # and remove the timestamps
- all_builds = [t[1] for t in all_builds]
-
- log.msg("ready to go: %s" % (all_builds,))
-
- return all_builds
-
-
- def xmlrpc_getAllBuildsInInterval(self, start, stop):
- """Return a list of builds that have completed after the 'start'
- timestamp and before the 'stop' timestamp. This looks at all
- Builders.
-
- The timestamps are integers, interpreted as standard unix timestamps
- (seconds since epoch).
-
- Each Build is returned as a tuple in the form::
- (buildername, buildnumber, build_end, branchname, revision,
- results, text)
-
- The buildnumber is an integer. 'build_end' is an integer (seconds
- since epoch) specifying when the build finished.
-
- The branchname is a string, which may be an empty string to indicate
- None (i.e. the default branch). The revision is a string whose
- meaning is specific to the VC system in use, and comes from the
- 'got_revision' build property. The results are expressed as a string,
- one of ('success', 'warnings', 'failure', 'exception'). The text is a
- list of short strings that ought to be joined by spaces and include
- slightly more data about the results of the build.
- """
- #log.msg("start: %s %s %s" % (start, type(start), start.__class__))
- log.msg("getAllBuildsInInterval: %d - %d" % (start, stop))
- all_builds = []
-
- for builder_name in self.status.getBuilderNames():
- builder = self.status.getBuilder(builder_name)
- for build_number in count(1):
- build = builder.getBuild(-build_number)
- if not build:
- break
- if not build.isFinished():
- continue
- (build_start, build_end) = build.getTimes()
- # in reality, builds are mostly ordered by start time. For
- # the purposes of this method, we pretend that they are
- # strictly ordered by end time, so that we can stop searching
- # when we start seeing builds that are outside the window.
- if build_end > stop:
- continue # keep looking
- if build_end < start:
- break # stop looking
-
- ss = build.getSourceStamp()
- branch = ss.branch
- if branch is None:
- branch = ""
- try:
- revision = build.getProperty("got_revision")
- except KeyError:
- revision = ""
- revision = str(revision)
-
- answer = (builder_name,
- build.getNumber(),
- build_end,
- branch,
- revision,
- Results[build.getResults()],
- build.getText(),
- )
- all_builds.append((build_end, answer))
- # we've gotten all the builds that we care about from this
- # particular builder, so now we can continue on the next builder
-
- # now we've gotten all the builds we're interested in. Sort them by
- # end time.
- all_builds.sort(lambda a,b: cmp(a[0], b[0]))
- # and remove the timestamps
- all_builds = [t[1] for t in all_builds]
-
- log.msg("ready to go: %s" % (all_builds,))
-
- return all_builds
-
- def xmlrpc_getBuild(self, builder_name, build_number):
- """Return information about a specific build.
-
- """
- builder = self.status.getBuilder(builder_name)
- build = builder.getBuild(build_number)
- info = {}
- info['builder_name'] = builder.getName()
- info['url'] = self.status.getURLForThing(build) or ''
- info['reason'] = build.getReason()
- info['slavename'] = build.getSlavename()
- info['results'] = build.getResults()
- info['text'] = build.getText()
- # Added to help out requests for build -N
- info['number'] = build.number
- ss = build.getSourceStamp()
- branch = ss.branch
- if branch is None:
- branch = ""
- info['branch'] = str(branch)
- try:
- revision = str(build.getProperty("got_revision"))
- except KeyError:
- revision = ""
- info['revision'] = str(revision)
- info['start'], info['end'] = build.getTimes()
-
- info_steps = []
- for s in build.getSteps():
- stepinfo = {}
- stepinfo['name'] = s.getName()
- stepinfo['start'], stepinfo['end'] = s.getTimes()
- stepinfo['results'] = s.getResults()
- info_steps.append(stepinfo)
- info['steps'] = info_steps
-
- info_logs = []
- for l in build.getLogs():
- loginfo = {}
- loginfo['name'] = l.getStep().getName() + "/" + l.getName()
- #loginfo['text'] = l.getText()
- loginfo['text'] = "HUGE"
- info_logs.append(loginfo)
- info['logs'] = info_logs
- return info
-