Package buildbot :: Package status :: Package web :: Module feeds
[hide private]
[frames] | no frames]

Source Code for Module buildbot.status.web.feeds

  1  # This module enables ATOM and RSS feeds from webstatus. 
  2  # 
  3  # It is based on "feeder.py" which was part of the Buildbot 
  4  # configuration for the Subversion project. The original file was 
  5  # created by Lieven Gobaerts and later adjusted by API 
  6  # (apinheiro@igalia.coma) and also here 
  7  # http://code.google.com/p/pybots/source/browse/trunk/master/Feeder.py 
  8  # 
  9  # All subsequent changes to feeder.py where made by Chandan-Dutta 
 10  # Chowdhury <chandan-dutta.chowdhury @ hp.com> and Gareth Armstrong 
 11  # <gareth.armstrong @ hp.com>. 
 12  # 
 13  # Those modifications are as follows: 
 14  # 1) the feeds are usable from baseweb.WebStatus 
 15  # 2) feeds are fully validated ATOM 1.0 and RSS 2.0 feeds, verified 
 16  #    with code from http://feedvalidator.org 
 17  # 3) nicer xml output 
 18  # 4) feeds can be filtered as per the /waterfall display with the 
 19  #    builder and category filters 
 20  # 5) cleaned up white space and imports 
 21  # 
 22  # Finally, the code was directly integrated into these two files, 
 23  # buildbot/status/web/feeds.py (you're reading it, ;-)) and 
 24  # buildbot/status/web/baseweb.py. 
 25   
 26  import os 
 27  import re 
 28  import sys 
 29  import time 
 30  from twisted.web import resource 
 31  from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION 
 32   
