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

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