diff options
Diffstat (limited to 'buildbot/buildbot/status/web/baseweb.py')
-rw-r--r-- | buildbot/buildbot/status/web/baseweb.py | 614 |
1 files changed, 614 insertions, 0 deletions
diff --git a/buildbot/buildbot/status/web/baseweb.py b/buildbot/buildbot/status/web/baseweb.py new file mode 100644 index 0000000..a963a9a --- /dev/null +++ b/buildbot/buildbot/status/web/baseweb.py @@ -0,0 +1,614 @@ + +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)) |