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
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
152 @defer.inlineCallbacks
154 text = ""
155 res = ""
156
157 t = "tinderbox:"
158
159 tree = yield build.render(self.tree)
160 text += "%s tree: %s\n" % (t, tree)
161
162
163
164 builddate = int(build.getTimes()[0])
165
166
167 if self.useChangeTime:
168 try:
169 builddate = build.getChanges()[-1].when
170 except:
171 pass
172 text += "%s builddate: %s\n" % (t, builddate)
173 text += "%s status: " % t
174
175 if results == "building":
176 res = "building"
177 text += res
178 elif results == SUCCESS:
179 res = "success"
180 text += res
181 elif results == WARNINGS:
182 res = "testfailed"
183 text += res
184 elif results in (EXCEPTION, RETRY):
185 res = "exception"
186 text += res
187 else:
188 res += "busted"
189 text += res
190
191 text += "\n";
192
193 if self.columnName is None:
194
195 text += "%s build: %s\n" % (t, name)
196 else:
197 columnName = yield build.render(self.columnName)
198 text += "%s build: %s\n" % (t, columnName)
199 text += "%s errorparser: %s\n" % (t, self.errorparser)
200
201
202 if results == "building":
203 text += "%s END\n" % t
204
205 else:
206 text += "%s binaryurl: %s\n" % (t, self.binaryURL)
207 text += "%s logcompression: %s\n" % (t, self.logCompression)
208
209
210 logEncoding = ""
211 tinderboxLogs = ""
212 for bs in build.getSteps():
213
214
215 shortText = "%s\n" % ' '.join(bs.getText()).encode('ascii', 'replace')
216
217
218 if not re.match(".*[^\s].*", shortText):
219 continue
220
221
222 if re.match(".+TinderboxPrint.*", shortText):
223 shortText = shortText.replace("TinderboxPrint",
224 "Tinderbox Print")
225 logs = bs.getLogs()
226
227 tinderboxLogs += "======== BuildStep started ========\n"
228 tinderboxLogs += shortText
229 tinderboxLogs += "=== Output ===\n"
230 for log in logs:
231 logText = log.getTextWithHeaders()
232
233
234
235
236
237 for line in logText.splitlines():
238 if re.match(".+TinderboxPrint.*", line):
239 line = line.replace("TinderboxPrint",
240 "Tinderbox Print")
241 tinderboxLogs += line + "\n"
242
243 tinderboxLogs += "=== Output ended ===\n"
244 tinderboxLogs += "======== BuildStep ended ========\n"
245
246 if self.logCompression == "bzip2":
247 cLog = bz2.compress(tinderboxLogs)
248 tinderboxLogs = base64.encodestring(cLog)
249 logEncoding = "base64"
250 elif self.logCompression == "gzip":
251 cLog = cStringIO.StringIO()
252 gz = gzip.GzipFile(mode="w", fileobj=cLog)
253 gz.write(tinderboxLogs)
254 gz.close()
255 cLog = cLog.getvalue()
256 tinderboxLogs = base64.encodestring(cLog)
257 logEncoding = "base64"
258
259 text += "%s logencoding: %s\n" % (t, logEncoding)
260 text += "%s END\n\n" % t
261 text += tinderboxLogs
262 text += "\n"
263
264 m = Message()
265 m.set_payload(text)
266
267 m['Date'] = formatdate(localtime=True)
268 m['Subject'] = self.subject % { 'result': res,
269 'builder': name,
270 }
271 m['From'] = self.fromaddr
272
273
274 d = defer.DeferredList([])
275 d.addCallback(self._gotRecipients, self.extraRecipients, m)
276 defer.returnValue((yield d))
277