1
2 from email.Message import Message
3 from email.Utils import formatdate
4
5 from zope.interface import implements
6 from twisted.internet import defer
7
8 from buildbot import interfaces
9 from buildbot.status import mail
10 from buildbot.status.builder import SUCCESS, WARNINGS, EXCEPTION, RETRY
11 from buildbot.steps.shell import WithProperties
12
13 import gzip, bz2, base64, re, cStringIO
14
15
16
17
19 """This is a Tinderbox status notifier. It can send e-mail to a number of
20 different tinderboxes or people. E-mails are sent at the beginning and
21 upon completion of each build. It can be configured to send out e-mails
22 for only certain builds.
23
24 The most basic usage is as follows::
25 TinderboxMailNotifier(fromaddr="buildbot@localhost",
26 tree="MyTinderboxTree",
27 extraRecipients=["tinderboxdaemon@host.org"])
28
29 The builder name (as specified in master.cfg) is used as the "build"
30 tinderbox option.
31
32 """
33 implements(interfaces.IEmailSender)
34
35 compare_attrs = ["extraRecipients", "fromaddr", "categories", "builders",
36 "addLogs", "relayhost", "subject", "binaryURL", "tree",
37 "logCompression", "errorparser", "columnName",
38 "useChangeTime"]
39
40 - def __init__(self, fromaddr, tree, extraRecipients,
41 categories=None, builders=None, relayhost="localhost",
42 subject="buildbot %(result)s in %(builder)s", binaryURL="",
43 logCompression="", errorparser="unix", columnName=None,
44 useChangeTime=False):
45 """
46 @type fromaddr: string
47 @param fromaddr: the email address to be used in the 'From' header.
48
49 @type tree: string
50 @param tree: The Tinderbox tree to post to.
51 When tree is a WithProperties instance it will be
52 interpolated as such. See WithProperties for more detail
53
54 @type extraRecipients: tuple of string
55 @param extraRecipients: E-mail addresses of recipients. This should at
56 least include the tinderbox daemon.
57
58 @type categories: list of strings
59 @param categories: a list of category names to serve status
60 information for. Defaults to None (all
61 categories). Use either builders or categories,
62 but not both.
63
64 @type builders: list of strings
65 @param builders: a list of builder names for which mail should be
66 sent. Defaults to None (send mail for all builds).
67 Use either builders or categories, but not both.
68
69 @type relayhost: string
70 @param relayhost: the host to which the outbound SMTP connection
71 should be made. Defaults to 'localhost'
72
73 @type subject: string
74 @param subject: a string to be used as the subject line of the message.
75 %(builder)s will be replaced with the name of the
76 %builder which provoked the message.
77 This parameter is not significant for the tinderbox
78 daemon.
79
80 @type binaryURL: string
81 @param binaryURL: If specified, this should be the location where final
82 binary for a build is located.
83 (ie. http://www.myproject.org/nightly/08-08-2006.tgz)
84 It will be posted to the Tinderbox.
85
86 @type logCompression: string
87 @param logCompression: The type of compression to use on the log.
88 Valid options are"bzip2" and "gzip". gzip is
89 only known to work on Python 2.4 and above.
90
91 @type errorparser: string
92 @param errorparser: The error parser that the Tinderbox server
93 should use when scanning the log file.
94 Default is "unix".
95
96 @type columnName: string
97 @param columnName: When columnName is None, use the buildername as
98 the Tinderbox column name. When columnName is a
99 string this exact string will be used for all
100 builders that this TinderboxMailNotifier cares
101 about (not recommended). When columnName is a
102 WithProperties instance it will be interpolated
103 as such. See WithProperties for more detail.
104 @type useChangeTime: bool
105 @param useChangeTime: When True, the time of the first Change for a
106 build is used as the builddate. When False,
107 the current time is used as the builddate.
108 """
109
110 mail.MailNotifier.__init__(self, fromaddr, categories=categories,
111 builders=builders, relayhost=relayhost,
112 subject=subject,
113 extraRecipients=extraRecipients,
114 sendToInterestedUsers=False)
115 assert isinstance(tree, basestring) \
116 or isinstance(tree, WithProperties), \
117 "tree must be a string or a WithProperties instance"
118 self.tree = tree
119 self.binaryURL = binaryURL
120 self.logCompression = logCompression
121 self.errorparser = errorparser
122 self.useChangeTime = useChangeTime
123 assert columnName is None or type(columnName) is str \
124 or isinstance(columnName, WithProperties), \
125 "columnName must be None, a string, or a WithProperties instance"
126 self.columnName = columnName
127
136
138 text = ""
139 res = ""
140
141 t = "tinderbox:"
142
143 if type(self.tree) is str:
144
145 text += "%s tree: %s\n" % (t, self.tree)
146 elif isinstance(self.tree, WithProperties):
147
148 text += "%s tree: %s\n" % (t, build.getProperties().render(self.tree))
149 else:
150 raise Exception("tree is an unhandled value")
151
152
153
154 builddate = int(build.getTimes()[0])
155
156
157 if self.useChangeTime:
158 try:
159 builddate = build.getChanges()[-1].when
160 except:
161 pass
162 text += "%s builddate: %s\n" % (t, builddate)
163 text += "%s status: " % t
164
165 if results == "building":
166 res = "building"
167 text += res
168 elif results == SUCCESS:
169 res = "success"
170 text += res
171 elif results == WARNINGS:
172 res = "testfailed"
173 text += res
174 elif results in (EXCEPTION, RETRY):
175 res = "exception"
176 text += res
177 else:
178 res += "busted"
179 text += res
180
181 text += "\n";
182
183 if self.columnName is None:
184
185 text += "%s build: %s\n" % (t, name)
186 elif type(self.columnName) is str:
187
188 text += "%s build: %s\n" % (t, self.columnName)
189 elif isinstance(self.columnName, WithProperties):
190
191 text += "%s build: %s\n" % (t, build.getProperties().render(self.columnName))
192 else:
193 raise Exception("columnName is an unhandled value")
194 text += "%s errorparser: %s\n" % (t, self.errorparser)
195
196
197 if results == "building":
198 text += "%s END\n" % t
199
200 else:
201 text += "%s binaryurl: %s\n" % (t, self.binaryURL)
202 text += "%s logcompression: %s\n" % (t, self.logCompression)
203
204
205 logEncoding = ""
206 tinderboxLogs = ""
207 for bs in build.getSteps():
208
209
210 shortText = "%s\n" % ' '.join(bs.getText()).encode('ascii', 'replace')
211
212
213 if not re.match(".*[^\s].*", shortText):
214 continue
215
216
217 if re.match(".+TinderboxPrint.*", shortText):
218 shortText = shortText.replace("TinderboxPrint",
219 "Tinderbox Print")
220 logs = bs.getLogs()
221
222 tinderboxLogs += "======== BuildStep started ========\n"
223 tinderboxLogs += shortText
224 tinderboxLogs += "=== Output ===\n"
225 for log in logs:
226 logText = log.getTextWithHeaders()
227
228
229
230
231
232 for line in logText.splitlines():
233 if re.match(".+TinderboxPrint.*", line):
234 line = line.replace("TinderboxPrint",
235 "Tinderbox Print")
236 tinderboxLogs += line + "\n"
237
238 tinderboxLogs += "=== Output ended ===\n"
239 tinderboxLogs += "======== BuildStep ended ========\n"
240
241 if self.logCompression == "bzip2":
242 cLog = bz2.compress(tinderboxLogs)
243 tinderboxLogs = base64.encodestring(cLog)
244 logEncoding = "base64"
245 elif self.logCompression == "gzip":
246 cLog = cStringIO.StringIO()
247 gz = gzip.GzipFile(mode="w", fileobj=cLog)
248 gz.write(tinderboxLogs)
249 gz.close()
250 cLog = cLog.getvalue()
251 tinderboxLogs = base64.encodestring(cLog)
252 logEncoding = "base64"
253
254 text += "%s logencoding: %s\n" % (t, logEncoding)
255 text += "%s END\n\n" % t
256 text += tinderboxLogs
257 text += "\n"
258
259 m = Message()
260 m.set_payload(text)
261
262 m['Date'] = formatdate(localtime=True)
263 m['Subject'] = self.subject % { 'result': res,
264 'builder': name,
265 }
266 m['From'] = self.fromaddr
267
268
269 d = defer.DeferredList([])
270 d.addCallback(self._gotRecipients, self.extraRecipients, m)
271 return d
272