Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(933)

Side by Side Diff: tools/check_git_config.py

Issue 482083004: Verify that gclient solution looks correct. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tools/check_git_push_access.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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:]))
OLDNEW
« no previous file with comments | « no previous file | tools/check_git_push_access.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698