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

# 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 

 

 

from email.Message import Message 

from email.Utils import formatdate 

 

from zope.interface import implements 

from twisted.internet import defer 

 

from buildbot import interfaces 

from buildbot.status import mail 

from buildbot.status.results import SUCCESS, WARNINGS, EXCEPTION, RETRY 

from buildbot.steps.shell import WithProperties 

 

import gzip, bz2, base64, re, cStringIO 

 

# TODO: docs, maybe a test of some sort just to make sure it actually imports 

# and can format email without raising an exception. 

 

class TinderboxMailNotifier(mail.MailNotifier): 

    """This is a Tinderbox status notifier. It can send e-mail to a number of 

    different tinderboxes or people. E-mails are sent at the beginning and 

    upon completion of each build. It can be configured to send out e-mails 

    for only certain builds. 

 

    The most basic usage is as follows:: 

        TinderboxMailNotifier(fromaddr="buildbot@localhost", 

                              tree="MyTinderboxTree", 

                              extraRecipients=["tinderboxdaemon@host.org"]) 

 

    The builder name (as specified in master.cfg) is used as the "build" 

    tinderbox option. 

 

    """ 

    implements(interfaces.IEmailSender) 

 

    compare_attrs = ["extraRecipients", "fromaddr", "categories", "builders", 

                     "addLogs", "relayhost", "subject", "binaryURL", "tree", 

                     "logCompression", "errorparser", "columnName", 

                     "useChangeTime"] 

 

    def __init__(self, fromaddr, tree, extraRecipients, 

                 categories=None, builders=None, relayhost="localhost", 

                 subject="buildbot %(result)s in %(builder)s", binaryURL="", 

                 logCompression="", errorparser="unix", columnName=None, 

                 useChangeTime=False): 

        """ 

        @type  fromaddr: string 

        @param fromaddr: the email address to be used in the 'From' header. 

 

        @type  tree: string 

        @param tree: The Tinderbox tree to post to. 

                     When tree is a WithProperties instance it will be 

                     interpolated as such. See WithProperties for more detail 

 

        @type  extraRecipients: tuple of string 

        @param extraRecipients: E-mail addresses of recipients. This should at 

                                least include the tinderbox daemon. 

 

        @type  categories: list of strings 

        @param categories: a list of category names to serve status 

                           information for. Defaults to None (all 

                           categories). Use either builders or categories, 

                           but not both. 

 

        @type  builders: list of strings 

        @param builders: a list of builder names for which mail should be 

                         sent. Defaults to None (send mail for all builds). 

                         Use either builders or categories, but not both. 

 

        @type  relayhost: string 

        @param relayhost: the host to which the outbound SMTP connection 

                          should be made. Defaults to 'localhost' 

 

        @type  subject: string 

        @param subject: a string to be used as the subject line of the message. 

                        %(builder)s will be replaced with the name of the 

                        %builder which provoked the message. 

                        This parameter is not significant for the tinderbox 

                        daemon. 

 

        @type  binaryURL: string 

        @param binaryURL: If specified, this should be the location where final 

                          binary for a build is located. 

                          (ie. http://www.myproject.org/nightly/08-08-2006.tgz) 

                          It will be posted to the Tinderbox. 

 

        @type  logCompression: string 

        @param logCompression: The type of compression to use on the log. 

                               Valid options are"bzip2" and "gzip". gzip is 

                               only known to work on Python 2.4 and above. 

 

        @type  errorparser: string 

        @param errorparser: The error parser that the Tinderbox server 

                            should use when scanning the log file. 

                            Default is "unix". 

 

        @type  columnName: string 

        @param columnName: When columnName is None, use the buildername as 

                           the Tinderbox column name. When columnName is a 

                           string this exact string will be used for all 

                           builders that this TinderboxMailNotifier cares 

                           about (not recommended). When columnName is a 

                           WithProperties instance it will be interpolated 

                           as such. See WithProperties for more detail. 

        @type  useChangeTime: bool 

        @param useChangeTime: When True, the time of the first Change for a 

                              build is used as the builddate. When False, 

                              the current time is used as the builddate. 

        """ 

 

        mail.MailNotifier.__init__(self, fromaddr, categories=categories, 

                                   builders=builders, relayhost=relayhost, 

                                   subject=subject, 

                                   extraRecipients=extraRecipients, 

                                   sendToInterestedUsers=False) 

        assert isinstance(tree, basestring) \ 

            or isinstance(tree, WithProperties), \ 

            "tree must be a string or a WithProperties instance" 

        self.tree = tree 

        self.binaryURL = binaryURL 

        self.logCompression = logCompression 

        self.errorparser = errorparser 

        self.useChangeTime = useChangeTime 

        assert columnName is None or type(columnName) is str \ 

            or isinstance(columnName, WithProperties), \ 

            "columnName must be None, a string, or a WithProperties instance" 

        self.columnName = columnName 

 

    def buildStarted(self, name, build): 

        builder = build.getBuilder() 

        if self.builders is not None and name not in self.builders: 

            return # ignore this Build 

        if self.categories is not None and \ 

                builder.category not in self.categories: 

            return # ignore this build 

        self.buildMessage(name, build, "building") 

 

    @defer.inlineCallbacks 

    def buildMessage(self, name, build, results): 

        text = "" 

        res = "" 

        # shortform 

        t = "tinderbox:" 

 

        tree = yield build.render(self.tree) 

        text += "%s tree: %s\n" % (t, tree) 

 

        # the start time 

        # getTimes() returns a fractioned time that tinderbox doesn't understand 

        builddate = int(build.getTimes()[0]) 

        # attempt to pull a Change time from this Build's Changes. 

        # if that doesn't work, fall back on the current time 

        if self.useChangeTime: 

            try: 

                builddate = build.getChanges()[-1].when 

            except: 

                pass 

        text += "%s builddate: %s\n" % (t, builddate) 

        text += "%s status: " % t 

 

        if results == "building": 

            res = "building" 

            text += res 

        elif results == SUCCESS: 

            res = "success" 

            text += res 

        elif results == WARNINGS: 

            res = "testfailed" 

            text += res 

        elif results in (EXCEPTION, RETRY): 

            res = "exception" 

            text += res 

        else: 

            res += "busted" 

            text += res 

 

        text += "\n"; 

 

        if self.columnName is None: 

            # use the builder name 

            text += "%s build: %s\n" % (t, name) 

        else: 

            columnName = yield build.render(self.columnName) 

            text += "%s build: %s\n" % (t, columnName) 

        text += "%s errorparser: %s\n" % (t, self.errorparser) 

 

        # if the build just started... 

        if results == "building": 

            text += "%s END\n" % t 

        # if the build finished... 

        else: 

            text += "%s binaryurl: %s\n" % (t, self.binaryURL) 

            text += "%s logcompression: %s\n" % (t, self.logCompression) 

 

            # logs will always be appended 

            logEncoding = "" 

            tinderboxLogs = "" 

            for bs in build.getSteps(): 

                # Make sure that shortText is a regular string, so that bad 

                # data in the logs don't generate UnicodeDecodeErrors 

                shortText = "%s\n" % ' '.join(bs.getText()).encode('ascii', 'replace') 

 

                # ignore steps that haven't happened 

                if not re.match(".*[^\s].*", shortText): 

                    continue 

                # we ignore TinderboxPrint's here so we can do things like: 

                # ShellCommand(command=['echo', 'TinderboxPrint:', ...]) 

                if re.match(".+TinderboxPrint.*", shortText): 

                    shortText = shortText.replace("TinderboxPrint", 

                                                  "Tinderbox Print") 

                logs = bs.getLogs() 

 

                tinderboxLogs += "======== BuildStep started ========\n" 

                tinderboxLogs += shortText 

                tinderboxLogs += "=== Output ===\n" 

                for log in logs: 

                    logText = log.getTextWithHeaders() 

                    # Because we pull in the log headers here we have to ignore 

                    # some of them. Basically, if we're TinderboxPrint'ing in 

                    # a ShellCommand, the only valid one(s) are at the start 

                    # of a line. The others are prendeded by whitespace, quotes, 

                    # or brackets/parentheses 

                    for line in logText.splitlines(): 

                        if re.match(".+TinderboxPrint.*", line): 

                            line = line.replace("TinderboxPrint", 

                                                "Tinderbox Print") 

                        tinderboxLogs += line + "\n" 

 

                tinderboxLogs += "=== Output ended ===\n" 

                tinderboxLogs += "======== BuildStep ended ========\n" 

 

            if self.logCompression == "bzip2": 

                cLog = bz2.compress(tinderboxLogs) 

                tinderboxLogs = base64.encodestring(cLog) 

                logEncoding = "base64" 

            elif self.logCompression == "gzip": 

                cLog = cStringIO.StringIO() 

                gz = gzip.GzipFile(mode="w", fileobj=cLog) 

                gz.write(tinderboxLogs) 

                gz.close() 

                cLog = cLog.getvalue() 

                tinderboxLogs = base64.encodestring(cLog) 

                logEncoding = "base64" 

 

            text += "%s logencoding: %s\n" % (t, logEncoding) 

            text += "%s END\n\n" % t 

            text += tinderboxLogs 

            text += "\n" 

 

        m = Message() 

        m.set_payload(text) 

 

        m['Date'] = formatdate(localtime=True) 

        m['Subject'] = self.subject % { 'result': res, 

                                        'builder': name, 

                                        } 

        m['From'] = self.fromaddr 

        # m['To'] is added later 

 

        d = defer.DeferredList([]) 

        d.addCallback(self._gotRecipients, self.extraRecipients, m) 

        defer.returnValue((yield d))