OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Script that attempts to push to a special git repository to verify that git | 6 """Script that attempts to push to a special git repository to verify that git |
7 credentials are configured correctly. It also attempts to fix misconfigurations | 7 credentials are configured correctly. It also verifies that gclient solution is |
8 if possible. | 8 configured to use git checkout. |
9 | 9 |
10 It will be added as gclient hook shortly before Chromium switches to git and | 10 It will be added as gclient hook shortly before Chromium switches to git and |
11 removed after the switch. | 11 removed after the switch. |
12 | 12 |
13 When running as hook in *.corp.google.com network it will also report status | 13 When running as hook in *.corp.google.com network it will also report status |
14 of the push attempt to the server (on appengine), so that chrome-infra team can | 14 of the push attempt to the server (on appengine), so that chrome-infra team can |
15 collect information about misconfigured Git accounts (to fix them). | 15 collect information about misconfigured Git accounts. |
16 | |
17 When invoked manually will do the access test and submit the report regardless | |
18 of where it is running. | |
19 """ | 16 """ |
20 | 17 |
21 import contextlib | 18 import contextlib |
19 import datetime | |
22 import errno | 20 import errno |
23 import getpass | 21 import getpass |
24 import json | 22 import json |
25 import logging | 23 import logging |
26 import netrc | 24 import netrc |
27 import optparse | 25 import optparse |
28 import os | 26 import os |
27 import pprint | |
29 import shutil | 28 import shutil |
30 import socket | 29 import socket |
31 import ssl | 30 import ssl |
32 import subprocess | 31 import subprocess |
33 import sys | 32 import sys |
34 import tempfile | 33 import tempfile |
35 import time | 34 import time |
36 import urllib2 | 35 import urllib2 |
37 import urlparse | 36 import urlparse |
38 | 37 |
39 | 38 |
40 # Absolute path to src/ directory. | 39 # Absolute path to src/ directory. |
41 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 40 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
42 | 41 |
42 # Absolute path to a file with gclient solutions. | |
43 GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient') | |
44 | |
43 # Incremented whenever some changes to scrip logic are made. Change in version | 45 # Incremented whenever some changes to scrip logic are made. Change in version |
44 # will cause the check to be rerun on next gclient runhooks invocation. | 46 # will cause the check to be rerun on next gclient runhooks invocation. |
45 CHECKER_VERSION = 0 | 47 CHECKER_VERSION = 0 |
46 | 48 |
49 # Do not attempt to upload a report after this date. | |
50 UPLOAD_DISABLE_TS = datetime.datetime(2014, 10, 1) | |
51 | |
47 # URL to POST json with results to. | 52 # URL to POST json with results to. |
48 MOTHERSHIP_URL = ( | 53 MOTHERSHIP_URL = ( |
49 'https://chromium-git-access.appspot.com/' | 54 'https://chromium-git-access.appspot.com/' |
50 'git_access/api/v1/reports/access_check') | 55 'git_access/api/v1/reports/access_check') |
51 | 56 |
52 # Repository to push test commits to. | 57 # Repository to push test commits to. |
53 TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test' | 58 TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test' |
54 | 59 |
60 # Git-compatible gclient solution. | |
61 GOOD_GCLIENT_SOLUTION = { | |
62 'name': 'src', | |
63 'deps_file': '.DEPS.git', | |
64 'managed': False, | |
65 'url': 'https://chromium.googlesource.com/chromium/src.git', | |
66 } | |
67 | |
55 # Possible chunks of git push response in case .netrc is misconfigured. | 68 # Possible chunks of git push response in case .netrc is misconfigured. |
56 BAD_ACL_ERRORS = ( | 69 BAD_ACL_ERRORS = ( |
57 '(prohibited by Gerrit)', | 70 '(prohibited by Gerrit)', |
58 'does not match your user account', | 71 'does not match your user account', |
59 'Invalid user name or password', | 72 'Invalid user name or password', |
60 ) | 73 ) |
61 | 74 |
62 | 75 |
63 def is_on_bot(): | 76 def is_on_bot(): |
64 """True when running under buildbot.""" | 77 """True when running under buildbot.""" |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
118 try: | 131 try: |
119 proc = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE) | 132 proc = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE) |
120 out, _ = proc.communicate() | 133 out, _ = proc.communicate() |
121 return out.strip() if proc.returncode == 0 else '' | 134 return out.strip() if proc.returncode == 0 else '' |
122 except OSError as exc: | 135 except OSError as exc: |
123 if exc.errno != errno.ENOENT: | 136 if exc.errno != errno.ENOENT: |
124 logging.exception('Unexpected error when calling git') | 137 logging.exception('Unexpected error when calling git') |
125 return '' | 138 return '' |
126 | 139 |
127 | 140 |
141 def read_gclient_solution(): | |
142 """Read information about 'src' gclient solution from .gclient file. | |
143 | |
144 Returns tuple: | |
145 (url, deps_file, managed) | |
146 or | |
147 (None, None, None) if no such solution. | |
148 """ | |
149 try: | |
150 env = {} | |
151 execfile(GCLIENT_CONFIG, env, env) | |
152 for sol in env['solutions']: | |
153 if sol['name'] == 'src': | |
154 return sol.get('url'), sol.get('deps_file'), sol.get('managed') | |
Vadim Sh.
2014/08/18 23:29:21
I intentionally skip custom_deps and other stuff.
| |
155 return None, None, None | |
156 except Exception: | |
157 logging.exception('Failed to read .gclient solution') | |
158 return None, None, None | |
159 | |
160 | |
128 def scan_configuration(): | 161 def scan_configuration(): |
129 """Scans local environment for git related configuration values.""" | 162 """Scans local environment for git related configuration values.""" |
130 # Git checkout? | 163 # Git checkout? |
131 is_git = is_using_git() | 164 is_git = is_using_git() |
132 | 165 |
133 # On Windows HOME should be set. | 166 # On Windows HOME should be set. |
134 if 'HOME' in os.environ: | 167 if 'HOME' in os.environ: |
135 netrc_path = os.path.join( | 168 netrc_path = os.path.join( |
136 os.environ['HOME'], | 169 os.environ['HOME'], |
137 '_netrc' if sys.platform.startswith('win') else '.netrc') | 170 '_netrc' if sys.platform.startswith('win') else '.netrc') |
138 else: | 171 else: |
139 netrc_path = None | 172 netrc_path = None |
140 | 173 |
141 # Netrc exists? | 174 # Netrc exists? |
142 is_using_netrc = netrc_path and os.path.exists(netrc_path) | 175 is_using_netrc = netrc_path and os.path.exists(netrc_path) |
143 | 176 |
144 # Read it. | 177 # Read it. |
145 netrc_obj = None | 178 netrc_obj = None |
146 if is_using_netrc: | 179 if is_using_netrc: |
147 try: | 180 try: |
148 netrc_obj = netrc.netrc(netrc_path) | 181 netrc_obj = netrc.netrc(netrc_path) |
149 except Exception: | 182 except Exception: |
150 logging.exception('Failed to read netrc from %s', netrc_path) | 183 logging.exception('Failed to read netrc from %s', netrc_path) |
151 netrc_obj = None | 184 netrc_obj = None |
152 | 185 |
186 # Read gclient 'src' solution. | |
187 gclient_url, gclient_deps, gclient_managed = read_gclient_solution() | |
188 | |
153 return { | 189 return { |
154 'checker_version': CHECKER_VERSION, | 190 'checker_version': CHECKER_VERSION, |
155 'is_git': is_git, | 191 'is_git': is_git, |
156 'is_home_set': 'HOME' in os.environ, | 192 'is_home_set': 'HOME' in os.environ, |
157 'is_using_netrc': is_using_netrc, | 193 'is_using_netrc': is_using_netrc, |
158 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0, | 194 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0, |
159 'git_version': get_git_version(), | 195 'git_version': get_git_version(), |
160 'platform': sys.platform, | 196 'platform': sys.platform, |
161 'username': getpass.getuser(), | 197 'username': getpass.getuser(), |
162 'git_user_email': read_git_config('user.email') if is_git else '', | 198 'git_user_email': read_git_config('user.email') if is_git else '', |
163 'git_user_name': read_git_config('user.name') if is_git else '', | 199 'git_user_name': read_git_config('user.name') if is_git else '', |
164 'chromium_netrc_email': | 200 'chromium_netrc_email': |
165 read_netrc_user(netrc_obj, 'chromium.googlesource.com'), | 201 read_netrc_user(netrc_obj, 'chromium.googlesource.com'), |
166 'chrome_internal_netrc_email': | 202 'chrome_internal_netrc_email': |
167 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'), | 203 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'), |
204 'gclient_deps': gclient_deps, | |
205 'gclient_managed': gclient_managed, | |
206 'gclient_url': gclient_url, | |
168 } | 207 } |
169 | 208 |
170 | 209 |
171 def last_configuration_path(): | 210 def last_configuration_path(): |
172 """Path to store last checked configuration.""" | 211 """Path to store last checked configuration.""" |
173 if is_using_git(): | 212 if is_using_git(): |
174 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json') | 213 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json') |
175 elif is_using_svn(): | 214 elif is_using_svn(): |
176 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json') | 215 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json') |
177 else: | 216 else: |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
236 self.append_to_log(out) | 275 self.append_to_log(out) |
237 return retcode | 276 return retcode |
238 | 277 |
239 def append_to_log(self, text): | 278 def append_to_log(self, text): |
240 if text: | 279 if text: |
241 self.log.append(text) | 280 self.log.append(text) |
242 if self.verbose: | 281 if self.verbose: |
243 logging.warning(text) | 282 logging.warning(text) |
244 | 283 |
245 | 284 |
246 def check_git_access(conf, report_url, verbose): | 285 def check_git_config(conf, report_url, verbose): |
247 """Attempts to push to a git repository, reports results to a server. | 286 """Attempts to push to a git repository, reports results to a server. |
248 | 287 |
249 Returns True if the check finished without incidents (push itself may | 288 Returns True if the check finished without incidents (push itself may |
250 have failed) and should NOT be retried on next invocation of the hook. | 289 have failed) and should NOT be retried on next invocation of the hook. |
251 """ | 290 """ |
252 logging.warning('Checking push access to the git repository...') | |
253 | |
254 # Don't even try to push if netrc is not configured. | 291 # Don't even try to push if netrc is not configured. |
255 if not conf['chromium_netrc_email']: | 292 if not conf['chromium_netrc_email']: |
256 return upload_report( | 293 return upload_report( |
257 conf, | 294 conf, |
258 report_url, | 295 report_url, |
259 verbose, | 296 verbose, |
260 push_works=False, | 297 push_works=False, |
261 push_log='', | 298 push_log='', |
262 push_duration_ms=0) | 299 push_duration_ms=0) |
263 | 300 |
264 # Ref to push to, each user has its own ref. | 301 # Ref to push to, each user has its own ref. |
265 ref = 'refs/push-test/%s' % conf['chromium_netrc_email'] | 302 ref = 'refs/push-test/%s' % conf['chromium_netrc_email'] |
266 | 303 |
267 push_works = False | 304 push_works = False |
268 flake = False | 305 flake = False |
269 started = time.time() | 306 started = time.time() |
270 try: | 307 try: |
308 logging.warning('Checking push access to the git repository...') | |
271 with temp_directory() as tmp: | 309 with temp_directory() as tmp: |
272 # Prepare a simple commit on a new timeline. | 310 # Prepare a simple commit on a new timeline. |
273 runner = Runner(tmp, verbose) | 311 runner = Runner(tmp, verbose) |
274 runner.run(['git', 'init', '.']) | 312 runner.run(['git', 'init', '.']) |
275 if conf['git_user_name']: | 313 if conf['git_user_name']: |
276 runner.run(['git', 'config', 'user.name', conf['git_user_name']]) | 314 runner.run(['git', 'config', 'user.name', conf['git_user_name']]) |
277 if conf['git_user_email']: | 315 if conf['git_user_email']: |
278 runner.run(['git', 'config', 'user.email', conf['git_user_email']]) | 316 runner.run(['git', 'config', 'user.email', conf['git_user_email']]) |
279 with open(os.path.join(tmp, 'timestamp'), 'w') as f: | 317 with open(os.path.join(tmp, 'timestamp'), 'w') as f: |
280 f.write(str(int(time.time() * 1000))) | 318 f.write(str(int(time.time() * 1000))) |
281 runner.run(['git', 'add', 'timestamp']) | 319 runner.run(['git', 'add', 'timestamp']) |
282 runner.run(['git', 'commit', '-m', 'Push test.']) | 320 runner.run(['git', 'commit', '-m', 'Push test.']) |
283 # Try to push multiple times if it fails due to issues other than ACLs. | 321 # Try to push multiple times if it fails due to issues other than ACLs. |
284 attempt = 0 | 322 attempt = 0 |
285 while attempt < 5: | 323 while attempt < 5: |
286 attempt += 1 | 324 attempt += 1 |
287 logging.info('Pushing to %s %s', TEST_REPO_URL, ref) | 325 logging.info('Pushing to %s %s', TEST_REPO_URL, ref) |
288 ret = runner.run(['git', 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f']) | 326 ret = runner.run(['git', 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f']) |
289 if not ret: | 327 if not ret: |
290 push_works = True | 328 push_works = True |
291 break | 329 break |
292 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS): | 330 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS): |
293 push_works = False | 331 push_works = False |
294 break | 332 break |
295 except Exception: | 333 except Exception: |
296 logging.exception('Unexpected exception when pushing') | 334 logging.exception('Unexpected exception when pushing') |
297 flake = True | 335 flake = True |
298 | 336 |
337 if push_works: | |
338 logging.warning('Git push works!') | |
339 else: | |
340 logging.warning( | |
341 'Git push doesn\'t work, which is fine if you are not a committer.') | |
342 | |
299 uploaded = upload_report( | 343 uploaded = upload_report( |
300 conf, | 344 conf, |
301 report_url, | 345 report_url, |
302 verbose, | 346 verbose, |
303 push_works=push_works, | 347 push_works=push_works, |
304 push_log='\n'.join(runner.log), | 348 push_log='\n'.join(runner.log), |
305 push_duration_ms=int((time.time() - started) * 1000)) | 349 push_duration_ms=int((time.time() - started) * 1000)) |
306 return uploaded and not flake | 350 return uploaded and not flake |
307 | 351 |
308 | 352 |
353 def check_gclient_config(conf): | |
354 """Shows warning if gclient solution is not properly configured for git.""" | |
355 current = { | |
356 'name': 'src', | |
357 'deps_file': conf['gclient_deps'], | |
358 'managed': conf['gclient_managed'], | |
359 'url': conf['gclient_url'], | |
360 } | |
361 if current != GOOD_GCLIENT_SOLUTION: | |
362 print '-' * 80 | |
363 print 'Your gclient solution is not set to use supported git workflow!' | |
364 print | |
365 print 'Your \'src\' solution (in %s):' % GCLIENT_CONFIG | |
366 print pprint.pformat(current, indent=2) | |
367 print | |
368 print 'Correct \'src\' solution to use git:' | |
369 print pprint.pformat(GOOD_GCLIENT_SOLUTION, indent=2) | |
370 print | |
371 print 'Please update your .gclient file ASAP.' | |
372 print '-' * 80 | |
373 | |
374 | |
309 def upload_report( | 375 def upload_report( |
310 conf, report_url, verbose, push_works, push_log, push_duration_ms): | 376 conf, report_url, verbose, push_works, push_log, push_duration_ms): |
311 """Posts report to the server, returns True if server accepted it. | 377 """Posts report to the server, returns True if server accepted it. |
312 | 378 |
313 Uploads the report only if script is running in Google corp network. Otherwise | 379 Uploads the report only if script is running in Google corp network. Otherwise |
314 just prints the report. | 380 just prints the report. |
315 """ | 381 """ |
316 report = conf.copy() | 382 report = conf.copy() |
317 report.update( | 383 report.update( |
318 push_works=push_works, | 384 push_works=push_works, |
319 push_log=push_log, | 385 push_log=push_log, |
320 push_duration_ms=push_duration_ms) | 386 push_duration_ms=push_duration_ms) |
321 | 387 |
322 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True) | 388 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True) |
323 | |
324 if push_works: | |
325 logging.warning('Git push works!') | |
326 else: | |
327 logging.warning( | |
328 'Git push doesn\'t work, which is fine if you are not a committer.') | |
329 | |
330 if verbose: | 389 if verbose: |
331 print 'Status of git push attempt:' | 390 print 'Status of git push attempt:' |
332 print as_bytes | 391 print as_bytes |
333 | 392 |
334 # Do not upload it outside of corp. | 393 # Do not upload it outside of corp or if server side is already disabled. |
335 if not is_in_google_corp(): | 394 if not is_in_google_corp() or datetime.datetime.now() > UPLOAD_DISABLE_TS: |
336 if verbose: | 395 if verbose: |
337 print ( | 396 print ( |
338 'You can send the above report to chrome-git-migration@google.com ' | 397 'You can send the above report to chrome-git-migration@google.com ' |
339 'if you need help to set up you committer git account.') | 398 'if you need help to set up you committer git account.') |
340 return True | 399 return True |
341 | 400 |
342 req = urllib2.Request( | 401 req = urllib2.Request( |
343 url=report_url, | 402 url=report_url, |
344 data=as_bytes, | 403 data=as_bytes, |
345 headers={'Content-Type': 'application/json; charset=utf-8'}) | 404 headers={'Content-Type': 'application/json; charset=utf-8'}) |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
379 '--verbose', | 438 '--verbose', |
380 action='store_true', | 439 action='store_true', |
381 help='More logging') | 440 help='More logging') |
382 options, args = parser.parse_args() | 441 options, args = parser.parse_args() |
383 if args: | 442 if args: |
384 parser.error('Unknown argument %s' % args) | 443 parser.error('Unknown argument %s' % args) |
385 logging.basicConfig( | 444 logging.basicConfig( |
386 format='%(message)s', | 445 format='%(message)s', |
387 level=logging.INFO if options.verbose else logging.WARN) | 446 level=logging.INFO if options.verbose else logging.WARN) |
388 | 447 |
389 # When invoked not as hook, always run the check. | 448 # When invoked not as a hook, always run the check. |
390 if not options.running_as_hook: | 449 if not options.running_as_hook: |
391 if check_git_access(scan_configuration(), options.report_url, True): | 450 config = scan_configuration() |
392 return 0 | 451 check_gclient_config(config) |
393 return 1 | 452 check_git_config(config, options.report_url, True) |
394 | |
395 # Otherwise, do it only on google owned, non-bot machines. | |
396 if is_on_bot() or not is_in_google_corp(): | |
397 logging.info('Skipping the check: bot or non corp.') | |
398 return 0 | 453 return 0 |
399 | 454 |
400 # Skip the check if current configuration was already checked. | 455 # Always do nothing on bots. |
456 if is_on_bot(): | |
457 return 0 | |
458 | |
459 # Read current config, verify gclient solution looks correct. | |
401 config = scan_configuration() | 460 config = scan_configuration() |
461 check_gclient_config(config) | |
462 | |
463 # Do not attempt to push from non-google owned machines. | |
464 if not is_in_google_corp(): | |
465 logging.info('Skipping git push check: non *.corp.google.com machine.') | |
466 return 0 | |
467 | |
468 # Skip git push check if current configuration was already checked. | |
402 if config == read_last_configuration(): | 469 if config == read_last_configuration(): |
403 logging.info('Check already performed, skipping.') | 470 logging.info('Check already performed, skipping.') |
404 return 0 | 471 return 0 |
405 | 472 |
406 # Run the check. Mark configuration as checked only on success. Ignore any | 473 # Run the check. Mark configuration as checked only on success. Ignore any |
407 # exceptions or errors. This check must not break gclient runhooks. | 474 # exceptions or errors. This check must not break gclient runhooks. |
408 try: | 475 try: |
409 ok = check_git_access(config, options.report_url, False) | 476 ok = check_git_config(config, options.report_url, False) |
410 if ok: | 477 if ok: |
411 write_last_configuration(config) | 478 write_last_configuration(config) |
412 else: | 479 else: |
413 logging.warning('Check failed and will be retried on the next run') | 480 logging.warning('Check failed and will be retried on the next run') |
414 except Exception: | 481 except Exception: |
415 logging.exception('Unexpected exception when performing git access check') | 482 logging.exception('Unexpected exception when performing git access check') |
416 return 0 | 483 return 0 |
417 | 484 |
418 | 485 |
419 if __name__ == '__main__': | 486 if __name__ == '__main__': |
420 sys.exit(main(sys.argv[1:])) | 487 sys.exit(main(sys.argv[1:])) |
OLD | NEW |