blob: 4907d85badb7c89d8823d03bbe9630232a0807e1 [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
Raul Tambre4cec36572019-09-22 17:30:3218from __future__ import print_function
19
[email protected]94c64122014-08-16 02:03:5520import contextlib
[email protected]46b32a82014-08-19 00:37:5721import datetime
[email protected]aa52d312014-08-18 20:28:5222import errno
[email protected]94c64122014-08-16 02:03:5523import getpass
24import json
25import logging
26import netrc
27import optparse
28import os
[email protected]46b32a82014-08-19 00:37:5729import pprint
[email protected]94c64122014-08-16 02:03:5530import shutil
31import socket
32import ssl
33import subprocess
34import sys
35import tempfile
36import time
37import urllib2
[email protected]aa52d312014-08-18 20:28:5238import urlparse
[email protected]94c64122014-08-16 02:03:5539
40
41# Absolute path to src/ directory.
42REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
43
[email protected]46b32a82014-08-19 00:37:5744# Absolute path to a file with gclient solutions.
45GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient')
46
[email protected]94c64122014-08-16 02:03:5547# Incremented whenever some changes to scrip logic are made. Change in version
48# will cause the check to be rerun on next gclient runhooks invocation.
[email protected]b3b918c72014-08-21 00:08:3249CHECKER_VERSION = 1
[email protected]94c64122014-08-16 02:03:5550
[email protected]46b32a82014-08-19 00:37:5751# Do not attempt to upload a report after this date.
52UPLOAD_DISABLE_TS = datetime.datetime(2014, 10, 1)
53
[email protected]94c64122014-08-16 02:03:5554# URL to POST json with results to.
55MOTHERSHIP_URL = (
56 'https://chromium-git-access.appspot.com/'
57 'git_access/api/v1/reports/access_check')
58
59# Repository to push test commits to.
60TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test'
61
[email protected]46b32a82014-08-19 00:37:5762# Git-compatible gclient solution.
63GOOD_GCLIENT_SOLUTION = {
64 'name': 'src',
Vadim Shtayuradaf35ab2014-08-23 02:08:5365 'deps_file': 'DEPS',
[email protected]46b32a82014-08-19 00:37:5766 'managed': False,
67 'url': 'https://chromium.googlesource.com/chromium/src.git',
68}
69
[email protected]94c64122014-08-16 02:03:5570# Possible chunks of git push response in case .netrc is misconfigured.
71BAD_ACL_ERRORS = (
72 '(prohibited by Gerrit)',
[email protected]aa52d312014-08-18 20:28:5273 'does not match your user account',
[email protected]b3b918c72014-08-21 00:08:3274 'Git repository not found',
[email protected]94c64122014-08-16 02:03:5575 'Invalid user name or password',
[email protected]e92d872c2014-08-19 21:02:0276 'Please make sure you have the correct access rights',
[email protected]94c64122014-08-16 02:03:5577)
78
[email protected]b3b918c72014-08-21 00:08:3279# Git executable to call.
80GIT_EXE = 'git.bat' if sys.platform == 'win32' else 'git'
81
[email protected]94c64122014-08-16 02:03:5582
83def is_on_bot():
84 """True when running under buildbot."""
85 return os.environ.get('CHROME_HEADLESS') == '1'
86
87
88def is_in_google_corp():
89 """True when running in google corp network."""
90 try:
91 return socket.getfqdn().endswith('.corp.google.com')
92 except socket.error:
93 logging.exception('Failed to get FQDN')
94 return False
95
96
97def is_using_git():
98 """True if git checkout is used."""
99 return os.path.exists(os.path.join(REPO_ROOT, '.git', 'objects'))
100
101
102def is_using_svn():
103 """True if svn checkout is used."""
104 return os.path.exists(os.path.join(REPO_ROOT, '.svn'))
105
106
107def read_git_config(prop):
[email protected]aa52d312014-08-18 20:28:52108 """Reads git config property of src.git repo.
109
110 Returns empty string in case of errors.
111 """
112 try:
113 proc = subprocess.Popen(
[email protected]b3b918c72014-08-21 00:08:32114 [GIT_EXE, 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT)
[email protected]aa52d312014-08-18 20:28:52115 out, _ = proc.communicate()
drott7813ff32015-08-18 06:27:00116 return out.strip().decode('utf-8')
[email protected]aa52d312014-08-18 20:28:52117 except OSError as exc:
118 if exc.errno != errno.ENOENT:
119 logging.exception('Unexpected error when calling git')
120 return ''
[email protected]94c64122014-08-16 02:03:55121
122
123def read_netrc_user(netrc_obj, host):
124 """Reads 'user' field of a host entry in netrc.
125
126 Returns empty string if netrc is missing, or host is not there.
127 """
128 if not netrc_obj:
129 return ''
130 entry = netrc_obj.authenticators(host)
131 if not entry:
132 return ''
133 return entry[0]
134
135
136def get_git_version():
137 """Returns version of git or None if git is not available."""
[email protected]aa52d312014-08-18 20:28:52138 try:
[email protected]b3b918c72014-08-21 00:08:32139 proc = subprocess.Popen([GIT_EXE, '--version'], stdout=subprocess.PIPE)
[email protected]aa52d312014-08-18 20:28:52140 out, _ = proc.communicate()
141 return out.strip() if proc.returncode == 0 else ''
142 except OSError as exc:
143 if exc.errno != errno.ENOENT:
144 logging.exception('Unexpected error when calling git')
145 return ''
[email protected]94c64122014-08-16 02:03:55146
147
[email protected]46b32a82014-08-19 00:37:57148def read_gclient_solution():
149 """Read information about 'src' gclient solution from .gclient file.
150
151 Returns tuple:
152 (url, deps_file, managed)
153 or
154 (None, None, None) if no such solution.
155 """
156 try:
157 env = {}
158 execfile(GCLIENT_CONFIG, env, env)
[email protected]97c58bc2014-08-20 18:15:30159 for sol in (env.get('solutions') or []):
160 if sol.get('name') == 'src':
[email protected]46b32a82014-08-19 00:37:57161 return sol.get('url'), sol.get('deps_file'), sol.get('managed')
162 return None, None, None
163 except Exception:
164 logging.exception('Failed to read .gclient solution')
165 return None, None, None
166
167
[email protected]b3b918c72014-08-21 00:08:32168def read_git_insteadof(host):
169 """Reads relevant insteadOf config entries."""
170 try:
171 proc = subprocess.Popen([GIT_EXE, 'config', '-l'], stdout=subprocess.PIPE)
172 out, _ = proc.communicate()
173 lines = []
174 for line in out.strip().split('\n'):
175 line = line.lower()
176 if 'insteadof=' in line and host in line:
177 lines.append(line)
178 return '\n'.join(lines)
179 except OSError as exc:
180 if exc.errno != errno.ENOENT:
181 logging.exception('Unexpected error when calling git')
182 return ''
183
184
[email protected]94c64122014-08-16 02:03:55185def scan_configuration():
186 """Scans local environment for git related configuration values."""
187 # Git checkout?
188 is_git = is_using_git()
189
190 # On Windows HOME should be set.
191 if 'HOME' in os.environ:
192 netrc_path = os.path.join(
193 os.environ['HOME'],
194 '_netrc' if sys.platform.startswith('win') else '.netrc')
195 else:
196 netrc_path = None
197
198 # Netrc exists?
199 is_using_netrc = netrc_path and os.path.exists(netrc_path)
200
201 # Read it.
202 netrc_obj = None
203 if is_using_netrc:
204 try:
205 netrc_obj = netrc.netrc(netrc_path)
206 except Exception:
207 logging.exception('Failed to read netrc from %s', netrc_path)
208 netrc_obj = None
209
[email protected]46b32a82014-08-19 00:37:57210 # Read gclient 'src' solution.
211 gclient_url, gclient_deps, gclient_managed = read_gclient_solution()
212
[email protected]94c64122014-08-16 02:03:55213 return {
214 'checker_version': CHECKER_VERSION,
215 'is_git': is_git,
216 'is_home_set': 'HOME' in os.environ,
217 'is_using_netrc': is_using_netrc,
218 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0,
219 'git_version': get_git_version(),
220 'platform': sys.platform,
221 'username': getpass.getuser(),
222 'git_user_email': read_git_config('user.email') if is_git else '',
223 'git_user_name': read_git_config('user.name') if is_git else '',
[email protected]b3b918c72014-08-21 00:08:32224 'git_insteadof': read_git_insteadof('chromium.googlesource.com'),
[email protected]94c64122014-08-16 02:03:55225 'chromium_netrc_email':
226 read_netrc_user(netrc_obj, 'chromium.googlesource.com'),
227 'chrome_internal_netrc_email':
228 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'),
[email protected]46b32a82014-08-19 00:37:57229 'gclient_deps': gclient_deps,
230 'gclient_managed': gclient_managed,
231 'gclient_url': gclient_url,
[email protected]94c64122014-08-16 02:03:55232 }
233
234
235def last_configuration_path():
236 """Path to store last checked configuration."""
237 if is_using_git():
[email protected]aa52d312014-08-18 20:28:52238 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55239 elif is_using_svn():
[email protected]aa52d312014-08-18 20:28:52240 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55241 else:
[email protected]aa52d312014-08-18 20:28:52242 return os.path.join(REPO_ROOT, '.check_git_push_access_conf.json')
[email protected]94c64122014-08-16 02:03:55243
244
245def read_last_configuration():
246 """Reads last checked configuration if it exists."""
247 try:
248 with open(last_configuration_path(), 'r') as f:
249 return json.load(f)
250 except (IOError, ValueError):
251 return None
252
253
254def write_last_configuration(conf):
255 """Writes last checked configuration to a file."""
256 try:
257 with open(last_configuration_path(), 'w') as f:
258 json.dump(conf, f, indent=2, sort_keys=True)
259 except IOError:
260 logging.exception('Failed to write JSON to %s', path)
261
262
263@contextlib.contextmanager
264def temp_directory():
265 """Creates a temp directory, then nukes it."""
266 tmp = tempfile.mkdtemp()
267 try:
268 yield tmp
269 finally:
270 try:
271 shutil.rmtree(tmp)
272 except (OSError, IOError):
273 logging.exception('Failed to remove temp directory %s', tmp)
274
275
276class Runner(object):
277 """Runs a bunch of commands in some directory, collects logs from them."""
278
[email protected]aa52d312014-08-18 20:28:52279 def __init__(self, cwd, verbose):
[email protected]94c64122014-08-16 02:03:55280 self.cwd = cwd
[email protected]aa52d312014-08-18 20:28:52281 self.verbose = verbose
[email protected]94c64122014-08-16 02:03:55282 self.log = []
283
284 def run(self, cmd):
[email protected]aa52d312014-08-18 20:28:52285 self.append_to_log('> ' + ' '.join(cmd))
286 retcode = -1
287 try:
288 proc = subprocess.Popen(
289 cmd,
290 stdout=subprocess.PIPE,
291 stderr=subprocess.STDOUT,
292 cwd=self.cwd)
293 out, _ = proc.communicate()
294 out = out.strip()
295 retcode = proc.returncode
296 except OSError as exc:
297 out = str(exc)
298 if retcode:
299 out += '\n(exit code: %d)' % retcode
300 self.append_to_log(out)
301 return retcode
302
303 def append_to_log(self, text):
304 if text:
305 self.log.append(text)
306 if self.verbose:
307 logging.warning(text)
[email protected]94c64122014-08-16 02:03:55308
309
[email protected]46b32a82014-08-19 00:37:57310def check_git_config(conf, report_url, verbose):
[email protected]94c64122014-08-16 02:03:55311 """Attempts to push to a git repository, reports results to a server.
312
313 Returns True if the check finished without incidents (push itself may
314 have failed) and should NOT be retried on next invocation of the hook.
315 """
[email protected]94c64122014-08-16 02:03:55316 # Don't even try to push if netrc is not configured.
317 if not conf['chromium_netrc_email']:
318 return upload_report(
319 conf,
320 report_url,
[email protected]aa52d312014-08-18 20:28:52321 verbose,
[email protected]94c64122014-08-16 02:03:55322 push_works=False,
323 push_log='',
324 push_duration_ms=0)
325
326 # Ref to push to, each user has its own ref.
327 ref = 'refs/push-test/%s' % conf['chromium_netrc_email']
328
329 push_works = False
330 flake = False
331 started = time.time()
332 try:
[email protected]46b32a82014-08-19 00:37:57333 logging.warning('Checking push access to the git repository...')
[email protected]94c64122014-08-16 02:03:55334 with temp_directory() as tmp:
335 # Prepare a simple commit on a new timeline.
[email protected]aa52d312014-08-18 20:28:52336 runner = Runner(tmp, verbose)
[email protected]b3b918c72014-08-21 00:08:32337 runner.run([GIT_EXE, 'init', '.'])
[email protected]94c64122014-08-16 02:03:55338 if conf['git_user_name']:
[email protected]b3b918c72014-08-21 00:08:32339 runner.run([GIT_EXE, 'config', 'user.name', conf['git_user_name']])
[email protected]94c64122014-08-16 02:03:55340 if conf['git_user_email']:
[email protected]b3b918c72014-08-21 00:08:32341 runner.run([GIT_EXE, 'config', 'user.email', conf['git_user_email']])
[email protected]94c64122014-08-16 02:03:55342 with open(os.path.join(tmp, 'timestamp'), 'w') as f:
343 f.write(str(int(time.time() * 1000)))
[email protected]b3b918c72014-08-21 00:08:32344 runner.run([GIT_EXE, 'add', 'timestamp'])
345 runner.run([GIT_EXE, 'commit', '-m', 'Push test.'])
[email protected]94c64122014-08-16 02:03:55346 # Try to push multiple times if it fails due to issues other than ACLs.
347 attempt = 0
348 while attempt < 5:
349 attempt += 1
350 logging.info('Pushing to %s %s', TEST_REPO_URL, ref)
[email protected]b3b918c72014-08-21 00:08:32351 ret = runner.run(
352 [GIT_EXE, 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f'])
[email protected]94c64122014-08-16 02:03:55353 if not ret:
354 push_works = True
355 break
356 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS):
357 push_works = False
358 break
359 except Exception:
360 logging.exception('Unexpected exception when pushing')
361 flake = True
362
[email protected]46b32a82014-08-19 00:37:57363 if push_works:
364 logging.warning('Git push works!')
365 else:
366 logging.warning(
367 'Git push doesn\'t work, which is fine if you are not a committer.')
368
[email protected]94c64122014-08-16 02:03:55369 uploaded = upload_report(
370 conf,
371 report_url,
[email protected]aa52d312014-08-18 20:28:52372 verbose,
[email protected]94c64122014-08-16 02:03:55373 push_works=push_works,
374 push_log='\n'.join(runner.log),
375 push_duration_ms=int((time.time() - started) * 1000))
376 return uploaded and not flake
377
378
[email protected]46b32a82014-08-19 00:37:57379def check_gclient_config(conf):
380 """Shows warning if gclient solution is not properly configured for git."""
[email protected]97c58bc2014-08-20 18:15:30381 # Ignore configs that do not have 'src' solution at all.
382 if not conf['gclient_url']:
383 return
[email protected]46b32a82014-08-19 00:37:57384 current = {
385 'name': 'src',
Vadim Shtayurad484d592014-08-23 02:49:13386 'deps_file': conf['gclient_deps'] or 'DEPS',
[email protected]b3b918c72014-08-21 00:08:32387 'managed': conf['gclient_managed'] or False,
[email protected]46b32a82014-08-19 00:37:57388 'url': conf['gclient_url'],
389 }
vadimsh910466b2014-08-24 23:03:42390 # After depot_tools r291592 both DEPS and .DEPS.git are valid.
391 good = GOOD_GCLIENT_SOLUTION.copy()
392 good['deps_file'] = current['deps_file']
[email protected]b3b918c72014-08-21 00:08:32393 if current == good:
394 return
395 # Show big warning if url or deps_file is wrong.
396 if current['url'] != good['url'] or current['deps_file'] != good['deps_file']:
Raul Tambre4cec36572019-09-22 17:30:32397 print('-' * 80)
398 print('Your gclient solution is not set to use supported git workflow!')
399 print()
400 print('Your \'src\' solution (in %s):' % GCLIENT_CONFIG)
401 print(pprint.pformat(current, indent=2))
402 print()
403 print('Correct \'src\' solution to use git:')
404 print(pprint.pformat(good, indent=2))
405 print()
406 print('Please update your .gclient file ASAP.')
407 print('-' * 80)
[email protected]b3b918c72014-08-21 00:08:32408 # Show smaller (additional) warning about managed workflow.
409 if current['managed']:
Raul Tambre4cec36572019-09-22 17:30:32410 print('-' * 80)
411 print('You are using managed gclient mode with git, which was deprecated '
412 'on 8/22/13:')
413 print('https://groups.google.com/a/chromium.org/'
414 'forum/#!topic/chromium-dev/n9N5N3JL2_U')
415 print()
416 print('It is strongly advised to switch to unmanaged mode. For more '
417 'information about managed mode and reasons for its deprecation see:')
418 print(
419 'http://www.chromium.org/developers/how-tos/get-the-code/gclient-managed-mode'
420 )
421 print()
422 print('There\'s also a large suite of tools to assist managing git '
423 'checkouts.\nSee \'man depot_tools\' (or read '
424 'depot_tools/man/html/depot_tools.html).')
425 print('-' * 80)
[email protected]46b32a82014-08-19 00:37:57426
427
[email protected]94c64122014-08-16 02:03:55428def upload_report(
[email protected]aa52d312014-08-18 20:28:52429 conf, report_url, verbose, push_works, push_log, push_duration_ms):
[email protected]94c64122014-08-16 02:03:55430 """Posts report to the server, returns True if server accepted it.
431
[email protected]aa52d312014-08-18 20:28:52432 Uploads the report only if script is running in Google corp network. Otherwise
433 just prints the report.
[email protected]94c64122014-08-16 02:03:55434 """
435 report = conf.copy()
436 report.update(
437 push_works=push_works,
438 push_log=push_log,
439 push_duration_ms=push_duration_ms)
440
441 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True)
[email protected]aa52d312014-08-18 20:28:52442 if verbose:
Raul Tambre4cec36572019-09-22 17:30:32443 print('Status of git push attempt:')
444 print(as_bytes)
[email protected]94c64122014-08-16 02:03:55445
[email protected]46b32a82014-08-19 00:37:57446 # Do not upload it outside of corp or if server side is already disabled.
447 if not is_in_google_corp() or datetime.datetime.now() > UPLOAD_DISABLE_TS:
[email protected]aa52d312014-08-18 20:28:52448 if verbose:
[email protected]94c64122014-08-16 02:03:55449 print (
450 'You can send the above report to [email protected] '
451 'if you need help to set up you committer git account.')
452 return True
453
454 req = urllib2.Request(
455 url=report_url,
456 data=as_bytes,
457 headers={'Content-Type': 'application/json; charset=utf-8'})
458
459 attempt = 0
460 success = False
461 while not success and attempt < 10:
462 attempt += 1
463 try:
[email protected]aa52d312014-08-18 20:28:52464 logging.warning(
465 'Attempting to upload the report to %s...',
466 urlparse.urlparse(report_url).netloc)
467 resp = urllib2.urlopen(req, timeout=5)
468 report_id = None
469 try:
470 report_id = json.load(resp)['report_id']
471 except (ValueError, TypeError, KeyError):
472 pass
473 logging.warning('Report uploaded: %s', report_id)
[email protected]94c64122014-08-16 02:03:55474 success = True
[email protected]94c64122014-08-16 02:03:55475 except (urllib2.URLError, socket.error, ssl.SSLError) as exc:
[email protected]aa52d312014-08-18 20:28:52476 logging.warning('Failed to upload the report: %s', exc)
[email protected]94c64122014-08-16 02:03:55477 return success
478
479
480def main(args):
481 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
482 parser.add_option(
483 '--running-as-hook',
484 action='store_true',
485 help='Set when invoked from gclient hook')
486 parser.add_option(
487 '--report-url',
488 default=MOTHERSHIP_URL,
489 help='URL to submit the report to')
490 parser.add_option(
491 '--verbose',
492 action='store_true',
493 help='More logging')
494 options, args = parser.parse_args()
495 if args:
496 parser.error('Unknown argument %s' % args)
497 logging.basicConfig(
498 format='%(message)s',
499 level=logging.INFO if options.verbose else logging.WARN)
500
[email protected]46b32a82014-08-19 00:37:57501 # When invoked not as a hook, always run the check.
[email protected]94c64122014-08-16 02:03:55502 if not options.running_as_hook:
[email protected]46b32a82014-08-19 00:37:57503 config = scan_configuration()
504 check_gclient_config(config)
505 check_git_config(config, options.report_url, True)
[email protected]94c64122014-08-16 02:03:55506 return 0
507
[email protected]46b32a82014-08-19 00:37:57508 # Always do nothing on bots.
509 if is_on_bot():
510 return 0
511
512 # Read current config, verify gclient solution looks correct.
[email protected]94c64122014-08-16 02:03:55513 config = scan_configuration()
[email protected]46b32a82014-08-19 00:37:57514 check_gclient_config(config)
515
516 # Do not attempt to push from non-google owned machines.
517 if not is_in_google_corp():
518 logging.info('Skipping git push check: non *.corp.google.com machine.')
519 return 0
520
521 # Skip git push check if current configuration was already checked.
[email protected]94c64122014-08-16 02:03:55522 if config == read_last_configuration():
523 logging.info('Check already performed, skipping.')
524 return 0
525
526 # Run the check. Mark configuration as checked only on success. Ignore any
527 # exceptions or errors. This check must not break gclient runhooks.
528 try:
[email protected]46b32a82014-08-19 00:37:57529 ok = check_git_config(config, options.report_url, False)
[email protected]94c64122014-08-16 02:03:55530 if ok:
531 write_last_configuration(config)
532 else:
533 logging.warning('Check failed and will be retried on the next run')
534 except Exception:
535 logging.exception('Unexpected exception when performing git access check')
536 return 0
537
538
539if __name__ == '__main__':
540 sys.exit(main(sys.argv[1:]))