OLD | NEW |
---|---|
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import base64 | |
5 import json | 6 import json |
6 import os | 7 import os |
7 import re | 8 import re |
8 import shutil | 9 import shutil |
9 | 10 |
10 from StringIO import StringIO | 11 from StringIO import StringIO |
11 | 12 |
12 try: | 13 try: |
13 # Create a block to work around evil sys.modules manipulation in | 14 # Create a block to work around evil sys.modules manipulation in |
14 # email/__init__.py that triggers pylint false positives. | 15 # email/__init__.py that triggers pylint false positives. |
15 # pylint: disable=E0611,F0401 | 16 # pylint: disable=E0611,F0401 |
16 from email.Message import Message | 17 from email.Message import Message |
17 from email.Utils import formatdate | 18 from email.Utils import formatdate |
18 except ImportError: | 19 except ImportError: |
19 raise | 20 raise |
20 | 21 |
21 from buildbot.process.properties import Properties | 22 from buildbot.process.properties import Properties |
22 from buildbot.schedulers.trysched import TryBase | 23 from buildbot.schedulers.trysched import TryBase |
23 | 24 |
24 from twisted.internet import defer, reactor, utils | 25 from twisted.internet import defer, reactor, utils |
25 from twisted.mail.smtp import SMTPSenderFactory | 26 from twisted.mail.smtp import SMTPSenderFactory |
26 from twisted.python import log | 27 from twisted.python import log |
27 | 28 |
29 from common.twisted_util.response import StringResponse | |
30 from master import gitiles_poller | |
28 from master.try_job_base import BadJobfile | 31 from master.try_job_base import BadJobfile |
29 | 32 |
30 | 33 |
31 class InvalidEtcBuild(BadJobfile): | 34 class InvalidEtcBuild(BadJobfile): |
32 pass | 35 pass |
33 | 36 |
34 | 37 |
35 def get_file_contents(poller, branch, file_path): | |
36 """Returns a Deferred to returns the file's content.""" | |
37 return utils.getProcessOutput( | |
38 poller.gitbin, | |
39 ['show', 'origin/%s:%s' % (branch, file_path)], | |
40 path=poller.workdir, | |
41 ) | |
42 | |
43 | |
44 def translate_v1_to_v2(parsed_job): | 38 def translate_v1_to_v2(parsed_job): |
45 """Translate tryjob desc from V1 to V2.""" | 39 """Translate tryjob desc from V1 to V2.""" |
46 parsed_job.setdefault('extra_args', []).append('--remote-trybot') | 40 parsed_job.setdefault('extra_args', []).append('--remote-trybot') |
47 parsed_job['version'] = 2 | 41 parsed_job['version'] = 2 |
48 | 42 |
49 | 43 |
50 def translate_v2_to_v3(parsed_job): | 44 def translate_v2_to_v3(parsed_job): |
51 """Translate tryjob desc from V2 to V3.""" | 45 """Translate tryjob desc from V2 to V3.""" |
52 # V3 --remote-patches format is not backwards compatible. | 46 # V3 --remote-patches format is not backwards compatible. |
53 if any(a.startswith('--remote-patches') | 47 if any(a.startswith('--remote-patches') |
(...skipping 15 matching lines...) Expand all Loading... | |
69 _TRANSLATION_FUNCS = { | 63 _TRANSLATION_FUNCS = { |
70 1 : translate_v1_to_v2, | 64 1 : translate_v1_to_v2, |
71 2 : translate_v2_to_v3, | 65 2 : translate_v2_to_v3, |
72 } | 66 } |
73 | 67 |
74 # Valid 'etc' builder targets. Specifically, this ensures: | 68 # Valid 'etc' builder targets. Specifically, this ensures: |
75 # - The build name doesn't begin with a flag ('--') | 69 # - The build name doesn't begin with a flag ('--') |
76 # - The build name doesn't contain spaces (to spill into extra args). | 70 # - The build name doesn't contain spaces (to spill into extra args). |
77 ETC_TARGET_RE = re.compile(r'^[a-zA-Z][\w-]+\w$') | 71 ETC_TARGET_RE = re.compile(r'^[a-zA-Z][\w-]+\w$') |
78 | 72 |
73 # Template path URL component to retrieve the Base64 contents of a file from | |
74 # Gitiles. | |
75 _GITILES_PATH_TMPL = '%(repo)s/+/%(revision)s/%(path)s?format=text' | |
76 | |
79 @classmethod | 77 @classmethod |
80 def updateJobDesc(cls, parsed_job): | 78 def updateJobDesc(cls, parsed_job): |
81 """Ensure job description is in the format we expect.""" | 79 """Ensure job description is in the format we expect.""" |
82 while parsed_job['version'] < cls._TRYJOB_FORMAT_VERSION: | 80 while parsed_job['version'] < cls._TRYJOB_FORMAT_VERSION: |
83 prev_ver = parsed_job['version'] | 81 prev_ver = parsed_job['version'] |
84 translation_func = cls._TRANSLATION_FUNCS[parsed_job['version']] | 82 translation_func = cls._TRANSLATION_FUNCS[parsed_job['version']] |
85 translation_func(parsed_job) | 83 translation_func(parsed_job) |
86 if parsed_job['version'] <= prev_ver: | 84 if parsed_job['version'] <= prev_ver: |
87 raise AssertionError('translation function %s not incrementing version!' | 85 raise AssertionError('translation function %s not incrementing version!' |
88 % str(translation_func)) | 86 % str(translation_func)) |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
192 # Calculate the buildroot for builds. | 190 # Calculate the buildroot for builds. |
193 config_dict = self.cbuildbot_configs.get(config) | 191 config_dict = self.cbuildbot_configs.get(config) |
194 if config_dict is None: | 192 if config_dict is None: |
195 buildroot = '/b/cbuild/etc_master' | 193 buildroot = '/b/cbuild/etc_master' |
196 elif config_dict['internal']: | 194 elif config_dict['internal']: |
197 buildroot = '/b/cbuild/internal_master' | 195 buildroot = '/b/cbuild/internal_master' |
198 else: | 196 else: |
199 buildroot = '/b/cbuild/external_master' | 197 buildroot = '/b/cbuild/external_master' |
200 props.setProperty('buildroot', buildroot, self._PROPERTY_SOURCE) | 198 props.setProperty('buildroot', buildroot, self._PROPERTY_SOURCE) |
201 | 199 |
202 props.setProperty('extra_args', options.get('extra_args', []), | |
203 self._PROPERTY_SOURCE) | |
204 props.setProperty('slaves_request', options.get('slaves_request', []), | 200 props.setProperty('slaves_request', options.get('slaves_request', []), |
205 self._PROPERTY_SOURCE) | 201 self._PROPERTY_SOURCE) |
206 props.setProperty('chromeos_config', config, self._PROPERTY_SOURCE) | 202 props.setProperty('cbb_config', config, self._PROPERTY_SOURCE) |
207 | 203 |
208 return props | 204 return props |
209 | 205 |
210 def create_buildset(self, ssid, parsed_job): | 206 def create_buildset(self, ssid, parsed_job): |
211 """Overriding base class method.""" | 207 """Overriding base class method.""" |
212 dlist = [] | 208 dlist = [] |
213 buildset_name = '%s:%s' % (parsed_job['user'], parsed_job['name']) | 209 buildset_name = '%s:%s' % (parsed_job['user'], parsed_job['name']) |
214 for bot in parsed_job['bot']: | 210 for bot in parsed_job['bot']: |
215 config = self.cbuildbot_configs.get(bot) | 211 config = self.cbuildbot_configs.get(bot) |
216 if config: | 212 if config: |
(...skipping 27 matching lines...) Expand all Loading... | |
244 m.set_type("text/html") | 240 m.set_type("text/html") |
245 m['Date'] = formatdate(localtime=True) | 241 m['Date'] = formatdate(localtime=True) |
246 m['Subject'] = 'Tryjob failed validation' | 242 m['Subject'] = 'Tryjob failed validation' |
247 m['From'] = self.from_addr | 243 m['From'] = self.from_addr |
248 m['Reply-To'] = self.reply_to | 244 m['Reply-To'] = self.reply_to |
249 result = defer.Deferred() | 245 result = defer.Deferred() |
250 sender_factory = SMTPSenderFactory(self.from_addr, emails, | 246 sender_factory = SMTPSenderFactory(self.from_addr, emails, |
251 StringIO(m.as_string()), result) | 247 StringIO(m.as_string()), result) |
252 reactor.connectTCP(self.smtp_host, 25, sender_factory) | 248 reactor.connectTCP(self.smtp_host, 25, sender_factory) |
253 | 249 |
254 @defer.deferredGenerator | 250 @defer.inlineCallbacks |
255 def gotChange(self, change, important): | 251 def gotChange(self, change, important): |
256 """Process the received data and send the queue buildset.""" | 252 """Process the received data and send the queue buildset.""" |
257 # Implicitly skips over non-files like directories. | |
258 if len(change.files) != 1: | |
259 # We only accept changes with 1 diff file. | |
260 raise BadJobfile( | |
261 'Try job with too many files %s' % (','.join(change.files))) | |
262 | |
263 # Find poller that this change came from. | 253 # Find poller that this change came from. |
264 for poller in self.pollers: | 254 for poller in self.pollers: |
265 if poller.repourl == change.repository: | 255 if not isinstance(poller, gitiles_poller.GitilesPoller): |
256 continue | |
257 if poller.repo_url == change.repository: | |
266 break | 258 break |
267 else: | 259 else: |
268 raise BadJobfile( | 260 raise BadJobfile( |
269 'Received tryjob from unsupported repository %s' % change.repository) | 261 'Received tryjob from unsupported repository %s' % change.repository) |
270 | 262 |
271 # pylint: disable=W0631 | 263 # pylint: disable=W0631 |
272 wfd = defer.waitForDeferred( | 264 file_contents = yield self.loadGitilesChangeFile(poller, change) |
273 get_file_contents(poller, change.branch, change.files[0])) | |
274 yield wfd | |
275 | 265 |
276 parsed = None | 266 parsed = {} |
277 try: | 267 try: |
278 parsed = self.load_job(wfd.getResult()) | 268 parsed = self.load_job(file_contents) |
279 self.validate_job(parsed) | 269 self.validate_job(parsed) |
280 self.updateJobDesc(parsed) | 270 self.updateJobDesc(parsed) |
281 except BadJobfile as e: | 271 except BadJobfile as e: |
282 self.send_validation_fail_email(parsed.setdefault('name', ''), | 272 self.send_validation_fail_email(parsed.setdefault('name', ''), |
283 parsed['email'], str(e)) | 273 parsed['email'], str(e)) |
284 raise | 274 raise |
285 except Exception as e: | 275 except Exception as e: |
286 print 'EXCEPTION:', e | 276 print 'EXCEPTION:', e |
287 import traceback | 277 import traceback |
288 traceback.print_exc() | 278 traceback.print_exc() |
289 raise | 279 raise |
290 | 280 |
291 # The sourcestamp/buildsets created will be merge-able. | 281 # The sourcestamp/buildsets created will be merge-able. |
292 d = self.master.db.sourcestamps.addSourceStamp( | 282 ssid = yield self.master.db.sourcestamps.addSourceStamp( |
293 branch=change.branch, | 283 branch=change.branch, |
294 revision=change.revision, | 284 revision=change.revision, |
295 project=change.project, | 285 project=change.project, |
296 repository=change.repository, | 286 repository=change.repository, |
297 changeids=[change.number]) | 287 changeids=[change.number]) |
298 d.addCallback(self.create_buildset, parsed) | 288 yield self.create_buildset(ssid, parsed) |
299 d.addErrback(log.err, "Failed to queue a try job!") | 289 |
290 @defer.inlineCallbacks | |
291 def loadGitilesChangeFile(self, poller, change): | |
292 if len(change.files) != 1: | |
293 # We only accept changes with 1 diff file. | |
294 raise BadJobfile( | |
David James
2015/09/15 19:35:51
What happens when somebody has a commit that affec
phobbs
2015/09/15 21:27:49
I think there would still be only one diff in that
David James
2015/09/15 21:34:37
I mean that somebody did a manual commit in the tr
dnj
2015/09/15 21:55:52
Yes, we expect a push that has been formulated by
| |
295 'Try job with too many files %s' % (','.join(change.files))) | |
296 | |
297 # Load the contents of the modified file. | |
298 path = self._GITILES_PATH_TMPL % { | |
299 'repo': poller.repo_path, | |
300 'revision': change.revision, | |
301 'path': change.files[0], | |
302 } | |
303 contents_b64 = yield poller.agent.request('GET', path, retry=5, | |
304 protocol=StringResponse.Get) | |
305 defer.returnValue(base64.b64decode(contents_b64)) | |
OLD | NEW |