blob: e8c401dd0a88d998cdf1b80a7ffa313547bd032f [file] [log] [blame]
# Copyright 2016 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.
"""Testable functions for Git_cookie_daemon."""
import argparse
import atexit
import cookielib
import functools
import logging
import logging.handlers
import os
import random
import requests
import shutil
import subprocess
import sys
import tempfile
import time
# https://chromium.googlesource.com/infra/infra/+/master/infra_libs/logs/README.md
LOGGER = logging.getLogger(__name__)
if sys.platform == 'win32': # pragma: no cover
GIT = 'C:\\setup\\depot_tools\\git.bat'
else: # pragma: no cover
GIT = '/usr/bin/git'
HOME_DIR = os.path.expanduser('~')
GIT_COOKIE_DIR = os.path.join(HOME_DIR, '.git-credential-cache')
GIT_COOKIE = os.path.join(GIT_COOKIE_DIR, 'cookie')
ACQUIRE_URL = ('http://metadata.google.internal/computeMetadata/v1/instance'
'/service-accounts/default/token')
COOKIE_SPEC = {
'version': 0,
'name': 'o',
'port': None,
'port_specified': False,
'domain': '.googlesource.com',
'domain_specified': True,
'domain_initial_dot': True,
'path': '/',
'path_specified': True,
'secure': True,
'discard': False,
'comment': None,
'comment_url': None,
'rest': None
}
class SubprocessFailed(Exception):
pass
def call(args, cwd=None): # pragma: no cover
# Run subprocess correctly.
args = [str(arg) for arg in args]
LOGGER.info('Calling: %r', args)
LOGGER.info(' in %s', cwd or os.getcwd())
out = subprocess.PIPE
proc = subprocess.Popen(args, cwd=cwd, stdout=out, stderr=subprocess.STDOUT)
# Stream output to LOGGER.
line = ''
while True:
buf = proc.stdout.read(1)
if not buf:
if line:
LOGGER.debug(line)
break
if buf == '\n':
LOGGER.debug(line)
line = ''
else:
line += buf
code = proc.wait()
if code:
LOGGER.error('%r exited with code %d', args[0], code)
else:
LOGGER.info('%r exited with code %d', args[0], code)
if code:
raise SubprocessFailed('%s failed with error %s' % (' '.join(args), code))
def ensure_git_cookie_daemon(): # pragma: no cover
LOGGER.info('Setting up git cookie daemon')
daemon = GitCookieDaemon(GIT_COOKIE, ACQUIRE_URL)
daemon.configure()
daemon.register_cleanup()
while True:
daemon.run()
class GitCookieDaemon(object):
def __init__(self, cookie_file, acquire_url, sleep=None):
self.cookie_file = cookie_file
self.acquire_url = acquire_url
self.expires = 0
self.sleep = sleep or time.sleep
def retry_on_error(self, num_retries, exception_type,
min_sleep=1, max_sleep=600): # pragma: no cover
"""Decorator to retry on exception (with exponential backoff).
Args:
num_retries: maximum number of retries or None to retry forever.
exception_type: exception class to catch and retry on.
min_sleep: seconds to sleep between reties minimum.
max_sleep: seconds to sleep between retries maximum.
"""
def _real_decorator(fn):
@functools.wraps(fn)
def _wrapper(*args, **kwargs):
tries = 0
cur_sleep = min_sleep
while True:
tries += 1
try:
return fn(*args, **kwargs)
except exception_type as e: # pragma: no cover
LOGGER.exception('Ran into exception %s', str(e))
if num_retries is not None and tries > num_retries:
LOGGER.error('It was the last attempt')
raise
LOGGER.info('Sleeping for %d seconds...', cur_sleep)
self.sleep(cur_sleep)
cur_sleep = min(cur_sleep * 2, max_sleep) + random.uniform(0, 1)
return _wrapper
return _real_decorator
def configure(self):
dirname = os.path.dirname(self.cookie_file)
try:
os.makedirs(dirname, 0700)
except OSError:
# If the directory exists.
pass
call([GIT, 'config', '--global', 'http.cookiefile', self.cookie_file])
def _get_token(self):
@self.retry_on_error(5, requests.exceptions.RequestException)
def _inner():
url = self.acquire_url
LOGGER.debug('Acquiring git token from %s', url)
r = requests.get(url, headers={'Metadata-Flavor': 'Google'}, timeout=60)
r.raise_for_status()
return r.json()
return _inner()
def update_cookie(self):
LOGGER.info('Updating Git Cookie')
token = self._get_token()
next_expires = time.time() + token['expires_in']
fd, tmp_jar = tempfile.mkstemp(dir=os.path.dirname(self.cookie_file))
os.close(fd) # We just need the namespace, we don't need the fd.
cookie_jar = cookielib.MozillaCookieJar(tmp_jar)
cookie_jar.set_cookie(
cookielib.Cookie(
value=token['access_token'], expires=next_expires, **COOKIE_SPEC))
cookie_jar.save()
shutil.move(tmp_jar, self.cookie_file)
os.chmod(self.cookie_file, 0700)
LOGGER.info('Git Cookie update success. Next update %s', next_expires)
# Refresh this 25 seconds before the next expiry
self.expires = next_expires - 25
def register_cleanup(self): # pragma: no cover
atexit.register(self.cleanup)
def cleanup(self): # pragma: no cover
for filename in os.listdir(os.path.dirname(self.cookie_file)):
full_path = os.path.join(os.path.dirname(self.cookie_file), filename)
if os.path.exists(full_path):
os.remove(full_path)
def run(self): # pragma: no cover
now = time.time()
expires = max(self.expires, now + 5)
try:
self.update_cookie()
except Exception:
LOGGER.exception('Failed to update git cookie')
self.sleep(expires - now)