#!/usr/bin/python3 import logging import logging.handlers import os import shlex import subprocess import sys import time import urllib.parse import configparser log = logging.getLogger('grok-shell') def setup_logging(): log.setLevel(logging.INFO) # rotate logs every sunday, and retain for 5 weeks handler = logging.handlers.TimedRotatingFileHandler( '/tmp/grok-shell.log', when='W6', backupCount=5) formatter = logging.Formatter( '%(asctime)s %(levelname)-5s %(name)s: %(message)s', datefmt='%H:%M:%S') handler.setFormatter(formatter) log.addHandler(handler) def _slave_local_fetch(cmd): cmd, repo = cmd.split(' ', 1) # git-shell command has an undocumented requirement that the repo be # wrapped in quotes assert repo[0] == "'" and repo[-1] == "'" repo = "'/srv/repositories/" + repo[1:] args = ['/usr/bin/git-shell', '-c', "%s %s" % (cmd, repo)] os.execv(args[0], args) def _get_master(): ini = configparser.ConfigParser() ini.read('/etc/grokmirror-repos.conf') master = ini.get(ini.sections()[0], 'site') return urllib.parse.urlparse(master).netloc def _slave_remote_cmd(user, cmd): # we have to use posix=False or shlex will try and strip out quotes for # an arg like 'people/foo.bar/repo'. The quotes are required by git-shell # as noted in _slave_local_fetch cmd = shlex.split(cmd, posix=False) args = ['/usr/bin/ssh', _get_master(), user] + cmd start = time.time() ret = subprocess.call(args) if ret == 0 and args[3] == 'git-receive-pack': log.info('remote update for %s %s took %d ms', user, cmd, time.time() - start) start = time.time() # help make the change show up on the local grok slave as quick as # possible, this won't update the grok-manifest, but our cron job will # take care of that when it runs next. repo = args[4] assert repo[0] == "'" and repo[-1] == "'" repo = '/srv/repositories/' + repo[1:-1] if not repo.endswith('.git'): repo += '.git' # only do updates to existing repos, let cron job handle clones if os.path.exists(repo): os.write(2, b'Updating local grokmirror with changes on master\n') subprocess.call( ['/usr/bin/git', 'remote', 'update', '--prune'], cwd=repo) log.info('local update for %s %s took %d ms', user, cmd, time.time() - start) sys.exit(ret) def _master_shell(cmd): user, cmd = cmd.split(' ', 1) os.environ['SSH_ORIGINAL_COMMAND'] = cmd os.environ['SSH_CONNECTION'] = '1' args = ['/home/git/gitolite/src/gitolite-shell', user] os.execv(args[0], args) if __name__ == '__main__': '''This script is called in 2 different ways: 1) on grokmirror slave, it will determine if we are doing a read or write operation. If a read, we'll keep the request local. If a write, we'll send a request to the master to complete 2) on grokmirror master, it will accept the remote command from #1 and handle its request. Both slave and master require special .ssh/authorzied_key entries to be in place. ''' setup_logging() cmd = os.environ.get('SSH_ORIGINAL_COMMAND', 'info') if len(sys.argv) < 2 or sys.argv[1] not in ('slave', 'master'): sys.exit('Usage: %s [slave|master] ...' % sys.argv[0]) shell_type = sys.argv[1] if shell_type == 'master': log.info('command from slave: %s', cmd) _master_shell(cmd) elif shell_type == 'slave': user = sys.argv[2] if cmd.startswith('git-upload-'): log.info('%s: local: %s', user, cmd) _slave_local_fetch(cmd) else: log.info('%s: remote: %s', user, cmd) _slave_remote_cmd(user, cmd)