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

Side by Side Diff: tools/check_git_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: add confirmation 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 read_git_config(prop):
80 """Reads git config property of src.git repo."""
81 proc = subprocess.Popen(
82 ['git', 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT)
83 out, _ = proc.communicate()
84 return out.strip()
85
86
87 def read_netrc_user(netrc_obj, host):
88 """Reads 'user' field of a host entry in netrc.
89
90 Returns empty string if netrc is missing, or host is not there.
91 """
92 if not netrc_obj:
93 return ''
94 entry = netrc_obj.authenticators(host)
95 if not entry:
96 return ''
97 return entry[0]
98
99
100 def scan_configuration():
101 """Scans local environment for git related configuration values."""
102 # Git checkout?
103 is_git = is_using_git()
104
105 # On Windows HOME should be set.
106 if 'HOME' in os.environ:
107 netrc_path = os.path.join(
108 os.environ['HOME'],
109 '_netrc' if sys.platform.startswith('win') else '.netrc')
110 else:
111 netrc_path = None
112
113 # Netrc exists?
114 is_using_netrc = netrc_path and os.path.exists(netrc_path)
115
116 # Read it.
117 netrc_obj = None
118 if is_using_netrc:
119 try:
120 netrc_obj = netrc.netrc(netrc_path)
121 except Exception:
122 logging.exception('Failed to read netrc from %s', netrc_path)
123 netrc_obj = None
124
125 return {
126 'checker_version': CHECKER_VERSION,
127 'is_git': is_git,
128 'is_home_set': 'HOME' in os.environ,
129 'is_using_netrc': is_using_netrc,
130 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0,
131 'platform': sys.platform,
132 'username': getpass.getuser(),
133 'git_user_email': read_git_config('user.email') if is_git else '',
134 'git_user_name': read_git_config('user.name') if is_git else '',
135 'chromium_netrc_email':
136 read_netrc_user(netrc_obj, 'chromium.googlesource.com'),
137 'chrome_internal_netrc_email':
138 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'),
139 }
140
141
142 def last_configuration_path():
143 """Path to store last checked configuration."""
144 if is_using_git():
145 return os.path.join(REPO_ROOT, '.git', 'check_git_access_conf.json')
146 else:
147 return os.path.join(REPO_ROOT, '.check_git_access_conf.json')
148
149
150 def read_last_configuration():
151 """Reads last checked configuration if it exists."""
152 try:
153 with open(last_configuration_path(), 'r') as f:
154 return json.load(f)
155 except (IOError, ValueError):
156 return None
157
158
159 def write_last_configuration(conf):
160 """Writes last checked configuration to a file."""
161 try:
162 with open(last_configuration_path(), 'w') as f:
163 json.dump(conf, f, indent=2, sort_keys=True)
164 except IOError:
165 logging.exception('Failed to write JSON to %s', path)
166
167
168 @contextlib.contextmanager
169 def temp_directory():
170 """Creates a temp directory, then nukes it."""
171 tmp = tempfile.mkdtemp()
172 try:
173 yield tmp
174 finally:
175 try:
176 shutil.rmtree(tmp)
177 except (OSError, IOError):
178 logging.exception('Failed to remove temp directory %s', tmp)
179
180
181 class Runner(object):
182 """Runs a bunch of commands in some directory, collects logs from them."""
183
184 def __init__(self, cwd):
185 self.cwd = cwd
186 self.log = []
187
188 def run(self, cmd):
189 log = ['> ' + ' '.join(cmd)]
190 proc = subprocess.Popen(
191 cmd,
192 stdout=subprocess.PIPE,
193 stderr=subprocess.STDOUT,
194 cwd=self.cwd)
195 out, _ = proc.communicate()
196 out = out.strip()
197 if out:
198 log.append(out)
199 if proc.returncode:
200 log.append('(exit code: %d)' % proc.returncode)
201 self.log.append('\n'.join(log))
202 return proc.returncode
203
204
205 def check_git_access(conf, report_url, interactive):
206 """Attempts to push to a git repository, reports results to a server.
207
208 Returns True if the check finished without incidents (push itself may
209 have failed) and should NOT be retried on next invocation of the hook.
210 """
211 logging.warning('Checking push access to the git repository...')
212
213 # Don't even try to push if netrc is not configured.
214 if not conf['chromium_netrc_email']:
215 return upload_report(
216 conf,
217 report_url,
218 interactive,
219 push_works=False,
220 push_log='',
221 push_duration_ms=0)
222
223 # Ref to push to, each user has its own ref.
224 ref = 'refs/push-test/%s' % conf['chromium_netrc_email']
225
226 push_works = False
227 flake = False
228 started = time.time()
229 try:
230 with temp_directory() as tmp:
231 # Prepare a simple commit on a new timeline.
232 runner = Runner(tmp)
233 runner.run(['git', 'init', '.'])
234 if conf['git_user_name']:
235 runner.run(['git', 'config', 'user.name', conf['git_user_name']])
236 if conf['git_user_email']:
237 runner.run(['git', 'config', 'user.email', conf['git_user_email']])
238 with open(os.path.join(tmp, 'timestamp'), 'w') as f:
239 f.write(str(int(time.time() * 1000)))
240 runner.run(['git', 'add', 'timestamp'])
241 runner.run(['git', 'commit', '-m', 'Push test.'])
242 # Try to push multiple times if it fails due to issues other than ACLs.
243 attempt = 0
244 while attempt < 5:
245 attempt += 1
246 logging.info('Pushing to %s %s', TEST_REPO_URL, ref)
247 ret = runner.run(['git', 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f'])
248 if not ret:
249 push_works = True
250 break
251 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS):
252 push_works = False
253 break
254 except Exception:
255 logging.exception('Unexpected exception when pushing')
256 flake = True
257
258 uploaded = upload_report(
259 conf,
260 report_url,
261 interactive,
262 push_works=push_works,
263 push_log='\n'.join(runner.log),
264 push_duration_ms=int((time.time() - started) * 1000))
265 return uploaded and not flake
266
267
268 def upload_report(
269 conf, report_url, interactive, push_works, push_log, push_duration_ms):
270 """Posts report to the server, returns True if server accepted it.
271
272 If interactive is True, will ask user for confirmation.
273 """
274 report = conf.copy()
275 report.update(
276 push_works=push_works,
277 push_log=push_log,
278 push_duration_ms=push_duration_ms)
279
280 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True)
281 if interactive:
282 print (
283 'The following information will be submitted to the service owned by '
284 'Chrome Infrastructure Team. This information will be accessed only by '
285 'Chrome Infrastructure Team and no one else:')
iannucci 2014/08/16 00:09:35 Let's just report for corp.google.com, and others
Vadim Sh. 2014/08/16 01:37:31 Done.
286 print as_bytes
287 prompt = 'Do you agree to submit this information? [y/n] '
288 if not raw_input(prompt).startswith(('y', 'Y')):
289 return False
290
291 req = urllib2.Request(
292 url=report_url,
293 data=as_bytes,
294 headers={'Content-Type': 'application/json; charset=utf-8'})
295
296 attempt = 0
297 success = False
298 while not success and attempt < 10:
299 attempt += 1
300 try:
301 logging.info('Attempting to upload the report to %s', report_url)
302 urllib2.urlopen(req, timeout=5)
303 success = True
304 logging.warning('Report uploaded.')
305 except (urllib2.URLError, socket.error, ssl.SSLError) as exc:
306 logging.info('Failed to upload the report: %s', exc)
307 return success
308
309
310 def main(args):
311 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
312 parser.add_option(
313 '--running-as-hook',
314 action='store_true',
315 help='Set when invoked from gclient hook')
316 parser.add_option(
317 '--report-url',
318 default=MOTHERSHIP_URL,
319 help='URL to submit the report to')
320 parser.add_option(
321 '--verbose',
322 action='store_true',
323 help='More logging')
324 options, args = parser.parse_args()
325 if args:
326 parser.error('Unknown argument %s' % args)
327 logging.basicConfig(
328 format='%(message)s',
329 level=logging.INFO if options.verbose else logging.WARN)
330
331 # When invoked not as hook, always run the check.
332 if not options.running_as_hook:
333 if check_git_access(scan_configuration(), options.report_url, True):
334 return 0
335 return 1
336
337 # Otherwise, do it only on google owned, non-bot machines.
338 if is_on_bot() or not is_in_google_corp():
339 logging.info('Skipping the check: bot or non corp.')
340 return 0
341
342 # Skip the check if current configuration was already checked.
343 config = scan_configuration()
344 if config == read_last_configuration():
345 logging.info('Check already performed, skipping.')
346 return 0
347
348 # Run the check. Mark configuration as checked only on success. Ignore any
349 # exceptions or errors. This check must not break gclient runhooks.
350 try:
351 ok = check_git_access(config, options.report_url, False)
352 if ok:
353 write_last_configuration(config)
354 else:
355 logging.warning('Check failed and will be retried on the next run')
356 except Exception:
357 logging.exception('Unexpected exception when performing git access check')
358 return 0
359
360
361 if __name__ == '__main__':
362 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