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.
6 """Script that attempts to push to a special git repository to verify that git
7 credentials are configured correctly. It also verifies that gclient solution is
8 configured to use git checkout.
10 It will be added as gclient hook shortly before Chromium switches to git and
11 removed after the switch.
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
15 collect information about misconfigured Git accounts.
39 # Absolute path to src/ directory.
40 REPO_ROOT
= os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
)))
42 # Absolute path to a file with gclient solutions.
43 GCLIENT_CONFIG
= os
.path
.join(os
.path
.dirname(REPO_ROOT
), '.gclient')
45 # 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.
49 # Do not attempt to upload a report after this date.
50 UPLOAD_DISABLE_TS
= datetime
.datetime(2014, 10, 1)
52 # URL to POST json with results to.
54 'https://chromium-git-access.appspot.com/'
55 'git_access/api/v1/reports/access_check')
57 # Repository to push test commits to.
58 TEST_REPO_URL
= 'https://chromium.googlesource.com/a/playground/access_test'
60 # Git-compatible gclient solution.
61 GOOD_GCLIENT_SOLUTION
= {
65 'url': 'https://chromium.googlesource.com/chromium/src.git',
68 # Possible chunks of git push response in case .netrc is misconfigured.
70 '(prohibited by Gerrit)',
71 'does not match your user account',
72 'Git repository not found',
73 'Invalid user name or password',
74 'Please make sure you have the correct access rights',
77 # Git executable to call.
78 GIT_EXE
= 'git.bat' if sys
.platform
== 'win32' else 'git'
82 """True when running under buildbot."""
83 return os
.environ
.get('CHROME_HEADLESS') == '1'
86 def is_in_google_corp():
87 """True when running in google corp network."""
89 return socket
.getfqdn().endswith('.corp.google.com')
91 logging
.exception('Failed to get FQDN')
96 """True if git checkout is used."""
97 return os
.path
.exists(os
.path
.join(REPO_ROOT
, '.git', 'objects'))
101 """True if svn checkout is used."""
102 return os
.path
.exists(os
.path
.join(REPO_ROOT
, '.svn'))
105 def read_git_config(prop
):
106 """Reads git config property of src.git repo.
108 Returns empty string in case of errors.
111 proc
= subprocess
.Popen(
112 [GIT_EXE
, 'config', prop
], stdout
=subprocess
.PIPE
, cwd
=REPO_ROOT
)
113 out
, _
= proc
.communicate()
114 return out
.strip().decode('utf-8')
115 except OSError as exc
:
116 if exc
.errno
!= errno
.ENOENT
:
117 logging
.exception('Unexpected error when calling git')
121 def read_netrc_user(netrc_obj
, host
):
122 """Reads 'user' field of a host entry in netrc.
124 Returns empty string if netrc is missing, or host is not there.
128 entry
= netrc_obj
.authenticators(host
)
134 def get_git_version():
135 """Returns version of git or None if git is not available."""
137 proc
= subprocess
.Popen([GIT_EXE
, '--version'], stdout
=subprocess
.PIPE
)
138 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')
146 def read_gclient_solution():
147 """Read information about 'src' gclient solution from .gclient file.
150 (url, deps_file, managed)
152 (None, None, None) if no such solution.
156 execfile(GCLIENT_CONFIG
, env
, env
)
157 for sol
in (env
.get('solutions') or []):
158 if sol
.get('name') == 'src':
159 return sol
.get('url'), sol
.get('deps_file'), sol
.get('managed')
160 return None, None, None
162 logging
.exception('Failed to read .gclient solution')
163 return None, None, None
166 def read_git_insteadof(host
):
167 """Reads relevant insteadOf config entries."""
169 proc
= subprocess
.Popen([GIT_EXE
, 'config', '-l'], stdout
=subprocess
.PIPE
)
170 out
, _
= proc
.communicate()
172 for line
in out
.strip().split('\n'):
174 if 'insteadof=' in line
and host
in 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')
183 def scan_configuration():
184 """Scans local environment for git related configuration values."""
186 is_git
= is_using_git()
188 # On Windows HOME should be set.
189 if 'HOME' in os
.environ
:
190 netrc_path
= os
.path
.join(
192 '_netrc' if sys
.platform
.startswith('win') else '.netrc')
197 is_using_netrc
= netrc_path
and os
.path
.exists(netrc_path
)
203 netrc_obj
= netrc
.netrc(netrc_path
)
205 logging
.exception('Failed to read netrc from %s', netrc_path
)
208 # Read gclient 'src' solution.
209 gclient_url
, gclient_deps
, gclient_managed
= read_gclient_solution()
212 'checker_version': CHECKER_VERSION
,
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 '',
222 'git_insteadof': read_git_insteadof('chromium.googlesource.com'),
223 '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'),
227 'gclient_deps': gclient_deps
,
228 'gclient_managed': gclient_managed
,
229 'gclient_url': gclient_url
,
233 def last_configuration_path():
234 """Path to store last checked configuration."""
236 return os
.path
.join(REPO_ROOT
, '.git', 'check_git_push_access_conf.json')
238 return os
.path
.join(REPO_ROOT
, '.svn', 'check_git_push_access_conf.json')
240 return os
.path
.join(REPO_ROOT
, '.check_git_push_access_conf.json')
243 def read_last_configuration():
244 """Reads last checked configuration if it exists."""
246 with
open(last_configuration_path(), 'r') as f
:
248 except (IOError, ValueError):
252 def write_last_configuration(conf
):
253 """Writes last checked configuration to a file."""
255 with
open(last_configuration_path(), 'w') as f
:
256 json
.dump(conf
, f
, indent
=2, sort_keys
=True)
258 logging
.exception('Failed to write JSON to %s', path
)
261 @contextlib.contextmanager
262 def temp_directory():
263 """Creates a temp directory, then nukes it."""
264 tmp
= tempfile
.mkdtemp()
270 except (OSError, IOError):
271 logging
.exception('Failed to remove temp directory %s', tmp
)
274 class Runner(object):
275 """Runs a bunch of commands in some directory, collects logs from them."""
277 def __init__(self
, cwd
, verbose
):
279 self
.verbose
= verbose
283 self
.append_to_log('> ' + ' '.join(cmd
))
286 proc
= subprocess
.Popen(
288 stdout
=subprocess
.PIPE
,
289 stderr
=subprocess
.STDOUT
,
291 out
, _
= proc
.communicate()
293 retcode
= proc
.returncode
294 except OSError as exc
:
297 out
+= '\n(exit code: %d)' % retcode
298 self
.append_to_log(out
)
301 def append_to_log(self
, text
):
303 self
.log
.append(text
)
305 logging
.warning(text
)
308 def check_git_config(conf
, report_url
, verbose
):
309 """Attempts to push to a git repository, reports results to a server.
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.
314 # Don't even try to push if netrc is not configured.
315 if not conf
['chromium_netrc_email']:
316 return upload_report(
324 # Ref to push to, each user has its own ref.
325 ref
= 'refs/push-test/%s' % conf
['chromium_netrc_email']
329 started
= time
.time()
331 logging
.warning('Checking push access to the git repository...')
332 with
temp_directory() as tmp
:
333 # Prepare a simple commit on a new timeline.
334 runner
= Runner(tmp
, verbose
)
335 runner
.run([GIT_EXE
, 'init', '.'])
336 if conf
['git_user_name']:
337 runner
.run([GIT_EXE
, 'config', 'user.name', conf
['git_user_name']])
338 if conf
['git_user_email']:
339 runner
.run([GIT_EXE
, 'config', 'user.email', conf
['git_user_email']])
340 with
open(os
.path
.join(tmp
, 'timestamp'), 'w') as f
:
341 f
.write(str(int(time
.time() * 1000)))
342 runner
.run([GIT_EXE
, 'add', 'timestamp'])
343 runner
.run([GIT_EXE
, 'commit', '-m', 'Push test.'])
344 # Try to push multiple times if it fails due to issues other than ACLs.
348 logging
.info('Pushing to %s %s', TEST_REPO_URL
, ref
)
350 [GIT_EXE
, 'push', TEST_REPO_URL
, 'HEAD:%s' % ref
, '-f'])
354 if any(x
in runner
.log
[-1] for x
in BAD_ACL_ERRORS
):
358 logging
.exception('Unexpected exception when pushing')
362 logging
.warning('Git push works!')
365 'Git push doesn\'t work, which is fine if you are not a committer.')
367 uploaded
= upload_report(
371 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
377 def check_gclient_config(conf
):
378 """Shows warning if gclient solution is not properly configured for git."""
379 # Ignore configs that do not have 'src' solution at all.
380 if not conf
['gclient_url']:
384 'deps_file': conf
['gclient_deps'] or 'DEPS',
385 'managed': conf
['gclient_managed'] or False,
386 'url': conf
['gclient_url'],
388 # After depot_tools r291592 both DEPS and .DEPS.git are valid.
389 good
= GOOD_GCLIENT_SOLUTION
.copy()
390 good
['deps_file'] = current
['deps_file']
393 # Show big warning if url or deps_file is wrong.
394 if current
['url'] != good
['url'] or current
['deps_file'] != good
['deps_file']:
396 print 'Your gclient solution is not set to use supported git workflow!'
398 print 'Your \'src\' solution (in %s):' % GCLIENT_CONFIG
399 print pprint
.pformat(current
, indent
=2)
401 print 'Correct \'src\' solution to use git:'
402 print pprint
.pformat(good
, indent
=2)
404 print 'Please update your .gclient file ASAP.'
406 # Show smaller (additional) warning about managed workflow.
407 if current
['managed']:
410 'You are using managed gclient mode with git, which was deprecated '
413 'https://groups.google.com/a/chromium.org/'
414 'forum/#!topic/chromium-dev/n9N5N3JL2_U')
417 'It is strongly advised to switch to unmanaged mode. For more '
418 'information about managed mode and reasons for its deprecation see:')
419 print 'http://www.chromium.org/developers/how-tos/get-the-code/gclient-managed-mode'
422 '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).')
429 conf
, report_url
, verbose
, push_works
, push_log
, push_duration_ms
):
430 """Posts report to the server, returns True if server accepted it.
432 Uploads the report only if script is running in Google corp network. Otherwise
433 just prints the report.
437 push_works
=push_works
,
439 push_duration_ms
=push_duration_ms
)
441 as_bytes
= json
.dumps({'access_check': report
}, indent
=2, sort_keys
=True)
443 print 'Status of git push attempt:'
446 # 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
:
450 'You can send the above report to chrome-git-migration@google.com '
451 'if you need help to set up you committer git account.')
454 req
= urllib2
.Request(
457 headers
={'Content-Type': 'application/json; charset=utf-8'})
461 while not success
and attempt
< 10:
465 'Attempting to upload the report to %s...',
466 urlparse
.urlparse(report_url
).netloc
)
467 resp
= urllib2
.urlopen(req
, timeout
=5)
470 report_id
= json
.load(resp
)['report_id']
471 except (ValueError, TypeError, KeyError):
473 logging
.warning('Report uploaded: %s', report_id
)
475 except (urllib2
.URLError
, socket
.error
, ssl
.SSLError
) as exc
:
476 logging
.warning('Failed to upload the report: %s', exc
)
481 parser
= optparse
.OptionParser(description
=sys
.modules
[__name__
].__doc
__)
485 help='Set when invoked from gclient hook')
488 default
=MOTHERSHIP_URL
,
489 help='URL to submit the report to')
494 options
, args
= parser
.parse_args()
496 parser
.error('Unknown argument %s' % args
)
498 format
='%(message)s',
499 level
=logging
.INFO
if options
.verbose
else logging
.WARN
)
501 # When invoked not as a hook, always run the check.
502 if not options
.running_as_hook
:
503 config
= scan_configuration()
504 check_gclient_config(config
)
505 check_git_config(config
, options
.report_url
, True)
508 # Always do nothing on bots.
512 # Read current config, verify gclient solution looks correct.
513 config
= scan_configuration()
514 check_gclient_config(config
)
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.')
521 # Skip git push check if current configuration was already checked.
522 if config
== read_last_configuration():
523 logging
.info('Check already performed, skipping.')
526 # Run the check. Mark configuration as checked only on success. Ignore any
527 # exceptions or errors. This check must not break gclient runhooks.
529 ok
= check_git_config(config
, options
.report_url
, False)
531 write_last_configuration(config
)
533 logging
.warning('Check failed and will be retried on the next run')
535 logging
.exception('Unexpected exception when performing git access check')
539 if __name__
== '__main__':
540 sys
.exit(main(sys
.argv
[1:]))