| OLD | NEW |
| (Empty) |
| 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, html | |
| 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 sha
red 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 description = '' | |
| 190 description += ('Date: %s<br/><br/>' % | |
| 191 time.strftime("%a, %d %b %Y %H:%M:%S GMT", | |
| 192 finishedTime)) | |
| 193 description += ('Full details available here: <a href="%s">%s</a><br
/>' % | |
| 194 (self.link, self.projectName)) | |
| 195 builder_summary_link = ('%s/builders/%s' % | |
| 196 (re.sub(r'/index.html', '', self.link), | |
| 197 build.getBuilder().getName())) | |
| 198 description += ('Build summary: <a href="%s">%s</a><br/><br/>' % | |
| 199 (builder_summary_link, | |
| 200 build.getBuilder().getName())) | |
| 201 description += ('Build details: <a href="%s">%s</a><br/><br/>' % | |
| 202 (link, link)) | |
| 203 description += ('Author list: <b>%s</b><br/><br/>' % | |
| 204 ",".join(build.getResponsibleUsers())) | |
| 205 | |
| 206 # Add information about the failing steps. | |
| 207 lastlog = '' | |
| 208 for s in build.getSteps(): | |
| 209 if s.getResults()[0] == FAILURE: | |
| 210 description += ('Failed step: <b>%s</b><br/>' % s.getName()) | |
| 211 | |
| 212 # Add the last 30 lines of each log. | |
| 213 for log in s.getLogs(): | |
| 214 lastlog += ('Last lines of build log "%s":<br/>' % log.g
etName()) | |
| 215 try: | |
| 216 logdata = log.getText() | |
| 217 except IOError: | |
| 218 # Probably the log file has been removed | |
| 219 logdata ='<b>log file not available</b>' | |
| 220 | |
| 221 lastlines = logdata.split('\n')[-30:] | |
| 222 lastlog += '<br/>'.join(lastlines) | |
| 223 lastlog += '<br/>' | |
| 224 description += '<br/>' | |
| 225 | |
| 226 data += self.item(title, description=description, lastlog=lastlog, | |
| 227 link=link, pubDate=finishedTime) | |
| 228 | |
| 229 return data | |
| 230 | |
| 231 def item(self, title='', link='', description='', pubDate=''): | |
| 232 """Generates xml for one item in the feed.""" | |
| 233 | |
| 234 class Rss20StatusResource(FeedResource): | |
| 235 def __init__(self, status, categories=None, title=None): | |
| 236 FeedResource.__init__(self, status, categories, title) | |
| 237 contentType = 'application/rss+xml' | |
| 238 | |
| 239 def header(self, request): | |
| 240 data = FeedResource.header(self, request) | |
| 241 data += ('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n
') | |
| 242 data += (' <channel>\n') | |
| 243 if self.title is None: | |
| 244 title = 'Build status of ' + self.projectName | |
| 245 else: | |
| 246 title = self.title | |
| 247 data += (' <title>%s</title>\n' % title) | |
| 248 if self.link is not None: | |
| 249 data += (' <link>%s</link>\n' % self.link) | |
| 250 link = re.sub(r'/index.html', '', self.link) | |
| 251 data += (' <atom:link href="%s/rss" rel="self" type="application/rss+
xml"/>\n' % link) | |
| 252 if self.language is not None: | |
| 253 data += (' <language>%s</language>\n' % self.language) | |
| 254 if self.description is not None: | |
| 255 data += (' <description>%s</description>\n' % self.description) | |
| 256 if self.pubdate is not None: | |
| 257 rfc822_pubdate = time.strftime("%a, %d %b %Y %H:%M:%S GMT", | |
| 258 self.pubdate) | |
| 259 data += (' <pubDate>%s</pubDate>\n' % rfc822_pubdate) | |
| 260 return data | |
| 261 | |
| 262 def item(self, title='', link='', description='', lastlog='', pubDate=''): | |
| 263 data = (' <item>\n') | |
| 264 data += (' <title>%s</title>\n' % title) | |
| 265 if link is not None: | |
| 266 data += (' <link>%s</link>\n' % link) | |
| 267 if (description is not None and lastlog is not None): | |
| 268 lastlog = lastlog.replace('<br/>', '\n') | |
| 269 lastlog = html.escape(lastlog) | |
| 270 lastlog = lastlog.replace('\n', '<br/>') | |
| 271 content = '<![CDATA[' | |
| 272 content += description | |
| 273 content += lastlog | |
| 274 content += ']]>' | |
| 275 data += (' <description>%s</description>\n' % content) | |
| 276 if pubDate is not None: | |
| 277 rfc822pubDate = time.strftime("%a, %d %b %Y %H:%M:%S GMT", | |
| 278 pubDate) | |
| 279 data += (' <pubDate>%s</pubDate>\n' % rfc822pubDate) | |
| 280 # Every RSS item must have a globally unique ID | |
| 281 guid = ('tag:%s@%s,%s:%s' % (self.user, self.hostname, | |
| 282 time.strftime("%Y-%m-%d", pubDate), | |
| 283 time.strftime("%Y%m%d%H%M%S", | |
| 284 pubDate))) | |
| 285 data += (' <guid isPermaLink="false">%s</guid>\n' % guid) | |
| 286 data += (' </item>\n') | |
| 287 return data | |
| 288 | |
| 289 def footer(self, request): | |
| 290 data = (' </channel>\n' | |
| 291 '</rss>') | |
| 292 return data | |
| 293 | |
| 294 class Atom10StatusResource(FeedResource): | |
| 295 def __init__(self, status, categories=None, title=None): | |
| 296 FeedResource.__init__(self, status, categories, title) | |
| 297 contentType = 'application/atom+xml' | |
| 298 | |
| 299 def header(self, request): | |
| 300 data = FeedResource.header(self, request) | |
| 301 data += '<feed xmlns="http://www.w3.org/2005/Atom">\n' | |
| 302 data += (' <id>%s</id>\n' % self.link) | |
| 303 if self.title is None: | |
| 304 title = 'Build status of ' + self.projectName | |
| 305 else: | |
| 306 title = self.title | |
| 307 data += (' <title>%s</title>\n' % title) | |
| 308 if self.link is not None: | |
| 309 link = re.sub(r'/index.html', '', self.link) | |
| 310 data += (' <link rel="self" href="%s/atom"/>\n' % link) | |
| 311 data += (' <link rel="alternate" href="%s/"/>\n' % link) | |
| 312 if self.description is not None: | |
| 313 data += (' <subtitle>%s</subtitle>\n' % self.description) | |
| 314 if self.pubdate is not None: | |
| 315 rfc3339_pubdate = time.strftime("%Y-%m-%dT%H:%M:%SZ", | |
| 316 self.pubdate) | |
| 317 data += (' <updated>%s</updated>\n' % rfc3339_pubdate) | |
| 318 data += (' <author>\n') | |
| 319 data += (' <name>Build Bot</name>\n') | |
| 320 data += (' </author>\n') | |
| 321 return data | |
| 322 | |
| 323 def item(self, title='', link='', description='', lastlog='', pubDate=''): | |
| 324 data = (' <entry>\n') | |
| 325 data += (' <title>%s</title>\n' % title) | |
| 326 if link is not None: | |
| 327 data += (' <link href="%s"/>\n' % link) | |
| 328 if (description is not None and lastlog is not None): | |
| 329 lastlog = lastlog.replace('<br/>', '\n') | |
| 330 lastlog = html.escape(lastlog) | |
| 331 lastlog = lastlog.replace('\n', '<br/>') | |
| 332 data += (' <content type="xhtml">\n') | |
| 333 data += (' <div xmlns="http://www.w3.org/1999/xhtml">\n') | |
| 334 data += (' %s\n' % description) | |
| 335 data += (' <pre xml:space="preserve">%s</pre>\n' % lastlog) | |
| 336 data += (' </div>\n') | |
| 337 data += (' </content>\n') | |
| 338 if pubDate is not None: | |
| 339 rfc3339pubDate = time.strftime("%Y-%m-%dT%H:%M:%SZ", | |
| 340 pubDate) | |
| 341 data += (' <updated>%s</updated>\n' % rfc3339pubDate) | |
| 342 # Every Atom entry must have a globally unique ID | |
| 343 # http://diveintomark.org/archives/2004/05/28/howto-atom-id | |
| 344 guid = ('tag:%s@%s,%s:%s' % (self.user, self.hostname, | |
| 345 time.strftime("%Y-%m-%d", pubDate), | |
| 346 time.strftime("%Y%m%d%H%M%S", | |
| 347 pubDate))) | |
| 348 data += (' <id>%s</id>\n' % guid) | |
| 349 data += (' <author>\n') | |
| 350 data += (' <name>Build Bot</name>\n') | |
| 351 data += (' </author>\n') | |
| 352 data += (' </entry>\n') | |
| 353 return data | |
| 354 | |
| 355 def footer(self, request): | |
| 356 data = ('</feed>') | |
| 357 return data | |
| OLD | NEW |