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 |