Index: tools/check_git_push_access.py |
diff --git a/tools/check_git_push_access.py b/tools/check_git_push_access.py |
deleted file mode 100755 |
index 90bb23d84bdc4ddda1374727730139245324dbc2..0000000000000000000000000000000000000000 |
--- a/tools/check_git_push_access.py |
+++ /dev/null |
@@ -1,420 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright 2014 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Script that attempts to push to a special git repository to verify that git |
-credentials are configured correctly. It also attempts to fix misconfigurations |
-if possible. |
- |
-It will be added as gclient hook shortly before Chromium switches to git and |
-removed after the switch. |
- |
-When running as hook in *.corp.google.com network it will also report status |
-of the push attempt to the server (on appengine), so that chrome-infra team can |
-collect information about misconfigured Git accounts (to fix them). |
- |
-When invoked manually will do the access test and submit the report regardless |
-of where it is running. |
-""" |
- |
-import contextlib |
-import errno |
-import getpass |
-import json |
-import logging |
-import netrc |
-import optparse |
-import os |
-import shutil |
-import socket |
-import ssl |
-import subprocess |
-import sys |
-import tempfile |
-import time |
-import urllib2 |
-import urlparse |
- |
- |
-# Absolute path to src/ directory. |
-REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
- |
-# Incremented whenever some changes to scrip logic are made. Change in version |
-# will cause the check to be rerun on next gclient runhooks invocation. |
-CHECKER_VERSION = 0 |
- |
-# URL to POST json with results to. |
-MOTHERSHIP_URL = ( |
- 'https://chromium-git-access.appspot.com/' |
- 'git_access/api/v1/reports/access_check') |
- |
-# Repository to push test commits to. |
-TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test' |
- |
-# Possible chunks of git push response in case .netrc is misconfigured. |
-BAD_ACL_ERRORS = ( |
- '(prohibited by Gerrit)', |
- 'does not match your user account', |
- 'Invalid user name or password', |
-) |
- |
- |
-def is_on_bot(): |
- """True when running under buildbot.""" |
- return os.environ.get('CHROME_HEADLESS') == '1' |
- |
- |
-def is_in_google_corp(): |
- """True when running in google corp network.""" |
- try: |
- return socket.getfqdn().endswith('.corp.google.com') |
- except socket.error: |
- logging.exception('Failed to get FQDN') |
- return False |
- |
- |
-def is_using_git(): |
- """True if git checkout is used.""" |
- return os.path.exists(os.path.join(REPO_ROOT, '.git', 'objects')) |
- |
- |
-def is_using_svn(): |
- """True if svn checkout is used.""" |
- return os.path.exists(os.path.join(REPO_ROOT, '.svn')) |
- |
- |
-def read_git_config(prop): |
- """Reads git config property of src.git repo. |
- |
- Returns empty string in case of errors. |
- """ |
- try: |
- proc = subprocess.Popen( |
- ['git', 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT) |
- out, _ = proc.communicate() |
- return out.strip() |
- except OSError as exc: |
- if exc.errno != errno.ENOENT: |
- logging.exception('Unexpected error when calling git') |
- return '' |
- |
- |
-def read_netrc_user(netrc_obj, host): |
- """Reads 'user' field of a host entry in netrc. |
- |
- Returns empty string if netrc is missing, or host is not there. |
- """ |
- if not netrc_obj: |
- return '' |
- entry = netrc_obj.authenticators(host) |
- if not entry: |
- return '' |
- return entry[0] |
- |
- |
-def get_git_version(): |
- """Returns version of git or None if git is not available.""" |
- try: |
- proc = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE) |
- out, _ = proc.communicate() |
- return out.strip() if proc.returncode == 0 else '' |
- except OSError as exc: |
- if exc.errno != errno.ENOENT: |
- logging.exception('Unexpected error when calling git') |
- return '' |
- |
- |
-def scan_configuration(): |
- """Scans local environment for git related configuration values.""" |
- # Git checkout? |
- is_git = is_using_git() |
- |
- # On Windows HOME should be set. |
- if 'HOME' in os.environ: |
- netrc_path = os.path.join( |
- os.environ['HOME'], |
- '_netrc' if sys.platform.startswith('win') else '.netrc') |
- else: |
- netrc_path = None |
- |
- # Netrc exists? |
- is_using_netrc = netrc_path and os.path.exists(netrc_path) |
- |
- # Read it. |
- netrc_obj = None |
- if is_using_netrc: |
- try: |
- netrc_obj = netrc.netrc(netrc_path) |
- except Exception: |
- logging.exception('Failed to read netrc from %s', netrc_path) |
- netrc_obj = None |
- |
- return { |
- 'checker_version': CHECKER_VERSION, |
- 'is_git': is_git, |
- 'is_home_set': 'HOME' in os.environ, |
- 'is_using_netrc': is_using_netrc, |
- 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0, |
- 'git_version': get_git_version(), |
- 'platform': sys.platform, |
- 'username': getpass.getuser(), |
- 'git_user_email': read_git_config('user.email') if is_git else '', |
- 'git_user_name': read_git_config('user.name') if is_git else '', |
- 'chromium_netrc_email': |
- read_netrc_user(netrc_obj, 'chromium.googlesource.com'), |
- 'chrome_internal_netrc_email': |
- read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'), |
- } |
- |
- |
-def last_configuration_path(): |
- """Path to store last checked configuration.""" |
- if is_using_git(): |
- return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json') |
- elif is_using_svn(): |
- return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json') |
- else: |
- return os.path.join(REPO_ROOT, '.check_git_push_access_conf.json') |
- |
- |
-def read_last_configuration(): |
- """Reads last checked configuration if it exists.""" |
- try: |
- with open(last_configuration_path(), 'r') as f: |
- return json.load(f) |
- except (IOError, ValueError): |
- return None |
- |
- |
-def write_last_configuration(conf): |
- """Writes last checked configuration to a file.""" |
- try: |
- with open(last_configuration_path(), 'w') as f: |
- json.dump(conf, f, indent=2, sort_keys=True) |
- except IOError: |
- logging.exception('Failed to write JSON to %s', path) |
- |
- |
-@contextlib.contextmanager |
-def temp_directory(): |
- """Creates a temp directory, then nukes it.""" |
- tmp = tempfile.mkdtemp() |
- try: |
- yield tmp |
- finally: |
- try: |
- shutil.rmtree(tmp) |
- except (OSError, IOError): |
- logging.exception('Failed to remove temp directory %s', tmp) |
- |
- |
-class Runner(object): |
- """Runs a bunch of commands in some directory, collects logs from them.""" |
- |
- def __init__(self, cwd, verbose): |
- self.cwd = cwd |
- self.verbose = verbose |
- self.log = [] |
- |
- def run(self, cmd): |
- self.append_to_log('> ' + ' '.join(cmd)) |
- retcode = -1 |
- try: |
- proc = subprocess.Popen( |
- cmd, |
- stdout=subprocess.PIPE, |
- stderr=subprocess.STDOUT, |
- cwd=self.cwd) |
- out, _ = proc.communicate() |
- out = out.strip() |
- retcode = proc.returncode |
- except OSError as exc: |
- out = str(exc) |
- if retcode: |
- out += '\n(exit code: %d)' % retcode |
- self.append_to_log(out) |
- return retcode |
- |
- def append_to_log(self, text): |
- if text: |
- self.log.append(text) |
- if self.verbose: |
- logging.warning(text) |
- |
- |
-def check_git_access(conf, report_url, verbose): |
- """Attempts to push to a git repository, reports results to a server. |
- |
- Returns True if the check finished without incidents (push itself may |
- have failed) and should NOT be retried on next invocation of the hook. |
- """ |
- logging.warning('Checking push access to the git repository...') |
- |
- # Don't even try to push if netrc is not configured. |
- if not conf['chromium_netrc_email']: |
- return upload_report( |
- conf, |
- report_url, |
- verbose, |
- push_works=False, |
- push_log='', |
- push_duration_ms=0) |
- |
- # Ref to push to, each user has its own ref. |
- ref = 'refs/push-test/%s' % conf['chromium_netrc_email'] |
- |
- push_works = False |
- flake = False |
- started = time.time() |
- try: |
- with temp_directory() as tmp: |
- # Prepare a simple commit on a new timeline. |
- runner = Runner(tmp, verbose) |
- runner.run(['git', 'init', '.']) |
- if conf['git_user_name']: |
- runner.run(['git', 'config', 'user.name', conf['git_user_name']]) |
- if conf['git_user_email']: |
- runner.run(['git', 'config', 'user.email', conf['git_user_email']]) |
- with open(os.path.join(tmp, 'timestamp'), 'w') as f: |
- f.write(str(int(time.time() * 1000))) |
- runner.run(['git', 'add', 'timestamp']) |
- runner.run(['git', 'commit', '-m', 'Push test.']) |
- # Try to push multiple times if it fails due to issues other than ACLs. |
- attempt = 0 |
- while attempt < 5: |
- attempt += 1 |
- logging.info('Pushing to %s %s', TEST_REPO_URL, ref) |
- ret = runner.run(['git', 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f']) |
- if not ret: |
- push_works = True |
- break |
- if any(x in runner.log[-1] for x in BAD_ACL_ERRORS): |
- push_works = False |
- break |
- except Exception: |
- logging.exception('Unexpected exception when pushing') |
- flake = True |
- |
- uploaded = upload_report( |
- conf, |
- report_url, |
- verbose, |
- push_works=push_works, |
- push_log='\n'.join(runner.log), |
- push_duration_ms=int((time.time() - started) * 1000)) |
- return uploaded and not flake |
- |
- |
-def upload_report( |
- conf, report_url, verbose, push_works, push_log, push_duration_ms): |
- """Posts report to the server, returns True if server accepted it. |
- |
- Uploads the report only if script is running in Google corp network. Otherwise |
- just prints the report. |
- """ |
- report = conf.copy() |
- report.update( |
- push_works=push_works, |
- push_log=push_log, |
- push_duration_ms=push_duration_ms) |
- |
- as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True) |
- |
- if push_works: |
- logging.warning('Git push works!') |
- else: |
- logging.warning( |
- 'Git push doesn\'t work, which is fine if you are not a committer.') |
- |
- if verbose: |
- print 'Status of git push attempt:' |
- print as_bytes |
- |
- # Do not upload it outside of corp. |
- if not is_in_google_corp(): |
- if verbose: |
- print ( |
- 'You can send the above report to chrome-git-migration@google.com ' |
- 'if you need help to set up you committer git account.') |
- return True |
- |
- req = urllib2.Request( |
- url=report_url, |
- data=as_bytes, |
- headers={'Content-Type': 'application/json; charset=utf-8'}) |
- |
- attempt = 0 |
- success = False |
- while not success and attempt < 10: |
- attempt += 1 |
- try: |
- logging.warning( |
- 'Attempting to upload the report to %s...', |
- urlparse.urlparse(report_url).netloc) |
- resp = urllib2.urlopen(req, timeout=5) |
- report_id = None |
- try: |
- report_id = json.load(resp)['report_id'] |
- except (ValueError, TypeError, KeyError): |
- pass |
- logging.warning('Report uploaded: %s', report_id) |
- success = True |
- except (urllib2.URLError, socket.error, ssl.SSLError) as exc: |
- logging.warning('Failed to upload the report: %s', exc) |
- return success |
- |
- |
-def main(args): |
- parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) |
- parser.add_option( |
- '--running-as-hook', |
- action='store_true', |
- help='Set when invoked from gclient hook') |
- parser.add_option( |
- '--report-url', |
- default=MOTHERSHIP_URL, |
- help='URL to submit the report to') |
- parser.add_option( |
- '--verbose', |
- action='store_true', |
- help='More logging') |
- options, args = parser.parse_args() |
- if args: |
- parser.error('Unknown argument %s' % args) |
- logging.basicConfig( |
- format='%(message)s', |
- level=logging.INFO if options.verbose else logging.WARN) |
- |
- # When invoked not as hook, always run the check. |
- if not options.running_as_hook: |
- if check_git_access(scan_configuration(), options.report_url, True): |
- return 0 |
- return 1 |
- |
- # Otherwise, do it only on google owned, non-bot machines. |
- if is_on_bot() or not is_in_google_corp(): |
- logging.info('Skipping the check: bot or non corp.') |
- return 0 |
- |
- # Skip the check if current configuration was already checked. |
- config = scan_configuration() |
- if config == read_last_configuration(): |
- logging.info('Check already performed, skipping.') |
- return 0 |
- |
- # Run the check. Mark configuration as checked only on success. Ignore any |
- # exceptions or errors. This check must not break gclient runhooks. |
- try: |
- ok = check_git_access(config, options.report_url, False) |
- if ok: |
- write_last_configuration(config) |
- else: |
- logging.warning('Check failed and will be retried on the next run') |
- except Exception: |
- logging.exception('Unexpected exception when performing git access check') |
- return 0 |
- |
- |
-if __name__ == '__main__': |
- sys.exit(main(sys.argv[1:])) |