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

Side by Side Diff: tools/check_git_push_access.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
« tools/check_git_config.py ('K') | « tools/check_git_config.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/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
7 credentials are configured correctly. It also attempts to fix misconfigurations
8 if possible.
9
10 It will be added as gclient hook shortly before Chromium switches to git and
11 removed after the switch.
12
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 (to fix them).
16
17 When invoked manually will do the access test and submit the report regardless
18 of where it is running.
19 """
20
21 import contextlib
22 import errno
23 import getpass
24 import json
25 import logging
26 import netrc
27 import optparse
28 import os
29 import shutil
30 import socket
31 import ssl
32 import subprocess
33 import sys
34 import tempfile
35 import time
36 import urllib2
37 import urlparse
38
39
40 # Absolute path to src/ directory.
41 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
43 # 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.
45 CHECKER_VERSION = 0
46
47 # URL to POST json with results to.
48 MOTHERSHIP_URL = (
49 'https://chromium-git-access.appspot.com/'
50 'git_access/api/v1/reports/access_check')
51
52 # Repository to push test commits to.
53 TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test'
54
55 # Possible chunks of git push response in case .netrc is misconfigured.
56 BAD_ACL_ERRORS = (
57 '(prohibited by Gerrit)',
58 'does not match your user account',
59 'Invalid user name or password',
60 )
61
62
63 def is_on_bot():
64 """True when running under buildbot."""
65 return os.environ.get('CHROME_HEADLESS') == '1'
66
67
68 def is_in_google_corp():
69 """True when running in google corp network."""
70 try:
71 return socket.getfqdn().endswith('.corp.google.com')
72 except socket.error:
73 logging.exception('Failed to get FQDN')
74 return False
75
76
77 def is_using_git():
78 """True if git checkout is used."""
79 return os.path.exists(os.path.join(REPO_ROOT, '.git', 'objects'))
80
81
82 def is_using_svn():
83 """True if svn checkout is used."""
84 return os.path.exists(os.path.join(REPO_ROOT, '.svn'))
85
86
87 def read_git_config(prop):
88 """Reads git config property of src.git repo.
89
90 Returns empty string in case of errors.
91 """
92 try:
93 proc = subprocess.Popen(
94 ['git', 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT)
95 out, _ = proc.communicate()
96 return out.strip()
97 except OSError as exc:
98 if exc.errno != errno.ENOENT:
99 logging.exception('Unexpected error when calling git')
100 return ''
101
102
103 def read_netrc_user(netrc_obj, host):
104 """Reads 'user' field of a host entry in netrc.
105
106 Returns empty string if netrc is missing, or host is not there.
107 """
108 if not netrc_obj:
109 return ''
110 entry = netrc_obj.authenticators(host)
111 if not entry:
112 return ''
113 return entry[0]
114
115
116 def get_git_version():
117 """Returns version of git or None if git is not available."""
118 try:
119 proc = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE)
120 out, _ = proc.communicate()
121 return out.strip() if proc.returncode == 0 else ''
122 except OSError as exc:
123 if exc.errno != errno.ENOENT:
124 logging.exception('Unexpected error when calling git')
125 return ''
126
127
128 def scan_configuration():
129 """Scans local environment for git related configuration values."""
130 # Git checkout?
131 is_git = is_using_git()
132
133 # On Windows HOME should be set.
134 if 'HOME' in os.environ:
135 netrc_path = os.path.join(
136 os.environ['HOME'],
137 '_netrc' if sys.platform.startswith('win') else '.netrc')
138 else:
139 netrc_path = None
140
141 # Netrc exists?
142 is_using_netrc = netrc_path and os.path.exists(netrc_path)
143
144 # Read it.
145 netrc_obj = None
146 if is_using_netrc:
147 try:
148 netrc_obj = netrc.netrc(netrc_path)
149 except Exception:
150 logging.exception('Failed to read netrc from %s', netrc_path)
151 netrc_obj = None
152
153 return {
154 'checker_version': CHECKER_VERSION,
155 'is_git': is_git,
156 'is_home_set': 'HOME' in os.environ,
157 'is_using_netrc': is_using_netrc,
158 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0,
159 'git_version': get_git_version(),
160 'platform': sys.platform,
161 'username': getpass.getuser(),
162 '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 '',
164 'chromium_netrc_email':
165 read_netrc_user(netrc_obj, 'chromium.googlesource.com'),
166 'chrome_internal_netrc_email':
167 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'),
168 }
169
170
171 def last_configuration_path():
172 """Path to store last checked configuration."""
173 if is_using_git():
174 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json')
175 elif is_using_svn():
176 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json')
177 else:
178 return os.path.join(REPO_ROOT, '.check_git_push_access_conf.json')
179
180
181 def read_last_configuration():
182 """Reads last checked configuration if it exists."""
183 try:
184 with open(last_configuration_path(), 'r') as f:
185 return json.load(f)
186 except (IOError, ValueError):
187 return None
188
189
190 def write_last_configuration(conf):
191 """Writes last checked configuration to a file."""
192 try:
193 with open(last_configuration_path(), 'w') as f:
194 json.dump(conf, f, indent=2, sort_keys=True)
195 except IOError:
196 logging.exception('Failed to write JSON to %s', path)
197
198
199 @contextlib.contextmanager
200 def temp_directory():
201 """Creates a temp directory, then nukes it."""
202 tmp = tempfile.mkdtemp()
203 try:
204 yield tmp
205 finally:
206 try:
207 shutil.rmtree(tmp)
208 except (OSError, IOError):
209 logging.exception('Failed to remove temp directory %s', tmp)
210
211
212 class Runner(object):
213 """Runs a bunch of commands in some directory, collects logs from them."""
214
215 def __init__(self, cwd, verbose):
216 self.cwd = cwd
217 self.verbose = verbose
218 self.log = []
219
220 def run(self, cmd):
221 self.append_to_log('> ' + ' '.join(cmd))
222 retcode = -1
223 try:
224 proc = subprocess.Popen(
225 cmd,
226 stdout=subprocess.PIPE,
227 stderr=subprocess.STDOUT,
228 cwd=self.cwd)
229 out, _ = proc.communicate()
230 out = out.strip()
231 retcode = proc.returncode
232 except OSError as exc:
233 out = str(exc)
234 if retcode:
235 out += '\n(exit code: %d)' % retcode
236 self.append_to_log(out)
237 return retcode
238
239 def append_to_log(self, text):
240 if text:
241 self.log.append(text)
242 if self.verbose:
243 logging.warning(text)
244
245
246 def check_git_access(conf, report_url, verbose):
247 """Attempts to push to a git repository, reports results to a server.
248
249 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.
251 """
252 logging.warning('Checking push access to the git repository...')
253
254 # Don't even try to push if netrc is not configured.
255 if not conf['chromium_netrc_email']:
256 return upload_report(
257 conf,
258 report_url,
259 verbose,
260 push_works=False,
261 push_log='',
262 push_duration_ms=0)
263
264 # Ref to push to, each user has its own ref.
265 ref = 'refs/push-test/%s' % conf['chromium_netrc_email']
266
267 push_works = False
268 flake = False
269 started = time.time()
270 try:
271 with temp_directory() as tmp:
272 # Prepare a simple commit on a new timeline.
273 runner = Runner(tmp, verbose)
274 runner.run(['git', 'init', '.'])
275 if conf['git_user_name']:
276 runner.run(['git', 'config', 'user.name', conf['git_user_name']])
277 if conf['git_user_email']:
278 runner.run(['git', 'config', 'user.email', conf['git_user_email']])
279 with open(os.path.join(tmp, 'timestamp'), 'w') as f:
280 f.write(str(int(time.time() * 1000)))
281 runner.run(['git', 'add', 'timestamp'])
282 runner.run(['git', 'commit', '-m', 'Push test.'])
283 # Try to push multiple times if it fails due to issues other than ACLs.
284 attempt = 0
285 while attempt < 5:
286 attempt += 1
287 logging.info('Pushing to %s %s', TEST_REPO_URL, ref)
288 ret = runner.run(['git', 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f'])
289 if not ret:
290 push_works = True
291 break
292 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS):
293 push_works = False
294 break
295 except Exception:
296 logging.exception('Unexpected exception when pushing')
297 flake = True
298
299 uploaded = upload_report(
300 conf,
301 report_url,
302 verbose,
303 push_works=push_works,
304 push_log='\n'.join(runner.log),
305 push_duration_ms=int((time.time() - started) * 1000))
306 return uploaded and not flake
307
308
309 def upload_report(
310 conf, report_url, verbose, push_works, push_log, push_duration_ms):
311 """Posts report to the server, returns True if server accepted it.
312
313 Uploads the report only if script is running in Google corp network. Otherwise
314 just prints the report.
315 """
316 report = conf.copy()
317 report.update(
318 push_works=push_works,
319 push_log=push_log,
320 push_duration_ms=push_duration_ms)
321
322 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:
331 print 'Status of git push attempt:'
332 print as_bytes
333
334 # Do not upload it outside of corp.
335 if not is_in_google_corp():
336 if verbose:
337 print (
338 'You can send the above report to chrome-git-migration@google.com '
339 'if you need help to set up you committer git account.')
340 return True
341
342 req = urllib2.Request(
343 url=report_url,
344 data=as_bytes,
345 headers={'Content-Type': 'application/json; charset=utf-8'})
346
347 attempt = 0
348 success = False
349 while not success and attempt < 10:
350 attempt += 1
351 try:
352 logging.warning(
353 'Attempting to upload the report to %s...',
354 urlparse.urlparse(report_url).netloc)
355 resp = urllib2.urlopen(req, timeout=5)
356 report_id = None
357 try:
358 report_id = json.load(resp)['report_id']
359 except (ValueError, TypeError, KeyError):
360 pass
361 logging.warning('Report uploaded: %s', report_id)
362 success = True
363 except (urllib2.URLError, socket.error, ssl.SSLError) as exc:
364 logging.warning('Failed to upload the report: %s', exc)
365 return success
366
367
368 def main(args):
369 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
370 parser.add_option(
371 '--running-as-hook',
372 action='store_true',
373 help='Set when invoked from gclient hook')
374 parser.add_option(
375 '--report-url',
376 default=MOTHERSHIP_URL,
377 help='URL to submit the report to')
378 parser.add_option(
379 '--verbose',
380 action='store_true',
381 help='More logging')
382 options, args = parser.parse_args()
383 if args:
384 parser.error('Unknown argument %s' % args)
385 logging.basicConfig(
386 format='%(message)s',
387 level=logging.INFO if options.verbose else logging.WARN)
388
389 # When invoked not as hook, always run the check.
390 if not options.running_as_hook:
391 if check_git_access(scan_configuration(), options.report_url, True):
392 return 0
393 return 1
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
399
400 # Skip the check if current configuration was already checked.
401 config = scan_configuration()
402 if config == read_last_configuration():
403 logging.info('Check already performed, skipping.')
404 return 0
405
406 # Run the check. Mark configuration as checked only on success. Ignore any
407 # exceptions or errors. This check must not break gclient runhooks.
408 try:
409 ok = check_git_access(config, options.report_url, False)
410 if ok:
411 write_last_configuration(config)
412 else:
413 logging.warning('Check failed and will be retried on the next run')
414 except Exception:
415 logging.exception('Unexpected exception when performing git access check')
416 return 0
417
418
419 if __name__ == '__main__':
420 sys.exit(main(sys.argv[1:]))
OLDNEW
« tools/check_git_config.py ('K') | « tools/check_git_config.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698