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

Side by Side Diff: tools/check_git_push_access.py

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

Powered by Google App Engine
This is Rietveld 408576698