3
# Licensed to the Apache Software Foundation (ASF) under one
4
# or more contributor license agreements. See the NOTICE file
5
# distributed with this work for additional information
6
# regarding copyright ownership. The ASF licenses this file
7
# to you under the Apache License, Version 2.0 (the
8
# "License"); you may not use this file except in compliance
9
# with the License. You may obtain a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing,
14
# software distributed under the License is distributed on an
15
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
# KIND, either express or implied. See the License for the
17
# specific language governing permissions and limitations
21
# This file is part of the Buildbot configuration for the Subversion project.
22
# The original file was created by Lieven Govaerts
24
# Minor changes made by API (apinheiro@igalia.com) in order to fit with our
25
# configuration and last buildbot changes
27
# Minor whitespace clean up, clean up imports, adapted to buildbot 0.7.7,
28
# and finally attempt to create valid atom and RSS feeds.
29
# Changes by Chandan-Dutta Chowdhury <chandan-dutta chowdhury @ hp com> and
30
# Gareth Armstrong <gareth armstrong @ hp com>
31
# Also integrate changes from
32
# http://code.google.com/p/pybots/source/browse/trunk/master/Feeder.py
33
# which adds ability to filter RSS feeds to specific builders.
34
# e.g. http://localhost:8012/rss?builder=builder-log4c-rhel-4-i386
41
from twisted.web.resource import Resource
43
from buildbot.status.web import baseweb
44
from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS
46
class XmlResource(Resource):
47
contentType = "text/xml; charset=UTF-8"
48
def render(self, request):
49
data = self.content(request)
50
request.setHeader("content-type", self.contentType)
51
if request.method == "HEAD":
52
request.setHeader("content-length", len(data))
56
def header (self, request):
57
data = ('<?xml version="1.0"?>\n')
59
def footer(self, request):
62
def content(self, request):
64
data += self.header(request)
65
data += self.body(request)
66
data += self.footer(request)
68
def body(self, request):
71
class FeedResource(XmlResource):
73
link = 'http://dummylink'
75
description = 'Dummy rss'
78
def __init__(self, status, categories=None):
80
self.categories = categories
81
self.link = self.status.getBuildbotURL()
82
self.title = 'Build status of ' + status.getProjectName()
83
self.description = 'List of FAILED builds'
84
self.pubdate = time.gmtime(int(time.time()))
86
def getBuilds(self, request):
88
# THIS is lifted straight from the WaterfallStatusResource Class in
89
# status/web/waterfall.py
91
# we start with all Builders available to this Waterfall: this is
92
# limited by the config-file -time categories= argument, and defaults
93
# to all defined Builders.
94
allBuilderNames = self.status.getBuilderNames(categories=self.categories)
95
builders = [self.status.getBuilder(name) for name in allBuilderNames]
97
# but if the URL has one or more builder= arguments (or the old show=
98
# argument, which is still accepted for backwards compatibility), we
99
# use that set of builders instead. We still don't show anything
100
# outside the config-file time set limited by categories=.
101
showBuilders = request.args.get("show", [])
102
showBuilders.extend(request.args.get("builder", []))
104
builders = [b for b in builders if b.name in showBuilders]
106
# now, if the URL has one or category= arguments, use them as a
107
# filter: only show those builders which belong to one of the given
109
showCategories = request.args.get("category", [])
111
builders = [b for b in builders if b.category in showCategories]
115
# Copy all failed builds in a new list.
116
# This could clearly be implemented much better if we had
117
# access to a global list of builds.
119
lastbuild = b.getLastFinishedBuild()
120
if lastbuild is None:
123
lastnr = lastbuild.getNumber()
128
build = b.getBuild(i)
133
results = build.getResults()
135
# only add entries for failed builds!
136
if results == FAILURE:
140
# stop for this builder when our total nr. of feeds is reached
141
if totalbuilds >= maxFeeds:
144
# Sort build list by date, youngest first.
145
if sys.version_info[:3] >= (2,4,0):
146
builds.sort(key=lambda build: build.getTimes(), reverse=True)
148
# If you need compatibility with python < 2.4, use this for
150
# We apply Decorate-Sort-Undecorate
151
deco = [(build.getTimes(), build) for build in builds]
154
builds = [build for (b1, build) in deco]
157
builds = builds[:min(len(builds), maxFeeds)]
160
def body (self, request):
162
builds = self.getBuilds(request)
165
start, finished = build.getTimes()
166
finishedTime = time.gmtime(int(finished))
167
projectName = self.status.getProjectName()
168
link = re.sub(r'index.html', "", self.status.getURLForThing(build))
170
# title: trunk r862265 (plus patch) failed on 'i686-debian-sarge1 shared gcc-3.3.5'
171
ss = build.getSourceStamp()
174
source += "Branch %s " % ss.branch
176
source += "Revision %s " % str(ss.revision)
178
source += " (plus patch)"
181
if (ss.branch is None and ss.revision is None and ss.patch is None
183
source += "Latest revision "
186
got_revision = build.getProperty("got_revision")
190
got_revision = str(got_revision)
191
if len(got_revision) > 40:
192
got_revision = "[revision string too long]"
193
source += "(Got Revision: %s)" % got_revision
194
title = ('%s failed on "%s"' %
195
(source, build.getBuilder().getName()))
197
# get name of the failed step and the last 30 lines of its log.
199
log = build.getLogs()[-1]
200
laststep = log.getStep().getName()
202
lastlog = log.getText()
204
# Probably the log file has been removed
205
lastlog='<b>log file not available</b>'
207
lines = re.split('\n', lastlog)
209
for logline in lines[max(0, len(lines)-30):]:
210
lastlog = lastlog + logline + '<br/>'
211
lastlog = lastlog.replace('\n', '<br/>')
214
description += ('Date: %s<br/><br/>' %
215
time.strftime("%a, %d %b %Y %H:%M:%S GMT",
217
description += ('Full details available here: <a href="%s">%s</a><br/>' % (self.link, projectName))
218
builder_summary_link = ('%s/builders/%s' %
219
(re.sub(r'/index.html', '', self.link),
220
build.getBuilder().getName()))
221
description += ('Build summary: <a href="%s">%s</a><br/><br/>' %
222
(builder_summary_link,
223
build.getBuilder().getName()))
224
description += ('Build details: <a href="%s">%s</a><br/><br/>' %
225
(link, self.link + link[1:]))
226
description += ('Author list: <b>%s</b><br/><br/>' %
227
",".join(build.getResponsibleUsers()))
228
description += ('Failed step: <b>%s</b><br/><br/>' % laststep)
229
description += 'Last lines of the build log:<br/>'
231
data += self.item(title, description=description, lastlog=lastlog,
232
link=link, pubDate=finishedTime)
236
def item(self, title='', link='', description='', pubDate=''):
237
"""Generates xml for one item in the feed."""
239
class Rss20StatusResource(FeedResource):
240
def __init__(self, status, categories=None):
241
FeedResource.__init__(self, status, categories)
242
contentType = 'application/rss+xml'
244
def header(self, request):
245
data = FeedResource.header(self, request)
246
data += ('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n')
247
data += (' <channel>\n')
248
if self.title is not None:
249
data += (' <title>%s</title>\n' % self.title)
250
if self.link is not None:
251
data += (' <link>%s</link>\n' % self.link)
252
link = re.sub(r'/index.html', '', self.link)
253
data += (' <atom:link href="%s/rss" rel="self" type="application/rss+xml"/>\n' % link)
254
if self.language is not None:
255
data += (' <language>%s</language>\n' % self.language)
256
if self.description is not None:
257
data += (' <description>%s</description>\n' % self.description)
258
if self.pubdate is not None:
259
rfc822_pubdate = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
261
data += (' <pubDate>%s</pubDate>\n' % rfc822_pubdate)
264
def item(self, title='', link='', description='', lastlog='', pubDate=''):
266
data += (' <title>%s</title>\n' % title)
268
data += (' <link>%s</link>\n' % link)
269
if (description is not None and lastlog is not None):
270
lastlog = re.sub(r'<br/>', "\n", lastlog)
271
lastlog = re.sub(r'&', "&", lastlog)
272
lastlog = re.sub(r"'", "'", lastlog)
273
lastlog = re.sub(r'"', """, lastlog)
274
lastlog = re.sub(r'<', '<', lastlog)
275
lastlog = re.sub(r'>', '>', lastlog)
276
lastlog = lastlog.replace('\n', '<br/>')
277
content = '<![CDATA['
278
content += description
281
data += (' <description>%s</description>\n' % content)
282
if pubDate is not None:
283
rfc822pubDate = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
285
data += (' <pubDate>%s</pubDate>\n' % rfc822pubDate)
286
# Every RSS item must have a globally unique ID
287
guid = ('tag:%s@%s,%s:%s' % (os.environ['USER'],
288
os.environ['HOSTNAME'],
289
time.strftime("%Y-%m-%d", pubDate),
290
time.strftime("%Y%m%d%H%M%S",
292
data += (' <guid isPermaLink="false">%s</guid>\n' % guid)
293
data += (' </item>\n')
296
def footer(self, request):
297
data = (' </channel>\n'
301
class Atom10StatusResource(FeedResource):
302
def __init__(self, status, categories=None):
303
FeedResource.__init__(self, status, categories)
304
contentType = 'application/atom+xml'
306
def header(self, request):
307
data = FeedResource.header(self, request)
308
data += '<feed xmlns="http://www.w3.org/2005/Atom">\n'
309
data += (' <id>%s</id>\n' % self.status.getBuildbotURL())
310
if self.title is not None:
311
data += (' <title>%s</title>\n' % self.title)
312
if self.link is not None:
313
link = re.sub(r'/index.html', '', self.link)
314
data += (' <link rel="self" href="%s/atom"/>\n' % link)
315
data += (' <link rel="alternate" href="%s/"/>\n' % link)
316
if self.description is not None:
317
data += (' <subtitle>%s</subtitle>\n' % self.description)
318
if self.pubdate is not None:
319
rfc3339_pubdate = time.strftime("%Y-%m-%dT%H:%M:%SZ",
321
data += (' <updated>%s</updated>\n' % rfc3339_pubdate)
322
data += (' <author>\n')
323
data += (' <name>Build Bot</name>\n')
324
data += (' </author>\n')
327
def item(self, title='', link='', description='', lastlog='', pubDate=''):
328
data = (' <entry>\n')
329
data += (' <title>%s</title>\n' % title)
331
data += (' <link href="%s"/>\n' % link)
332
if (description is not None and lastlog is not None):
333
lastlog = re.sub(r'<br/>', "\n", lastlog)
334
lastlog = re.sub(r'&', "&", lastlog)
335
lastlog = re.sub(r"'", "'", lastlog)
336
lastlog = re.sub(r'"', """, lastlog)
337
lastlog = re.sub(r'<', '<', lastlog)
338
lastlog = re.sub(r'>', '>', lastlog)
339
data += (' <content type="xhtml">\n')
340
data += (' <div xmlns="http://www.w3.org/1999/xhtml">\n')
341
data += (' %s\n' % description)
342
data += (' <pre xml:space="preserve">%s</pre>\n' % lastlog)
343
data += (' </div>\n')
344
data += (' </content>\n')
345
if pubDate is not None:
346
rfc3339pubDate = time.strftime("%Y-%m-%dT%H:%M:%SZ",
348
data += (' <updated>%s</updated>\n' % rfc3339pubDate)
349
# Every Atom entry must have a globally unique ID
350
# http://diveintomark.org/archives/2004/05/28/howto-atom-id
351
guid = ('tag:%s@%s,%s:%s' % (os.environ['USER'],
352
os.environ['HOSTNAME'],
353
time.strftime("%Y-%m-%d", pubDate),
354
time.strftime("%Y%m%d%H%M%S",
356
data += (' <id>%s</id>\n' % guid)
357
data += (' <author>\n')
358
data += (' <name>Build Bot</name>\n')
359
data += (' </author>\n')
360
data += (' </entry>\n')
363
def footer(self, request):
367
class WebStatusWithFeeds(baseweb.WebStatus):
368
"""Override the standard WebStatus class to add RSS and Atom feeds.
370
This adds the following web resources in addition to /waterfall:
374
The same "branch" and "category" query arguments can be passed
376
e.g. http://mybot.buildbot.com:8012/rss?branch=&builder=builder-log4c-rhel-4-i386
378
http://mybot.buildbot.com:8012/rss?branch=&category=log4c
382
baseweb.WebStatus.setupSite(self)
384
status = self.parent.getStatus()
385
sr = self.site.resource
387
rss = Rss20StatusResource(status, categories=None)
388
sr.putChild("rss", rss)
389
atom = Atom10StatusResource(status, categories=None)
390
sr.putChild("atom", atom)