import time
import os
import re
import sys
from twisted.web.resource import Resource
from buildbot.status.web import baseweb
from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS
class XmlResource(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 = 'Dummy'
link = 'http://dummylink'
language = 'en-us'
description = 'Dummy rss'
status = None
def __init__(self, status, categories=None):
self.status = status
self.categories = categories
self.link = self.status.getBuildbotURL()
self.title = 'Build status of ' + status.getProjectName()
self.description = 'List of FAILED builds'
self.pubdate = time.gmtime(int(time.time()))
def getBuilds(self, request):
builds = []
allBuilderNames = self.status.getBuilderNames(categories=self.categories)
builders = [self.status.getBuilder(name) for name in allBuilderNames]
showBuilders = request.args.get("show", [])
showBuilders.extend(request.args.get("builder", []))
if showBuilders:
builders = [b for b in builders if b.name in showBuilders]
showCategories = request.args.get("category", [])
if showCategories:
builders = [b for b in builders if b.category in showCategories]
maxFeeds = 25
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()
if results == FAILURE:
totalbuilds += 1
builds.append(build)
if totalbuilds >= maxFeeds:
break
if sys.version_info[:3] >= (2,4,0):
builds.sort(key=lambda build: build.getTimes(), reverse=True)
else:
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))
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()))
if build.getLogs():
log = build.getLogs()[-1]
laststep = log.getStep().getName()
try:
lastlog = log.getText()
except IOError:
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):
FeedResource.__init__(self, status, categories)
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 not None:
data += (' <title>%s</title>\n' % self.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'&', "&", lastlog)
lastlog = re.sub(r"'", "'", lastlog)
lastlog = re.sub(r'"', """, lastlog)
lastlog = re.sub(r'<', '<', lastlog)
lastlog = re.sub(r'>', '>', 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)
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):
FeedResource.__init__(self, status, categories)
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 not None:
data += (' <title>%s</title>\n' % self.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'&', "&", lastlog)
lastlog = re.sub(r"'", "'", lastlog)
lastlog = re.sub(r'"', """, lastlog)
lastlog = re.sub(r'<', '<', lastlog)
lastlog = re.sub(r'>', '>', 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)
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
class WebStatusWithFeeds(baseweb.WebStatus):
"""Override the standard WebStatus class to add RSS and Atom feeds.
This adds the following web resources in addition to /waterfall:
/rss
/atom
The same "branch" and "category" query arguments can be passed
as with /waterfall
e.g. http://mybot.buildbot.com:8012/rss?branch=&builder=builder-log4c-rhel-4-i386
or
http://mybot.buildbot.com:8012/rss?branch=&category=log4c
"""
def setupSite(self):
baseweb.WebStatus.setupSite(self)
status = self.parent.getStatus()
sr = self.site.resource
rss = Rss20StatusResource(status, categories=None)
sr.putChild("rss", rss)
atom = Atom10StatusResource(status, categories=None)
sr.putChild("atom", atom)