blob: 51b13017882f645b5426f6d9c439b7d21ec87cd6 [file] [log] [blame]
[email protected]94c64122014-08-16 02:03:551#!/usr/bin/env python
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
4# found in the LICENSE file.
5
6"""Script that attempts to push to a special git repository to verify that git
[email protected]46b32a82014-08-19 00:37:577credentials are configured correctly. It also verifies that gclient solution is
8configured to use git checkout.
[email protected]94c64122014-08-16 02:03:559
10It will be added as gclient hook shortly before Chromium switches to git and
11removed after the switch.
12
13When running as hook in *.corp.google.com network it will also report status
14of the push attempt to the server (on appengine), so that chrome-infra team can
[email protected]46b32a82014-08-19 00:37:5715collect information about misconfigured Git accounts.
[email protected]94c64122014-08-16 02:03:5516"""
17
18import contextlib
[email protected]46b32a82014-08-19 00:37:5719import datetime
[email protected]aa52d312014-08-18 20:28:5220import errno
[email protected]94c64122014-08-16 02:03:5521import getpass
22import json
23import logging
24import netrc
25import optparse
26import os
[email protected]46b32a82014-08-19 00:37:5727import pprint
[email protected]94c64122014-08-16 02:03:5528import shutil
29import socket
30import ssl
31import subprocess
32import sys
33import tempfile
34import time
35import urllib2
[email protected]aa52d312014-08-18 20:28:5236import urlparse
[email protected]94c64122014-08-16 02:03:5537
38
39# Absolute path to src/ directory.
40REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
41
[email protected]46b32a82014-08-19 00:37:5742# Absolute path to a file with gclient solutions.
43GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient')
44
[email protected]94c64122014-08-16 02:03:5545# Incremented whenever some changes to scrip logic are made. Change in version
46# will cause the check to be rerun on next gclient runhooks invocation.
[email protected]b3b918c72014-08-21 00:08:3247CHECKER_VERSION = 1
[email protected]94c64122014-08-16 02:03:5548
[email protected]46b32a82014-08-19 00:37:5749# Do not attempt to upload a report after this date.
50UPLOAD_DISABLE_TS = datetime.datetime(2014, 10, 1)
51
[email protected]94c64122014-08-16 02:03:5552# URL to POST json with results to.
53MOTHERSHIP_URL = (
54 'https://chromium-git-access.appspot.com/'
55 'git_access/api/v1/reports/access_check')
56
57# Repository to push test commits to.
58TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test'
59
[email protected]46b32a82014-08-19 00:37:5760# Git-compatible gclient solution.
61GOOD_GCLIENT_SOLUTION = {
62 'name': 'src',
Vadim Shtayuradaf35ab2014-08-23 02:08:5363 'deps_file': 'DEPS',
[email protected]46b32a82014-08-19 00:37:5764 'managed': False,
65 'url': 'https://chromium.googlesource.com/chromium/src.git',
66}
67
[email protected]94c64122014-08-16 02:03:5568# Possible chunks of git push response in case .netrc is misconfigured.
69BAD_ACL_ERRORS = (
70 '(prohibited by Gerrit)',
[email protected]aa52d312014-08-18 20:28:5271 'does not match your user account',
[email protected]b3b918c72014-08-21 00:08:3272 'Git repository not found',
[email protected]94c64122014-08-16 02:03:5573 'Invalid user name or password',
[email protected]e92d872c2014-08-19 21:02:0274 'Please make sure you have the correct access rights',
[email protected]94c64122014-08-16 02:03:5575)
76
[email protected]b3b918c72014-08-21 00:08:3277# Git executable to call.
78GIT_EXE = 'git.bat' if sys.platform == 'win32' else 'git'
79
[email protected]94c64122014-08-16 02:03:5580
81def is_on_bot():
82 """True when running under buildbot."""
83 return os.environ.get('CHROME_HEADLESS') == '1'
84
85
86def is_in_google_corp():
87 """True when running in google corp network."""
88 try:
89 return socket.getfqdn().endswith('.corp.google.com')
90 except socket.error:
91 logging.exception('Failed to get FQDN')
92 return False
93
94
95def is_using_git():
96 """True if git checkout is used."""
97 return os.path.exists(os.path.join(REPO_ROOT, '.git', 'objects'))
98
99
100def is_using_svn():
101 """True if svn checkout is used."""
102 return os.path.exists(os.path.join(REPO_ROOT, '.svn'))
103
104
105def read_git_config(prop):
[email protected]aa52d312014-08-18 20:28:52106 """Reads git config property of src.git repo.
107
108 Returns empty string in case of errors.
109 """
110 try:
111 proc = subprocess.Popen(
[email protected]b3b918c72014-08-21 00:08:32112 [GIT_EXE, 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT)
[email protected]aa52d312014-08-18 20:28:52113 out, _ = proc.communicate()
114 return out.strip()
115 except OSError as exc:
116 if exc.errno != errno.ENOENT:
117 logging.exception('Unexpected error when calling git')
118 return ''
[email protected]94c64122014-08-16 02:03:55119
120
121def read_netrc_user(netrc_obj, host):
122 """Reads 'user' field of a host entry in netrc.
123
124 Returns empty string if netrc is missing, or host is not there.
125 """
126 if not netrc_obj:
127 return ''
128 entry = netrc_obj.authenticators(host)
129 if not entry:
130 return ''
131 return entry[0]
132
133
134def get_git_version():
135 """Returns version of git or None if git is not available."""
[email protected]aa52d312014-08-18 20:28:52136 try:
[email protected]b3b918c72014-08-21 00:08:32137 proc = subprocess.Popen([GIT_EXE, '--version'], stdout=subprocess.PIPE)
[email protected]aa52d312014-08-18 20:28:52138 out, _ = proc.communicate()
139 return out.strip() if proc.returncode == 0 else ''
140 except OSError as exc:
141 if exc.errno != errno.ENOENT:
142 logging.exception('Unexpected error when calling git')
143 return ''
[email protected]94c64122014-08-16 02:03:55144
145
[email protected]46b32a82014-08-19 00:37:57146def read_gclient_solution():
147 """Read information about 'src' gclient solution from .gclient file.
148
149 Returns tuple:
150 (url, deps_file, managed)
151 or
152 (None, None, None) if no such solution.
153 """
154 try:
155 env = {}
156 execfile(GCLIENT_CONFIG, env, env)
[email protected]97c58bc2014-08-20 18:15:30157 for sol in (env.get('solutions') or []):
158 if sol.get('name') == 'src':
[email protected]46b32a82014-08-19 00:37:57159 return sol.get('url'), sol.get('deps_file'), sol.get('managed')
160 return None, None, None
161 except Exception:
162 logging.exception('Failed to read .gclient solution')
163 return None, None, None
164
165
[email protected]b3b918c72014-08-21 00:08:32166def read_git_insteadof(host):
167 """Reads relevant insteadOf config entries."""
168 try:
169 proc = subprocess.Popen([GIT_EXE, 'config', '-l'], stdout=subprocess.PIPE)
170 out, _ = proc.communicate()
171 lines = []
172 for line in out.strip().split('\n'):
173 line = line.lower()
174 if 'insteadof=' in line and host in line:
175 lines.append(line)
176 return '\n'.join(lines)
177 except OSError as exc:
178 if exc.errno != errno.ENOENT:
179 logging.exception('Unexpected error when calling git')
180 return ''
181
182
[email protected]94c64122014-08-16 02:03:55183def scan_configuration():
184 """Scans local environment for git related configuration values."""
185 # Git checkout?
186 is_git = is_using_git()
187
188 # On Windows HOME should be set.
189 if 'HOME' in os.environ:
190 netrc_path = os.path.join(
191 os.environ['HOME'],
192 '_netrc' if sys.platform.startswith('win') else '.netrc')
193 else:
194 netrc_path = None
195
196 # Netrc exists?
197 is_using_netrc = netrc_path and os.path.exists(netrc_path)
198
199 # Read it.
200 netrc_obj = None
201 if is_using_netrc:
202 try:
203 netrc_obj = netrc.netrc(netrc_path)
204 except Exception:
205 logging.exception('Failed to read netrc from %s', netrc_path)
206 netrc_obj = None
207
[email protected]46b32a82014-08-19 00:37:57208 # Read gclient 'src' solution.
209 gclient_url, gclient_deps, gclient_managed = read_gclient_solution()
210
[email protected]94c64122014-08-16 02:03:55211 return {
212 'checker_version': CHECKER_VERSION,
213 'is_git': is_git,
214 'is_home_set': 'HOME' in os.environ,
215 'is_using_netrc': is_using_netrc,
216 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0,
217 'git_version': get_git_version(),
218 'platform': sys.platform,
219 'username': getpass.getuser(),
220 'git_user_email': read_git_config('user.email') if is_git else '',
221 'git_user_name': read_git_config('user.name') if is_git else '',
[email protected]b3b918c72014-08-21 00:08:32222 'git_insteadof': read_git_insteadof('chromium.googlesource.com'),
[email protected]94c64122014-08-16 02:03:55223 'chromium_netrc_email':
224 read_netrc_user(netrc_obj, 'chromium.googlesource.com'),
225 'chrome_internal_netrc_email':
226 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'),
[email protected]46b32a82014-08-19 00:37:57227 'gclient_deps': gclient_deps,
228 'gclient_managed': gclient_managed,
229 'gclient_url': gclient_url,
[email protected]94c64122014-08-16 02:03:55230 }
231
232
233def last_configuration_path():
234 """Path to store last checked configuration."""
235 if is_using_git():
[email protected]aa52d312014-08-18 20:28:52236 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55237 elif is_using_svn():
[email protected]aa52d312014-08-18 20:28:52238 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55239 else:
[email protected]aa52d312014-08-18 20:28:52240 return os.path.join(REPO_ROOT, '.check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55241
242
243def read_last_configuration():
244 """Reads last checked configuration if it exists."""
245 try:
246 with open(last_configuration_path(), 'r') as f:
247 return json.load(f)
248 except (IOError, ValueError):
249 return None
250
251
252def write_last_configuration(conf):
253 """Writes last checked configuration to a file."""
254 try:
255 with open(last_configuration_path(), 'w') as f:
256 json.dump(conf, f, indent=2, sort_keys=True)
257 except IOError:
258 logging.exception('Failed to write JSON to %s', path)
259
260
261@contextlib.contextmanager
262def temp_directory():
263 """Creates a temp directory, then nukes it."""
264 tmp = tempfile.mkdtemp()
265 try:
266 yield tmp
267 finally:
268 try:
269 shutil.rmtree(tmp)
270 except (OSError, IOError):
271 logging.exception('Failed to remove temp directory %s', tmp)
272
273
274class Runner(object):
275 """Runs a bunch of commands in some directory, collects logs from them."""
276
[email protected]aa52d312014-08-18 20:28:52277 def __init__(self, cwd, verbose):
[email protected]94c64122014-08-16 02:03:55278 self.cwd = cwd
[email protected]aa52d312014-08-18 20:28:52279 self.verbose = verbose
[email protected]94c64122014-08-16 02:03:55280 self.log = []
281
282 def run(self, cmd):
[email protected]aa52d312014-08-18 20:28:52283 self.append_to_log('> ' + ' '.join(cmd))
284 retcode = -1
285 try:
286 proc = subprocess.Popen(
287 cmd,
288 stdout=subprocess.PIPE,
289 stderr=subprocess.STDOUT,
290 cwd=self.cwd)
291 out, _ = proc.communicate()
292 out = out.strip()
293 retcode = proc.returncode
294 except OSError as exc:
295 out = str(exc)
296 if retcode:
297 out += '\n(exit code: %d)' % retcode
298 self.append_to_log(out)
299 return retcode
300
301 def append_to_log(self, text):
302 if text:
303 self.log.append(text)
304 if self.verbose:
305 logging.warning(text)
[email protected]94c64122014-08-16 02:03:55306
307
[email protected]46b32a82014-08-19 00:37:57308def check_git_config(conf, report_url, verbose):
[email protected]94c64122014-08-16 02:03:55309 """Attempts to push to a git repository, reports results to a server.
310
311 Returns True if the check finished without incidents (push itself may
312 have failed) and should NOT be retried on next invocation of the hook.
313 """
[email protected]94c64122014-08-16 02:03:55314 # Don't even try to push if netrc is not configured.
315 if not conf['chromium_netrc_email']:
316 return upload_report(
317 conf,
318 report_url,
[email protected]aa52d312014-08-18 20:28:52319 verbose,
[email protected]94c64122014-08-16 02:03:55320 push_works=False,
321 push_log='',
322 push_duration_ms=0)
323
324 # Ref to push to, each user has its own ref.
325 ref = 'refs/push-test/%s' % conf['chromium_netrc_email']
326
327 push_works = False
328 flake = False
329 started = time.time()
330 try:
[email protected]46b32a82014-08-19 00:37:57331 logging.warning('Checking push access to the git repository...')
[email protected]94c64122014-08-16 02:03:55332 with temp_directory() as tmp:
333 # Prepare a simple commit on a new timeline.
[email protected]aa52d312014-08-18 20:28:52334 runner = Runner(tmp, verbose)
[email protected]b3b918c72014-08-21 00:08:32335 runner.run([GIT_EXE, 'init', '.'])
[email protected]94c64122014-08-16 02:03:55336 if conf['git_user_name']:
[email protected]b3b918c72014-08-21 00:08:32337 runner.run([GIT_EXE, 'config', 'user.name', conf['git_user_name']])
[email protected]94c64122014-08-16 02:03:55338 if conf['git_user_email']:
[email protected]b3b918c72014-08-21 00:08:32339 runner.run([GIT_EXE, 'config', 'user.email', conf['git_user_email']])
[email protected]94c64122014-08-16 02:03:55340 with open(os.path.join(tmp, 'timestamp'), 'w') as f:
341 f.write(str(int(time.time() * 1000)))
[email protected]b3b918c72014-08-21 00:08:32342 runner.run([GIT_EXE, 'add', 'timestamp'])
343 runner.run([GIT_EXE, 'commit', '-m', 'Push test.'])
[email protected]94c64122014-08-16 02:03:55344 # Try to push multiple times if it fails due to issues other than ACLs.
345 attempt = 0
346 while attempt < 5:
347 attempt += 1
348 logging.info('Pushing to %s %s', TEST_REPO_URL, ref)
[email protected]b3b918c72014-08-21 00:08:32349 ret = runner.run(
350 [GIT_EXE, 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f'])
[email protected]94c64122014-08-16 02:03:55351 if not ret:
352 push_works = True
353 break
354 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS):
355 push_works = False
356 break
357 except Exception:
358 logging.exception('Unexpected exception when pushing')
359 flake = True
360
[email protected]46b32a82014-08-19 00:37:57361 if push_works:
362 logging.warning('Git push works!')
363 else:
364 logging.warning(
365 'Git push doesn\'t work, which is fine if you are not a committer.')
366
[email protected]94c64122014-08-16 02:03:55367 uploaded = upload_report(
368 conf,
369 report_url,
[email protected]aa52d312014-08-18 20:28:52370 verbose,
[email protected]94c64122014-08-16 02:03:55371 push_works=push_works,
372 push_log='\n'.join(runner.log),
373 push_duration_ms=int((time.time() - started) * 1000))
374 return uploaded and not flake
375
376
[email protected]46b32a82014-08-19 00:37:57377def check_gclient_config(conf):
378 """Shows warning if gclient solution is not properly configured for git."""
[email protected]97c58bc2014-08-20 18:15:30379 # Ignore configs that do not have 'src' solution at all.
380 if not conf['gclient_url']:
381 return
[email protected]46b32a82014-08-19 00:37:57382 current = {
383 'name': 'src',
Vadim Shtayurad484d592014-08-23 02:49:13384 'deps_file': conf['gclient_deps'] or 'DEPS',
[email protected]b3b918c72014-08-21 00:08:32385 'managed': conf['gclient_managed'] or False,
[email protected]46b32a82014-08-19 00:37:57386 'url': conf['gclient_url'],
387 }
[email protected]b3b918c72014-08-21 00:08:32388 good = GOOD_GCLIENT_SOLUTION
389 if current == good:
390 return
391 # Show big warning if url or deps_file is wrong.
392 if current['url'] != good['url'] or current['deps_file'] != good['deps_file']:
[email protected]46b32a82014-08-19 00:37:57393 print '-' * 80
394 print 'Your gclient solution is not set to use supported git workflow!'
395 print
396 print 'Your \'src\' solution (in %s):' % GCLIENT_CONFIG
397 print pprint.pformat(current, indent=2)
398 print
399 print 'Correct \'src\' solution to use git:'
[email protected]b3b918c72014-08-21 00:08:32400 print pprint.pformat(good, indent=2)
[email protected]46b32a82014-08-19 00:37:57401 print
402 print 'Please update your .gclient file ASAP.'
403 print '-' * 80
[email protected]b3b918c72014-08-21 00:08:32404 # Show smaller (additional) warning about managed workflow.
405 if current['managed']:
406 print '-' * 80
407 print (
408 'You are using managed gclient mode with git, which was deprecated '
409 'on 8/22/13:')
410 print (
411 'https://groups.google.com/a/chromium.org/'
412 'forum/#!topic/chromium-dev/n9N5N3JL2_U')
413 print
414 print (
415 'It is strongly advised to switch to unmanaged mode. For more '
416 'information about managed mode and reasons for its deprecation see:')
417 print 'http://www.chromium.org/developers/how-tos/get-the-code#Managed_mode'
418 print
419 print (
420 'There\'s also a large suite of tools to assist managing git '
421 'checkouts.\nSee \'man depot_tools\' (or read '
422 'depot_tools/man/html/depot_tools.html).')
423 print '-' * 80
[email protected]46b32a82014-08-19 00:37:57424
425
[email protected]94c64122014-08-16 02:03:55426def upload_report(
[email protected]aa52d312014-08-18 20:28:52427 conf, report_url, verbose, push_works, push_log, push_duration_ms):
[email protected]94c64122014-08-16 02:03:55428 """Posts report to the server, returns True if server accepted it.
429
[email protected]aa52d312014-08-18 20:28:52430 Uploads the report only if script is running in Google corp network. Otherwise
431 just prints the report.
[email protected]94c64122014-08-16 02:03:55432 """
433 report = conf.copy()
434 report.update(
435 push_works=push_works,
436 push_log=push_log,
437 push_duration_ms=push_duration_ms)
438
439 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True)
[email protected]aa52d312014-08-18 20:28:52440 if verbose:
[email protected]94c64122014-08-16 02:03:55441 print 'Status of git push attempt:'
442 print as_bytes
443
[email protected]46b32a82014-08-19 00:37:57444 # Do not upload it outside of corp or if server side is already disabled.
445 if not is_in_google_corp() or datetime.datetime.now() > UPLOAD_DISABLE_TS:
[email protected]aa52d312014-08-18 20:28:52446 if verbose:
[email protected]94c64122014-08-16 02:03:55447 print (
448 'You can send the above report to [email protected] '
449 'if you need help to set up you committer git account.')
450 return True
451
452 req = urllib2.Request(
453 url=report_url,
454 data=as_bytes,
455 headers={'Content-Type': 'application/json; charset=utf-8'})
456
457 attempt = 0
458 success = False
459 while not success and attempt < 10:
460 attempt += 1
461 try:
[email protected]aa52d312014-08-18 20:28:52462 logging.warning(
463 'Attempting to upload the report to %s...',
464 urlparse.urlparse(report_url).netloc)
465 resp = urllib2.urlopen(req, timeout=5)
466 report_id = None
467 try:
468 report_id = json.load(resp)['report_id']
469 except (ValueError, TypeError, KeyError):
470 pass
471 logging.warning('Report uploaded: %s', report_id)
[email protected]94c64122014-08-16 02:03:55472 success = True
[email protected]94c64122014-08-16 02:03:55473 except (urllib2.URLError, socket.error, ssl.SSLError) as exc:
[email protected]aa52d312014-08-18 20:28:52474 logging.warning('Failed to upload the report: %s', exc)
[email protected]94c64122014-08-16 02:03:55475 return success
476
477
478def main(args):
479 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
480 parser.add_option(
481 '--running-as-hook',
482 action='store_true',
483 help='Set when invoked from gclient hook')
484 parser.add_option(
485 '--report-url',
486 default=MOTHERSHIP_URL,
487 help='URL to submit the report to')
488 parser.add_option(
489 '--verbose',
490 action='store_true',
491 help='More logging')
492 options, args = parser.parse_args()
493 if args:
494 parser.error('Unknown argument %s' % args)
495 logging.basicConfig(
496 format='%(message)s',
497 level=logging.INFO if options.verbose else logging.WARN)
498
[email protected]46b32a82014-08-19 00:37:57499 # When invoked not as a hook, always run the check.
[email protected]94c64122014-08-16 02:03:55500 if not options.running_as_hook:
[email protected]46b32a82014-08-19 00:37:57501 config = scan_configuration()
502 check_gclient_config(config)
503 check_git_config(config, options.report_url, True)
[email protected]94c64122014-08-16 02:03:55504 return 0
505
[email protected]46b32a82014-08-19 00:37:57506 # Always do nothing on bots.
507 if is_on_bot():
508 return 0
509
510 # Read current config, verify gclient solution looks correct.
[email protected]94c64122014-08-16 02:03:55511 config = scan_configuration()
[email protected]46b32a82014-08-19 00:37:57512 check_gclient_config(config)
513
514 # Do not attempt to push from non-google owned machines.
515 if not is_in_google_corp():
516 logging.info('Skipping git push check: non *.corp.google.com machine.')
517 return 0
518
519 # Skip git push check if current configuration was already checked.
[email protected]94c64122014-08-16 02:03:55520 if config == read_last_configuration():
521 logging.info('Check already performed, skipping.')
522 return 0
523
524 # Run the check. Mark configuration as checked only on success. Ignore any
525 # exceptions or errors. This check must not break gclient runhooks.
526 try:
[email protected]46b32a82014-08-19 00:37:57527 ok = check_git_config(config, options.report_url, False)
[email protected]94c64122014-08-16 02:03:55528 if ok:
529 write_last_configuration(config)
530 else:
531 logging.warning('Check failed and will be retried on the next run')
532 except Exception:
533 logging.exception('Unexpected exception when performing git access check')
534 return 0
535
536
537if __name__ == '__main__':
538 sys.exit(main(sys.argv[1:]))