33 -class XmlResource(resource.Resource):
34 contentType = "text/xml; charset=UTF-8"
35 - def render(self, request):
36 data = self.content(request) 37 request.setHeader("content-type", self.contentType) 38 if request.method == "HEAD": 39 request.setHeader("content-length", len(data)) 40 return '' 41 return data
42 docType = ''
43 - def header (self, request):
44 data = ('<?xml version="1.0"?>\n') 45 return data
46 - def footer(self, request):
47 data = '' 48 return data
49 - def content(self, request):
50 data = self.docType 51 data += self.header(request) 52 data += self.body(request) 53 data += self.footer(request) 54 return data
55 - def body(self, request):
56 return ''
57
58 -class FeedResource(XmlResource):
59 title = None 60 link = 'http://dummylink' 61 language = 'en-us' 62 description = 'Dummy rss' 63 status = None 64
65 - def __init__(self, status, categories=None, title=None):
66 self.status = status 67 self.categories = categories 68 self.title = title 69 self.projectName = self.status.getProjectName() 70 self.link = self.status.getBuildbotURL() 71 self.description = 'List of FAILED builds' 72 self.pubdate = time.gmtime(int(time.time())) 73 self.user = self.getEnv(['USER', 'USERNAME'], 'buildmaster') 74 self.hostname = self.getEnv(['HOSTNAME', 'COMPUTERNAME'], 75 'buildmaster')
76
77 - def getEnv(self, keys, fallback):
78 for key in keys: 79 if key in os.environ: 80 return os.environ[key] 81 return fallback
82
83 - def getBuilds(self, request):
84 builds = [] 85 # THIS is lifted straight from the WaterfallStatusResource Class in 86 # status/web/waterfall.py 87 # 88 # we start with all Builders available to this Waterfall: this is 89 # limited by the config-file -time categories= argument, and defaults 90 # to all defined Builders. 91 allBuilderNames = self.status.getBuilderNames(categories=self.categories) 92 builders = [self.status.getBuilder(name) for name in allBuilderNames] 93 94 # but if the URL has one or more builder= arguments (or the old show= 95 # argument, which is still accepted for backwards compatibility), we 96 # use that set of builders instead. We still don't show anything 97 # outside the config-file time set limited by categories=. 98 showBuilders = request.args.get("show", []) 99 showBuilders.extend(request.args.get("builder", [])) 100 if showBuilders: 101 builders = [b for b in builders if b.name in showBuilders] 102 103 # now, if the URL has one or category= arguments, use them as a 104 # filter: only show those builders which belong to one of the given 105 # categories. 106 showCategories = request.args.get("category", []) 107 if showCategories: 108 builders = [b for b in builders if b.category in showCategories] 109 110 maxFeeds = 25 111 112 # Copy all failed builds in a new list. 113 # This could clearly be implemented much better if we had 114 # access to a global list of builds. 115 for b in builders: 116 lastbuild = b.getLastFinishedBuild() 117 if lastbuild is None: 118 continue 119 120 lastnr = lastbuild.getNumber() 121 122 totalbuilds = 0 123 i = lastnr 124 while i >= 0: 125 build = b.getBuild(i) 126 i -= 1 127 if not build: 128 continue 129 130 results = build.getResults() 131 132 # only add entries for failed builds! 133 if results == FAILURE: 134 totalbuilds += 1 135 builds.append(build) 136 137 # stop for this builder when our total nr. of feeds is reached 138 if totalbuilds >= maxFeeds: 139 break 140 141 # Sort build list by date, youngest first. 142 # To keep compatibility with python < 2.4, use this for sorting instead: 143 # We apply Decorate-Sort-Undecorate 144 deco = [(build.getTimes(), build) for build in builds] 145 deco.sort() 146 deco.reverse() 147 builds = [build for (b1, build) in deco] 148 149 if builds: 150 builds = builds[:min(len(builds), maxFeeds)] 151 return builds
152
153 - def body (self, request):
154 data = '' 155 builds = self.getBuilds(request) 156 157 for build in builds: 158 start, finished = build.getTimes() 159 finishedTime = time.gmtime(int(finished)) 160 link = re.sub(r'index.html', "", self.status.getURLForThing(build)) 161 162 # title: trunk r22191 (plus patch) failed on 'i686-debian-sarge1 shared gcc-3.3.5' 163 ss = build.getSourceStamp() 164 source = "" 165 if ss.branch: 166 source += "Branch %s " % ss.branch 167 if ss.revision: 168 source += "Revision %s " % str(ss.revision) 169 if ss.patch: 170 source += " (plus patch)" 171 if ss.changes: 172 pass 173 if (ss.branch is None and ss.revision is None and ss.patch is None 174 and not ss.changes): 175 source += "Latest revision " 176 got_revision = None 177 try: 178 got_revision = build.getProperty("got_revision") 179 except KeyError: 180 pass 181 if got_revision: 182 got_revision = str(got_revision) 183 if len(got_revision) > 40: 184 got_revision = "[revision string too long]" 185 source += "(Got Revision: %s)" % got_revision 186 title = ('%s failed on "%s"' % 187 (source, build.getBuilder().getName())) 188 189 # get name of the failed step and the last 30 lines of its log. 190 if build.getLogs(): 191 log = build.getLogs()[-1] 192 laststep = log.getStep().getName() 193 try: 194 lastlog = log.getText() 195 except IOError: 196 # Probably the log file has been removed 197 lastlog='<b>log file not available</b>' 198 199 lines = re.split('\n', lastlog) 200 lastlog = '' 201 for logline in lines[max(0, len(lines)-30):]: 202 lastlog = lastlog + logline + '<br/>' 203 lastlog = lastlog.replace('\n', '<br/>') 204 205 description = '' 206 description += ('Date: %s<br/><br/>' % 207 time.strftime("%a, %d %b %Y %H:%M:%S GMT", 208 finishedTime)) 209 description += ('Full details available here: <a href="%s">%s</a><br/>' % 210 (self.link, self.projectName)) 211 builder_summary_link = ('%s/builders/%s' % 212 (re.sub(r'/index.html', '', self.link), 213 build.getBuilder().getName())) 214 description += ('Build summary: <a href="%s">%s</a><br/><br/>' % 215 (builder_summary_link, 216 build.getBuilder().getName())) 217 description += ('Build details: <a href="%s">%s</a><br/><br/>' % 218 (link, link)) 219 description += ('Author list: <b>%s</b><br/><br/>' % 220 ",".join(build.getResponsibleUsers())) 221 description += ('Failed step: <b>%s</b><br/><br/>' % laststep) 222 description += 'Last lines of the build log:<br/>' 223 224 data += self.item(title, description=description, lastlog=lastlog, 225 link=link, pubDate=finishedTime) 226 227 return data
228
229 - def item(self, title='', link='', description='', pubDate=''):
230 """Generates xml for one item in the feed."""
231
232 -class Rss20StatusResource(FeedResource):
233 - def __init__(self, status, categories=None, title=None):
234 FeedResource.__init__(self, status, categories, title) 235 contentType = 'application/rss+xml'
236
237 - def header(self, request):
238 data = FeedResource.header(self, request) 239 data += ('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n') 240 data += (' <channel>\n') 241 if self.title is None: 242 title = 'Build status of ' + self.projectName 243 else: 244 title = self.title 245 data += (' <title>%s</title>\n' % title) 246 if self.link is not None: 247 data += (' <link>%s</link>\n' % self.link) 248 link = re.sub(r'/index.html', '', self.link) 249 data += (' <atom:link href="%s/rss" rel="self" type="application/rss+xml"/>\n' % link) 250 if self.language is not None: 251 data += (' <language>%s</language>\n' % self.language) 252 if self.description is not None: 253 data += (' <description>%s</description>\n' % self.description) 254 if self.pubdate is not None: 255 rfc822_pubdate = time.strftime("%a, %d %b %Y %H:%M:%S GMT", 256 self.pubdate) 257 data += (' <pubDate>%s</pubDate>\n' % rfc822_pubdate) 258 return data
259
260 - def item(self, title='', link='', description='', lastlog='', pubDate=''):
261 data = (' <item>\n') 262 data += (' <title>%s</title>\n' % title) 263 if link is not None: 264 data += (' <link>%s</link>\n' % link) 265 if (description is not None and lastlog is not None): 266 lastlog = re.sub(r'<br/>', "\n", lastlog) 267 lastlog = re.sub(r'&', "&amp;", lastlog) 268 lastlog = re.sub(r"'", "&apos;", lastlog) 269 lastlog = re.sub(r'"', "&quot;", lastlog) 270 lastlog = re.sub(r'<', '&lt;', lastlog) 271 lastlog = re.sub(r'>', '&gt;', lastlog) 272 lastlog = lastlog.replace('\n', '<br/>') 273 content = '<![CDATA[' 274 content += description 275 content += lastlog 276 content += ']]>' 277 data += (' <description>%s</description>\n' % content) 278 if pubDate is not None: 279 rfc822pubDate = time.strftime("%a, %d %b %Y %H:%M:%S GMT", 280 pubDate) 281 data += (' <pubDate>%s</pubDate>\n' % rfc822pubDate) 282 # Every RSS item must have a globally unique ID 283 guid = ('tag:%s@%s,%s:%s' % (self.user, self.hostname, 284 time.strftime("%Y-%m-%d", pubDate), 285 time.strftime("%Y%m%d%H%M%S", 286 pubDate))) 287 data += (' <guid isPermaLink="false">%s</guid>\n' % guid) 288 data += (' </item>\n') 289 return data
290
291 - def footer(self, request):
292 data = (' </channel>\n' 293 '</rss>') 294 return data
295
296 -class Atom10StatusResource(FeedResource):
297 - def __init__(self, status, categories=None, title=None):
298 FeedResource.__init__(self, status, categories, title) 299 contentType = 'application/atom+xml'
300
301 - def header(self, request):
302 data = FeedResource.header(self, request) 303 data += '<feed xmlns="http://www.w3.org/2005/Atom">\n' 304 data += (' <id>%s</id>\n' % self.link) 305 if self.title is None: 306 title = 'Build status of ' + self.projectName 307 else: 308 title = self.title 309 data += (' <title>%s</title>\n' % title) 310 if self.link is not None: 311 link = re.sub(r'/index.html', '', self.link) 312 data += (' <link rel="self" href="%s/atom"/>\n' % link) 313 data += (' <link rel="alternate" href="%s/"/>\n' % link) 314 if self.description is not None: 315 data += (' <subtitle>%s</subtitle>\n' % self.description) 316 if self.pubdate is not None: 317 rfc3339_pubdate = time.strftime("%Y-%m-%dT%H:%M:%SZ", 318 self.pubdate) 319 data += (' <updated>%s</updated>\n' % rfc3339_pubdate) 320 data += (' <author>\n') 321 data += (' <name>Build Bot</name>\n') 322 data += (' </author>\n') 323 return data
324
325 - def item(self, title='', link='', description='', lastlog='', pubDate=''):
326 data = (' <entry>\n') 327 data += (' <title>%s</title>\n' % title) 328 if link is not None: 329 data += (' <link href="%s"/>\n' % link) 330 if (description is not None and lastlog is not None): 331 lastlog = re.sub(r'<br/>', "\n", lastlog) 332 lastlog = re.sub(r'&', "&amp;", lastlog) 333 lastlog = re.sub(r"'", "&apos;", lastlog) 334 lastlog = re.sub(r'"', "&quot;", lastlog) 335 lastlog = re.sub(r'<', '&lt;', lastlog) 336 lastlog = re.sub(r'>', '&gt;', lastlog) 337 data += (' <content type="xhtml">\n') 338 data += (' <div xmlns="http://www.w3.org/1999/xhtml">\n') 339 data += (' %s\n' % description) 340 data += (' <pre xml:space="preserve">%s</pre>\n' % lastlog) 341 data += (' </div>\n') 342 data += (' </content>\n') 343 if pubDate is not None: 344 rfc3339pubDate = time.strftime("%Y-%m-%dT%H:%M:%SZ", 345 pubDate) 346 data += (' <updated>%s</updated>\n' % rfc3339pubDate) 347 # Every Atom entry must have a globally unique ID 348 # http://diveintomark.org/archives/2004/05/28/howto-atom-id 349 guid = ('tag:%s@%s,%s:%s' % (self.user, self.hostname, 350 time.strftime("%Y-%m-%d", pubDate), 351 time.strftime("%Y%m%d%H%M%S", 352 pubDate))) 353 data += (' <id>%s</id>\n' % guid) 354 data += (' <author>\n') 355 data += (' <name>Build Bot</name>\n') 356 data += (' </author>\n') 357 data += (' </entry>\n') 358 return data
359
360 - def footer(self, request):
361 data = ('</feed>') 362 return data
363