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