1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

# This file is part of Buildbot.  Buildbot is free software: you can 

# redistribute it and/or modify it under the terms of the GNU General Public 

# License as published by the Free Software Foundation, version 2. 

# 

# This program is distributed in the hope that it will be useful, but WITHOUT 

# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 

# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 

# details. 

# 

# You should have received a copy of the GNU General Public License along with 

# this program; if not, write to the Free Software Foundation, Inc., 51 

# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 

# 

# Copyright Buildbot Team Members 

 

# This module enables ATOM and RSS feeds from webstatus. 

# 

# It is based on "feeder.py" which was part of the Buildbot 

# configuration for the Subversion project. The original file was 

# created by Lieven Gobaerts and later adjusted by API 

# (apinheiro@igalia.coma) and also here 

# http://code.google.com/p/pybots/source/browse/trunk/master/Feeder.py 

# 

# All subsequent changes to feeder.py where made by Chandan-Dutta 

# Chowdhury <chandan-dutta.chowdhury @ hp.com> and Gareth Armstrong 

# <gareth.armstrong @ hp.com>. 

# 

# Those modifications are as follows: 

# 1) the feeds are usable from baseweb.WebStatus 

# 2) feeds are fully validated ATOM 1.0 and RSS 2.0 feeds, verified 

#    with code from http://feedvalidator.org 

# 3) nicer xml output 

# 4) feeds can be filtered as per the /waterfall display with the 

#    builder and category filters 

# 5) cleaned up white space and imports 

# 

# Finally, the code was directly integrated into these two files, 

# buildbot/status/web/feeds.py (you're reading it, ;-)) and 

# buildbot/status/web/baseweb.py. 

 

import os 

import re 

import time 

from twisted.web import resource 

from buildbot.status.builder import FAILURE 

 

class XmlResource(resource.Resource): 

    contentType = "text/xml; charset=UTF-8" 

    docType = '' 

 

    def getChild(self, name, request): 

        return self 

 

    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 

 

_abbr_day = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 

_abbr_mon = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 

            'Sep', 'Oct', 'Nov', 'Dec'] 

 

def rfc822_time(tstamp): 

    res = time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", 

                                       tstamp) 

    res = res % (tstamp.tm_wday, tstamp.tm_mon) 

    return res 

 

