Chromium Code Reviews| 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 |