class FeedResource(XmlResource): 

    pageTitle = None 

    link = 'http://dummylink' 

    language = 'en-us' 

    description = 'Dummy rss' 

    status = None 

 

    def __init__(self, status, categories=None, pageTitle=None): 

        self.status = status 

        self.categories = categories 

        self.pageTitle = pageTitle 

        self.title = self.status.getTitle() 

        self.link = self.status.getBuildbotURL() 

        self.description = 'List of builds' 

        self.pubdate = time.gmtime(int(time.time())) 

        self.user = self.getEnv(['USER', 'USERNAME'], 'buildmaster') 

        self.hostname = self.getEnv(['HOSTNAME', 'COMPUTERNAME'], 

                                    'buildmaster') 

        self.children = {} 

 

    def getEnv(self, keys, fallback): 

        for key in keys: 

            if key in os.environ: 

                return os.environ[key] 

        return fallback 

 

    def getBuilds(self, request): 

        builds = [] 

        # THIS is lifted straight from the WaterfallStatusResource Class in 

        # status/web/waterfall.py 

        # 

        # we start with all Builders available to this Waterfall: this is 

        # limited by the config-file -time categories= argument, and defaults 

        # to all defined Builders. 

        allBuilderNames = self.status.getBuilderNames(categories=self.categories) 

        builders = [self.status.getBuilder(name) for name in allBuilderNames] 

 

        # but if the URL has one or more builder= arguments (or the old show= 

        # argument, which is still accepted for backwards compatibility), we 

        # use that set of builders instead. We still don't show anything 

        # outside the config-file time set limited by categories=. 

        showBuilders = request.args.get("show", []) 

        showBuilders.extend(request.args.get("builder", [])) 

        if showBuilders: 

            builders = [b for b in builders if b.name in showBuilders] 

 

        # now, if the URL has one or category= arguments, use them as a 

        # filter: only show those builders which belong to one of the given 

        # categories. 

        showCategories = request.args.get("category", []) 

        if showCategories: 

            builders = [b for b in builders if b.category in showCategories] 

 

        failures_only = request.args.get("failures_only", "false") 

 

        maxFeeds = 25 

 

        # Copy all failed builds in a new list. 

        # This could clearly be implemented much better if we had 

        # access to a global list of builds. 

        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 failures_only == "false" or results == FAILURE: 

                    totalbuilds += 1 

                    builds.append(build) 

 

                # stop for this builder when our total nr. of feeds is reached 

                if totalbuilds >= maxFeeds: 

                    break 

 

        # Sort build list by date, youngest first. 

        # To keep compatibility with python < 2.4, use this for sorting instead: 

        # We apply Decorate-Sort-Undecorate 

        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 content(self, request): 

        builds = self.getBuilds(request) 

 

        build_cxts = [] 

 

        for build in builds: 

            start, finished = build.getTimes() 

            finishedTime = time.gmtime(int(finished)) 

            link = re.sub(r'index.html', "", self.status.getURLForThing(build)) 

 

            # title: trunk r22191 (plus patch) failed on 

            # 'i686-debian-sarge1 shared gcc-3.3.5' 

            ss_list = build.getSourceStamps() 

            all_got_revisions = build.getAllGotRevisions() 

            src_cxts = [] 

            for ss in ss_list: 

                sc = {} 

                sc['codebase'] = ss.codebase 

                if (ss.branch is None and ss.revision is None and ss.patch is None 

                    and not ss.changes): 

                    sc['repository'] = None 

                    sc['branch'] = None 

                    sc['revision'] = "Latest revision" 

                else: 

                    sc['repository'] = ss.repository 

                    sc['branch'] = ss.branch 

                    got_revision = all_got_revisions.get(ss.codebase, None) 

                    if got_revision: 

                        sc['revision'] = got_revision 

                    else: 

                        sc['revision'] = str(ss.revision) 

                if ss.patch: 

                    sc['revision'] += " (plus patch)" 

                if ss.changes: 

                    pass 

                src_cxts.append(sc) 

            failflag = (build.getResults() != FAILURE) 

            pageTitle = ('Builder "%s" has %s' % 

                (build.getBuilder().getName(), ["failed","succeeded"][failflag],) 

                        ) 

 

            # Add information about the failing steps. 

            failed_steps = [] 

            log_lines = [] 

            for s in build.getSteps(): 

                if s.getResults()[0] == FAILURE: 

                    failed_steps.append(s.getName()) 

 

                    # Add the last 30 lines of each log. 

                    for log in s.getLogs(): 

                        log_lines.append('Last lines of build log "%s":' % 

                                         log.getName()) 

                        log_lines.append([]) 

                        try: 

                            logdata = log.getText() 

                        except IOError: 

                            # Probably the log file has been removed 

                            logdata ='** log file not available **' 

                        unilist = list() 

                        for line in logdata.split('\n')[-30:]: 

                            unilist.append(unicode(line,'utf-8')) 

                        log_lines.extend(unilist) 

 

            bc = {} 

            bc['sources'] = src_cxts 

            bc['date'] = rfc822_time(finishedTime) 

            bc['summary_link'] = ('%sbuilders/%s' % 

                                  (self.link, 

                                   build.getBuilder().getName())) 

            bc['name'] = build.getBuilder().getName() 

            bc['number'] = build.getNumber() 

            bc['responsible_users'] = build.getResponsibleUsers() 

            bc['failed_steps'] = failed_steps 

            bc['pageTitle'] = pageTitle 

            bc['link'] = link 

            bc['log_lines'] = log_lines 

 

            if finishedTime is not None: 

                bc['rfc822_pubdate'] = rfc822_time(finishedTime) 

                bc['rfc3339_pubdate'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", 

                                               finishedTime) 

 

                # Every RSS/Atom item must have a globally unique ID 

                guid = ('tag:%s@%s,%s:%s' % 

                        (self.user, self.hostname, 

                         time.strftime("%Y-%m-%d", finishedTime), 

                         time.strftime("%Y%m%d%H%M%S", finishedTime))) 

                bc['guid'] = guid 

 

            build_cxts.append(bc) 

 

        pageTitle = self.pageTitle 

        if not pageTitle: 

            pageTitle = 'Build status of %s' % self.title 

 

        cxt = {} 

        cxt['pageTitle'] = pageTitle 

        cxt['title_url'] = self.link 

        cxt['title'] = self.title 

        cxt['language'] = self.language 

        cxt['description'] = self.description 

        if self.pubdate is not None: 

            cxt['rfc822_pubdate'] = rfc822_time( self.pubdate) 

            cxt['rfc3339_pubdate'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", 

                                                   self.pubdate) 

 

        cxt['builds'] = build_cxts 

        template = request.site.buildbot_service.templates.get_template(self.template_file) 

        return template.render(**cxt).encode('utf-8').strip() 

 

class Rss20StatusResource(FeedResource): 

    # contentType = 'application/rss+xml' (browser dependent) 

    template_file = 'feed_rss20.xml' 

 

    def __init__(self, status, categories=None, pageTitle=None): 

        FeedResource.__init__(self, status, categories, pageTitle) 

 

class Atom10StatusResource(FeedResource): 

    # contentType = 'application/atom+xml' (browser dependent) 

    template_file = 'feed_atom10.xml' 

 

    def __init__(self, status, categories=None, pageTitle=None): 

        FeedResource.__init__(self, status, categories, pageTitle)